diff --git a/README.md b/README.md index 99006d3..34ff6ce 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,26 @@ Supybot-Tweety # Twitter client for Supybot - Description +Description +----------- - This is a supybot client. +This is a Supybot plugin to work with Twitter. It allows a user to search for Tweets, +display specific tweets and timelines from a user's account, and display Trends. + +It has been updated to work with the oAuth requirement in v1.1 API along with their +updated endpoints. + +For working v1.1 API clients, I am aware of only this and ProgVal's Twitter client. +This is a much watered down version of ProgVal's Twitter client. It only includes +read-only features (no risk of accidental Tweeting) that most folks use: +tweet display, tweet searching and trends. Instructions ------------ -1.) Install the dependencies. You can go the pip route or install via source, depending on your setup. You will need: - 1. Install oauth2: sudo pip install oauth2 - +1.) On an up-to-date Python 2.7+ system, one dependency is needed. + You can go the pip route or install via source, depending on your setup. You will need: + 1. Install oauth2: pip install oauth2 + 2.) You need some keys from Twitter. See http://dev.twitter.com. Steps are: 1. If you plan to use a dedicated Twitter account, create a new twitter account. 2. Go to dev.twitter.com and log in. @@ -19,15 +30,33 @@ Instructions 4. Fill out the information. Name does not matter. 5. default is read-only. Since we're not tweeting from this bot/code, you're fine here. 6. Your 4 magic strings (2 tokens and 2 secrets) are shown. - 7. Once you /msg yourbot load Tweety, you need to set these keys: - /msg bot config plugins.Tweety.consumer_key xxxxx - /msg bot config plugins.Tweety.consumer_secret xxxxx - /msg bot config plugins.Tweety.access_key xxxxx - /msg bot config plugins.Tweety.access_secret xxxxx + 7. Once you /msg load Tweety, you need to set these keys: + * /msg config plugins.Tweety.consumerKey xxxxx + * /msg config plugins.Tweety.consumerSecret xxxxx + * /msg config plugins.Tweety.accessKey xxxxx + * /msg config plugins.Tweety.accessSecret xxxxx + 8. Next, I suggest you /msg config search Tweety. There are a lot of options here. + 9. Things should work fine from here providing your keys are right. Examples -------- -# Documentation +Background +---------- +Hoaas, on GitHub, started this plugin with basics for Twitter and I started to submit +ideas and code. After a bit, the plugin was mature but Twitter, in 2012, put out the +notice that everything was changing with their move to v1.1 of the API. The client had +no oAuth code, was independent of any Python library, so it needed a major rewrite. I +decided to take this part on, using chunks of code from an oAuth/Twitter wrapper and +later rewriting/refactoring many of the existing functions with the massive structural +changes. - +So, as I take over, I must acknowledge the work done by Hoaas: +http://github.com/Hoaas/ +Much/almost all of the oAuth code ideas came from: +https://github.com/jpittman/OAuth-Python-Twitter + +Documentation +------------- + +* https://dev.twitter.com/docs/api/1.1 diff --git a/__init__.py b/__init__.py index fde7715..b0d8185 100644 --- a/__init__.py +++ b/__init__.py @@ -1,32 +1,5 @@ # -*- coding: utf-8 -*- -### -# Copyright (c) 2011-2013, Terje Hoås, spline -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions, and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions, and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author of this software nor the name of -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written consent. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - +# Copyright (c) 2013, spline ### """ diff --git a/config.py b/config.py index ff4e406..c56ed18 100644 --- a/config.py +++ b/config.py @@ -1,32 +1,5 @@ # -*- coding: utf-8 -*- -### -# Copyright (c) 2011-2013, Terje Hoås-spline -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions, and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions, and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author of this software nor the name of -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written consent. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - +# Copyright (c) 2013, spline ### import supybot.conf as conf @@ -56,5 +29,4 @@ conf.registerChannelValue(Tweety,'maxResults',registry.Integer(10, """Maximum nu conf.registerChannelValue(Tweety,'outputColorTweets',registry.Boolean(False, """When outputting Tweets, display them with some color.""")) conf.registerChannelValue(Tweety,'hideHashtagsTrends',registry.Boolean(False, """When displaying trends, should we display #hashtags? Default is no.""")) - # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=250: diff --git a/plugin.py b/plugin.py index 167f58b..0e734be 100644 --- a/plugin.py +++ b/plugin.py @@ -1,32 +1,5 @@ # -*- coding: utf-8 -*- -### -# Copyright (c) 2011-2013, Terje Hoås, spline -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions, and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions, and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author of this software nor the name of -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written consent. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - +# Copyright (c) 2013, spline ### # my libs @@ -43,8 +16,7 @@ import unicodedata # oauthtwitter import urlparse import oauth2 as oauth - -#supybot libs +# supybot libs import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins @@ -52,9 +24,8 @@ import supybot.ircutils as ircutils import supybot.callbacks as callbacks -# OAuthApi class from https://github.com/jpittman/OAuth-Python-Twitter -# mainly kept intact but modified for Twitter API v1.1 and unncessary things removed. class OAuthApi: + """ OAuth class to work with Twitter v1.1 API.""" def __init__(self, consumer_key, consumer_secret, token=None, token_secret=None): if token and token_secret: token = oauth.Token(token, token_secret) @@ -64,20 +35,19 @@ class OAuthApi: self._signature_method = oauth.SignatureMethod_HMAC_SHA1() self._access_token = token - def _FetchUrl(self,url, http_method=None, parameters=None): + def _FetchUrl(self,url, parameters=None): extra_params = {} if parameters: extra_params.update(parameters) - req = self._makeOAuthRequest(url, params=extra_params, http_method=http_method) + req = self._makeOAuthRequest(url, params=extra_params) opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=1)) url = req.to_url() - #callbacks.log.info(str(url)) url_data = opener.open(url).read() opener.close() return url_data - def _makeOAuthRequest(self, url, token=None, params=None, http_method="GET"): + def _makeOAuthRequest(self, url, token=None, params=None): oauth_base_params = { 'oauth_version': "1.0", 'oauth_nonce': oauth.generate_nonce(), @@ -91,14 +61,14 @@ class OAuthApi: if not token: token = self._access_token - request = oauth.Request(method=http_method,url=url,parameters=params) + request = oauth.Request(method="GET", url=url, parameters=params) request.sign_request(self._signature_method, self._Consumer, token) return request - def ApiCall(self, call, type="GET", parameters={}): + def ApiCall(self, call, parameters={}): return_value = [] try: - data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", type, parameters) + data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", parameters) except urllib2.HTTPError, e: return e except urllib2.URLError, e: @@ -106,17 +76,9 @@ class OAuthApi: else: return data -# now, begin our actual code. -# APIDOCS https://dev.twitter.com/docs/api/1.1 -# TODO: centralize logging in. Add something to display error codes in the log while displaying error to irc. -# TODO: work on colorizing tweets better. -# TODO: maybe make an encode wrapper that can utilize strip_accents? -# TODO: langs in search to validate against: https://dev.twitter.com/docs/api/1.1/get/help/languages class Tweety(callbacks.Plugin): - """Simply use the commands available in this plugin. Allows fetching of the - latest tween from a specified twitter handle, and listing of top ten - trending tweets.""" + """Public Twitter class for working with the API.""" threaded = True def __init__(self, irc): @@ -127,6 +89,7 @@ class Tweety(callbacks.Plugin): self._checkAuthorization() def _checkAuthorization(self): + """ Check if we have our keys and can auth.""" if not self.twitterApi: failTest = False for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'): @@ -181,23 +144,18 @@ class Tweety(callbacks.Plugin): return re.sub("&#?\w+;", fixup, text) def _time_created_at(self, s): - """ - Takes a datetime string object that comes from twitter and twitter search timelines and returns a relative date. - """ - # twitter search and timelines use different timeformats - # timeline's created_at Tue May 08 10:58:49 +0000 2012 - # search's created_at Thu, 06 Oct 2011 19:41:12 +0000 - try: + """Return relative delta.""" + + 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] except ValueError: - try: + try: # search's created_at Thu, 06 Oct 2011 19:41:12 +0000 ddate = time.strptime(s, "%a, %d %b %Y %H:%M:%S +0000")[:-2] except ValueError: return "unknown" # do the math - created_at = datetime(*ddate, tzinfo=None) - d = datetime.utcnow() - created_at + d = datetime.utcnow() - datetime(*ddate, tzinfo=None) # now parse and return. if d.days: @@ -210,7 +168,6 @@ class Tweety(callbacks.Plugin): rel_time = "%ss ago" % (d.seconds) return rel_time - def _outputTweet(self, irc, msg, nick, name, text, time, tweetid): """ Takes a group of strings and outputs a Tweet to IRC. Used for tsearch and twitter. @@ -239,7 +196,6 @@ class Tweety(callbacks.Plugin): irc.reply(ret) - def _createShortUrl(self, nick, tweetid): """ Takes a nick and tweetid and returns a shortened URL via is.gd service. @@ -254,7 +210,6 @@ class Tweety(callbacks.Plugin): except: return False - def _woeid_lookup(self, lookup): """ Use Yahoo's API to look-up a WOEID. @@ -285,7 +240,7 @@ class Tweety(callbacks.Plugin): ########################## def woeidlookup(self, irc, msg, args, lookup): - """[location] + """ Search Yahoo's WOEID DB for a location. Useful for the trends variable. """ @@ -299,27 +254,20 @@ class Tweety(callbacks.Plugin): # RATELIMITING - # https://dev.twitter.com/docs/api/1.1/get/application/rate_limit_status - # https://dev.twitter.com/docs/rate-limiting/1.1 - # https://dev.twitter.com/docs/rate-limiting/1.1/limits - #< X-Rate-Limit-Limit: 15 - #< X-Rate-Limit-Remaining: 13 - #< X-Rate-Limit-Reset: 1357963140 / time.now() + def ratelimits(self, irc, msg, args): """ Display current rate limits for your twitter API account. """ data = self.twitterApi.ApiCall('application/rate_limit_status') #, parameters={'resources':optstatus}) - try: data = json.loads(data) except: - irc.reply("Failed to lookup rate limit data. Something might have gone wrong. Data: %s" % data) + irc.reply("Failed to lookup ratelimit data: %s" % data) return data = data.get('resources', None) - if not data: # simple check if we have part of the json dict. irc.reply("Failed to fetch application rate limit status. Something could be wrong with Twitter.") self.log.error(data) @@ -337,8 +285,6 @@ class Tweety(callbacks.Plugin): ratelimits = wrap(ratelimits) - - def trends(self, irc, msg, args, getopts, optwoeid): """[--exclude] @@ -372,8 +318,8 @@ class Tweety(callbacks.Plugin): irc.reply("ERROR: Cannot load trends: {0}".format(data)) # error also throws 404. return + # package together in object and output. ttrends = string.join([trend['name'].encode('utf-8') for trend in data[0]['trends']], " | ") - irc.reply("Top 10 Twitter Trends in {0} :: {1}".format(ircutils.bold(location), ttrends)) trends = wrap(trends, [getopts({'exclude':''}), optional('text')]) @@ -425,7 +371,6 @@ class Tweety(callbacks.Plugin): tsearch = wrap(tsearch, [getopts({'num':('int'), 'searchtype':('literal', ('popular', 'mixed', 'recent')), 'lang':('somethingWithoutSpaces')}), ('text')]) - def twitter(self, irc, msg, args, optlist, optnick): """[--noreply] [--nort] [--num number] | <--id id> | [--info nick] diff --git a/test.py b/test.py index a503e13..1c7b7af 100644 --- a/test.py +++ b/test.py @@ -1,32 +1,5 @@ # -*- coding: utf-8 -*- -### -# Copyright (c) 2011-2013, Terje Hoås, spline -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions, and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions, and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author of this software nor the name of -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written consent. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - +# Copyright (c) 2013, spline ### from supybot.test import *