Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[run]
branch = True
omit =
*_test.py
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ ENV/

# Rope project settings
.ropeproject

# IDE
.idea/
23 changes: 23 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
sudo: required

language: python

python:
- "3.5"

services:
- mongodb

env:
MONGODB_URI: 127.0.0.1
MONGODB_DB_NAME: test

before_install:
- pip install -r requirements.txt
- pip install coveralls

script:
- py.test --cov todo --cov-report term-missing

after_success:
- coveralls
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.5

ENV PYTHONUNBUFFERED 1

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY requirements.txt /usr/src/app/
RUN pip install --default-timeout=1000 --no-cache-dir -r requirements.txt

ADD . /usr/src/app
2 changes: 0 additions & 2 deletions README.md

This file was deleted.

26 changes: 26 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Todo-bot
--------

.. image:: https://travis-ci.org/botstory/todo-bot.svg?branch=develop
:target: https://travis-ci.org/botstory/todo-bot

.. image:: https://coveralls.io/repos/github/botstory/todo-bot/badge.svg?branch=develop
:target: https://coveralls.io/github/botstory/todo-bot?branch=develop


Simple example of using bot-story framework, inspired by http://todomvc.com

Based on `BotStory framework <https://github.com/botstory/bot-story>`_.

Stack
~~~~~

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

`:snake:` `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

`:ship:` `Docker Container <https://www.docker.com/>`_ for Mongo and Python

`:tractor:` `Travic-CI <https://travis-ci.org/>`_ - test and coverage
17 changes: 17 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: '2'
services:
mongo:
image: mongo
bot:
build: .
command: ./scripts/start.sh
environment:
- API_PORT=8080
- PROJECT_NAME=todo
- PYTHONPATH=/usr/src/app
ports:
- "80:8080"
volumes:
- .:/usr/src/app
depends_on:
- mongo
8 changes: 8 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
botstory==0.0.43
gunicorn==19.6.0
pytest==3.0.4
pytest-aiohttp==0.1.2
pytest-asyncio==0.5.0
pytest-catchlog==1.2.2
pytest-cov==2.4.0
pytest-mock==1.4.0
22 changes: 22 additions & 0 deletions scripts/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

export PYTHONPATH=${PYTHONPATH}:$(pwd)

echo "PYTHONPATH"
echo ${PYTHONPATH}

echo "====================================================="
echo ""
echo " Setup"
echo ""
echo "====================================================="

python ./${PROJECT_NAME}/main.py --setup

echo "====================================================="
echo ""
echo " Start"
echo ""
echo "====================================================="

gunicorn ${PROJECT_NAME}.wsgi:app --bind 0.0.0.0:${API_PORT} --log-file - --reload --worker-class aiohttp.worker.GunicornWebWorker
Empty file added todo/__init__.py
Empty file.
116 changes: 116 additions & 0 deletions todo/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import asyncio
import argparse
import botstory
from botstory.integrations import aiohttp, fb, mongodb
from botstory.integrations.ga import tracker
import logging
import os
import sys

from todo import stories

BOT_NAME = 'todo-bot'

logger = logging.getLogger('todo-bot')
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()


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


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


def parse_args(args):
parser = argparse.ArgumentParser(prog=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


def main():
parsed, parser = parse_args(sys.argv[1:])
if parsed.setup:
return setup()

if parsed.start:
return start(forever=True)

parser.print_help()


if __name__ == '__main__':
main()
92 changes: 92 additions & 0 deletions todo/main_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import aiohttp
import contextlib
from io import StringIO
import os
import pytest
from unittest.mock import Mock

from . import 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


@pytest.mark.asyncio
async def test_text_echo(event_loop):
async with test_utils.SandboxBot(event_loop, main.Bot()) as sandbox:
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"
},
"recipient": {
"id": "PAGE_ID"
},
"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() == {
'message': {
'text': '<React on text message>'
},
'recipient': {'id': 'USER_ID'},
}


def test_parser_empry_arguments():
parsed, _ = main.parse_args([])
assert parsed.setup is not True
assert parsed.start is not True


def test_parser_setup_arguments():
parsed, _ = main.parse_args(['--setup'])
assert parsed.setup is True
assert parsed.start is not True


def test_parser_start_arguments():
parsed, _ = main.parse_args(['--start'])
assert parsed.setup is False
assert parsed.start is True


def test_show_help_if_no_any_arguments():
main.sys.argv = []
temp_stdout = StringIO()
with contextlib.redirect_stdout(temp_stdout):
main.main()
output = temp_stdout.getvalue().strip()
assert 'help' in output
assert 'setup' in output
assert 'start' in output


def test_setup_bot_on_setup_argument(mocker):
main.sys.argv = ['', '--setup']
handler = mocker.patch('todo.main.setup')
main.main()
assert handler.called is True


def test_start_bot_on_setup_argument(mocker):
main.sys.argv = ['', '--start']
handler = mocker.patch('todo.main.start')
main.main()
assert handler.called is True
43 changes: 43 additions & 0 deletions todo/stories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from botstory.middlewares import any, text
import logging

logger = logging.getLogger(__name__)

logger.debug('parse stories')


def setup(story):
@story.on_start()
def on_start_story():
"""
User just pressed `get started` button so we can greet him
"""

@story.part()
async def greetings(message):
logger.info('greetings')
await story.say('<Motivate user to act>', message['user'])

@story.on(receive=text.Any())
def text_story():
"""
React on any text message
"""

logger.debug('parse echo story')

@story.part()
async def echo(message):
logger.info('echo')
await story.say('<React on text message>', message['user'])

@story.on(receive=any.Any())
def any_story():
"""
And all the rest messages as well
"""

@story.part()
async def something_else(message):
logger.info('something_else')
await story.say('<React on unknown message>', message['user'])
Loading