From 352ce81bc9001ebec056e168630132e9c384db0e Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 24 Jan 2016 17:50:46 -0600 Subject: [PATCH 1/5] twitter: add field for mentions output channel this removes an old, general field, which was once used for multiple things, but we should break apart its functional usage and also just refer to our IrcChannel object anyway this is the easy half of issue #3 --- ...4_add_mentions_output_channel_to_config.py | 24 +++++++++++++++++++ dr_botzo/twitter/models.py | 6 ++++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 dr_botzo/twitter/migrations/0004_add_mentions_output_channel_to_config.py 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/models.py b/dr_botzo/twitter/models.py index 50425e9..da4037a 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') @@ -13,10 +15,12 @@ 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) + class Meta: permissions = ( ('send_tweets', "Can send tweets via IRC"), From e8e42cc580a20509a276c714eb095f4acee88ff7 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Sun, 24 Jan 2016 18:46:06 -0600 Subject: [PATCH 2/5] twitter: replace since_id with mentions_since_id we'll want to treat the since_ids for mentions and (theoretical) timelines differently, i think, so might as well just set up the split out field now --- ...lace_since_id_with_replies_specific_one.py | 23 +++++++++++++++++++ dr_botzo/twitter/models.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 dr_botzo/twitter/migrations/0005_replace_since_id_with_replies_specific_one.py 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/models.py b/dr_botzo/twitter/models.py index da4037a..ea73f98 100644 --- a/dr_botzo/twitter/models.py +++ b/dr_botzo/twitter/models.py @@ -14,12 +14,12 @@ class TwitterClient(models.Model): """Track twitter settings and similar.""" - since_id = models.PositiveIntegerField() 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 = ( From 6cbf5f3d96f841f5f0ff605ab29b760313a4fd13 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Tue, 26 Jan 2016 00:08:46 -0600 Subject: [PATCH 3/5] twitter: pull the tweet reply method into two _return_tweet_or_retweet_text used to both determine the proper tweet text and bot.reply() with it to the provided event. if we're not reacting to an irc event, this obviously won't work, so this pulls the method into two things so that we can use the string formatting code without necessary needing an event --- dr_botzo/twitter/ircplugin.py | 40 +++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/dr_botzo/twitter/ircplugin.py b/dr_botzo/twitter/ircplugin.py index b58e701..2dd208a 100644 --- a/dr_botzo/twitter/ircplugin.py +++ b/dr_botzo/twitter/ircplugin.py @@ -3,7 +3,6 @@ import logging import time -import requests import twython from django.conf import settings @@ -94,7 +93,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 +130,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 +232,37 @@ 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 _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 +284,7 @@ class Twitter(Plugin): if print_id: reply = reply + " [{0:d}]".format(tweet['id']) - return self.bot.reply(event, reply) + return reply plugin = Twitter From 3b78e9b894e695106f487105dabdf7c9a8850f99 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Tue, 26 Jan 2016 00:39:51 -0600 Subject: [PATCH 4/5] twitter: method for polling mentions timeline not stitched together in this commit, so it'll work but it won't be started --- dr_botzo/twitter/ircplugin.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/dr_botzo/twitter/ircplugin.py b/dr_botzo/twitter/ircplugin.py index 2dd208a..1df3dfd 100644 --- a/dr_botzo/twitter/ircplugin.py +++ b/dr_botzo/twitter/ircplugin.py @@ -28,6 +28,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): @@ -79,6 +81,8 @@ class Twitter(Plugin): 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.poll_mentions = False + super(Twitter, self).stop() def handle_getstatus(self, connection, event, match): @@ -232,6 +236,29 @@ class Twitter(Plugin): log.error("twitter settings object does not exist") return self.bot.reply(event, "twitter module not configured") + 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. From 9d5b9e070be1ecaca9abcea86d43119c44f8a372 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Tue, 26 Jan 2016 23:14:42 -0600 Subject: [PATCH 5/5] twitter: start/stop mention poll thread via irc includes a migration for a new permission for this, naturally. with this, the poll thread can be started and actually do stuff --- dr_botzo/twitter/ircplugin.py | 24 +++++++++++++++++++ .../0006_add_manage_threads_permission.py | 18 ++++++++++++++ dr_botzo/twitter/models.py | 1 + 3 files changed, 43 insertions(+) create mode 100644 dr_botzo/twitter/migrations/0006_add_manage_threads_permission.py diff --git a/dr_botzo/twitter/ircplugin.py b/dr_botzo/twitter/ircplugin.py index 1df3dfd..6e97c94 100644 --- a/dr_botzo/twitter/ircplugin.py +++ b/dr_botzo/twitter/ircplugin.py @@ -1,6 +1,7 @@ """Access to Twitter through bot commands.""" import logging +import threading import time import twython @@ -49,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: @@ -80,11 +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.""" 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 ea73f98..0020c4b 100644 --- a/dr_botzo/twitter/models.py +++ b/dr_botzo/twitter/models.py @@ -24,4 +24,5 @@ class TwitterClient(models.Model): class Meta: permissions = ( ('send_tweets', "Can send tweets via IRC"), + ('manage_threads', "Can start/stop polling threads via IRC"), )