diff --git a/Dockerfile b/Dockerfile
index 0b7671d..edea7e8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.5
+FROM python:3.6
ENV PYTHONUNBUFFERED 1
diff --git a/README.rst b/README.rst
index 46391d2..755be24 100644
--- a/README.rst
+++ b/README.rst
@@ -15,7 +15,7 @@ Stack
`:rocket:` auto deploying `landing page `_ (`sources `_)
-`:snake:` `AsyncIO `_ and `AioHTTP `_
+`:snake:` Python 3.6 and `AsyncIO `_ and `AioHTTP `_
`:package:` `Mongodb `_ - storage for user and session
@@ -31,3 +31,11 @@ Test performance of webhook
.. code-block:: bash
WEBHOOK_URL= ./scripts/performance.sh
+
+
+Bot Checklist
+~~~~~~~~~~~~~
+Common checklist for common bot
+
+- [ ] Mindmap. Could use `Coggle `_. `Example `_
+- [ ] Describe bot profile at */__init__.py*.
diff --git a/boilerplate/__init__.py b/boilerplate/__init__.py
index e69de29..deec513 100644
--- a/boilerplate/__init__.py
+++ b/boilerplate/__init__.py
@@ -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!'
diff --git a/boilerplate/bot.py b/boilerplate/bot.py
new file mode 100644
index 0000000..082af83
--- /dev/null
+++ b/boilerplate/bot.py
@@ -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()
diff --git a/boilerplate/main.py b/boilerplate/main.py
index 25b6007..764e6db 100644
--- a/boilerplate/main.py
+++ b/boilerplate/main.py
@@ -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
diff --git a/boilerplate/main_test.py b/boilerplate/main_test.py
index 29d5bbb..fc93f9a 100644
--- a/boilerplate/main_test.py
+++ b/boilerplate/main_test.py
@@ -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': ''
+ '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
diff --git a/boilerplate/stories/__init__.py b/boilerplate/stories/__init__.py
new file mode 100644
index 0000000..c1f5b61
--- /dev/null
+++ b/boilerplate/stories/__init__.py
@@ -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)
diff --git a/boilerplate/stories/greetings/__init__.py b/boilerplate/stories/greetings/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/boilerplate/stories/greetings/greeting_stories.py b/boilerplate/stories/greetings/greeting_stories.py
new file mode 100644
index 0000000..00cb712
--- /dev/null
+++ b/boilerplate/stories/greetings/greeting_stories.py
@@ -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: ',
+ user=ctx['user'])
diff --git a/boilerplate/stories/help/__init__.py b/boilerplate/stories/help/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/boilerplate/stories/help/help_stories.py b/boilerplate/stories/help/help_stories.py
new file mode 100644
index 0000000..52c98d7
--- /dev/null
+++ b/boilerplate/stories/help/help_stories.py
@@ -0,0 +1,33 @@
+from botstory.middlewares import any, option, sticker, text
+import emoji
+import logging
+
+import boilerplate
+from boilerplate.utils import inject_first_name
+
+logger = logging.getLogger(__name__)
+
+
+def setup(story):
+ @story.on(receive=any.Any())
+ def unhandled_message():
+ @story.part()
+ async def say_something(ctx):
+ logger.warning('# Unhandled message')
+
+ # TODO:
+ # - store somewhere information about those messages
+ # - add quick_replies
+
+ help_msg = emoji.emojize(boilerplate.SHORT_HELP, use_aliases=True)
+ await story.say(
+ inject_first_name(help_msg, ctx['user']),
+ user=ctx['user']
+ )
+
+ await story.say(
+ 'For example: ',
+ user=ctx['user']
+ )
+
+ logger.debug('# end of say_something')
diff --git a/boilerplate/stories/query/__init__.py b/boilerplate/stories/query/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/boilerplate/stories/query/query_stories.py b/boilerplate/stories/query/query_stories.py
new file mode 100644
index 0000000..c4e07ee
--- /dev/null
+++ b/boilerplate/stories/query/query_stories.py
@@ -0,0 +1,23 @@
+from botstory.middlewares import any, option, sticker, text
+import emoji
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def setup(story):
+ @story.on(text.EqualCaseIgnore('hello, world!'))
+ def earth_message():
+ @story.part()
+ async def say_something(ctx):
+ logger.warning('# Unhandled message')
+
+ # TODO:
+ # - store somewhere information about those messages
+ # - add quick_replies
+
+ await story.say(
+ emoji.emojize(':earth:', use_aliases=True),
+ user=ctx['user']
+ )
+ logger.debug('# end of say_something')
diff --git a/boilerplate/utils/__init__.py b/boilerplate/utils/__init__.py
new file mode 100644
index 0000000..5a2ed76
--- /dev/null
+++ b/boilerplate/utils/__init__.py
@@ -0,0 +1,6 @@
+def get_user_first_name_safe(user):
+ return user['first_name'] or 'friend'
+
+
+def inject_first_name(msg, user):
+ return msg.replace('{{user_first_name}}', get_user_first_name_safe(user))
diff --git a/requirements.txt b/requirements.txt
index 48f3087..9100ae1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,6 @@
-botstory==0.0.46
+botstory==0.1.5
+
+emoji==0.4.5
gunicorn==19.6.0
pytest==3.0.6
pytest-aiohttp==0.1.3
diff --git a/version.txt b/version.txt
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/version.txt
@@ -0,0 +1 @@
+0.1.0