Merge branch 'twitterbot' into 'master'

Twitter: poll the bot's mentions feed, print to channel

This uses the Twitter client's method for checking the mentions timeline
in a new thread (which can be started/stopped via IRC) to print mentions
to a specified IRC channel.

See merge request !5
This commit is contained in:
Brian S. Stephan 2016-01-26 23:22:13 -06:00
commit 21941b3392
5 changed files with 157 additions and 8 deletions

View File

@ -1,9 +1,9 @@
"""Access to Twitter through bot commands.""" """Access to Twitter through bot commands."""
import logging import logging
import threading
import time import time
import requests
import twython import twython
from django.conf import settings from django.conf import settings
@ -29,6 +29,8 @@ class Twitter(Plugin):
self.temp_token = None self.temp_token = None
self.temp_token_secret = None self.temp_token_secret = None
self.poll_mentions = False
super(Twitter, self).__init__(bot, connection, event) super(Twitter, self).__init__(bot, connection, event)
def start(self): def start(self):
@ -48,6 +50,10 @@ class Twitter(Plugin):
self.handle_auth, -20) self.handle_auth, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+replyto\s+(\S+)\s+(.*)', self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+replyto\s+(\S+)\s+(.*)',
self.handle_replyto, -20) 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 getting the stored auth tokens and logging in
try: 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_gettoken)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_auth) 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_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() 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): def handle_getstatus(self, connection, event, match):
"""Get a status by tweet ID.""" """Get a status by tweet ID."""
@ -94,7 +121,8 @@ class Twitter(Plugin):
status = match.group(3) status = match.group(3)
try: try:
tweet = self.twit.show_status(id=status) 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: except Exception as e:
log.error("couldn't obtain status") log.error("couldn't obtain status")
log.exception(e) log.exception(e)
@ -130,8 +158,8 @@ class Twitter(Plugin):
tweets = self.twit.get_user_timeline(screen_name=user, count=count, include_rts=True) tweets = self.twit.get_user_timeline(screen_name=user, count=count, include_rts=True)
if tweets: if tweets:
tweet = tweets[-1*index] tweet = tweets[-1*index]
return self._return_tweet_or_retweet_text(event, tweet=tweet, print_source=print_source, return self._reply_with_tweet_or_retweet_text(event, tweet=tweet, print_source=print_source,
print_id=print_id) print_id=print_id)
except Exception as e: except Exception as e:
log.error("couldn't obtain status") log.error("couldn't obtain status")
log.exception(e) log.exception(e)
@ -232,9 +260,60 @@ class Twitter(Plugin):
log.error("twitter settings object does not exist") log.error("twitter settings object does not exist")
return self.bot.reply(event, "twitter module not configured") 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 """Return a string of the author and text body of a status, accounting for whether
or not the fetched status is a retweet. 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) retweet = getattr(tweet, 'retweeted_status', None)
@ -256,7 +335,7 @@ class Twitter(Plugin):
if print_id: if print_id:
reply = reply + " [{0:d}]".format(tweet['id']) reply = reply + " [{0:d}]".format(tweet['id'])
return self.bot.reply(event, reply) return reply
plugin = Twitter plugin = Twitter

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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'))},
),
]

View File

@ -4,6 +4,8 @@ import logging
from django.db import models from django.db import models
from ircbot.models import IrcChannel
log = logging.getLogger('twitter.models') log = logging.getLogger('twitter.models')
@ -12,12 +14,15 @@ class TwitterClient(models.Model):
"""Track twitter settings and similar.""" """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 = models.CharField(max_length=256, default='', blank=True)
oauth_token_secret = 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: class Meta:
permissions = ( permissions = (
('send_tweets', "Can send tweets via IRC"), ('send_tweets', "Can send tweets via IRC"),
('manage_threads', "Can start/stop polling threads via IRC"),
) )