diff --git a/dr_botzo/twitter/ircplugin.py b/dr_botzo/twitter/ircplugin.py index b58e701..6e97c94 100644 --- a/dr_botzo/twitter/ircplugin.py +++ b/dr_botzo/twitter/ircplugin.py @@ -1,9 +1,9 @@ """Access to Twitter through bot commands.""" import logging +import threading import time -import requests import twython from django.conf import settings @@ -29,6 +29,8 @@ class Twitter(Plugin): self.temp_token = None self.temp_token_secret = None + self.poll_mentions = False + super(Twitter, self).__init__(bot, connection, event) def start(self): @@ -48,6 +50,10 @@ class Twitter(Plugin): self.handle_auth, -20) self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+replyto\s+(\S+)\s+(.*)', self.handle_replyto, -20) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+mentionpoll\s+start', + self.handle_start_mentionpoll, -20) + self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+mentionpoll\s+stop', + self.handle_stop_mentionpoll, -20) # try getting the stored auth tokens and logging in try: @@ -79,9 +85,30 @@ class Twitter(Plugin): self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_gettoken) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_auth) self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_replyto) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_start_mentionpoll) + self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_stop_mentionpoll) + + self.poll_mentions = False super(Twitter, self).stop() + def handle_start_mentionpoll(self, connection, event, match): + if not has_permission(event.source, 'twitter.manage_threads'): + return self.bot.reply(event, "You do not have permission to start/stop threads.") + + self.poll_mentions = True + t = threading.Thread(target=self.thread_watch_mentions) + t.daemon = True + t.start() + self.bot.reply(event, "Now polling for mentions.") + + def handle_stop_mentionpoll(self, connection, event, match): + if not has_permission(event.source, 'twitter.manage_threads'): + return self.bot.reply(event, "You do not have permission to start/stop threads.") + + self.poll_mentions = False + self.bot.reply(event, "No longer polling for mentions.") + def handle_getstatus(self, connection, event, match): """Get a status by tweet ID.""" @@ -94,7 +121,8 @@ class Twitter(Plugin): status = match.group(3) try: tweet = self.twit.show_status(id=status) - return self._return_tweet_or_retweet_text(event, tweet=tweet, print_source=print_source, print_id=print_id) + return self._reply_with_tweet_or_retweet_text(event, tweet=tweet, print_source=print_source, + print_id=print_id) except Exception as e: log.error("couldn't obtain status") log.exception(e) @@ -130,8 +158,8 @@ class Twitter(Plugin): tweets = self.twit.get_user_timeline(screen_name=user, count=count, include_rts=True) if tweets: tweet = tweets[-1*index] - return self._return_tweet_or_retweet_text(event, tweet=tweet, print_source=print_source, - print_id=print_id) + return self._reply_with_tweet_or_retweet_text(event, tweet=tweet, print_source=print_source, + print_id=print_id) except Exception as e: log.error("couldn't obtain status") log.exception(e) @@ -232,9 +260,60 @@ class Twitter(Plugin): log.error("twitter settings object does not exist") return self.bot.reply(event, "twitter module not configured") - def _return_tweet_or_retweet_text(self, event, tweet, print_source=False, print_id=True): + def thread_watch_mentions(self, sleep_time=60): + """Poll mentions from Twitter every sleep_time seconds. + + :param sleep_time: second to sleep between checks + :type sleep_time: int + """ + + while self.poll_mentions: + twittersettings = TwitterClient.objects.get(pk=1) + out_chan = twittersettings.mentions_output_channel.name + since_id = twittersettings.mentions_since_id + + mentions = self.twit.get_mentions_timeline(since_id=since_id) + for mention in mentions: + reply = self._return_tweet_or_retweet_text(tweet=mention, print_source=True) + self.bot.privmsg(out_chan, reply) + since_id = mention['id'] if mention['id'] > since_id else since_id + + twittersettings.mentions_since_id = since_id + twittersettings.save() + + time.sleep(sleep_time) + + def _reply_with_tweet_or_retweet_text(self, event, tweet, print_source=False, print_id=True): + """Do a bot.reply() with the appropriate text representation of the given tweet. + + See _return_tweet_or_retweet_text for details. + + :param event: the irc event to use for the reply + :type event: Event + :param tweet: the tweet (from twython) to inspect and return a string for + :type tweet: dict + :param print_source: whether or not to print the tweet's author (default False) + :type print_source: bool + :param print_id: whether or not to print the tweet's ID (default True) + :type print_id: bool + :returns: tweet text suitable for printing + :rtype: str + """ + + return self.bot.reply(event, self._return_tweet_or_retweet_text(tweet, print_source, print_id)) + + def _return_tweet_or_retweet_text(self, tweet, print_source=False, print_id=True): """Return a string of the author and text body of a status, accounting for whether or not the fetched status is a retweet. + + :param tweet: the tweet (from twython) to inspect and return a string for + :type tweet: dict + :param print_source: whether or not to print the tweet's author (default False) + :type print_source: bool + :param print_id: whether or not to print the tweet's ID (default True) + :type print_id: bool + :returns: tweet text suitable for printing + :rtype: str """ retweet = getattr(tweet, 'retweeted_status', None) @@ -256,7 +335,7 @@ class Twitter(Plugin): if print_id: reply = reply + " [{0:d}]".format(tweet['id']) - return self.bot.reply(event, reply) + return reply plugin = Twitter diff --git a/dr_botzo/twitter/migrations/0004_add_mentions_output_channel_to_config.py b/dr_botzo/twitter/migrations/0004_add_mentions_output_channel_to_config.py new file mode 100644 index 0000000..f04a04d --- /dev/null +++ b/dr_botzo/twitter/migrations/0004_add_mentions_output_channel_to_config.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ircbot', '0014_auto_20160116_1955'), + ('twitter', '0003_auto_20150620_0951'), + ] + + operations = [ + migrations.RemoveField( + model_name='twitterclient', + name='output_channel', + ), + migrations.AddField( + model_name='twitterclient', + name='mentions_output_channel', + field=models.ForeignKey(blank=True, related_name='mentions_twitter_client', null=True, to='ircbot.IrcChannel', default=None), + ), + ] diff --git a/dr_botzo/twitter/migrations/0005_replace_since_id_with_replies_specific_one.py b/dr_botzo/twitter/migrations/0005_replace_since_id_with_replies_specific_one.py new file mode 100644 index 0000000..37b7d88 --- /dev/null +++ b/dr_botzo/twitter/migrations/0005_replace_since_id_with_replies_specific_one.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('twitter', '0004_add_mentions_output_channel_to_config'), + ] + + operations = [ + migrations.RemoveField( + model_name='twitterclient', + name='since_id', + ), + migrations.AddField( + model_name='twitterclient', + name='mentions_since_id', + field=models.PositiveIntegerField(default=1, blank=True), + ), + ] diff --git a/dr_botzo/twitter/migrations/0006_add_manage_threads_permission.py b/dr_botzo/twitter/migrations/0006_add_manage_threads_permission.py new file mode 100644 index 0000000..d154df5 --- /dev/null +++ b/dr_botzo/twitter/migrations/0006_add_manage_threads_permission.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('twitter', '0005_replace_since_id_with_replies_specific_one'), + ] + + operations = [ + migrations.AlterModelOptions( + name='twitterclient', + options={'permissions': (('send_tweets', 'Can send tweets via IRC'), ('manage_threads', 'Can start/stop polling threads via IRC'))}, + ), + ] diff --git a/dr_botzo/twitter/models.py b/dr_botzo/twitter/models.py index 50425e9..0020c4b 100644 --- a/dr_botzo/twitter/models.py +++ b/dr_botzo/twitter/models.py @@ -4,6 +4,8 @@ import logging from django.db import models +from ircbot.models import IrcChannel + log = logging.getLogger('twitter.models') @@ -12,12 +14,15 @@ class TwitterClient(models.Model): """Track twitter settings and similar.""" - since_id = models.PositiveIntegerField() - output_channel = models.CharField(max_length=200, default='', blank=True) oauth_token = models.CharField(max_length=256, default='', blank=True) oauth_token_secret = models.CharField(max_length=256, default='', blank=True) + mentions_output_channel = models.ForeignKey(IrcChannel, related_name='mentions_twitter_client', default=None, + null=True, blank=True) + mentions_since_id = models.PositiveIntegerField(default=1, blank=True) + class Meta: permissions = ( ('send_tweets', "Can send tweets via IRC"), + ('manage_threads', "Can start/stop polling threads via IRC"), )