Skip to content

Commit

Permalink
Twitter integration (#11)
Browse files Browse the repository at this point in the history
* added twitter features (using tweepy api):
** streaming
** new followers get start message
** following new followers (even if it followed while bot was offline)
* fixing the setup in requirements
* added MANIFEST.in containing the requirements.txt, the setup should work now
* installing pytest and pytest-mock in travis to run the tests
  • Loading branch information
greenkey committed Apr 21, 2017
1 parent 375aca9 commit 89edf51
Show file tree
Hide file tree
Showing 9 changed files with 528 additions and 17 deletions.
1 change: 1 addition & 0 deletions MANIFEST
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pychatbot/bot.py
pychatbot/endpoints/__init__.py
pychatbot/endpoints/http.py
pychatbot/endpoints/telegram.py
requirements.txt
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ include MANIFEST
include Makefile
include pytest.ini
include tox.ini
include requirements.txt
recursive-include tests *.py
1 change: 1 addition & 0 deletions pychatbot/endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

from .http import HttpEndpoint
from .telegram import TelegramEndpoint
from .twitter import TwitterEndpoint
138 changes: 138 additions & 0 deletions pychatbot/endpoints/twitter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
""" Twitter endpoint for a pychatbot bot, use this to connect your bot to
Twitter.
This module needs `tweepy` library.
"""

from __future__ import absolute_import
from time import sleep
import json

import tweepy


class MyStreamListener(tweepy.StreamListener):
""" This class will listen for `on_data` events on the twitter stream and
then it will dispatch them to the endpoint.the
"""

def set_endpoint(self, endpoint):
""" Sets the endpoint instance to use when an event happens """
self.endpoint = endpoint

def on_data(self, raw_data):
""" Called when data arrives this method dispatch the event
to the right endpoint's method.
"""
data = json.loads(raw_data)

if 'direct_message' in data:
self.endpoint.process_new_direct_message(data['direct_message'])

elif data.get('event', '') == 'follow':
self.endpoint.process_new_follower(data['source'])


class TwitterEndpoint(object):
""" Twitter endpoint for a pychatbot bot.
Example usage:
>>> ep = TwitterEndpoint(
... consumer_key='consumer_key',
... consumer_secret='consumer_secret',
... access_token='access_token',
... access_token_secret='access_token_secret'
... )
>>> bot.add_endpoint(ep)
>>> bot.run()
"""

def __init__(self, consumer_key, consumer_secret,
access_token, access_token_secret,):
self._bot = None
self._last_processed_dm = 0
self._polling_should_run = False
self._polling_is_running = False

self._auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
self._auth.set_access_token(access_token, access_token_secret)

self._api = tweepy.API(self._auth)

self._stream = None

def set_bot(self, bot):
""" Sets the main bot, the bot must be an instance of
`pychatbot.bot.Bot`.
TwitterEndpoint will use the bot to know how to behave.
"""
self._bot = bot

def run(self):
"""Starts the polling for new DMs."""

self.check_new_followers()

self._polling_should_run = True
self.start_polling()

def stop(self):
"""Make the polling for new DMs stop."""

self._polling_should_run = False
self._stream.disconnect()

def start_polling(self):
""" Strats an infinite loop to see if there are new events.
The loop ends when the `self._polling_should_run` will be false
(set `True` by `self.run` and `False` by `self.stop`)
"""

stream_listener = MyStreamListener()
stream_listener.set_endpoint(self)
self._stream = tweepy.Stream(
auth=self._api.auth,
listener=stream_listener
)

self._stream.userstream(async=True)

self._polling_is_running = True

def process_new_direct_message(self, direct_message):
""" Method called for each new DMs arrived.
"""
response = self._bot.process(in_message=direct_message['text'])

self._api.send_direct_message(
text=response,
user_id=direct_message['sender']['id']
)

self._last_processed_dm = direct_message['id']

def process_new_follower(self, user):
""" Follow the user if it isn't already followed.
This method should be called at startup for all the followers and
when a new user follow us.
"""
already_friends = self._api.friends_ids()
if user['id'] not in already_friends:
self._api.create_friendship(user_id=user['id'])

self._api.send_direct_message(
text=self._bot.start(),
user_id=user['id']
)

def check_new_followers(self):
""" For each follower (not friend) process it (follow and start bot).
"""
[
self.process_new_follower({'id': uid})
for uid in self._api.followers_ids()
]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
python-telegram-bot
tweepy
33 changes: 17 additions & 16 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
#!/usr/bin/env python

from setuptools import setup
import os

import pychatbot


def read(filename):
with open(os.path.join(os.path.dirname(__file__), filename)) as file:
return file.read()
import pip

try:
from setuptools import setup
except ImportError:
from distutils.core import setup


def get_requirements(reqfile):
try:
requirements = pip.req.parse_requirements(
reqfile, session=pip.download.PipSession())
except TypeError:
requirements = pip.req.parse_requirements(reqfile)

def get_requirements(req):
if req.startswith('-r'):
for line in read(req.split()[1]).split("\n"):
for subreq in get_requirements(line):
yield subreq
elif req:
yield req
return [str(item.req) for item in requirements if item.req]


setup(
Expand All @@ -30,9 +31,9 @@ def get_requirements(req):
packages=['pychatbot', 'pychatbot.endpoints'],
include_package_data=True,
platforms='any',
keywords=['chatbot', 'telegram'],
tests_require=list(get_requirements('-r requirements-dev.txt')),
install_requires=list(get_requirements('-r requirements.txt')),
keywords=['chat', 'chatbot', 'telegram', 'twitter'],
tests_require=['pytest'],
install_requires=get_requirements('requirements.txt'),
classifiers=[
'Development Status :: 2 - Pre-Alpha',
"Programming Language :: Python :: 2",
Expand Down
17 changes: 17 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import pytest
from time import time, sleep


@pytest.fixture
Expand Down Expand Up @@ -32,3 +33,19 @@ def create(bot, endpoint):
yield create

fixture['bot'].stop()


def wait_for(check_callback, polling_time=0.1, timeout=5):
""" Creates an infinite loop, exit from it when the `check_callback` returns
True.
`polling_time` (seconds) is the time to wait between each try
`timeout` (seconds) is the total time to wait before giving up
"""
start = time()
while time() - start < timeout:
if check_callback():
break
sleep(polling_time)
else:
assert False
assert True

0 comments on commit 89edf51

Please sign in to comment.