diff --git a/chatterbot/adapters/io/__init__.py b/chatterbot/adapters/io/__init__.py
index 9676fa59b..28efc9409 100644
--- a/chatterbot/adapters/io/__init__.py
+++ b/chatterbot/adapters/io/__init__.py
@@ -1,5 +1,6 @@
from .io import IOAdapter
from .terminal import TerminalAdapter
from .io_json import JsonAdapter
+from .twitter_io import TwitterAdapter
from .no_output import NoOutputAdapter
diff --git a/chatterbot/adapters/io/twitter_io.py b/chatterbot/adapters/io/twitter_io.py
new file mode 100644
index 000000000..25fe350f5
--- /dev/null
+++ b/chatterbot/adapters/io/twitter_io.py
@@ -0,0 +1,205 @@
+# -*- encoding: utf-8 -*-
+from chatterbot.adapters.io import IOAdapter
+from chatterbot.conversation import Statement
+import twitter
+import threading
+import time
+
+try:
+ from queue import Queue
+except ImportError:
+ # Use the python 2 queue import
+ from Queue import Queue
+
+
+class SimulatedAnnealingScheduler(object):
+ """
+ This class implements a simulated annealing algorithm to determine
+ the correct schedule for running a function.
+ The benefit of this class is that it is more efficient than interval
+ checking when a given function may yield a greater probability of
+ returning similar consecutive results.
+ """
+
+ def __init__(self, function, comparison_function, interval=5):
+ """
+ Takes a function to be run periodically and a comparison function to
+ determine if the result of the function is true or false.
+ """
+ self.function = function
+ self.check = comparison_function
+
+ self.interval = interval
+
+ # INTERVAL_MIN = 1 second
+ self.INTERVAL_MIN = 1
+
+ # INTERVAL_MAX = number of seconds in 1 day
+ self.INTERVAL_MAX = 60 * 60 * 24
+
+ self.INCREMENT_AMOUNT = 2
+ self.DECREMENT_AMOUNT = 2
+
+ self.thread = threading.Thread(target=self.start, args=())
+ self.thread.daemon = True
+ self.thread.start()
+
+ def get_temperature(self, scaling_factor=10):
+ pass
+
+ def decrease_interval(self):
+ """
+ Decrement the interval as long as doing so will not cause it to
+ decrease past the predefined minimum.
+ """
+ if (self.interval - self.DECREMENT_AMOUNT) >= self.INTERVAL_MIN:
+ self.interval -= self.DECREMENT_AMOUNT
+
+ def increase_interval(self):
+ """
+ Increment the interval as long as doing so will not cause it to
+ increase past the predefined maximum.
+ """
+ if (self.interval + self.INCREMENT_AMOUNT) <= self.INTERVAL_MAX:
+ self.interval += self.INCREMENT_AMOUNT
+
+ def start(self):
+ while True:
+ result = self.function()
+
+ if self.check(result):
+ self.increase_interval()
+ else:
+ self.decrease_interval()
+
+ time.sleep(self.interval)
+
+ def stop(self):
+ self.thread.stop()
+
+
+class TwitterAdapter(IOAdapter):
+
+ def __init__(self, **kwargs):
+ super(TwitterAdapter, self).__init__(**kwargs)
+
+ self.api = twitter.Api(
+ consumer_key=kwargs["twitter_consumer_key"],
+ consumer_secret=kwargs["twitter_consumer_secret"],
+ access_token_key=kwargs["twitter_access_token_key"],
+ access_token_secret=kwargs["twitter_access_token_secret"]
+ )
+
+ self.mention_queue = Queue()
+ self.direct_message_queue = Queue()
+
+ self.message_checker = SimulatedAnnealingScheduler(
+ self.get_messages,
+ self.is_new_message
+ )
+
+ def post_update(self, message):
+ return self.api.PostUpdate(message)
+
+ def favorite(self, tweet_id):
+ return self.api.CreateFavorite(id=tweet_id)
+
+ def follow(self, username):
+ return self.api.CreateFriendship(screen_name=username)
+
+ def get_list_users(self, username, slug):
+ return self.api.GetListMembers(None, slug, owner_screen_name=username)
+
+ def get_mentions(self):
+ mentions = self.api.GetMentions()
+
+ print "get_mentions:", mentions
+ return mentions
+
+ def is_new_message(self, data):
+ print "message data:"
+ for d in data:
+ print "\t", d.text
+ print "\t", d
+
+ return True
+
+ def get_messages(self):
+ return self.api.GetDirectMessages(count=5)
+
+ def search(self, q, count=1, result_type="mixed"):
+ return self.api.GetSearch(term=q, count=count, result_type=result_type)
+
+ def get_related_messages(self, text):
+ results = search(text, count=50)
+ replies = []
+ non_replies = []
+
+ for result in results["statuses"]:
+
+ # Select only results that are replies
+ if result["in_reply_to_status_id_str"] is not None:
+ message = result["text"]
+ replies.append(message)
+
+ # Save a list of other results in case a reply cannot be found
+ else:
+ message = result["text"]
+ non_replies.append(message)
+
+ if len(replies) > 0:
+ return replies
+
+ return non_replies
+
+ def reply(self, tweet_id, message):
+ """
+ Reply to a tweet
+ """
+ return self.api.PostUpdate(message, in_reply_to_status_id=tweet_id)
+
+ def tweet_to_friends(self, username, slug, greetings, debug=False):
+ """
+ Tweet one random message to the next friend in a list every hour.
+ The tweet will not be sent and will be printed to the console when in
+ debug mode.
+ """
+ from time import time, sleep
+ from random import choice
+
+ # Get the list of robots
+ robots = self.get_list_users(username, slug=slug)
+
+ for robot in robots:
+ message = ("@" + robot + " " + choice(greetings)).strip("\n")
+
+ if debug is True:
+ print(message)
+ else:
+ sleep(3600-time() % 3600)
+ t.statuses.update(status=message)
+
+ def has_responeded_to_message(self, message_id):
+ # TODO
+ pass
+
+ def process_input(self):
+ """
+ This method should check twitter for new mentions and
+ return them as Statement objects.
+ """
+ # Download a list of recent mentions
+ mentions = self.get_mentions()
+
+ print "MENTIONS:", mentions
+
+ for mention in mentions:
+ mention = Statement(mention.text)
+
+ # Add the mention to the mention queue if a response has not been made
+ if not self.has_responeded_to_message(mention):
+ self.mention_queue.put(mention)
+
+ def process_response(self, input_statement):
+ return input_statement
+
diff --git a/examples/terminal_example.py b/examples/terminal_example.py
index 73c323abf..2a8fdb15d 100644
--- a/examples/terminal_example.py
+++ b/examples/terminal_example.py
@@ -37,3 +37,4 @@
except (KeyboardInterrupt, EOFError, SystemExit):
break
+
diff --git a/examples/twitter_example.py b/examples/twitter_example.py
index cf6d0f7c4..81844fe20 100644
--- a/examples/twitter_example.py
+++ b/examples/twitter_example.py
@@ -1,31 +1,41 @@
+from chatterbot import ChatBot
+from settings import TWITTER
+import time
+
'''
-Respond to mentions on twitter.
-The bot will follow the user who mentioned it and
-favorite the post in which the mention was made.
+The bot will respond to mentions and direct messages on twitter.
+To use this example, create a new settings.py file.
+Define the following in settings.py:
+
+ TWITTER = {}
+ TWITTER["CONSUMER_KEY"] = "your-twitter-public-key"
+ TWITTER["CONSUMER_SECRET"] = "your-twitter-sceret-key"
'''
+
chatbot = ChatBot("ChatterBot",
storage_adapter="chatterbot.adapters.storage.JsonDatabaseAdapter",
logic_adapter="chatterbot.adapters.logic.ClosestMatchAdapter",
io_adapter="chatterbot.adapters.io.TwitterAdapter",
- database="../database.db")
+ database="../database.db",
+ twitter_consumer_key=TWITTER["CONSUMER_KEY"],
+ twitter_consumer_secret=TWITTER["CONSUMER_SECRET"],
+ twitter_access_token_key=TWITTER["ACCESS_TOKEN"],
+ twitter_access_token_secret=TWITTER["ACCESS_TOKEN_SECRET"]
+)
-for mention in chatbot.get_mentions():
+time.sleep(200)
- '''
- Check to see if the post has been favorited
- We will use this as a check for whether or not to respond to it.
- Only respond to unfavorited mentions.
- '''
+'''
+while True:
+ try:
+ user_input = chatbot.get_input()
- if not mention["favorited"]:
- screen_name = mention["user"]["screen_name"]
- text = mention["text"]
- response = chatbot.get_response(text)
+ bot_input = chatbot.get_response(user_input)
- print(text)
- print(response)
+ # Pause before checking for the next message
+ time.sleep(25)
- chatbot.follow(screen_name)
- chatbot.favorite(mention["id"])
- chatbot.reply(mention["id"], response)
+ except (KeyboardInterrupt, EOFError, SystemExit):
+ break
+'''
diff --git a/requirements.txt b/requirements.txt
index 01168a522..cb375184d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,4 @@ fuzzywuzzy>=0.8.0
jsondatabase>=0.0.7
nltk<4.0.0
pymongo>=3.0.3,<4.0.0
+python-twitter>=2.2.0
diff --git a/tests/io_adapter_tests/test_twitter_adapter.py b/tests/io_adapter_tests/test_twitter_adapter.py
new file mode 100644
index 000000000..4289cbc60
--- /dev/null
+++ b/tests/io_adapter_tests/test_twitter_adapter.py
@@ -0,0 +1,22 @@
+from unittest import TestCase
+from chatterbot.adapters.io import TwitterAdapter
+
+
+class TwitterAdapterTests(TestCase):
+ pass
+
+ '''
+ def setUp(self):
+ self.adapter = TwitterAdapter(
+ twitter_consumer_key="blahblahblah",
+ twitter_consumer_secret="nullvoidnullvoidnullvoid",
+ twitter_access_token_key="blahblahblah",
+ twitter_access_token_secret="nullvoidnullvoidnullvoid"
+ )
+
+ def test_get_mentions(self):
+ from.twitter_data.mentions import MENTIONS
+
+ mentions = self.adapter.get_mentions()
+ '''
+
diff --git a/tests/io_adapter_tests/twitter_data/__init__.py b/tests/io_adapter_tests/twitter_data/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/io_adapter_tests/twitter_data/mentions.py b/tests/io_adapter_tests/twitter_data/mentions.py
new file mode 100644
index 000000000..f13fb066d
--- /dev/null
+++ b/tests/io_adapter_tests/twitter_data/mentions.py
@@ -0,0 +1,248 @@
+# Sample data consisting of two mentions
+MENTIONS = [
+ {
+ u'contributors':None,
+ u'truncated':False,
+ u'text':u'The CS&IT lab will be open to alumni during Homecoming, Saturday 10am-Noon. See what students are doing, like @SalviusRobot',
+ u'is_quote_status':False,
+ u'in_reply_to_status_id':None,
+ u'id':649703760697143296,
+ u'favorite_count':1,
+ u'source':u'Twitter Web Client',
+ u'retweeted':True,
+ u'coordinates':None,
+ u'entities':{
+ u'symbols':[],
+ u'user_mentions':[
+ {
+ u'id':712978718,
+ u'indices':[
+ 114,
+ 127
+ ],
+ u'id_str':u'712978718',
+ u'screen_name':u'SalviusRobot',
+ u'name':u'Salvius the Robot'
+ }
+ ],
+ u'hashtags':[],
+ u'urls':[]
+ },
+ u'in_reply_to_screen_name':None,
+ u'id_str':u'649703760697143296',
+ u'retweet_count':1,
+ u'in_reply_to_user_id':None,
+ u'favorited':True,
+ u'user':{
+ u'follow_request_sent':False,
+ u'has_extended_profile':False,
+ u'profile_use_background_image':True,
+ u'default_profile_image':False,
+ u'id':2885705333,
+ u'profile_background_image_url_https':u'https://abs.twimg.com/images/themes/theme14/bg.gif',
+ u'verified':False,
+ u'profile_text_color':u'333333',
+ u'profile_image_url_https':u'https://pbs.twimg.com/profile_images/535493614294409216/MzEZDbOR_normal.jpeg',
+ u'profile_sidebar_fill_color':u'EFEFEF',
+ u'entities':{
+ u'url':{
+ u'urls':[
+ {
+ u'url':u'http://t.co/NJTahpXBlu',
+ u'indices':[
+ 0,
+ 22
+ ],
+ u'expanded_url':u'http://www1.wne.edu/artsandsciences/index.cfm?selection=doc.4836',
+ u'display_url':u'www1.wne.edu/artsandscience\u2026'
+ }
+ ]
+ },
+ u'description':{
+ u'urls':[]
+ }
+ },
+ u'followers_count':47,
+ u'profile_sidebar_border_color':u'EEEEEE',
+ u'id_str':u'2885705333',
+ u'profile_background_color':u'131516',
+ u'listed_count':0,
+ u'is_translation_enabled':False,
+ u'utc_offset':None,
+ u'statuses_count':56,
+ u'description':u'The Department of Computer Science and Information Technology at @WNEUniversity',
+ u'friends_count':38,
+ u'location':u'Springfield, MA',
+ u'profile_link_color':u'009999',
+ u'profile_image_url':u'http://pbs.twimg.com/profile_images/535493614294409216/MzEZDbOR_normal.jpeg',
+ u'following':True,
+ u'geo_enabled':False,
+ u'profile_banner_url':u'https://pbs.twimg.com/profile_banners/2885705333/1416506808',
+ u'profile_background_image_url':u'http://abs.twimg.com/images/themes/theme14/bg.gif',
+ u'screen_name':u'WNECSIT',
+ u'lang':u'en',
+ u'profile_background_tile':True,
+ u'favourites_count':1,
+ u'name':u'WNE CS and IT Dept.',
+ u'notifications':False,
+ u'url':u'http://t.co/NJTahpXBlu',
+ u'created_at':u'Thu Nov 20 15:09:36+0000 2014',
+ u'contributors_enabled':False,
+ u'time_zone':None,
+ u'protected':False,
+ u'default_profile':False,
+ u'is_translator':False
+ },
+ u'geo':None,
+ u'in_reply_to_user_id_str':None,
+ u'lang':u'en',
+ u'created_at':u'Thu Oct 01 21:53:42+0000 2015',
+ u'in_reply_to_status_id_str':None,
+ u'place':None
+ },
+
+ {
+ u'contributors':None,
+ u'truncated':False,
+ u'text':u"Lookin' good, @salviusrobot ! | Salvius: A Humanoid Robot Born from a Junkyard http://t.co/NYOVJVlzhW #Recycle #robots #robotech #makers",
+ u'is_quote_status':False,
+ u'in_reply_to_status_id':None,
+ u'id':620985108204384257,
+ u'favorite_count':1,
+ u'source':u'TweetDeck',
+ u'retweeted':False,
+ u'coordinates':None,
+ u'entities':{
+ u'symbols':[
+
+ ],
+ u'user_mentions':[
+ {
+ u'id':712978718,
+ u'indices':[
+ 14,
+ 27
+ ],
+ u'id_str':u'712978718',
+ u'screen_name':u'SalviusRobot',
+ u'name':u'Salvius the Robot'
+ }
+ ],
+ u'hashtags':[
+ {
+ u'indices':[
+ 102,
+ 110
+ ],
+ u'text':u'Recycle'
+ },
+ {
+ u'indices':[
+ 111,
+ 118
+ ],
+ u'text':u'robots'
+ },
+ {
+ u'indices':[
+ 119,
+ 128
+ ],
+ u'text':u'robotech'
+ },
+ {
+ u'indices':[
+ 129,
+ 136
+ ],
+ u'text':u'makers'
+ }
+ ],
+ u'urls':[
+ {
+ u'url':u'http://t.co/NYOVJVlzhW',
+ u'indices':[
+ 79,
+ 101
+ ],
+ u'expanded_url':u'http://makezine.com/2015/07/13/salvius-humanoid-robot-born-junkyard/',
+ u'display_url':u'makezine.com/2015/07/13/sal\u2026'
+ }
+ ]
+ },
+ u'in_reply_to_screen_name':None,
+ u'id_str':u'620985108204384257',
+ u'retweet_count':0,
+ u'in_reply_to_user_id':None,
+ u'favorited':False,
+ u'user':{
+ u'follow_request_sent':False,
+ u'has_extended_profile':False,
+ u'profile_use_background_image':True,
+ u'default_profile_image':False,
+ u'id':1592524638,
+ u'profile_background_image_url_https':u'https://abs.twimg.com/images/themes/theme1/bg.png',
+ u'verified':False,
+ u'profile_text_color':u'333333',
+ u'profile_image_url_https':u'https://pbs.twimg.com/profile_images/606888255175270401/6lmq5ZP5_normal.png',
+ u'profile_sidebar_fill_color':u'DDEEF6',
+ u'entities':{
+ u'url':{
+ u'urls':[
+ {
+ u'url':u'http://t.co/fHiRO93C8e',
+ u'indices':[
+ 0,
+ 22
+ ],
+ u'expanded_url':u'http://SDMakerFaire.org',
+ u'display_url':u'SDMakerFaire.org'
+ }
+ ]
+ },
+ u'description':{
+ u'urls':[
+
+ ]
+ }
+ },
+ u'followers_count':601,
+ u'profile_sidebar_border_color':u'C0DEED',
+ u'id_str':u'1592524638',
+ u'profile_background_color':u'C0DEED',
+ u'listed_count':69,
+ u'is_translation_enabled':False,
+ u'utc_offset':-28800,
+ u'statuses_count':1305,
+ u'description':u'A hands-on feast of invention, creativity, & a celebration of the Maker movement. #MakerFaire comes to #BalboaPark Oct 3+4! Tell us:what will you make?',
+ u'friends_count':307,
+ u'location':u'San Diego, CA',
+ u'profile_link_color':u'0084B4',
+ u'profile_image_url':u'http://pbs.twimg.com/profile_images/606888255175270401/6lmq5ZP5_normal.png',
+ u'following':False,
+ u'geo_enabled':True,
+ u'profile_banner_url':u'https://pbs.twimg.com/profile_banners/1592524638/1436210641',
+ u'profile_background_image_url':u'http://abs.twimg.com/images/themes/theme1/bg.png',
+ u'screen_name':u'SDMakerFaire',
+ u'lang':u'en',
+ u'profile_background_tile':False,
+ u'favourites_count':1107,
+ u'name':u'SDMakerFaire',
+ u'notifications':False,
+ u'url':u'http://t.co/fHiRO93C8e',
+ u'created_at':u'Sun Jul 14 04:45:55+0000 2013',
+ u'contributors_enabled':False,
+ u'time_zone':u'Pacific Time (US & Canada)',
+ u'protected':False,
+ u'default_profile':True,
+ u'is_translator':False
+ },
+ u'geo':None,
+ u'in_reply_to_user_id_str':None,
+ u'possibly_sensitive':False,
+ u'lang':u'en',
+ u'created_at':u'Tue Jul 14 15:56:01+0000 2015',
+ u'in_reply_to_status_id_str':None,
+ u'place':None
+ }
+]
diff --git a/tests/test_simulated_annealing_scheduler.py b/tests/test_simulated_annealing_scheduler.py
new file mode 100644
index 000000000..9a307f738
--- /dev/null
+++ b/tests/test_simulated_annealing_scheduler.py
@@ -0,0 +1,54 @@
+from chatterbot.adapters.io.twitter_io import SimulatedAnnealingScheduler
+from unittest import TestCase
+
+
+class DummyEndpoint(object):
+ def __init__(self):
+ self.index = 0
+ self.data = [
+ True,
+ False,
+ False,
+ True,
+ ]
+
+ def get(self):
+ result = self.data[self.index]
+ index += 1
+
+ if self.index == len(self.data):
+ self.index = 0
+
+ return result
+
+
+class SimulatedAnnealingSchedulerTests(TestCase):
+
+ def setUp(self):
+ self.endpoint = DummyEndpoint()
+
+ self.scheduler = SimulatedAnnealingScheduler(
+ self.function,
+ self.check
+ )
+
+ def function(self):
+ return self.endpoint.get()
+
+ def check(self, data):
+ return data
+
+ def test_decrease_interval(self):
+ interval_before = self.scheduler.interval
+ self.scheduler.decrease_interval()
+ interval_after = self.scheduler.interval
+
+ self.assertTrue(interval_before > interval_after)
+
+ def test_increase_interval(self):
+ interval_before = self.scheduler.interval
+ self.scheduler.increase_interval()
+ interval_after = self.scheduler.interval
+
+ self.assertTrue(interval_before < interval_after)
+