Skip to content

Commit

Permalink
Merge pull request #52 from botstory/feature/based-on-rss-bot
Browse files Browse the repository at this point in the history
Feature/based on rss bot
  • Loading branch information
hyzhak authored Oct 14, 2017
2 parents 52fe48e + 143d1ff commit 8f22924
Show file tree
Hide file tree
Showing 16 changed files with 274 additions and 104 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.5
FROM python:3.6

ENV PYTHONUNBUFFERED 1

Expand Down
10 changes: 9 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Stack

`:rocket:` auto deploying `landing page <https://botstory.github.io/boilerplate-bot/>`_ (`sources <https://github.com/botstory/boilerplate-bot-landing>`_)

`:snake:` `AsyncIO <https://docs.python.org/3/library/asyncio.html>`_ and `AioHTTP <http://aiohttp.readthedocs.io/en/stable/>`_
`:snake:` Python 3.6 and `AsyncIO <https://docs.python.org/3/library/asyncio.html>`_ and `AioHTTP <http://aiohttp.readthedocs.io/en/stable/>`_

`:package:` `Mongodb <https://www.mongodb.com/>`_ - storage for user and session

Expand All @@ -31,3 +31,11 @@ Test performance of webhook
.. code-block:: bash
WEBHOOK_URL=<url-to-webhook-of-your-bot> ./scripts/performance.sh
Bot Checklist
~~~~~~~~~~~~~
Common checklist for common bot

- [ ] Mindmap. Could use `Coggle <https://coggle.it/>`_. `Example <https://coggle.it/diagram/WcgsjGjgVAABxW_M>`_
- [ ] Describe bot profile at *<chatbot-name>/__init__.py*.
25 changes: 25 additions & 0 deletions boilerplate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os

print('os.path.dirname(os.path.realpath(__file__))')
print(os.path.dirname(os.path.realpath(__file__)))

with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'version.txt')) as version_file:
__version__ = version_file.read().strip()

# some general information about this bot

BOT_ID = 'boilerplatebot'

BOT_NAME = 'Boilerplate bot'

GITHUB_URL = 'https://github.com/botstory/boilerplate-bot/'

SHORT_INTO = 'Hello dear {{user_first_name}}!\n' \
'I\'m ' + BOT_NAME + ' assistant based on BotStory framework.\n' \
'I\'m here to help you to get boilerplate and make new bot from it.\n'

SHORT_HELP = SHORT_INTO + \
'\n' \
'All sources could be find here:\n' \
':package: ' + GITHUB_URL + ',\n' \
'feedback and contribution are welcomed!'
95 changes: 95 additions & 0 deletions boilerplate/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import botstory
from botstory.integrations import aiohttp, fb, mongodb
from botstory.integrations.ga import tracker
import logging
import os

from boilerplate import BOT_ID, BOT_NAME, GITHUB_URL, SHORT_INTO, stories


# TODO: here should be all documents
DOCUMENTS = ()

logger = logging.getLogger(BOT_NAME)
logger.setLevel(logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)


class Bot:
def __init__(self):
self.story = botstory.Story()

def init(self, auto_start, fake_http_session):
self.story.use(fb.FBInterface(
# will show on initial screen
greeting_text=SHORT_INTO,

# you should get on admin panel for the Messenger Product in Token Generation section
page_access_token=os.environ.get('FB_ACCESS_TOKEN', 'TEST_TOKEN'),
# menu of the bot that user has access all the time
persistent_menu=[
{
'type': 'postback',
'title': 'Hi!',
'payload': 'ABOUT_ME',
}, {
'type': 'nested',
'title': 'Help',
'call_to_actions': [
{
'type': 'web_url',
'title': 'Source Code',
'url': GITHUB_URL,
}, {
'type': 'postback',
'title': 'About',
'payload': 'ABOUT_ME',
},
],
},
],
# should be the same as in admin panel for the Webhook Product
webhook_url='/webhook{}'.format(os.environ.get('FB_WEBHOOK_URL_SECRET_PART', '')),
webhook_token=os.environ.get('FB_WEBHOOK_TOKEN', None),
))

# Interface for HTTP
http = self.story.use(aiohttp.AioHttpInterface(
port=int(os.environ.get('PORT', 8080)),
auto_start=auto_start,
))

# User and Session storage
db = self.story.use(mongodb.MongodbInterface(
uri=os.environ.get('MONGODB_URI', 'mongo'),
db_name=os.environ.get('MONGODB_DB_NAME', BOT_ID),
))

self.story.use(tracker.GAStatistics(
tracking_id=os.environ.get('GA_ID'),
))

# for test purpose
http.session = fake_http_session

stories.setup(self.story)
return http, db

async def setup(self, fake_http_session=None):
logger.info('# setup')
self.init(auto_start=False, fake_http_session=fake_http_session)
await self.story.setup()

async def start(self, auto_start=True, fake_http_session=None):
logger.info('# start')
http, db_integration = self.init(auto_start, fake_http_session)
await self.story.setup()
await self.story.start()
for document in DOCUMENTS:
document.setup(db_integration.db)
return http.app

async def stop(self):
logger.info('# stop')
await self.story.stop()
self.story.clear()
87 changes: 9 additions & 78 deletions boilerplate/main.py
Original file line number Diff line number Diff line change
@@ -1,101 +1,32 @@
import asyncio
import argparse
import botstory
from botstory.integrations import aiohttp, fb, mongodb
from botstory.integrations.ga import tracker
import logging
import os
from boilerplate import bot
import sys

from boilerplate import stories

BOT_NAME = 'boiledplate'

logger = logging.getLogger('boilerplate-bot')
logger = logging.getLogger('main.py')
logger.setLevel(logging.DEBUG)


class Bot:
def __init__(self):
self.story = botstory.Story()
stories.setup(self.story)

def init(self, auto_start, fake_http_session):
self.story.use(fb.FBInterface(
# will show on initial screen
greeting_text='Hello dear {{user_first_name}}! '
'I'' m demo bot of BotStory framework.',
# you should get on admin panel for the Messenger Product in Token Generation section
page_access_token=os.environ.get('FB_ACCESS_TOKEN', 'TEST_TOKEN'),
# menu of the bot that user has access all the time
persistent_menu=[{
'type': 'postback',
'title': 'Monkey Business',
'payload': 'MONKEY_BUSINESS'
}, {
'type': 'web_url',
'title': 'Source Code',
'url': 'https://github.com/botstory/bot-story/'
}],
# should be the same as in admin panel for the Webhook Product
webhook_url='/webhook{}'.format(os.environ.get('FB_WEBHOOK_URL_SECRET_PART', '')),
webhook_token=os.environ.get('FB_WEBHOOK_TOKEN', None),
))

# Interface for HTTP
http = self.story.use(aiohttp.AioHttpInterface(
port=int(os.environ.get('API_PORT', 8080)),
auto_start=auto_start,
))

# User and Session storage
self.story.use(mongodb.MongodbInterface(
uri=os.environ.get('MONGODB_URI', 'mongo'),
db_name=os.environ.get('MONGODB_DB_NAME', 'echobot'),
))

self.story.use(tracker.GAStatistics(
tracking_id=os.environ.get('GA_ID'),
))

# for test purpose
http.session = fake_http_session
return http

async def setup(self, fake_http_session=None):
logger.info('setup')
self.init(auto_start=False, fake_http_session=fake_http_session)
await self.story.setup()

async def start(self, auto_start=True, fake_http_session=None):
logger.info('start')
http = self.init(auto_start, fake_http_session)
await self.story.start()
return http.app

async def stop(self):
logger.info('stop')
await self.story.stop()
self.story.clear()
logging.basicConfig(level=logging.DEBUG)


def setup():
bot = Bot()
b = bot.Bot()
loop = asyncio.get_event_loop()
loop.run_until_complete(bot.setup())
loop.run_until_complete(b.setup())


def start(forever=False):
bot = Bot()
b = bot.Bot()
loop = asyncio.get_event_loop()
app = loop.run_until_complete(bot.start(auto_start=forever))
app = loop.run_until_complete(b.start(auto_start=forever))
if forever:
bot.story.forever(loop)
b.story.forever(loop)
return app


def parse_args(args):
parser = argparse.ArgumentParser(prog=BOT_NAME)
parser = argparse.ArgumentParser(prog=bot.BOT_NAME)
parser.add_argument('--setup', action='store_true', default=False, help='setup bot')
parser.add_argument('--start', action='store_true', default=False, help='start bot')
return parser.parse_args(args), parser
Expand Down
56 changes: 33 additions & 23 deletions boilerplate/main_test.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,65 @@
import aiohttp
import asyncio
import botstory
import contextlib
import emoji
from io import StringIO
import os
import pytest
from unittest.mock import Mock

from . import main, test_utils
from boilerplate import bot, main, test_utils


@pytest.mark.asyncio
async def test_start_bot(event_loop):
async with test_utils.SandboxBot(event_loop, main.Bot()) as sandbox:
assert len(sandbox.fb.history) == 0
async with test_utils.SandboxBot(event_loop, bot.Bot()) as sandbox:
assert len(sandbox.fb.history) > 0


@pytest.mark.asyncio
async def test_text_echo(event_loop):
async with test_utils.SandboxBot(event_loop, main.Bot()) as sandbox:
async def test_earth_message(event_loop):
async with test_utils.SandboxBot(event_loop, bot.Bot()) as sandbox:
initial_history_length = len(sandbox.fb.history)
await test_utils.post('http://0.0.0.0:{}/webhook'.format(os.environ.get('API_PORT', 8080)),
json={
"object": "page",
"entry": [{
"id": "PAGE_ID",
"time": 1458692752478,
"messaging": [{
"sender": {
"id": "USER_ID"
'object': 'page',
'entry': [{
'id': 'PAGE_ID',
'time': 1458692752478,
'messaging': [{
'sender': {
'id': 'USER_ID'
},
"recipient": {
"id": "PAGE_ID"
'recipient': {
'id': 'PAGE_ID'
},
"timestamp": 1458692752478,
"message": {
"mid": "mid.1457764197618:41d102a3e1ae206a38",
"seq": 73,
"text": "hello, world!",
'timestamp': 1458692752478,
'message': {
'mid': 'mid.1457764197618:41d102a3e1ae206a38',
'seq': 73,
'text': 'hello, world!',
}
}]
}]
})

assert len(sandbox.fb.history) == 1
assert await sandbox.fb.history[0]['request'].json() == {
# use it because we spawn fb handler process and return 200Ok
await asyncio.sleep(0.1)

# we can't use initial_history_length + 1
# because it is very likely that we don't have user USER_ID in our user's collection
# and fb handler will ask user's profile meanwhile process income message
assert len(sandbox.fb.history) > initial_history_length
assert await sandbox.fb.history[-1]['request'].json() == {
'message': {
'text': '<React on text message>'
'text': emoji.emojize(':earth:', use_aliases=False),
},
'recipient': {'id': 'USER_ID'},
}


def test_parser_empry_arguments():
def test_parser_empty_arguments():
parsed, _ = main.parse_args([])
assert parsed.setup is not True
assert parsed.start is not True
Expand Down
18 changes: 18 additions & 0 deletions boilerplate/stories/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from boilerplate.stories.greetings import greeting_stories
from boilerplate.stories.help import help_stories
from boilerplate.stories.query import query_stories

story_modules = (
greeting_stories,
query_stories,

# last hope :)
# if we haven't handle message before,
# then show help message to user
help_stories,
)


def setup(story):
for m in story_modules:
m.setup(story)
Empty file.
18 changes: 18 additions & 0 deletions boilerplate/stories/greetings/greeting_stories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging

import boilerplate
from boilerplate.utils import inject_first_name
# from boilerplate.query import query_stories

logger = logging.getLogger(__name__)


def setup(story):
@story.on_start()
def on_start_story():
@story.part()
async def greet(ctx):
await story.say(inject_first_name(boilerplate.SHORT_INTO, ctx['user']),
user=ctx['user'])
await story.say('For example: <give one random recommendation from bot + quick_replies>',
user=ctx['user'])
Empty file.
Loading

0 comments on commit 8f22924

Please sign in to comment.