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) +