Tweety: keep history on reload/restart

This commit is contained in:
oddluck 2020-03-20 11:21:59 +00:00
parent 84997877ac
commit 07c2ed854d
2 changed files with 31 additions and 53 deletions

View File

@ -29,10 +29,10 @@
### ###
# my libs # my libs
import urllib.request, urllib.error, urllib.parse import urllib.request, urllib.error
import json import json
import requests import requests
import urllib import os
# libraries for time_created_at # libraries for time_created_at
import time import time
from datetime import datetime from datetime import datetime
@ -47,9 +47,9 @@ from supybot.commands import *
import supybot.plugins as plugins import supybot.plugins as plugins
import supybot.ircutils as ircutils import supybot.ircutils as ircutils
import supybot.callbacks as callbacks import supybot.callbacks as callbacks
import supybot.conf as conf
import supybot.world as world
import supybot.log as log import supybot.log as log
from bs4 import BeautifulSoup
import os
class OAuthApi: class OAuthApi:
"""OAuth class to work with Twitter v1.1 API.""" """OAuth class to work with Twitter v1.1 API."""
@ -62,11 +62,9 @@ class OAuthApi:
def _FetchUrl(self,url, parameters=None): def _FetchUrl(self,url, parameters=None):
"""Fetch a URL with oAuth. Returns a string containing the body of the response.""" """Fetch a URL with oAuth. Returns a string containing the body of the response."""
extra_params = {} extra_params = {}
if parameters: if parameters:
extra_params.update(parameters) extra_params.update(parameters)
req = self._makeOAuthRequest(url, params=extra_params) req = self._makeOAuthRequest(url, params=extra_params)
opener = urllib.request.build_opener(urllib.request.HTTPHandler(debuglevel=0)) opener = urllib.request.build_opener(urllib.request.HTTPHandler(debuglevel=0))
url = req.to_url() url = req.to_url()
@ -76,18 +74,15 @@ class OAuthApi:
def _makeOAuthRequest(self, url, token=None, params=None): def _makeOAuthRequest(self, url, token=None, params=None):
"""Make a OAuth request from url and parameters. Returns oAuth object.""" """Make a OAuth request from url and parameters. Returns oAuth object."""
oauth_base_params = { oauth_base_params = {
'oauth_version': "1.0", 'oauth_version': "1.0",
'oauth_nonce': oauth.generate_nonce(), 'oauth_nonce': oauth.generate_nonce(),
'oauth_timestamp': int(time.time()) 'oauth_timestamp': int(time.time())
} }
if params: if params:
params.update(oauth_base_params) params.update(oauth_base_params)
else: else:
params = oauth_base_params params = oauth_base_params
if not token: if not token:
token = self._access_token token = self._access_token
request = oauth.Request(method="GET", url=url, parameters=params) request = oauth.Request(method="GET", url=url, parameters=params)
@ -96,7 +91,6 @@ class OAuthApi:
def ApiCall(self, call, parameters={}): def ApiCall(self, call, parameters={}):
"""Calls the twitter API with 'call' and returns the twitter object (JSON).""" """Calls the twitter API with 'call' and returns the twitter object (JSON)."""
try: try:
data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", parameters) data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", parameters)
except urllib.error.HTTPError as e: # http error code. except urllib.error.HTTPError as e: # http error code.
@ -115,13 +109,28 @@ class Tweety(callbacks.Plugin):
self.__parent = super(Tweety, self) self.__parent = super(Tweety, self)
self.__parent.__init__(irc) self.__parent.__init__(irc)
self.twitterApi = False self.twitterApi = False
self.since_id = {}
if not self.twitterApi: if not self.twitterApi:
self._checkAuthorization() self._checkAuthorization()
self.data_file = conf.supybot.directories.data.dirize("tweety.json")
if os.path.exists(self.data_file):
with open(self.data_file) as f:
self.since_id = json.load(f)
else:
log.debug("Tweety: Creating new since_id DB")
self.since_id = {}
world.flushers.append(self._flush_db)
def _flush_db(self):
with open(self.data_file, 'w') as f:
json.dump(self.since_id, f)
def die(self):
world.flushers.remove(self._flush_db)
self._flush_db()
super().die()
def _httpget(self, url, h=None, d=None, l=False): def _httpget(self, url, h=None, d=None, l=False):
"""General HTTP resource fetcher. Pass headers via h, data via d, and to log via l.""" """General HTTP resource fetcher. Pass headers via h, data via d, and to log via l."""
try: try:
if h and d: if h and d:
page = utils.web.getUrl(url, headers=h, data=d) page = utils.web.getUrl(url, headers=h, data=d)
@ -137,13 +146,10 @@ class Tweety(callbacks.Plugin):
self.log.error("ERROR opening {0} message: {1}".format(url, e)) self.log.error("ERROR opening {0} message: {1}".format(url, e))
return None return None
def _shortenUrl(self, url): def _shortenUrl(self, url):
"""Shortens a long URL into a short one.""" """Shortens a long URL into a short one."""
url_enc = urllib.parse.quote_plus(url)
try: try:
data = requests.get('http://tinyurl.com/api-create.php?url={0}'.format(url_enc), timeout=10) data = requests.get('http://tinyurl.com/api-create.php?url={0}'.format(url), timeout=5)
except (requests.exceptions.RequestException, requests.exceptions.HTTPError) as e: except (requests.exceptions.RequestException, requests.exceptions.HTTPError) as e:
log.debug('Tweety: error retrieving tiny url: {0}'.format(e)) log.debug('Tweety: error retrieving tiny url: {0}'.format(e))
return return
@ -152,7 +158,6 @@ class Tweety(callbacks.Plugin):
def _checkAuthorization(self): def _checkAuthorization(self):
""" Check if we have our keys and can auth.""" """ Check if we have our keys and can auth."""
if not self.twitterApi: # if not set, try and auth. if not self.twitterApi: # if not set, try and auth.
failTest = False # first check that we have all 4 keys. failTest = False # first check that we have all 4 keys.
for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'): for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'):
@ -211,7 +216,6 @@ class Tweety(callbacks.Plugin):
def _unescape(self, text): def _unescape(self, text):
"""Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)"""
# quick dump \n and \r, usually coming from bots that autopost html. # quick dump \n and \r, usually coming from bots that autopost html.
text = text.replace('\n', ' ').replace('\r', ' ') text = text.replace('\n', ' ').replace('\r', ' ')
# now the actual unescape. # now the actual unescape.
@ -239,7 +243,6 @@ class Tweety(callbacks.Plugin):
""" """
Return relative time delta between now and s (dt string). Return relative time delta between now and s (dt string).
""" """
try: # timeline's created_at Tue May 08 10:58:49 +0000 2012 try: # timeline's created_at Tue May 08 10:58:49 +0000 2012
ddate = time.strptime(s, "%a %b %d %H:%M:%S +0000 %Y")[:-2] ddate = time.strptime(s, "%a %b %d %H:%M:%S +0000 %Y")[:-2]
except ValueError: except ValueError:
@ -279,18 +282,14 @@ class Tweety(callbacks.Plugin):
# short url the link to the tweet? # short url the link to the tweet?
if self.registryValue('addShortUrl', msg.args[0]): if self.registryValue('addShortUrl', msg.args[0]):
url = self._shortenUrl("https://twitter.com/{0}/status/{1}".format(nick, tweetid)) url = self._shortenUrl("https://twitter.com/{0}/status/{1}".format(nick, tweetid))
if url:
text += " {0}".format(url)
# add in the end with the text + tape. # add in the end with the text + tape.
if self.registryValue('colorTweetURLs', msg.args[0]): # color urls. if self.registryValue('colorTweetURLs', msg.args[0]): # color urls.
text = re.sub(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)', self._red(r'\1'), text) text = re.sub(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)', self._red(r'\1'), text)
if url: ret += ": {0} ({1})".format(text, self._bold(time))
ret += ": {0} {1} ({2})".format(text, url, self._bold(time))
else:
ret += ": {0} ({1})".format(text, self._bold(time))
else: # only bold time. no text color. else: # only bold time. no text color.
if url: ret += ": {0} ({1})".format(text, self._bold(time))
ret += ": {0} {1} ({2})".format(text, url, self._bold(time))
else:
ret += ": {0} ({1})".format(text, self._bold(time))
# now return. # now return.
return ret return ret
@ -318,20 +317,17 @@ class Tweety(callbacks.Plugin):
Search Yahoo's WOEID DB for a location. Useful for the trends variable. Search Yahoo's WOEID DB for a location. Useful for the trends variable.
Ex: London or Boston Ex: London or Boston
""" """
woeid = self._woeid_lookup(lookup) woeid = self._woeid_lookup(lookup)
if woeid: if woeid:
irc.reply("WOEID: {0} for '{1}'".format(self._bold(woeid), lookup)) irc.reply("WOEID: {0} for '{1}'".format(self._bold(woeid), lookup))
else: else:
irc.reply("ERROR: Something broke trying to find a WOEID for '{0}'".format(lookup)) irc.reply("ERROR: Something broke trying to find a WOEID for '{0}'".format(lookup))
woeidlookup = wrap(woeidlookup, ['text']) woeidlookup = wrap(woeidlookup, ['text'])
def ratelimits(self, irc, msg, args): def ratelimits(self, irc, msg, args):
""" """
Display current rate limits for your twitter API account. Display current rate limits for your twitter API account.
""" """
# before we do anything, make sure we have a twitterApi object. # before we do anything, make sure we have a twitterApi object.
if not self.twitterApi: if not self.twitterApi:
irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.")
@ -367,25 +363,21 @@ class Tweety(callbacks.Plugin):
def trends(self, irc, msg, args, getopts, optwoeid): def trends(self, irc, msg, args, getopts, optwoeid):
"""[--exclude] [location] """[--exclude] [location]
Returns the top Twitter trends for a specific location. Use optional argument location for trends. Returns the top Twitter trends for a specific location. Use optional argument location for trends.
Defaults to worldwide and can be set via config variable. Defaults to worldwide and can be set via config variable.
Use --exclude to not include #hashtags in trends data. Use --exclude to not include #hashtags in trends data.
Ex: Boston or --exclude London Ex: Boston or --exclude London
""" """
# enforce +voice or above to use command? # enforce +voice or above to use command?
if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check? if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check?
if ircutils.isChannel(msg.args[0]): # are we in a channel? if ircutils.isChannel(msg.args[0]): # are we in a channel?
if not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they + or @? if not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they + or @?
irc.error("ERROR: You have to be at least voiced to use the trends command in {0}.".format(msg.args[0])) irc.error("ERROR: You have to be at least voiced to use the trends command in {0}.".format(msg.args[0]))
return return
# before we do anything, make sure we have a twitterApi object. # before we do anything, make sure we have a twitterApi object.
if not self.twitterApi: if not self.twitterApi:
irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.")
return return
# default arguments. # default arguments.
args = {'id': self.registryValue('woeid', msg.args[0]), args = {'id': self.registryValue('woeid', msg.args[0]),
'exclude': self.registryValue('hideHashtagsTrends', msg.args[0])} 'exclude': self.registryValue('hideHashtagsTrends', msg.args[0])}
@ -427,31 +419,26 @@ class Tweety(callbacks.Plugin):
location = data[0]['locations'][0]['name'] location = data[0]['locations'][0]['name']
ttrends = " | ".join([trend['name'] for trend in data[0]['trends']]) ttrends = " | ".join([trend['name'] for trend in data[0]['trends']])
irc.reply("Top Twitter Trends in {0} :: {1}".format(self._bold(location), ttrends)) irc.reply("Top Twitter Trends in {0} :: {1}".format(self._bold(location), ttrends))
trends = wrap(trends, [getopts({'exclude':''}), optional('text')]) trends = wrap(trends, [getopts({'exclude':''}), optional('text')])
def tsearch(self, irc, msg, args, optlist, optterm): def tsearch(self, irc, msg, args, optlist, optterm):
"""[--num number] [--searchtype mixed|recent|popular] [--lang xx] [--new] <term> """[--num number] [--searchtype mixed|recent|popular] [--lang xx] [--new] <term>
Searches Twitter for the <term> and returns the most recent results. Searches Twitter for the <term> and returns the most recent results.
--num is number of results. --num is number of results.
--searchtype being recent, popular or mixed. Popular is the default. --searchtype being recent, popular or mixed. Popular is the default.
--new returns new messages since last search for <term> in channel. --new returns new messages since last search for <term> in channel.
Ex: --num 3 breaking news Ex: --num 3 breaking news
""" """
# enforce +voice or above to use command? # enforce +voice or above to use command?
if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check? if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check?
if ircutils.isChannel(msg.args[0]): # are we in a channel? if ircutils.isChannel(msg.args[0]): # are we in a channel?
if not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they + or @? if not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they + or @?
irc.error("ERROR: You have to be at least voiced to use the tsearch command in {0}.".format(msg.args[0])) irc.error("ERROR: You have to be at least voiced to use the tsearch command in {0}.".format(msg.args[0]))
return return
# before we do anything, make sure we have a twitterApi object. # before we do anything, make sure we have a twitterApi object.
if not self.twitterApi: if not self.twitterApi:
irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.")
return return
self.since_id.setdefault(msg.channel, {}) self.since_id.setdefault(msg.channel, {})
self.since_id[msg.channel].setdefault('{0}'.format(optterm), None) self.since_id[msg.channel].setdefault('{0}'.format(optterm), None)
new = False new = False
@ -503,16 +490,14 @@ class Tweety(callbacks.Plugin):
# build output string and output. # build output string and output.
output = self._outputTweet(irc, msg, nick, name, verified, text, date, tweetid) output = self._outputTweet(irc, msg, nick, name, verified, text, date, tweetid)
irc.reply(output) irc.reply(output)
tsearch = wrap(tsearch, [getopts({'num':('int'), tsearch = wrap(tsearch, [getopts({'num':('int'),
'searchtype':('literal', ('popular', 'mixed', 'recent')), 'searchtype':('literal', ('popular', 'mixed', 'recent')),
'lang':('somethingWithoutSpaces'), 'lang':('somethingWithoutSpaces'),
'new':''}), 'new':''}),
('text')]) ('text')])
def twitter(self, irc, msg, args, optlist, optnick, opturl): def twitter(self, irc, msg, args, optlist, optnick):
"""[--noreply] [--nort] [--num <##>] [--id <id#>] [--info] [--new] <nick> """[--noreply] [--nort] [--num <##>] [--info] [--new] [--id <id#>] <nick>
Returns last tweet or --num tweets. Shows all tweets, including RT and reply. Returns last tweet or --num tweets. Shows all tweets, including RT and reply.
To not display replies or RT's, use --noreply or --nort. To not display replies or RT's, use --noreply or --nort.
Return new tweets since you last checked in channel with --new. Return new tweets since you last checked in channel with --new.
@ -520,7 +505,6 @@ class Tweety(callbacks.Plugin):
Return information on user with --info. Return information on user with --info.
Ex: --info CNN | --id 337197009729622016 | --num 3 CNN Ex: --info CNN | --id 337197009729622016 | --num 3 CNN
""" """
self.since_id.setdefault(msg.channel, {}) self.since_id.setdefault(msg.channel, {})
self.since_id[msg.channel].setdefault('{0}'.format(optnick), None) self.since_id[msg.channel].setdefault('{0}'.format(optnick), None)
# enforce +voice or above to use command? # enforce +voice or above to use command?
@ -529,12 +513,10 @@ class Tweety(callbacks.Plugin):
if not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they + or @? if not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they + or @?
irc.error("ERROR: You have to be at least voiced to use the twitter command in {0}.".format(msg.args[0])) irc.error("ERROR: You have to be at least voiced to use the twitter command in {0}.".format(msg.args[0]))
return return
# before we do anything, make sure we have a twitterApi object. # before we do anything, make sure we have a twitterApi object.
if not self.twitterApi: if not self.twitterApi:
irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.")
return return
# now begin # now begin
optnick = optnick.replace('@','') # strip @ from input if given. optnick = optnick.replace('@','') # strip @ from input if given.
# default options. # default options.
@ -629,9 +611,6 @@ class Tweety(callbacks.Plugin):
return return
# no errors, so we process data conditionally. # no errors, so we process data conditionally.
if args['id']: # If --id was given for a single tweet. if args['id']: # If --id was given for a single tweet.
url = ''
if opturl:
url = ' - {}'.format(self._shortenUrl(opturl))
text = self._unescape(data.get('full_text')) or self._unescape(data.get('text')) text = self._unescape(data.get('full_text')) or self._unescape(data.get('text'))
nick = self._unescape(data["user"].get('screen_name')) nick = self._unescape(data["user"].get('screen_name'))
name = self._unescape(data["user"].get('name')) name = self._unescape(data["user"].get('name'))
@ -640,7 +619,6 @@ class Tweety(callbacks.Plugin):
tweetid = data.get('id') tweetid = data.get('id')
# prepare string to output and send to irc. # prepare string to output and send to irc.
output = self._outputTweet(irc, msg, nick, name, verified, text, relativeTime, tweetid) output = self._outputTweet(irc, msg, nick, name, verified, text, relativeTime, tweetid)
output += url
irc.reply(output) irc.reply(output)
return return
elif args['info']: # --info to return info on a Twitter user. elif args['info']: # --info to return info on a Twitter user.
@ -694,14 +672,14 @@ class Tweety(callbacks.Plugin):
# prepare string to output and send to irc. # prepare string to output and send to irc.
output = self._outputTweet(irc, msg, nick, name, verified, text, relativeTime, tweetid) output = self._outputTweet(irc, msg, nick, name, verified, text, relativeTime, tweetid)
irc.reply(output) irc.reply(output)
twitter = wrap(twitter, [getopts({'noreply':'', twitter = wrap(twitter, [getopts({'noreply':'',
'nort':'', 'nort':'',
'info':'', 'info':'',
'id':'', 'id':'',
'url':'', 'url':'',
'new':'', 'new':'',
'num':('int')}), ('somethingWithoutSpaces'), optional('somethingWithoutSpaces')]) 'num':('int')}),
('somethingWithoutSpaces')])
Class = Tweety Class = Tweety

View File

@ -1,3 +1,3 @@
oauth2 oauth2
requests requests
beautifulsoup4