From 17c7c7e361803ec23fbdbee2c4db95009c18a582 Mon Sep 17 00:00:00 2001 From: spline Date: Tue, 23 Oct 2012 12:01:22 -0700 Subject: [PATCH 01/63] Initial commit --- .gitignore | 27 +++++++++++++++++++++++++++ README.md | 4 ++++ 2 files changed, 31 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f24cd99 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg diff --git a/README.md b/README.md new file mode 100644 index 0000000..39a5819 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +Supybot-Tweety +============== + +Supybot Twitter plugin for Twitter API v1.1 (fork of Hoaas's work) \ No newline at end of file From 2449f9a42e1b6b13af357b1f83b1aa4690a4ee01 Mon Sep 17 00:00:00 2001 From: spline Date: Tue, 23 Oct 2012 15:03:06 -0400 Subject: [PATCH 02/63] Initial push.. --- README.md | 4 - README.txt | 48 ++++ __init__.py | 68 ++++++ config.py | 55 +++++ plugin.py | 646 ++++++++++++++++++++++++++++++++++++++++++++++++++++ test.py | 38 ++++ 6 files changed, 855 insertions(+), 4 deletions(-) delete mode 100644 README.md create mode 100644 README.txt create mode 100644 __init__.py create mode 100644 config.py create mode 100644 plugin.py create mode 100644 test.py diff --git a/README.md b/README.md deleted file mode 100644 index 39a5819..0000000 --- a/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Supybot-Tweety -============== - -Supybot Twitter plugin for Twitter API v1.1 (fork of Hoaas's work) \ No newline at end of file diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..1e857f0 --- /dev/null +++ b/README.txt @@ -0,0 +1,48 @@ +Supply a username and get the latest tweet(s). Or an ID to a tweet. Or search on twitter. Or view latest trends. Does not require a user account or apikey or anything like that. Just point and shoot. +This plugin does NOT relay tweets in real time. It only fetches data from Twitter when commands are called. + + +Examples: +12:38:02 <@Hoaas> !twitter cnn +12:38:03 <@Bunisher> @CNN (CNN): RT @AC360 Because of #AC360's investigation, Senate Finance Committee is demanding answers from a charity for disabled vets. Find out why 8p (10 hours ago) + +12:38:48 <@Hoaas> !twitter --num 5 RealTimeWWII +12:38:49 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): All French ships left in Mediterranean being ordered to attack UK warships on sight, thanks to yesterday's British attack at Mers-El-Kébir. (1 hour ago) +12:38:51 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): After negotiation, French ships at Alexandria have peacefully surrendered to Royal Navy- they've yet to hear of attack at Mers-el-Kébir. (16 hours ago) +12:38:54 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): As part of peace treaty, France must give Madagascar to Germany, so that all European Jews can be deported there: http://t.co/GiRq06O1 (15 hours ago) +12:38:55 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): 6.05PM British ships have stopped firing. 1,297 French sailors are dead. 4th largest Navy in the world is decimated. http://t.co/8Iob4NlK (17 hours ago) +12:38:56 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): Trapped at anchor, French ships can't evade brutal shelling; only 2 in range to fire back. They can do nothing but die. http://t.co/6NDT5hDp (17 hours ago) + +12:39:43 <@Hoaas> !twitter --id 220454083016921088 +12:39:45 <@Bunisher> @TheScienceGuy (Bill Nye): The Higgs is real !?! The results are good to 5 standard deviations. Yikes. Whoa. Wow. This could change the world... (51 minutes ago) + +12:40:05 <@Hoaas> !twitter --info TheScienceGuy +12:40:07 <@Bunisher> @TheScienceGuy (Bill Nye): http://billnye.com Science Educator seeks to change the world... 29 friends, 319377 followers. Los Angeles, CA, USA + +12:42:00 <@Hoaas> !twitter --num 5 wilw +12:42:01 <@Bunisher> @wilw (Wil Wheaton): Holy crap, @amazonmp3 is doing some incredible deals until midnight PDT. Tons of great albums for a buck. (5 hours ago) +12:42:02 <@Bunisher> @wilw (Wil Wheaton): Drr…Drr…Drr. (5 hours ago) +12:42:03 <@Bunisher> @wilw (Wil Wheaton): Did I make another Robert Evans tweet, because it was amusing to me? You bet your ass I did, and I did it right there in front of everyone. (1 days ago) +12:42:04 <@Bunisher> @wilw (Wil Wheaton): (Kids, ask your parents. Then go find that weird relative who loves movies from the 70s and she'll explain it to you.) (1 days ago) +12:42:05 <@Bunisher> @wilw (Wil Wheaton): Do I write tweets that are rhetorical questions, and read them to myself in the voice of Robert Evans? You bet I do. (1 days ago) + +12:42:09 <@Hoaas> !twitter --rt --num 5 wilw +12:42:10 <@Bunisher> @wilw (Wil Wheaton): Holy crap, @amazonmp3 is doing some incredible deals until midnight PDT. Tons of great albums for a buck. (5 hours ago) +12:42:12 <@Bunisher> @wilw (Wil Wheaton): Drr…Drr…Drr. (5 hours ago) +12:42:14 <@Bunisher> @wilw (Wil Wheaton): RT @The_RobertEvans: @wilw Am I a fan of Wil Wheaton? "You bet your ass I am. " (7 hours ago) +12:42:15 <@Bunisher> @wilw (Wil Wheaton): RT @amazonmp3: Hear one of the finest voices in rock on the latest Florence + the Machine album for $.99 thru midnight: http://t.co/gEXg5Tbb (5 hours ago) +12:42:16 <@Bunisher> @wilw (Wil Wheaton): RT @amazonmp3: Beach House's Bloom is super dreamy. For $.99 you don't have to take our word for it. Ends midnight Pacific: http://t.co/ ... (6 hours ago) + +12:41:38 <@Hoaas> !twitter --reply wilw +12:41:40 <@Bunisher> @wilw (Wil Wheaton): @chicazul ugh. Region locking makes baby jeebus cry ... until he gets a VPN. (4 hours ago) + +12:43:13 <@Hoaas> !twitter --rt --reply --num 5 wilw +12:43:14 <@Bunisher> @wilw (Wil Wheaton): @chicazul ugh. Region locking makes baby jeebus cry ... until he gets a VPN. (4 hours ago) +12:43:15 <@Bunisher> @wilw (Wil Wheaton): @amazonmp3 thanks for the rockin' sale :) (4 hours ago) +12:43:16 <@Bunisher> @wilw (Wil Wheaton): Holy crap, @amazonmp3 is doing some incredible deals until midnight PDT. Tons of great albums for a buck. (5 hours ago) +12:43:18 <@Bunisher> @wilw (Wil Wheaton): @undeux USA! USA! USA! (5 hours ago) +12:43:19 <@Bunisher> @wilw (Wil Wheaton): RT @amazonmp3: Hear one of the finest voices in rock on the latest Florence + the Machine album for $.99 thru midnight: http://t.co/gEXg5Tbb (5 hours ago) + + +12:40:29 <@Hoaas> !tagdef ff +12:40:31 <@Bunisher> Tagdef: #ff #ff is the same as (short for) #followfriday. http://tagdef.com/ff diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..de28c68 --- /dev/null +++ b/__init__.py @@ -0,0 +1,68 @@ +# coding=utf8 +### +# Copyright (c) 2011, Terje Hoås +# 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. + +### + +""" +Add a description of the plugin (to be presented to the user inside the wizard) +here. This should describe *what* the plugin does. +""" + +import supybot +import supybot.world as world + +# Use this for the version of this plugin. You may wish to put a CVS keyword +# in here if you're keeping the plugin in CVS or some similar system. +__version__ = "" + +# XXX Replace this with an appropriate author or supybot.Author instance. +__author__ = supybot.Author('Terje Hoås', 'Hoaas', 'terjehoaas@gmail.com') + +# This is a dictionary mapping supybot.Author instances to lists of +# contributions. +__contributors__ = {} + +# This is a url where the most recent plugin package can be downloaded. +__url__ = '' # 'http://supybot.com/Members/yourname/Tweety/download' + +import config +import plugin +reload(plugin) # In case we're being reloaded. +reload(config) +# Add more reloads here if you add third-party modules and want them to be +# reloaded when this plugin is reloaded. Don't forget to import them as well! + +if world.testing: + import test + +Class = plugin.Class +configure = config.configure + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/config.py b/config.py new file mode 100644 index 0000000..17f34fb --- /dev/null +++ b/config.py @@ -0,0 +1,55 @@ +# coding=utf8 +### +# Copyright (c) 2011, Terje Hoås +# 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. + +### + +import supybot.conf as conf +import supybot.registry as registry + +def configure(advanced): + # This will be called by supybot to configure this module. advanced is + # a bool that specifies whether the user identified himself as an advanced + # user or not. You should effect your configuration by manipulating the + # registry as appropriate. + from supybot.questions import expect, anything, something, yn + conf.registerPlugin('Tweety', True) + + +Tweety = conf.registerPlugin('Tweety') +conf.registerGlobalValue(Tweety, 'consumerKey', registry.String('', """The consumer key of the application.""")) +conf.registerGlobalValue(Tweety, 'consumerSecret', registry.String('', """The consumer secret of the application.""", private=True)) +conf.registerGlobalValue(Tweety, 'accessKey', registry.String('', """The Twitter Access Token key for the bot's account""")) +conf.registerGlobalValue(Tweety, 'accessSecret', registry.String('', """The Twitter Access Token secret for the bot's account""", private=True)) +conf.registerChannelValue(Tweety, 'hideRealName', registry.Boolean(False, """Do not show real name when displaying tweets.""")) +conf.registerChannelValue(Tweety, 'addShortUrl', registry.Boolean(False, """Whether or not to add a short URL to the tweets.""")) +conf.registerChannelValue(Tweety, 'woeid', registry.Integer(1, """Where On Earth ID. World Wide is 1. USA is 23424977.""")) +conf.registerChannelValue(Tweety, 'defaultResults', registry.Integer(3, """Default number of results to return on searches.""")) +conf.registerChannelValue(Tweety, 'maxResults', registry.Integer(10, """Maximum number of results to return on searches and lookups.""")) + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: diff --git a/plugin.py b/plugin.py new file mode 100644 index 0000000..c652cec --- /dev/null +++ b/plugin.py @@ -0,0 +1,646 @@ +# coding=utf8 +### +# Copyright (c) 2011-2012, 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. + +### + +import urllib, urllib2 +import json +import datetime +import string +import supybot.utils as utils +from supybot.commands import * +import supybot.plugins as plugins +import supybot.ircutils as ircutils +import supybot.callbacks as callbacks + +#libraries for time_created_at +import time +from datetime import tzinfo, datetime, timedelta + +# for unescape +import re, htmlentitydefs + +# oauthtwitter +import time +import urlparse +import oauth2 as oauth + +# OAuthApi class from https://github.com/jpittman/OAuth-Python-Twitter +# mainly kept intact but modified for Twitter API v1.1 and unncessary things removed. + +REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' +ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' +AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' +SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' + +class OAuthApi: + def __init__(self, consumer_key, consumer_secret, token=None, token_secret=None): + if token and token_secret: + token = oauth.Token(token, token_secret) + else: + token = None + self._Consumer = oauth.Consumer(consumer_key, consumer_secret) + self._signature_method = oauth.SignatureMethod_HMAC_SHA1() + self._access_token = token + + def _GetOpener(self): + opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=1)) + return opener + + def _FetchUrl(self, + url, + http_method=None, + parameters=None): + '''Fetch a URL, optionally caching for a specified time. + + Args: + url: The URL to retrieve + http_method: + One of "GET" or "POST" to state which kind + of http call is being made + parameters: + A dict whose key/value pairs should encoded and added + to the query string, or generated into post data. [OPTIONAL] + depending on the http_method parameter + + Returns: + A string containing the body of the response. + ''' + # Build the extra parameters dict + extra_params = {} + if parameters: + extra_params.update(parameters) + + req = self._makeOAuthRequest(url, params=extra_params, + http_method=http_method) + + # Get a url opener that can handle Oauth basic auth + callbacks.log.info(str(extra_params)) + opener = self._GetOpener() + + if http_method == "POST": + encoded_post_data = req.to_postdata() + # Removed the following line due to the fact that OAuth2 request objects do not have this function + # This does not appear to have any adverse impact on the operation of the toolset + #url = req.get_normalized_http_url() + else: + url = req.to_url() + encoded_post_data = "" + + callbacks.log.info(str(url)) + + if encoded_post_data: + url_data = opener.open(url, encoded_post_data).read() + callbacks.log.debug(url) + else: + url_data = opener.open(url).read() + callbacks.log.debug(url) + opener.close() + + # Always return the latest version + return url_data + + def _makeOAuthRequest(self, url, token=None, + params=None, http_method="GET"): + '''Make a OAuth request from url and parameters + + Args: + url: The Url to use for creating OAuth Request + parameters: + The URL parameters + http_method: + The HTTP method to use + Returns: + A OAauthRequest object + ''' + + oauth_base_params = { + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(), + 'oauth_timestamp': int(time.time()) + } + + if params: + params.update(oauth_base_params) + else: + params = oauth_base_params + + if not token: + token = self._access_token + request = oauth.Request(method=http_method,url=url,parameters=params) + request.sign_request(self._signature_method, self._Consumer, token) + return request + + def getAuthorizationURL(self, token, url=AUTHORIZATION_URL): + '''Create a signed authorization URL + + Authorization provides the user with a VERIFIER which they may in turn provide to + the consumer. This key authorizes access. Used primarily for clients. + + Returns: + A signed OAuthRequest authorization URL + ''' + return "%s?oauth_token=%s" % (url, token['oauth_token']) + + def getAuthenticationURL(self, token, url=SIGNIN_URL, force_login=False): + '''Create a signed authentication URL + + Authentication allows a user to directly authorize Twitter access with a click. + Used primarily for web-apps. + + Returns: + A signed OAuthRequest authentication URL + ''' + auth_url = "%s?oauth_token=%s" % (url, token['oauth_token']) + if force_login: + auth_url += "&force_login=1" + return auth_url + + def getRequestToken(self, url=REQUEST_TOKEN_URL): + '''Get a Request Token from Twitter + + Returns: + A OAuthToken object containing a request token + ''' + resp, content = oauth.Client(self._Consumer).request(url, "GET") + if resp['status'] != '200': + raise Exception("Invalid response %s." % resp['status']) + + return dict(urlparse.parse_qsl(content)) + + def getAccessToken(self, token, verifier=None, url=ACCESS_TOKEN_URL): + '''Get a Request Token from Twitter + + Note: Verifier is required if you AUTHORIZED, it can be skipped if you AUTHENTICATED + + Returns: + A OAuthToken object containing a request token + ''' + token = oauth.Token(token['oauth_token'], token['oauth_token_secret']) + if verifier: + token.set_verifier(verifier) + client = oauth.Client(self._Consumer, token) + + resp, content = client.request(url, "POST") + return dict(urlparse.parse_qsl(content)) + + def ApiCall(self, call, type="GET", parameters={}): + '''Calls the twitter API + + Args: + call: The name of the api call (ie. account/rate_limit_status) + type: One of "GET" or "POST" + parameters: Parameters to pass to the Twitter API call + Returns: + Returns the twitter.User object + ''' + return_value = [] + # We use this try block to make the request in case we run into one of Twitter's many 503 (temporarily unavailable) errors. + # Other error handling may end up being useful as well. + try: + data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", type, parameters) + + # This is the most common error type you'll get. Twitter is good about returning codes, too + # Chances are that most of the time you run into this, it's going to be a 503 "service temporarily unavailable". That's a fail whale. + except urllib2.HTTPError, e: + return e + # Getting an URLError usually means you didn't even hit Twitter's servers. This means something has gone TERRIBLY WRONG somewhere. + except urllib2.URLError, e: + return e + else: + return json.loads(data) + +# now, begin our actual code. + +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.""" + threaded = True + + # APIDOCS https://dev.twitter.com/docs/api/1.1 + + def _strip_accents(self, string): + """Return a string containing the normalized ascii string.""" + import unicodedata + return unicodedata.normalize('NFKD', unicode(string)).encode('ASCII', 'ignore') + + + def _unescape(self, text): + text = text.replace("\n", " ") + def fixup(m): + text = m.group(0) + if text[:2] == "&#": + # character reference + try: + if text[:3] == "&#x": + return unichr(int(text[3:-1], 16)) + else: + return unichr(int(text[2:-1])) + except (ValueError, OverflowError): + pass + else: + # named entity + try: + text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + except KeyError: + pass + return text # leave as is + 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. + """ + + plural = lambda n: n > 1 and "s" or "" + + # 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: + ddate = time.strptime(s, "%a %b %d %H:%M:%S +0000 %Y")[:-2] + except ValueError: + try: + ddate = time.strptime(s, "%a, %d %b %Y %H:%M:%S +0000")[:-2] + except ValueError: + return "", "" + + created_at = datetime(*ddate, tzinfo=None) + d = datetime.utcnow() - created_at + + if d.days: + rel_time = "%s days ago" % d.days + elif d.seconds > 3600: + hours = d.seconds / 3600 + rel_time = "%s hour%s ago" % (hours, plural(hours)) + elif 60 <= d.seconds < 3600: + minutes = d.seconds / 60 + rel_time = "%s minute%s ago" % (minutes, plural(minutes)) + elif 30 < d.seconds < 60: + rel_time = "less than a minute ago" + else: + rel_time = "less than %s second%s ago" % (d.seconds, plural(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. + """ + + ret = ircutils.underline(ircutils.bold("@" + nick)) + hideName = self.registryValue('hideRealName', msg.args[0]) + if not hideName: + ret += " ({0})".format(name) + ret += ": {0} ({1})".format(text, ircutils.bold(time)) + if self.registryValue('addShortUrl', msg.args[0]): + url = self._createShortUrl(nick, tweetid) + if (url): + ret += " {0}".format(url) + irc.reply(ret) + + + def _createShortUrl(self, nick, tweetid): + """ + Takes an input URL and returns a shortened URL via is.gd service. + """ + + longurl = "https://twitter.com/#!/{0}/status/{1}".format(nick, tweetid) + try: + req = urllib2.Request("http://is.gd/api.php?longurl=" + urllib.quote(longurl)) + f = urllib2.urlopen(req) + shorturl = f.read() + return shorturl + except: + return False + + + def _woeid_lookup(self, lookup): + """ + Use Yahoo's API to look-up a WOEID. + """ + + query = "select * from geo.places where text='%s'" % lookup + + params = { + "q": query, + "format":"json", + "diagnostics":"false", + "env":"store://datatables.org/alltableswithkeys" + } + + try: + response = urllib2.urlopen("http://query.yahooapis.com/v1/public/yql",urllib.urlencode(params)) + data = json.loads(response.read()) + + if data['query']['count'] > 1: + woeid = data['query']['results']['place'][0]['woeid'] + else: + woeid = data['query']['results']['place']['woeid'] + + except Exception, err: + return None + + return woeid + + + def woeidlookup(self, irc, msg, args, lookup): + """[location] + Search Yahoo's WOEID DB for a location. Useful for the trends variable. + """ + + woeid = self._woeid_lookup(lookup) + if woeid: + irc.reply(("I found WOEID: %s while searching for: '%s'") % (ircutils.bold(woeid), lookup)) + else: + irc.reply(("Something broke while looking up: '%s'") % (lookup)) + + woeidlookup = wrap(woeidlookup, ['text']) + + + # https://dev.twitter.com/docs/api/1.1/get/application/rate_limit_status + def ratelimits(self, irc, msg, args, optstatus): + """ + Display current rate limits for your twitter API account. + """ + + # statuses, search, trends, users + + try: + twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + except: + irc.reply("Failed to authorize with twitter.") + return + + try: + data = twitter.ApiCall('application/rate_limit_status', parameters={'resources':optstatus}) #', parameters={'id':woeid}) + except: + irc.reply("Failed to lookup rate limit data. Something might have gone wrong.") + 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.") + return + + # {u'trends': {u'/trends/available': {u'reset': 1351018255, u'limit': 15, u'remaining': 15}, u'/trends/closest': {u'reset': 1351018255, u'limit': 15, u'remaining': 15}, u'/trends/place': + # {u'reset': 1351018249, u'limit': 15, u'remaining': 14}}} + + irc.reply(data) + + ratelimits = wrap(ratelimits, [('somethingWithoutSpaces')]) + + + # https://dev.twitter.com/docs/api/1.1/get/trends/place + def trends(self, irc, msg, args, optwoeid): + """ + Returns the Top 10 Twitter trends for a specific location. Use optional argument location for trends, otherwise will use config variable. + """ + + # work with woeid. 1 is world, the default. can be set via input or via config. + if optwoeid: + try: + woeid = self._woeid_lookup(optwoeid) + except: + woeid = self.registryValue('woeid', msg.args[0]) # Where On Earth ID + else: + woeid = self.registryValue('woeid', msg.args[0]) # Where On Earth ID + + try: + twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + except: + irc.reply("Failed to authorize with twitter.") + return + + try: + data = twitter.ApiCall('trends/place', parameters={'id':woeid}) + except: + irc.reply("Failed to lookup trends data. Something might have gone wrong.") + return + + try: + location = data[0]['locations'][0]['name'] + except: + irc.reply("ERROR: Location not found for: %s" % optwoeid) # error also throws 404. + self.log.info('ERROR: Location not found for: %s' % optwoeid) + self.log.info('DATA: %s' % data) + return + + ttrends = string.join([trend['name'] for trend in data[0]['trends']], " | ") + + retvalue = "Top 10 Twitter Trends in {0} :: {1}".format(ircutils.bold(location), ttrends) + irc.reply(retvalue) + + trends = wrap(trends, [optional('text')]) + + # https://dev.twitter.com/docs/api/1.1/get/search/tweets + def tsearch(self, irc, msg, args, optlist, optterm): + """ [--num number] [--searchtype mixed,recent,popular] [--lang xx] + + Searches Twitter for the and returns the most recent results. + Number is number of results. Must be a number higher than 0 and max 10. + searchtype being recent, popular or mixed. Popular is the default. + """ + + tsearchArgs = {'include_entities':'false', 'count': self.registryValue('defaultResults', msg.args[0]), 'lang':'en', 'q':urllib.quote(optterm)} + + if optlist: + for (key, value) in optlist: + if key == 'num': + max = self.registryValue('maxResults', msg.args[0]) + if args['num'] > max or args['num'] <= 0: + irc.reply("Error: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(args['num'], max)) + return + else: + tsearchArgs['count'] = value + if key == 'searchtype': + tsearchArgs['result_type'] = value # limited by getopts to valid values. + if key == 'lang': + lang = value + tsearchArgs['lang'] = lang # lang . Uses ISO-639 codes like 'en' http://en.wikipedia.org/wiki/ISO_639-1 + + try: + twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + except: + irc.reply("Failed to authorize with twitter.") + return + + try: + data = twitter.ApiCall('search/tweets', parameters=tsearchArgs) + except: + irc.reply("Failed to get search results. Twitter broken?") + return + + results = data.get('statuses', None) # data returned as a dict + + if not results or len(results) == 0: + irc.reply("Error: No Twitter Search results found for '{0}'".format(optterm)) + return + else: + for result in results: + nick = result['user']['name'].encode('utf-8') + name = result["user"]['screen_name'].encode('utf-8') + text = self._unescape(result["text"]).encode('utf-8') + date = self._time_created_at(result["created_at"]) + tweetid = result["id_str"] + self._outputTweet(irc, msg, nick, name, text, date, tweetid) + + tsearch = wrap(tsearch, [getopts({'num':('int'), 'searchtype':('literal', ('popular', 'mixed', 'recent')), 'lang':('something')}), ('text')]) + + + def twitter(self, irc, msg, args, optlist, optnick): + """[--reply] [--rt] [--num number] | <--id id> | [--info nick] + + Returns last tweet or 'number' tweets (max 10). Only replies tweets that are + @replies or retweets if specified with the appropriate arguments. + Or returns tweet with id 'id'. + Or returns information on user with --info. + """ + + # INFO https://dev.twitter.com/docs/api/1.1/get/users/show + # ID https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid + # TIMELINE https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline + + optnick = optnick.replace('@','') # strip @ from usernames + + args = {'id': False, 'rt': False, 'reply': False, 'num': self.registryValue('defaultResults', msg.args[0]), 'info': False} + + # handle input optlist. + if optlist: + for (key, value) in optlist: + if key == 'id': + args['id'] = True + if key == 'rt': + args['rt'] = True + if key == 'reply': + args['reply'] = True + if key == 'num': + if value > self.registryValue('maxResults', msg.args[0]) or value <= 0: + irc.reply("Error: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(value, max)) + return + else: + args['num'] = value + if key == 'info': + args['info'] = True + + # handle the three different rest api endpoint urls + twitterArgs dict for options. + if args['id']: + apiUrl = 'statuses/show' + twitterArgs = {'id': optnick, 'include_entities':'false'} + elif args['info']: + apiUrl = 'users/show' + twitterArgs = {'screen_name': optnick, 'include_entities':'false'} + else: + apiUrl = 'statuses/user_timeline' + twitterArgs = {'screen_name': optnick, 'count': args['num']} + if args['rt']: # When set to false, the timeline will strip any native retweets + twitterArgs['include_rts'] = 'true' + else: + twitterArgs['include_rts'] = 'false' + + if args['reply']: # This parameter will prevent replies from appearing in the returned timeline. + twitterArgs['exclude_replies'] = 'false' + else: + twitterArgs['exclude_replies'] = 'true' # testing. default to true. + + # now with and call the api. + try: + twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + except: + irc.reply("Failed to authorize with twitter.") + return + + try: + data = twitter.ApiCall(apiUrl, parameters=twitterArgs) + except: + irc.reply("Failed to get user's timeline. Twitter broken?") + return + + #self.log.info(str(data)) + + # process the data. + if args['id']: # If --id was given for a single tweet. + text = self._unescape(data.get('text', None).encode('utf-8')) + nick = data["user"]["screen_name"].encode('utf-8') + name = data["user"]["name"].encode('utf-8') + relativeTime = self._time_created_at(data.get('created_at', None)) + tweetid = data.get('id', None) + self._outputTweet(irc, msg, nick, name, text, relativeTime, tweetid) + return + + + elif args['info']: # Works with --info to return info on a Twitter user. + location = data.get('location', None) + followers = data.get('followers_count', None) + friends = data.get('friends_count', None) + description = data.get('description', None) + screen_name = data.get('screen_name', None) + name = data.get('name', None) + url = data.get('url', None) + + # build ret, output string + ret = ircutils.underline(ircutils.bold("@" + optnick)) + ret += " ({0}):".format(name.encode('utf-8')) + if url: + ret += " {0}".format(ircutils.underline(url.encode('utf-8'))) + if description: + ret += " {0}".format(description.encode('utf-8')) + ret += " [{0} friends,".format(ircutils.bold(friends)) + ret += " {0} followers.".format(ircutils.bold(followers)) + if location: + ret += " Location: {0}]".format(location.encode('utf-8')) + else: + ret += "]" + + irc.reply(ret) + return + + else: # Else, its the user's timeline. Count is handled above in the GET request. + if len(data) == 0: + irc.reply("User: {0} has not tweeted yet.".format(optnick)) + return + + for tweet in data: + text = self._unescape(tweet.get('text', None)).encode('utf-8') + nick = tweet["user"]["screen_name"].encode('utf-8') + name = tweet["user"]["name"].encode('utf-8') + tweetid = tweet.get('id', None) + relativeTime = self._time_created_at(tweet.get('created_at', None)) + self._outputTweet(irc, msg, nick, name, text, relativeTime, tweetid) + + twitter = wrap(twitter, [getopts({'reply':'', 'rt': '', 'info': '', 'id': '', 'num': ('int')}), ('something')]) + +Class = Tweety + + +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=279: diff --git a/test.py b/test.py new file mode 100644 index 0000000..bf7a23b --- /dev/null +++ b/test.py @@ -0,0 +1,38 @@ +# coding=utf8 +### +# Copyright (c) 2011, Terje Hoås +# 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. + +### + +from supybot.test import * + +class TweetyTestCase(PluginTestCase): + plugins = ('Tweety',) + + +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From 816cc4b6bbdfeca938401da4178f5f66fdd45942 Mon Sep 17 00:00:00 2001 From: spline Date: Thu, 25 Oct 2012 18:56:08 -0400 Subject: [PATCH 03/63] Misc fixes in tsearch. We also add in separate config variables for max/search results. --- plugin.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/plugin.py b/plugin.py index c652cec..04f3cec 100644 --- a/plugin.py +++ b/plugin.py @@ -388,6 +388,8 @@ class Tweety(callbacks.Plugin): # 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 def ratelimits(self, irc, msg, args, optstatus): """ Display current rate limits for your twitter API account. @@ -472,22 +474,21 @@ class Tweety(callbacks.Plugin): searchtype being recent, popular or mixed. Popular is the default. """ - tsearchArgs = {'include_entities':'false', 'count': self.registryValue('defaultResults', msg.args[0]), 'lang':'en', 'q':urllib.quote(optterm)} + tsearchArgs = {'include_entities':'false', 'count': self.registryValue('defaultSearchResults', msg.args[0]), 'lang':'en', 'q':urllib.quote(optterm)} if optlist: for (key, value) in optlist: if key == 'num': - max = self.registryValue('maxResults', msg.args[0]) - if args['num'] > max or args['num'] <= 0: - irc.reply("Error: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(args['num'], max)) + max = self.registryValue('maxSearchResults', msg.args[0]) + if value > max or value <= 0: + irc.reply("Error: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(value, max)) return else: tsearchArgs['count'] = value if key == 'searchtype': tsearchArgs['result_type'] = value # limited by getopts to valid values. - if key == 'lang': - lang = value - tsearchArgs['lang'] = lang # lang . Uses ISO-639 codes like 'en' http://en.wikipedia.org/wiki/ISO_639-1 + if key == 'lang': # lang . Uses ISO-639 codes like 'en' http://en.wikipedia.org/wiki/ISO_639-1 + tsearchArgs['lang'] = value try: twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) @@ -510,12 +511,12 @@ class Tweety(callbacks.Plugin): for result in results: nick = result['user']['name'].encode('utf-8') name = result["user"]['screen_name'].encode('utf-8') - text = self._unescape(result["text"]).encode('utf-8') - date = self._time_created_at(result["created_at"]) - tweetid = result["id_str"] + text = self._unescape(result.get('text', None).encode('utf-8')) # look also at the unicode strip here. + date = self._time_created_at(result.get('created_at', None)) + tweetid = result.get('id_str', None) self._outputTweet(irc, msg, nick, name, text, date, tweetid) - tsearch = wrap(tsearch, [getopts({'num':('int'), 'searchtype':('literal', ('popular', 'mixed', 'recent')), 'lang':('something')}), ('text')]) + tsearch = wrap(tsearch, [getopts({'num':('int'), 'searchtype':('literal', ('popular', 'mixed', 'recent')), 'lang':('somethingWithoutSpaces')}), ('text')]) def twitter(self, irc, msg, args, optlist, optnick): From 9a92017ba9da1b08d9f6a47a83e76a8a0590fcce Mon Sep 17 00:00:00 2001 From: spline Date: Thu, 25 Oct 2012 20:23:43 -0400 Subject: [PATCH 04/63] Moved some of the encodes to make the get cleaner. --- plugin.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/plugin.py b/plugin.py index 04f3cec..d11bf6d 100644 --- a/plugin.py +++ b/plugin.py @@ -509,12 +509,12 @@ class Tweety(callbacks.Plugin): return else: for result in results: - nick = result['user']['name'].encode('utf-8') - name = result["user"]['screen_name'].encode('utf-8') - text = self._unescape(result.get('text', None).encode('utf-8')) # look also at the unicode strip here. + nick = result['user'].get('screen_name', None) + name = result["user"].get('name', None) + text = self._unescape(result.get('text', None) # look also at the unicode strip here. date = self._time_created_at(result.get('created_at', None)) tweetid = result.get('id_str', None) - self._outputTweet(irc, msg, nick, name, text, date, tweetid) + self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), date, tweetid) tsearch = wrap(tsearch, [getopts({'num':('int'), 'searchtype':('literal', ('popular', 'mixed', 'recent')), 'lang':('somethingWithoutSpaces')}), ('text')]) @@ -586,17 +586,15 @@ class Tweety(callbacks.Plugin): except: irc.reply("Failed to get user's timeline. Twitter broken?") return - - #self.log.info(str(data)) # process the data. if args['id']: # If --id was given for a single tweet. - text = self._unescape(data.get('text', None).encode('utf-8')) - nick = data["user"]["screen_name"].encode('utf-8') - name = data["user"]["name"].encode('utf-8') + text = self._unescape(data.get('text', None)) + nick = data["user"].get('screen_name', None) + name = data["user"].get('name', None) relativeTime = self._time_created_at(data.get('created_at', None)) tweetid = data.get('id', None) - self._outputTweet(irc, msg, nick, name, text, relativeTime, tweetid) + self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) return @@ -632,12 +630,12 @@ class Tweety(callbacks.Plugin): return for tweet in data: - text = self._unescape(tweet.get('text', None)).encode('utf-8') - nick = tweet["user"]["screen_name"].encode('utf-8') - name = tweet["user"]["name"].encode('utf-8') + text = self._unescape(tweet.get('text', None)) + nick = tweet["user"].get('screen_name', None) + name = tweet["user"].get('name', None) tweetid = tweet.get('id', None) relativeTime = self._time_created_at(tweet.get('created_at', None)) - self._outputTweet(irc, msg, nick, name, text, relativeTime, tweetid) + self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) twitter = wrap(twitter, [getopts({'reply':'', 'rt': '', 'info': '', 'id': '', 'num': ('int')}), ('something')]) From ff0bf344f16a972421f9acd74b8e51e004079f73 Mon Sep 17 00:00:00 2001 From: spline Date: Thu, 25 Oct 2012 20:29:37 -0400 Subject: [PATCH 05/63] Finished up the nort/noreply. I reversed the default behavior because most people want RT/reply by default. --- plugin.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/plugin.py b/plugin.py index d11bf6d..8b3e608 100644 --- a/plugin.py +++ b/plugin.py @@ -511,7 +511,7 @@ class Tweety(callbacks.Plugin): for result in results: nick = result['user'].get('screen_name', None) name = result["user"].get('name', None) - text = self._unescape(result.get('text', None) # look also at the unicode strip here. + text = self._unescape(result.get('text', None)) # look also at the unicode strip here. date = self._time_created_at(result.get('created_at', None)) tweetid = result.get('id_str', None) self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), date, tweetid) @@ -520,10 +520,10 @@ class Tweety(callbacks.Plugin): def twitter(self, irc, msg, args, optlist, optnick): - """[--reply] [--rt] [--num number] | <--id id> | [--info nick] + """[--noreply] [--nort] [--num number] | <--id id> | [--info nick] - Returns last tweet or 'number' tweets (max 10). Only replies tweets that are - @replies or retweets if specified with the appropriate arguments. + Returns last tweet or 'number' tweets (max 10). Shows all tweets, including rt and reply. + To not display replies or RT's, use --noreply or --nort, respectively. Or returns tweet with id 'id'. Or returns information on user with --info. """ @@ -532,19 +532,19 @@ class Tweety(callbacks.Plugin): # ID https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid # TIMELINE https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline - optnick = optnick.replace('@','') # strip @ from usernames + optnick = optnick.replace('@','') # strip @ from input if given. - args = {'id': False, 'rt': False, 'reply': False, 'num': self.registryValue('defaultResults', msg.args[0]), 'info': False} + args = {'id': False, 'nort': False, 'noreply': False, 'num': self.registryValue('defaultResults', msg.args[0]), 'info': False} # handle input optlist. if optlist: for (key, value) in optlist: if key == 'id': args['id'] = True - if key == 'rt': - args['rt'] = True - if key == 'reply': - args['reply'] = True + if key == 'nort': + args['nort'] = True + if key == 'noreply': + args['noreply'] = True if key == 'num': if value > self.registryValue('maxResults', msg.args[0]) or value <= 0: irc.reply("Error: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(value, max)) @@ -564,15 +564,15 @@ class Tweety(callbacks.Plugin): else: apiUrl = 'statuses/user_timeline' twitterArgs = {'screen_name': optnick, 'count': args['num']} - if args['rt']: # When set to false, the timeline will strip any native retweets - twitterArgs['include_rts'] = 'true' - else: + if args['nort']: # When set to false, the timeline will strip any native retweets twitterArgs['include_rts'] = 'false' - - if args['reply']: # This parameter will prevent replies from appearing in the returned timeline. - twitterArgs['exclude_replies'] = 'false' else: - twitterArgs['exclude_replies'] = 'true' # testing. default to true. + twitterArgs['include_rts'] = 'true' + + if args['noreply']: # This parameter will prevent replies from appearing in the returned timeline. + twitterArgs['exclude_replies'] = 'true' + else: + twitterArgs['exclude_replies'] = 'false' # testing. default to true. # now with and call the api. try: @@ -637,7 +637,7 @@ class Tweety(callbacks.Plugin): relativeTime = self._time_created_at(tweet.get('created_at', None)) self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) - twitter = wrap(twitter, [getopts({'reply':'', 'rt': '', 'info': '', 'id': '', 'num': ('int')}), ('something')]) + twitter = wrap(twitter, [getopts({'noreply':'', 'nort': '', 'info': '', 'id': '', 'num': ('int')}), ('something')]) Class = Tweety From 0e0a14db45d0feecf06c474557712bb00340986f Mon Sep 17 00:00:00 2001 From: spline Date: Thu, 25 Oct 2012 21:16:48 -0400 Subject: [PATCH 06/63] Add in color option + more cleanups. --- plugin.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/plugin.py b/plugin.py index 8b3e608..1e75daa 100644 --- a/plugin.py +++ b/plugin.py @@ -253,6 +253,7 @@ class Tweety(callbacks.Plugin): def _unescape(self, text): + """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" text = text.replace("\n", " ") def fixup(m): text = m.group(0) @@ -317,15 +318,26 @@ class Tweety(callbacks.Plugin): Takes a group of strings and outputs a Tweet to IRC. Used for tsearch and twitter. """ - ret = ircutils.underline(ircutils.bold("@" + nick)) - hideName = self.registryValue('hideRealName', msg.args[0]) - if not hideName: + outputColorTweets = self.registryValue('outputColorTweets', msg.args[0]) + + if outputColorTweets: + ret = ircutils.underline(ircutils.mircColor(("@" + nick), 'blue')) + else: + ret = ircutils.underline(ircutils.bold("@" + nick)) + + if not self.registryValue('hideRealName', msg.args[0]): # show realname in tweet output? ret += " ({0})".format(name) - ret += ": {0} ({1})".format(text, ircutils.bold(time)) + + # add in the end with the text + tape + if outputColorTweets: + ret += ": {0} ({1})".format(text, ircutils.mircColor(time, 'yellow')) + else: + ret += ": {0} ({1})".format(text, ircutils.bold(time)) + if self.registryValue('addShortUrl', msg.args[0]): - url = self._createShortUrl(nick, tweetid) - if (url): + if self._createShortUrl(nick, tweetid): ret += " {0}".format(url) + irc.reply(ret) From b9751c9dcafc79bd0f54ef3d44c559aa80dbb840 Mon Sep 17 00:00:00 2001 From: spline Date: Thu, 25 Oct 2012 21:19:06 -0400 Subject: [PATCH 07/63] Need the updated config.py for color. --- config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index 17f34fb..7f116df 100644 --- a/config.py +++ b/config.py @@ -49,7 +49,11 @@ conf.registerGlobalValue(Tweety, 'accessSecret', registry.String('', """The Twit conf.registerChannelValue(Tweety, 'hideRealName', registry.Boolean(False, """Do not show real name when displaying tweets.""")) conf.registerChannelValue(Tweety, 'addShortUrl', registry.Boolean(False, """Whether or not to add a short URL to the tweets.""")) conf.registerChannelValue(Tweety, 'woeid', registry.Integer(1, """Where On Earth ID. World Wide is 1. USA is 23424977.""")) -conf.registerChannelValue(Tweety, 'defaultResults', registry.Integer(3, """Default number of results to return on searches.""")) -conf.registerChannelValue(Tweety, 'maxResults', registry.Integer(10, """Maximum number of results to return on searches and lookups.""")) +conf.registerChannelValue(Tweety, 'defaultSearchResults', registry.Integer(3, """Default number of results to return on searches.""")) +conf.registerChannelValue(Tweety, 'maxSearchResults', registry.Integer(10, """Maximum number of results to return on searches""")) +conf.registerChannelValue(Tweety, 'defaultResults', registry.Integer(1, """Default number of results to return on timelines.""")) +conf.registerChannelValue(Tweety, 'maxResults', registry.Integer(10, """Maximum number of results to return on timelines.""")) +conf.registerChannelValue(Tweety, 'outputColorTweets', registry.Boolean(False, """When outputting Tweets, display them with some color.""")) + # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From e4d10f07cb0452f407ff1260065974e2490a3a98 Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 26 Oct 2012 00:25:01 -0400 Subject: [PATCH 08/63] Fix up ratelimits a little. --- plugin.py | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/plugin.py b/plugin.py index 1e75daa..7c3ed1a 100644 --- a/plugin.py +++ b/plugin.py @@ -237,15 +237,14 @@ class OAuthApi: return json.loads(data) # now, begin our actual code. - +# APIDOCS https://dev.twitter.com/docs/api/1.1 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.""" threaded = True - # APIDOCS https://dev.twitter.com/docs/api/1.1 - + def _strip_accents(self, string): """Return a string containing the normalized ascii string.""" import unicodedata @@ -275,7 +274,7 @@ class Tweety(callbacks.Plugin): return text # leave as is 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. @@ -343,7 +342,7 @@ class Tweety(callbacks.Plugin): def _createShortUrl(self, nick, tweetid): """ - Takes an input URL and returns a shortened URL via is.gd service. + Takes a nick and tweetid and returns a shortened URL via is.gd service. """ longurl = "https://twitter.com/#!/{0}/status/{1}".format(nick, tweetid) @@ -398,17 +397,15 @@ class Tweety(callbacks.Plugin): woeidlookup = wrap(woeidlookup, ['text']) - + # 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 - def ratelimits(self, irc, msg, args, optstatus): + def ratelimits(self, irc, msg, args): """ Display current rate limits for your twitter API account. """ - # statuses, search, trends, users - try: twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) except: @@ -416,23 +413,29 @@ class Tweety(callbacks.Plugin): return try: - data = twitter.ApiCall('application/rate_limit_status', parameters={'resources':optstatus}) #', parameters={'id':woeid}) + data = twitter.ApiCall('application/rate_limit_status') #, parameters={'resources':optstatus}) except: irc.reply("Failed to lookup rate limit data. Something might have gone wrong.") 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) return - - # {u'trends': {u'/trends/available': {u'reset': 1351018255, u'limit': 15, u'remaining': 15}, u'/trends/closest': {u'reset': 1351018255, u'limit': 15, u'remaining': 15}, u'/trends/place': - # {u'reset': 1351018249, u'limit': 15, u'remaining': 14}}} - - irc.reply(data) - ratelimits = wrap(ratelimits, [('somethingWithoutSpaces')]) + # we only have resources needed in here. def below works with each entry properly. + resourcelist = ['trends/place', 'search/tweets', 'users/show/:id', 'statuses/show/:id', 'statuses/user_timeline/:id'] + + for resource in resourcelist: + family, endpoint = resource.split('/', 1) # need to split each entry on /, resource family is [0], append / to entry. + resourcedict = data.get(family, None) + endpoint = resourcedict.get("/"+resource, None) + irc.reply("{0} :: {1}".format(resource, endpoint)) + # endpoint is {u'reset': 1351226072, u'limit': 15, u'remaining': 15} + + ratelimits = wrap(ratelimits) # https://dev.twitter.com/docs/api/1.1/get/trends/place @@ -531,6 +534,9 @@ class Tweety(callbacks.Plugin): tsearch = wrap(tsearch, [getopts({'num':('int'), 'searchtype':('literal', ('popular', 'mixed', 'recent')), 'lang':('somethingWithoutSpaces')}), ('text')]) + # INFO https://dev.twitter.com/docs/api/1.1/get/users/show + # ID https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid + # TIMELINE https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline def twitter(self, irc, msg, args, optlist, optnick): """[--noreply] [--nort] [--num number] | <--id id> | [--info nick] @@ -539,11 +545,7 @@ class Tweety(callbacks.Plugin): Or returns tweet with id 'id'. Or returns information on user with --info. """ - - # INFO https://dev.twitter.com/docs/api/1.1/get/users/show - # ID https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid - # TIMELINE https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline - + optnick = optnick.replace('@','') # strip @ from input if given. args = {'id': False, 'nort': False, 'noreply': False, 'num': self.registryValue('defaultResults', msg.args[0]), 'info': False} @@ -584,7 +586,7 @@ class Tweety(callbacks.Plugin): if args['noreply']: # This parameter will prevent replies from appearing in the returned timeline. twitterArgs['exclude_replies'] = 'true' else: - twitterArgs['exclude_replies'] = 'false' # testing. default to true. + twitterArgs['exclude_replies'] = 'false' # now with and call the api. try: From 1a0e6546c9d30ea2fe64d7be1e03ea7a610862c2 Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 26 Oct 2012 00:26:11 -0400 Subject: [PATCH 09/63] Add in some TODOs. --- plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin.py b/plugin.py index 7c3ed1a..3e34653 100644 --- a/plugin.py +++ b/plugin.py @@ -238,6 +238,10 @@ class OAuthApi: # 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? + 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 From 9b58ae83825951e3d0865af1c6faeb8753960fb5 Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 26 Oct 2012 00:36:15 -0400 Subject: [PATCH 10/63] Improve strip_accents --- plugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 3e34653..741aa7f 100644 --- a/plugin.py +++ b/plugin.py @@ -252,7 +252,10 @@ class Tweety(callbacks.Plugin): def _strip_accents(self, string): """Return a string containing the normalized ascii string.""" import unicodedata - return unicodedata.normalize('NFKD', unicode(string)).encode('ASCII', 'ignore') + if isinstance(s, unicode): + return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') + else: + return s def _unescape(self, text): From ab48ce05df4e4f8c55b0d9a26ec8c5aaced0fd89 Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 26 Oct 2012 02:27:02 -0400 Subject: [PATCH 11/63] Add a little more --- plugin.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugin.py b/plugin.py index 741aa7f..ccef25e 100644 --- a/plugin.py +++ b/plugin.py @@ -241,6 +241,7 @@ class OAuthApi: # 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 @@ -257,6 +258,25 @@ class Tweety(callbacks.Plugin): else: return s + + #def _checkCredentials(self): + #apiKey = self.registryValue('ffApiKey') + #if not apiKey or apiKey == "Not set": + # irc.reply("API key not set. see 'config help supybot.plugins.NFL.ffApiKey'.") + # return + # twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + + + def _highlightUrl(self, text): + URL_REGEX = compile_regex('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+') + + + def _encode(string): + try: + return string.encode(stdout.encoding, 'replace') + except AttributeError: + return string + def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" From ea5b33638af9be5cd90dda3807a8bd4a33d53a6a Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 26 Oct 2012 06:52:35 -0400 Subject: [PATCH 12/63] Add in color url support in text for tweets. --- plugin.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugin.py b/plugin.py index ccef25e..960d966 100644 --- a/plugin.py +++ b/plugin.py @@ -267,11 +267,7 @@ class Tweety(callbacks.Plugin): # twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) - def _highlightUrl(self, text): - URL_REGEX = compile_regex('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+') - - - def _encode(string): + def _encode(self, string): try: return string.encode(stdout.encoding, 'replace') except AttributeError: @@ -356,8 +352,9 @@ class Tweety(callbacks.Plugin): # add in the end with the text + tape if outputColorTweets: + text = re.sub(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)', ircutils.mircColor(r'\1', 'red'), text) # color urls. ret += ": {0} ({1})".format(text, ircutils.mircColor(time, 'yellow')) - else: + else: ret += ": {0} ({1})".format(text, ircutils.bold(time)) if self.registryValue('addShortUrl', msg.args[0]): From 2cfbf3d654fbf36907a207183bce97f082cdd735 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 27 Oct 2012 12:49:23 -0400 Subject: [PATCH 13/63] Add in tests for keys. --- plugin.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/plugin.py b/plugin.py index 960d966..62125ee 100644 --- a/plugin.py +++ b/plugin.py @@ -249,22 +249,38 @@ class Tweety(callbacks.Plugin): trending tweets.""" threaded = True - - def _strip_accents(self, string): - """Return a string containing the normalized ascii string.""" - import unicodedata - if isinstance(s, unicode): - return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore') - else: - return s + def __init__(self, irc): + self.__parent = super(Tweety, self) + self.__parent.__init__(irc) + haveAuthKeys = self._checkCredentials() + def _checkCredentials(self): + failTest = False + checkKeys = ['consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'] + for checkKey in checkKeys: + try: + testKey = self.registryValue(checkKey) + except: + failTest = True + break + + if failTest: + self.log.info('Failed') + return False + else: + self.log.info('Passed') + return True + + #def re_encode(input_string, decoder = 'utf-8', encoder = 'utf=8'): + #try: + #output_string = unicodedata.normalize('NFD',\ + #input_string.decode(decoder)).encode(encoder) + #except UnicodeError: + #output_string = unicodedata.normalize('NFD',\ + #input_string.decode('ascii', 'replace')).encode(encoder) + #return output_string - #def _checkCredentials(self): - #apiKey = self.registryValue('ffApiKey') - #if not apiKey or apiKey == "Not set": - # irc.reply("API key not set. see 'config help supybot.plugins.NFL.ffApiKey'.") - # return - # twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + def _encode(self, string): From b8ef2b72e796a97c4e8c9159aca12516aa8c4836 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 27 Oct 2012 12:56:30 -0400 Subject: [PATCH 14/63] Streamline checkCredentials on suggestion from ProgVal. --- plugin.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/plugin.py b/plugin.py index 62125ee..d7b3737 100644 --- a/plugin.py +++ b/plugin.py @@ -256,8 +256,7 @@ class Tweety(callbacks.Plugin): def _checkCredentials(self): failTest = False - checkKeys = ['consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'] - for checkKey in checkKeys: + for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'): try: testKey = self.registryValue(checkKey) except: @@ -280,16 +279,6 @@ class Tweety(callbacks.Plugin): #input_string.decode('ascii', 'replace')).encode(encoder) #return output_string - - - - def _encode(self, string): - try: - return string.encode(stdout.encoding, 'replace') - except AttributeError: - return string - - def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" text = text.replace("\n", " ") From f40a232155d3d038f98baf5b024e9e522f5dfa09 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 27 Oct 2012 14:54:17 -0400 Subject: [PATCH 15/63] More fixes. --- plugin.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/plugin.py b/plugin.py index d7b3737..fbf4a9a 100644 --- a/plugin.py +++ b/plugin.py @@ -255,6 +255,7 @@ class Tweety(callbacks.Plugin): haveAuthKeys = self._checkCredentials() def _checkCredentials(self): + """Check for all 4 requires keys on Twitter auth.""" failTest = False for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'): try: @@ -264,10 +265,10 @@ class Tweety(callbacks.Plugin): break if failTest: - self.log.info('Failed') + self.log.error('Failed getting keys') return False else: - self.log.info('Passed') + self.log.info('Passed getting keys') return True #def re_encode(input_string, decoder = 'utf-8', encoder = 'utf=8'): @@ -425,6 +426,7 @@ class Tweety(callbacks.Plugin): irc.reply(("Something broke while looking up: '%s'") % (lookup)) woeidlookup = wrap(woeidlookup, ['text']) + # RATELIMITING # https://dev.twitter.com/docs/api/1.1/get/application/rate_limit_status @@ -497,17 +499,17 @@ class Tweety(callbacks.Plugin): try: location = data[0]['locations'][0]['name'] except: - irc.reply("ERROR: Location not found for: %s" % optwoeid) # error also throws 404. - self.log.info('ERROR: Location not found for: %s' % optwoeid) - self.log.info('DATA: %s' % data) + irc.reply("ERROR: Cannot load trends: {0}".format(data)) # error also throws 404. + self.log.info("Trends error data: {0}".format(data)) return - ttrends = string.join([trend['name'] for trend in data[0]['trends']], " | ") + ttrends = string.join([trend['name'].encode('utf-8') for trend in data[0]['trends']], " | ") retvalue = "Top 10 Twitter Trends in {0} :: {1}".format(ircutils.bold(location), ttrends) irc.reply(retvalue) trends = wrap(trends, [optional('text')]) + # https://dev.twitter.com/docs/api/1.1/get/search/tweets def tsearch(self, irc, msg, args, optlist, optterm): @@ -629,6 +631,14 @@ class Tweety(callbacks.Plugin): except: irc.reply("Failed to get user's timeline. Twitter broken?") return + + # final sanity check for json + try: + data = json.loads(data) + except: + irc.reply("ERROR: Failed to parse data from Twitter: {0}".format(data)) + self.log.error(str(data)) + return # process the data. if args['id']: # If --id was given for a single tweet. From 961f0ddf048d3e51b5a8a54d535df93e258db383 Mon Sep 17 00:00:00 2001 From: spline Date: Wed, 28 Nov 2012 14:55:39 -0500 Subject: [PATCH 16/63] Add in option for expanding short urls. Code not in plugin.py, yet. Leaves open for future functionality. --- config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config.py b/config.py index 7f116df..c2bb5fc 100644 --- a/config.py +++ b/config.py @@ -54,6 +54,7 @@ conf.registerChannelValue(Tweety, 'maxSearchResults', registry.Integer(10, """Ma conf.registerChannelValue(Tweety, 'defaultResults', registry.Integer(1, """Default number of results to return on timelines.""")) conf.registerChannelValue(Tweety, 'maxResults', registry.Integer(10, """Maximum number of results to return on timelines.""")) conf.registerChannelValue(Tweety, 'outputColorTweets', registry.Boolean(False, """When outputting Tweets, display them with some color.""")) +conf.registerChannelValue(Tweety, 'expandShortUrls', registry.Boolean(False, """When outputting Tweets, expand short urls (like from t.co, etc.).""")) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From a718e301443b8ea877adbcb46e30cedfc9809b50 Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 30 Nov 2012 12:09:09 -0500 Subject: [PATCH 17/63] Latest --- README.txt | 46 +++++++++++++++++++--------------------------- plugin.py | 52 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/README.txt b/README.txt index 1e857f0..cfc2cd8 100644 --- a/README.txt +++ b/README.txt @@ -1,6 +1,25 @@ +Overview: Supply a username and get the latest tweet(s). Or an ID to a tweet. Or search on twitter. Or view latest trends. Does not require a user account or apikey or anything like that. Just point and shoot. This plugin does NOT relay tweets in real time. It only fetches data from Twitter when commands are called. +This is forked from Hoaas' Tweety plugin at: http://github.com/Hoaas/Supybot-Plugins to support Twitter API v1.1 and add in a few features: + +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 + +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. + 3. Click create an application. + 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 Examples: 12:38:02 <@Hoaas> !twitter cnn @@ -19,30 +38,3 @@ Examples: 12:40:05 <@Hoaas> !twitter --info TheScienceGuy 12:40:07 <@Bunisher> @TheScienceGuy (Bill Nye): http://billnye.com Science Educator seeks to change the world... 29 friends, 319377 followers. Los Angeles, CA, USA -12:42:00 <@Hoaas> !twitter --num 5 wilw -12:42:01 <@Bunisher> @wilw (Wil Wheaton): Holy crap, @amazonmp3 is doing some incredible deals until midnight PDT. Tons of great albums for a buck. (5 hours ago) -12:42:02 <@Bunisher> @wilw (Wil Wheaton): Drr…Drr…Drr. (5 hours ago) -12:42:03 <@Bunisher> @wilw (Wil Wheaton): Did I make another Robert Evans tweet, because it was amusing to me? You bet your ass I did, and I did it right there in front of everyone. (1 days ago) -12:42:04 <@Bunisher> @wilw (Wil Wheaton): (Kids, ask your parents. Then go find that weird relative who loves movies from the 70s and she'll explain it to you.) (1 days ago) -12:42:05 <@Bunisher> @wilw (Wil Wheaton): Do I write tweets that are rhetorical questions, and read them to myself in the voice of Robert Evans? You bet I do. (1 days ago) - -12:42:09 <@Hoaas> !twitter --rt --num 5 wilw -12:42:10 <@Bunisher> @wilw (Wil Wheaton): Holy crap, @amazonmp3 is doing some incredible deals until midnight PDT. Tons of great albums for a buck. (5 hours ago) -12:42:12 <@Bunisher> @wilw (Wil Wheaton): Drr…Drr…Drr. (5 hours ago) -12:42:14 <@Bunisher> @wilw (Wil Wheaton): RT @The_RobertEvans: @wilw Am I a fan of Wil Wheaton? "You bet your ass I am. " (7 hours ago) -12:42:15 <@Bunisher> @wilw (Wil Wheaton): RT @amazonmp3: Hear one of the finest voices in rock on the latest Florence + the Machine album for $.99 thru midnight: http://t.co/gEXg5Tbb (5 hours ago) -12:42:16 <@Bunisher> @wilw (Wil Wheaton): RT @amazonmp3: Beach House's Bloom is super dreamy. For $.99 you don't have to take our word for it. Ends midnight Pacific: http://t.co/ ... (6 hours ago) - -12:41:38 <@Hoaas> !twitter --reply wilw -12:41:40 <@Bunisher> @wilw (Wil Wheaton): @chicazul ugh. Region locking makes baby jeebus cry ... until he gets a VPN. (4 hours ago) - -12:43:13 <@Hoaas> !twitter --rt --reply --num 5 wilw -12:43:14 <@Bunisher> @wilw (Wil Wheaton): @chicazul ugh. Region locking makes baby jeebus cry ... until he gets a VPN. (4 hours ago) -12:43:15 <@Bunisher> @wilw (Wil Wheaton): @amazonmp3 thanks for the rockin' sale :) (4 hours ago) -12:43:16 <@Bunisher> @wilw (Wil Wheaton): Holy crap, @amazonmp3 is doing some incredible deals until midnight PDT. Tons of great albums for a buck. (5 hours ago) -12:43:18 <@Bunisher> @wilw (Wil Wheaton): @undeux USA! USA! USA! (5 hours ago) -12:43:19 <@Bunisher> @wilw (Wil Wheaton): RT @amazonmp3: Hear one of the finest voices in rock on the latest Florence + the Machine album for $.99 thru midnight: http://t.co/gEXg5Tbb (5 hours ago) - - -12:40:29 <@Hoaas> !tagdef ff -12:40:31 <@Bunisher> Tagdef: #ff #ff is the same as (short for) #followfriday. http://tagdef.com/ff diff --git a/plugin.py b/plugin.py index fbf4a9a..ec2d528 100644 --- a/plugin.py +++ b/plugin.py @@ -46,8 +46,10 @@ from datetime import tzinfo, datetime, timedelta # for unescape import re, htmlentitydefs +# reencode +import unicodedata + # oauthtwitter -import time import urlparse import oauth2 as oauth @@ -101,7 +103,7 @@ class OAuthApi: http_method=http_method) # Get a url opener that can handle Oauth basic auth - callbacks.log.info(str(extra_params)) + #callbacks.log.info(str(extra_params)) opener = self._GetOpener() if http_method == "POST": @@ -113,7 +115,7 @@ class OAuthApi: url = req.to_url() encoded_post_data = "" - callbacks.log.info(str(url)) + #callbacks.log.info(str(url)) if encoded_post_data: url_data = opener.open(url, encoded_post_data).read() @@ -254,6 +256,7 @@ class Tweety(callbacks.Plugin): self.__parent.__init__(irc) haveAuthKeys = self._checkCredentials() + def _checkCredentials(self): """Check for all 4 requires keys on Twitter auth.""" failTest = False @@ -261,16 +264,33 @@ class Tweety(callbacks.Plugin): try: testKey = self.registryValue(checkKey) except: + self.log.debug("Failed checking keys. We're missing the config value for: {0}. Please set this and try again.".format(checkKey)) failTest = True break if failTest: - self.log.error('Failed getting keys') + self.log.error('Failed getting keys. You must set all 4 keys in config variables.') return False else: - self.log.info('Passed getting keys') return True - + + + def _expandLinks(self, tweet): + # if not surl.startswith('http://') and not surl.startswith('https://'): + _tco_link_re = re.compile(u'http://t.co/[a-zA-Z0-9]+') + try: + req_url = 'http://api.longurl.org/v2/expand?format=json&url=' + qurl + req = urllib2.Request(req_url, headers={'User-Agent': 'Python-longurl/1.0'}) + lookup = json.loads(urllib2.urlopen(req).read()) + return lookup.get('long-url', None) + except urllib2.HTTPError as e: + self.log.debug('http error {0} when trying to shorten {1}'.format(e, qurl) + return None + except urllib2.URLError as e: + self.log.debug('http error {0} when trying to shorten {1}'.format(e, qurl) + return None + + #def re_encode(input_string, decoder = 'utf-8', encoder = 'utf=8'): #try: #output_string = unicodedata.normalize('NFD',\ @@ -279,6 +299,7 @@ class Tweety(callbacks.Plugin): #output_string = unicodedata.normalize('NFD',\ #input_string.decode('ascii', 'replace')).encode(encoder) #return output_string + def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" @@ -391,7 +412,6 @@ class Tweety(callbacks.Plugin): """ query = "select * from geo.places where text='%s'" % lookup - params = { "q": query, "format":"json", @@ -412,7 +432,10 @@ class Tweety(callbacks.Plugin): return None return woeid - + + ########################## + ### PUBLIC FUNCTIONS ##### + ########################## def woeidlookup(self, irc, msg, args, lookup): """[location] @@ -495,12 +518,13 @@ class Tweety(callbacks.Plugin): except: irc.reply("Failed to lookup trends data. Something might have gone wrong.") return + + #self.log.info(str(type(data))) try: location = data[0]['locations'][0]['name'] except: irc.reply("ERROR: Cannot load trends: {0}".format(data)) # error also throws 404. - self.log.info("Trends error data: {0}".format(data)) return ttrends = string.join([trend['name'].encode('utf-8') for trend in data[0]['trends']], " | ") @@ -631,15 +655,7 @@ class Tweety(callbacks.Plugin): except: irc.reply("Failed to get user's timeline. Twitter broken?") return - - # final sanity check for json - try: - data = json.loads(data) - except: - irc.reply("ERROR: Failed to parse data from Twitter: {0}".format(data)) - self.log.error(str(data)) - return - + # process the data. if args['id']: # If --id was given for a single tweet. text = self._unescape(data.get('text', None)) From 72b3af33a4d941f89ceff8c44c4f9af36bdac2db Mon Sep 17 00:00:00 2001 From: spline Date: Sun, 23 Dec 2012 16:36:34 -0500 Subject: [PATCH 18/63] Temp fix --- plugin.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/plugin.py b/plugin.py index ec2d528..f3b5fa8 100644 --- a/plugin.py +++ b/plugin.py @@ -283,13 +283,6 @@ class Tweety(callbacks.Plugin): req = urllib2.Request(req_url, headers={'User-Agent': 'Python-longurl/1.0'}) lookup = json.loads(urllib2.urlopen(req).read()) return lookup.get('long-url', None) - except urllib2.HTTPError as e: - self.log.debug('http error {0} when trying to shorten {1}'.format(e, qurl) - return None - except urllib2.URLError as e: - self.log.debug('http error {0} when trying to shorten {1}'.format(e, qurl) - return None - #def re_encode(input_string, decoder = 'utf-8', encoder = 'utf=8'): #try: From 471f54fb87e32b1cc9fa2f7430e930bf21fd856c Mon Sep 17 00:00:00 2001 From: spline Date: Sun, 23 Dec 2012 16:37:42 -0500 Subject: [PATCH 19/63] Temp fix #2 --- plugin.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugin.py b/plugin.py index f3b5fa8..f240c79 100644 --- a/plugin.py +++ b/plugin.py @@ -278,11 +278,10 @@ class Tweety(callbacks.Plugin): def _expandLinks(self, tweet): # if not surl.startswith('http://') and not surl.startswith('https://'): _tco_link_re = re.compile(u'http://t.co/[a-zA-Z0-9]+') - try: - req_url = 'http://api.longurl.org/v2/expand?format=json&url=' + qurl - req = urllib2.Request(req_url, headers={'User-Agent': 'Python-longurl/1.0'}) - lookup = json.loads(urllib2.urlopen(req).read()) - return lookup.get('long-url', None) + req_url = 'http://api.longurl.org/v2/expand?format=json&url=' + qurl + req = urllib2.Request(req_url, headers={'User-Agent': 'Python-longurl/1.0'}) + lookup = json.loads(urllib2.urlopen(req).read()) + return lookup.get('long-url', None) #def re_encode(input_string, decoder = 'utf-8', encoder = 'utf=8'): #try: From 11c84f81d0dce7e8e6696b47a099589194ffd6a8 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 12 Jan 2013 11:49:04 -0500 Subject: [PATCH 20/63] Clone README over to markdown. --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..44bb284 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +Supybot-Tweety +====== +*Twitter client for Supybot + +Overview +-------- +Supply a username and get the latest tweet(s). Or an ID to a tweet. Or search on twitter. Or view latest trends. Does not require a user account or apikey or anything like that. Just point and shoot. +This plugin does NOT relay tweets in real time. It only fetches data from Twitter when commands are called. + +This is forked from Hoaas' Tweety plugin at: http://github.com/Hoaas/Supybot-Plugins to support Twitter API v1.1 and add in a few features: + +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 + +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. + 3. Click create an application. + 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 + +Examples +-------- + +# Documentation + + From d509670646632d8218effa7185713fabb3e5e556 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 12 Jan 2013 11:50:03 -0500 Subject: [PATCH 21/63] rm README.txt --- README.txt | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 README.txt diff --git a/README.txt b/README.txt deleted file mode 100644 index cfc2cd8..0000000 --- a/README.txt +++ /dev/null @@ -1,40 +0,0 @@ -Overview: -Supply a username and get the latest tweet(s). Or an ID to a tweet. Or search on twitter. Or view latest trends. Does not require a user account or apikey or anything like that. Just point and shoot. -This plugin does NOT relay tweets in real time. It only fetches data from Twitter when commands are called. - -This is forked from Hoaas' Tweety plugin at: http://github.com/Hoaas/Supybot-Plugins to support Twitter API v1.1 and add in a few features: - -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 - -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. - 3. Click create an application. - 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 - -Examples: -12:38:02 <@Hoaas> !twitter cnn -12:38:03 <@Bunisher> @CNN (CNN): RT @AC360 Because of #AC360's investigation, Senate Finance Committee is demanding answers from a charity for disabled vets. Find out why 8p (10 hours ago) - -12:38:48 <@Hoaas> !twitter --num 5 RealTimeWWII -12:38:49 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): All French ships left in Mediterranean being ordered to attack UK warships on sight, thanks to yesterday's British attack at Mers-El-Kébir. (1 hour ago) -12:38:51 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): After negotiation, French ships at Alexandria have peacefully surrendered to Royal Navy- they've yet to hear of attack at Mers-el-Kébir. (16 hours ago) -12:38:54 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): As part of peace treaty, France must give Madagascar to Germany, so that all European Jews can be deported there: http://t.co/GiRq06O1 (15 hours ago) -12:38:55 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): 6.05PM British ships have stopped firing. 1,297 French sailors are dead. 4th largest Navy in the world is decimated. http://t.co/8Iob4NlK (17 hours ago) -12:38:56 <@Bunisher> @RealTimeWWII (WW2 Tweets from 1940): Trapped at anchor, French ships can't evade brutal shelling; only 2 in range to fire back. They can do nothing but die. http://t.co/6NDT5hDp (17 hours ago) - -12:39:43 <@Hoaas> !twitter --id 220454083016921088 -12:39:45 <@Bunisher> @TheScienceGuy (Bill Nye): The Higgs is real !?! The results are good to 5 standard deviations. Yikes. Whoa. Wow. This could change the world... (51 minutes ago) - -12:40:05 <@Hoaas> !twitter --info TheScienceGuy -12:40:07 <@Bunisher> @TheScienceGuy (Bill Nye): http://billnye.com Science Educator seeks to change the world... 29 friends, 319377 followers. Los Angeles, CA, USA - From 24031f9f1bd08d00fe94f01f614d98aea75e509b Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 12 Jan 2013 11:50:50 -0500 Subject: [PATCH 22/63] Updating my work here. Still in flux. Streamlining a ton here and centralizing the Twitter API call. Still unfinished. Updated trends with exclude. --- config.py | 32 ++--- plugin.py | 378 +++++++++++++++++++----------------------------------- 2 files changed, 149 insertions(+), 261 deletions(-) diff --git a/config.py b/config.py index c2bb5fc..ff4e406 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,6 @@ -# coding=utf8 +# -*- coding: utf-8 -*- ### -# Copyright (c) 2011, Terje Hoås +# Copyright (c) 2011-2013, Terje Hoås-spline # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -42,19 +42,19 @@ def configure(advanced): Tweety = conf.registerPlugin('Tweety') -conf.registerGlobalValue(Tweety, 'consumerKey', registry.String('', """The consumer key of the application.""")) -conf.registerGlobalValue(Tweety, 'consumerSecret', registry.String('', """The consumer secret of the application.""", private=True)) -conf.registerGlobalValue(Tweety, 'accessKey', registry.String('', """The Twitter Access Token key for the bot's account""")) -conf.registerGlobalValue(Tweety, 'accessSecret', registry.String('', """The Twitter Access Token secret for the bot's account""", private=True)) -conf.registerChannelValue(Tweety, 'hideRealName', registry.Boolean(False, """Do not show real name when displaying tweets.""")) -conf.registerChannelValue(Tweety, 'addShortUrl', registry.Boolean(False, """Whether or not to add a short URL to the tweets.""")) -conf.registerChannelValue(Tweety, 'woeid', registry.Integer(1, """Where On Earth ID. World Wide is 1. USA is 23424977.""")) -conf.registerChannelValue(Tweety, 'defaultSearchResults', registry.Integer(3, """Default number of results to return on searches.""")) -conf.registerChannelValue(Tweety, 'maxSearchResults', registry.Integer(10, """Maximum number of results to return on searches""")) -conf.registerChannelValue(Tweety, 'defaultResults', registry.Integer(1, """Default number of results to return on timelines.""")) -conf.registerChannelValue(Tweety, 'maxResults', registry.Integer(10, """Maximum number of results to return on timelines.""")) -conf.registerChannelValue(Tweety, 'outputColorTweets', registry.Boolean(False, """When outputting Tweets, display them with some color.""")) -conf.registerChannelValue(Tweety, 'expandShortUrls', registry.Boolean(False, """When outputting Tweets, expand short urls (like from t.co, etc.).""")) +conf.registerGlobalValue(Tweety,'consumerKey',registry.String('', """The consumer key of the application.""")) +conf.registerGlobalValue(Tweety,'consumerSecret',registry.String('', """The consumer secret of the application.""", private=True)) +conf.registerGlobalValue(Tweety,'accessKey',registry.String('', """The Twitter Access Token key for the bot's account""")) +conf.registerGlobalValue(Tweety,'accessSecret',registry.String('', """The Twitter Access Token secret for the bot's account""", private=True)) +conf.registerChannelValue(Tweety,'hideRealName',registry.Boolean(False, """Do not show real name when displaying tweets.""")) +conf.registerChannelValue(Tweety,'addShortUrl',registry.Boolean(False, """Whether or not to add a short URL to the tweets.""")) +conf.registerChannelValue(Tweety,'woeid',registry.Integer(1, """Where On Earth ID. World Wide is 1. USA is 23424977.""")) +conf.registerChannelValue(Tweety,'defaultSearchResults',registry.Integer(3, """Default number of results to return on searches.""")) +conf.registerChannelValue(Tweety,'maxSearchResults',registry.Integer(10, """Maximum number of results to return on searches""")) +conf.registerChannelValue(Tweety,'defaultResults',registry.Integer(1, """Default number of results to return on timelines.""")) +conf.registerChannelValue(Tweety,'maxResults',registry.Integer(10, """Maximum number of results to return on timelines.""")) +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=79: +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=250: diff --git a/plugin.py b/plugin.py index f240c79..661698f 100644 --- a/plugin.py +++ b/plugin.py @@ -1,6 +1,6 @@ -# coding=utf8 +# -*- coding: utf-8 -*- ### -# Copyright (c) 2011-2012, Terje Hoås, spline +# Copyright (c) 2011-2013, Terje Hoås, spline # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -29,38 +29,50 @@ ### +# my libs import urllib, urllib2 import json -import datetime import string +# libraries for time_created_at +import time +from datetime import tzinfo, datetime, timedelta +# for unescape +import re, htmlentitydefs +# reencode +import unicodedata +# oauthtwitter +import urlparse +import oauth2 as oauth + +#supybot libs import supybot.utils as utils from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks -#libraries for time_created_at -import time -from datetime import tzinfo, datetime, timedelta +# Twitter error classes from the sixohsix/Twitter API. +class TwitterError(Exception): + """Base Exception thrown by the Twitter object when there is a general error interacting with the API.""" + pass -# for unescape -import re, htmlentitydefs +class TwitterHTTPError(TwitterError): + """Exception thrown by the Twitter object when there is an HTTP error interacting with twitter.com.""" + def __init__(self, e, uri, format, uriparts): + self.e = e + self.uri = uri + self.format = format + self.uriparts = uriparts -# reencode -import unicodedata - -# oauthtwitter -import urlparse -import oauth2 as oauth + def __str__(self): + return ( + "Twitter sent status %i for URL: %s.%s using parameters: " + "(%s)\ndetails: %s" %( + self.e.code, self.uri, self.format, self.uriparts, + self.e.fp.read())) # OAuthApi class from https://github.com/jpittman/OAuth-Python-Twitter # mainly kept intact but modified for Twitter API v1.1 and unncessary things removed. - -REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' -ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' -AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' -SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' - class OAuthApi: def __init__(self, consumer_key, consumer_secret, token=None, token_secret=None): if token and token_secret: @@ -70,83 +82,26 @@ class OAuthApi: self._Consumer = oauth.Consumer(consumer_key, consumer_secret) self._signature_method = oauth.SignatureMethod_HMAC_SHA1() self._access_token = token - - def _GetOpener(self): - opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=1)) - return opener - def _FetchUrl(self, - url, - http_method=None, - parameters=None): - '''Fetch a URL, optionally caching for a specified time. - - Args: - url: The URL to retrieve - http_method: - One of "GET" or "POST" to state which kind - of http call is being made - parameters: - A dict whose key/value pairs should encoded and added - to the query string, or generated into post data. [OPTIONAL] - depending on the http_method parameter - - Returns: - A string containing the body of the response. - ''' - # Build the extra parameters dict + def _FetchUrl(self,url, http_method=None, parameters=None): extra_params = {} if parameters: - extra_params.update(parameters) + extra_params.update(parameters) - req = self._makeOAuthRequest(url, params=extra_params, - http_method=http_method) - - # Get a url opener that can handle Oauth basic auth - #callbacks.log.info(str(extra_params)) - opener = self._GetOpener() - - if http_method == "POST": - encoded_post_data = req.to_postdata() - # Removed the following line due to the fact that OAuth2 request objects do not have this function - # This does not appear to have any adverse impact on the operation of the toolset - #url = req.get_normalized_http_url() - else: - url = req.to_url() - encoded_post_data = "" - - #callbacks.log.info(str(url)) - - if encoded_post_data: - url_data = opener.open(url, encoded_post_data).read() - callbacks.log.debug(url) - else: - url_data = opener.open(url).read() - callbacks.log.debug(url) + req = self._makeOAuthRequest(url, params=extra_params, http_method=http_method) + 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() - - # Always return the latest version return url_data - def _makeOAuthRequest(self, url, token=None, - params=None, http_method="GET"): - '''Make a OAuth request from url and parameters - - Args: - url: The Url to use for creating OAuth Request - parameters: - The URL parameters - http_method: - The HTTP method to use - Returns: - A OAauthRequest object - ''' - + def _makeOAuthRequest(self, url, token=None, params=None, http_method="GET"): oauth_base_params = { - 'oauth_version': "1.0", - 'oauth_nonce': oauth.generate_nonce(), - 'oauth_timestamp': int(time.time()) - } + 'oauth_version': "1.0", + 'oauth_nonce': oauth.generate_nonce(), + 'oauth_timestamp': int(time.time()) + } if params: params.update(oauth_base_params) @@ -159,84 +114,19 @@ class OAuthApi: request.sign_request(self._signature_method, self._Consumer, token) return request - def getAuthorizationURL(self, token, url=AUTHORIZATION_URL): - '''Create a signed authorization URL - - Authorization provides the user with a VERIFIER which they may in turn provide to - the consumer. This key authorizes access. Used primarily for clients. - - Returns: - A signed OAuthRequest authorization URL - ''' - return "%s?oauth_token=%s" % (url, token['oauth_token']) - - def getAuthenticationURL(self, token, url=SIGNIN_URL, force_login=False): - '''Create a signed authentication URL - - Authentication allows a user to directly authorize Twitter access with a click. - Used primarily for web-apps. - - Returns: - A signed OAuthRequest authentication URL - ''' - auth_url = "%s?oauth_token=%s" % (url, token['oauth_token']) - if force_login: - auth_url += "&force_login=1" - return auth_url - - def getRequestToken(self, url=REQUEST_TOKEN_URL): - '''Get a Request Token from Twitter - - Returns: - A OAuthToken object containing a request token - ''' - resp, content = oauth.Client(self._Consumer).request(url, "GET") - if resp['status'] != '200': - raise Exception("Invalid response %s." % resp['status']) - - return dict(urlparse.parse_qsl(content)) - - def getAccessToken(self, token, verifier=None, url=ACCESS_TOKEN_URL): - '''Get a Request Token from Twitter - - Note: Verifier is required if you AUTHORIZED, it can be skipped if you AUTHENTICATED - - Returns: - A OAuthToken object containing a request token - ''' - token = oauth.Token(token['oauth_token'], token['oauth_token_secret']) - if verifier: - token.set_verifier(verifier) - client = oauth.Client(self._Consumer, token) - - resp, content = client.request(url, "POST") - return dict(urlparse.parse_qsl(content)) - def ApiCall(self, call, type="GET", parameters={}): - '''Calls the twitter API - - Args: - call: The name of the api call (ie. account/rate_limit_status) - type: One of "GET" or "POST" - parameters: Parameters to pass to the Twitter API call - Returns: - Returns the twitter.User object - ''' return_value = [] - # We use this try block to make the request in case we run into one of Twitter's many 503 (temporarily unavailable) errors. - # Other error handling may end up being useful as well. try: data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", type, parameters) - - # This is the most common error type you'll get. Twitter is good about returning codes, too - # Chances are that most of the time you run into this, it's going to be a 503 "service temporarily unavailable". That's a fail whale. except urllib2.HTTPError, e: - return e - # Getting an URLError usually means you didn't even hit Twitter's servers. This means something has gone TERRIBLY WRONG somewhere. + if (e.code == 304): + return [] + else: + raise TwitterHTTPError(e, uri, self.format, arg_data) except urllib2.URLError, e: return e else: - return json.loads(data) + return data # now, begin our actual code. # APIDOCS https://dev.twitter.com/docs/api/1.1 @@ -254,45 +144,53 @@ class Tweety(callbacks.Plugin): def __init__(self, irc): self.__parent = super(Tweety, self) self.__parent.__init__(irc) - haveAuthKeys = self._checkCredentials() + self.twitter = False + if not self.twitter: + self._checkAuthorization() - - def _checkCredentials(self): - """Check for all 4 requires keys on Twitter auth.""" - failTest = False - for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'): - try: - testKey = self.registryValue(checkKey) - except: - self.log.debug("Failed checking keys. We're missing the config value for: {0}. Please set this and try again.".format(checkKey)) - failTest = True - break + def _checkAuthorization(self): + if not self.twitter: + failTest = False + for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'): + try: + testKey = self.registryValue(checkKey) + except: + self.log.debug("Failed checking keys. We're missing the config value for: {0}. Please set this and try again.".format(checkKey)) + failTest = True + break - if failTest: - self.log.error('Failed getting keys. You must set all 4 keys in config variables.') - return False + if failTest: + self.log.error('Failed getting keys. You must set all 4 keys in config variables.') + return False + + self.log.info("Got all 4 keys. Now trying to auth up with Twitter.") + twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + data = twitter.ApiCall('account/verify_credentials') + + try: + testjson = json.loads(data) + except: + self.log.debug("Failed logging in. Returned: %s" % data) + return False + + if testjson: + self.log.info("I have successfully authorized and logged in to Twitter using your credentials.") + self.twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) else: - return True + pass + def _strip_accents(string): + import unicodedata + return unicodedata.normalize('NFKD', unicode(string)).encode('ASCII', 'ignore') - def _expandLinks(self, tweet): - # if not surl.startswith('http://') and not surl.startswith('https://'): - _tco_link_re = re.compile(u'http://t.co/[a-zA-Z0-9]+') - req_url = 'http://api.longurl.org/v2/expand?format=json&url=' + qurl - req = urllib2.Request(req_url, headers={'User-Agent': 'Python-longurl/1.0'}) - lookup = json.loads(urllib2.urlopen(req).read()) - return lookup.get('long-url', None) - - #def re_encode(input_string, decoder = 'utf-8', encoder = 'utf=8'): - #try: - #output_string = unicodedata.normalize('NFD',\ - #input_string.decode(decoder)).encode(encoder) - #except UnicodeError: - #output_string = unicodedata.normalize('NFD',\ - #input_string.decode('ascii', 'replace')).encode(encoder) - #return output_string + def _convert_to_utf8_str(arg): + # written by Michael Norton (http://docondev.blogspot.com/) + if isinstance(arg, unicode): + arg = arg.encode('utf-8') + elif not isinstance(arg, str): + arg = str(arg) + return arg - def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" text = text.replace("\n", " ") @@ -321,37 +219,31 @@ class Tweety(callbacks.Plugin): """ Takes a datetime string object that comes from twitter and twitter search timelines and returns a relative date. """ - - plural = lambda n: n > 1 and "s" or "" - # 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: ddate = time.strptime(s, "%a %b %d %H:%M:%S +0000 %Y")[:-2] except ValueError: try: ddate = time.strptime(s, "%a, %d %b %Y %H:%M:%S +0000")[:-2] except ValueError: - return "", "" + return "unknown" + # do the math created_at = datetime(*ddate, tzinfo=None) d = datetime.utcnow() - created_at + # now parse and return. if d.days: - rel_time = "%s days ago" % d.days - elif d.seconds > 3600: - hours = d.seconds / 3600 - rel_time = "%s hour%s ago" % (hours, plural(hours)) + rel_time = "%sd ago" % d.days + elif d.seconds > 3600: + rel_time = "%sh ago" % (d.seconds / 3600) elif 60 <= d.seconds < 3600: - minutes = d.seconds / 60 - rel_time = "%s minute%s ago" % (minutes, plural(minutes)) - elif 30 < d.seconds < 60: - rel_time = "less than a minute ago" + rel_time = "%sm ago" % (d.seconds / 60) else: - rel_time = "less than %s second%s ago" % (d.seconds, plural(d.seconds)) - return rel_time + rel_time = "%ss ago" % (d.seconds) + return rel_time def _outputTweet(self, irc, msg, nick, name, text, time, tweetid): @@ -408,8 +300,7 @@ class Tweety(callbacks.Plugin): "q": query, "format":"json", "diagnostics":"false", - "env":"store://datatables.org/alltableswithkeys" - } + "env":"store://datatables.org/alltableswithkeys"} try: response = urllib2.urlopen("http://query.yahooapis.com/v1/public/yql",urllib.urlencode(params)) @@ -419,11 +310,10 @@ class Tweety(callbacks.Plugin): woeid = data['query']['results']['place'][0]['woeid'] else: woeid = data['query']['results']['place']['woeid'] - + return woeid except Exception, err: + self.log.error("Error looking up %s :: %s" % (lookup,err)) return None - - return woeid ########################## ### PUBLIC FUNCTIONS ##### @@ -446,24 +336,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 + # https://dev.twitter.com/docs/rate-limiting/1.1/limits def ratelimits(self, irc, msg, args): """ Display current rate limits for your twitter API account. """ - try: - twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) - except: - irc.reply("Failed to authorize with twitter.") - return + data = self.twitter.ApiCall('application/rate_limit_status') #, parameters={'resources':optstatus}) try: - data = twitter.ApiCall('application/rate_limit_status') #, parameters={'resources':optstatus}) + data = json.loads(data) except: - irc.reply("Failed to lookup rate limit data. Something might have gone wrong.") - return - + irc.reply("Failed to lookup rate limit data. Something might have gone wrong. Data: %s" % data) + return + data = data.get('resources', None) if not data: # simple check if we have part of the json dict. @@ -483,36 +369,39 @@ class Tweety(callbacks.Plugin): ratelimits = wrap(ratelimits) + #< X-Rate-Limit-Limit: 15 + #< X-Rate-Limit-Remaining: 13 + #< X-Rate-Limit-Reset: 1357963140 / time.now() + + # https://dev.twitter.com/docs/api/1.1/get/trends/place - def trends(self, irc, msg, args, optwoeid): + def trends(self, irc, msg, args, getopts, optwoeid): """ - Returns the Top 10 Twitter trends for a specific location. Use optional argument location for trends, otherwise will use config variable. + Returns the Top 10 Twitter trends for a specific location. + Use optional argument location for trends. Ex: trends Boston + Use --exclude to not include #hashtags in trends data. """ - + + args = {'id':self.registryValue('woeid', msg.args[0]),'exclude':self.registryValue('hideHashtagsTrends', msg.args[0])} + if getopts: + for (key,value) in getopts: + if key=='exclude': # remove hashtags from trends. + args['exclude'] = 'hashtags' + # work with woeid. 1 is world, the default. can be set via input or via config. if optwoeid: - try: - woeid = self._woeid_lookup(optwoeid) - except: - woeid = self.registryValue('woeid', msg.args[0]) # Where On Earth ID - else: - woeid = self.registryValue('woeid', msg.args[0]) # Where On Earth ID + woeid = self._woeid_lookup(optwoeid) + if woeid: + args['id'] = woeid + data = self.twitter.ApiCall('trends/place', parameters=args) try: - twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + data = json.loads(data) except: - irc.reply("Failed to authorize with twitter.") + irc.reply("Failed to lookup trends data. Something might have gone wrong. DATA: %s" % data) return - - try: - data = twitter.ApiCall('trends/place', parameters={'id':woeid}) - except: - irc.reply("Failed to lookup trends data. Something might have gone wrong.") - return - - #self.log.info(str(type(data))) - + try: location = data[0]['locations'][0]['name'] except: @@ -521,10 +410,9 @@ class Tweety(callbacks.Plugin): ttrends = string.join([trend['name'].encode('utf-8') for trend in data[0]['trends']], " | ") - retvalue = "Top 10 Twitter Trends in {0} :: {1}".format(ircutils.bold(location), ttrends) - irc.reply(retvalue) + irc.reply("Top 10 Twitter Trends in {0} :: {1}".format(ircutils.bold(location), ttrends)) - trends = wrap(trends, [optional('text')]) + trends = wrap(trends, [getopts({'exclude':''}), optional('text')]) # https://dev.twitter.com/docs/api/1.1/get/search/tweets From 6ea598b157d8dfb3b6dbc5df05cdbafe7737130f Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 12 Jan 2013 11:51:17 -0500 Subject: [PATCH 23/63] Updated header for copyright stuff. --- __init__.py | 8 ++++---- test.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/__init__.py b/__init__.py index de28c68..fde7715 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ -# coding=utf8 +# -*- coding: utf-8 -*- ### -# Copyright (c) 2011, Terje Hoås +# Copyright (c) 2011-2013, Terje Hoås, spline # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -42,7 +42,7 @@ import supybot.world as world __version__ = "" # XXX Replace this with an appropriate author or supybot.Author instance. -__author__ = supybot.Author('Terje Hoås', 'Hoaas', 'terjehoaas@gmail.com') +__author__ = supybot.Author('reticulatingspline', 'spline', 'spline') # This is a dictionary mapping supybot.Author instances to lists of # contributions. @@ -65,4 +65,4 @@ Class = plugin.Class configure = config.configure -# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: +# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=250: diff --git a/test.py b/test.py index bf7a23b..a503e13 100644 --- a/test.py +++ b/test.py @@ -1,6 +1,6 @@ -# coding=utf8 +# -*- coding: utf-8 -*- ### -# Copyright (c) 2011, Terje Hoås +# Copyright (c) 2011-2013, Terje Hoås, spline # All rights reserved. # # Redistribution and use in source and binary forms, with or without From f4fe2b42137f94feaf4b3f70a8d964e518396a5d Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 12 Jan 2013 12:11:45 -0500 Subject: [PATCH 24/63] Update README --- README.txt | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 README.txt diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..8e7c697 --- /dev/null +++ b/README.txt @@ -0,0 +1,38 @@ +Supybot-Tweety +====== +*Twitter client for Supybot + +Overview +-------- +Supply a username and get the latest tweet(s). Or an ID to a tweet. Or search on twitter. Or view latest trends. Does not require a user account or apikey or anything like that. Just point and shoot. +This plugin does NOT relay tweets in real time. It only fetches data from Twitter when commands are called. + +This is forked from Hoaas' Tweety plugin at: http://github.com/Hoaas/Supybot-Plugins to support Twitter API v1.1 and add in a few features: + +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 + +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. + 3. Click create an application. + 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 + +Examples +-------- + +* Documentation + + # INFO https://dev.twitter.com/docs/api/1.1/get/users/show + # ID https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid + # TIMELINE https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline + From e5bebd401d84ce47c654464a0bdd24d731914099 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 12 Jan 2013 12:41:00 -0500 Subject: [PATCH 25/63] Fixed a stupid bug due to names. --- README.txt | 38 ---------------------- plugin.py | 95 ++++++++++++------------------------------------------ 2 files changed, 21 insertions(+), 112 deletions(-) delete mode 100644 README.txt diff --git a/README.txt b/README.txt deleted file mode 100644 index 8e7c697..0000000 --- a/README.txt +++ /dev/null @@ -1,38 +0,0 @@ -Supybot-Tweety -====== -*Twitter client for Supybot - -Overview --------- -Supply a username and get the latest tweet(s). Or an ID to a tweet. Or search on twitter. Or view latest trends. Does not require a user account or apikey or anything like that. Just point and shoot. -This plugin does NOT relay tweets in real time. It only fetches data from Twitter when commands are called. - -This is forked from Hoaas' Tweety plugin at: http://github.com/Hoaas/Supybot-Plugins to support Twitter API v1.1 and add in a few features: - -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 - -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. - 3. Click create an application. - 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 - -Examples --------- - -* Documentation - - # INFO https://dev.twitter.com/docs/api/1.1/get/users/show - # ID https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid - # TIMELINE https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline - diff --git a/plugin.py b/plugin.py index 661698f..a395f46 100644 --- a/plugin.py +++ b/plugin.py @@ -51,25 +51,6 @@ import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks -# Twitter error classes from the sixohsix/Twitter API. -class TwitterError(Exception): - """Base Exception thrown by the Twitter object when there is a general error interacting with the API.""" - pass - -class TwitterHTTPError(TwitterError): - """Exception thrown by the Twitter object when there is an HTTP error interacting with twitter.com.""" - def __init__(self, e, uri, format, uriparts): - self.e = e - self.uri = uri - self.format = format - self.uriparts = uriparts - - def __str__(self): - return ( - "Twitter sent status %i for URL: %s.%s using parameters: " - "(%s)\ndetails: %s" %( - self.e.code, self.uri, self.format, self.uriparts, - self.e.fp.read())) # OAuthApi class from https://github.com/jpittman/OAuth-Python-Twitter # mainly kept intact but modified for Twitter API v1.1 and unncessary things removed. @@ -119,10 +100,7 @@ class OAuthApi: try: data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", type, parameters) except urllib2.HTTPError, e: - if (e.code == 304): - return [] - else: - raise TwitterHTTPError(e, uri, self.format, arg_data) + return e except urllib2.URLError, e: return e else: @@ -144,12 +122,12 @@ class Tweety(callbacks.Plugin): def __init__(self, irc): self.__parent = super(Tweety, self) self.__parent.__init__(irc) - self.twitter = False - if not self.twitter: + self.twitterApi = False + if not self.twitterApi: self._checkAuthorization() def _checkAuthorization(self): - if not self.twitter: + if not self.twitterApi: failTest = False for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'): try: @@ -164,8 +142,8 @@ class Tweety(callbacks.Plugin): return False self.log.info("Got all 4 keys. Now trying to auth up with Twitter.") - twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) - data = twitter.ApiCall('account/verify_credentials') + twitterApi = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + data = twitterApi.ApiCall('account/verify_credentials') try: testjson = json.loads(data) @@ -175,22 +153,10 @@ class Tweety(callbacks.Plugin): if testjson: self.log.info("I have successfully authorized and logged in to Twitter using your credentials.") - self.twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + self.twitterApi = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) else: pass - def _strip_accents(string): - import unicodedata - return unicodedata.normalize('NFKD', unicode(string)).encode('ASCII', 'ignore') - - def _convert_to_utf8_str(arg): - # written by Michael Norton (http://docondev.blogspot.com/) - if isinstance(arg, unicode): - arg = arg.encode('utf-8') - elif not isinstance(arg, str): - arg = str(arg) - return arg - def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" text = text.replace("\n", " ") @@ -337,12 +303,15 @@ class Tweety(callbacks.Plugin): # 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.twitter.ApiCall('application/rate_limit_status') #, parameters={'resources':optstatus}) + data = self.twitterApi.ApiCall('application/rate_limit_status') #, parameters={'resources':optstatus}) try: data = json.loads(data) @@ -369,15 +338,11 @@ class Tweety(callbacks.Plugin): ratelimits = wrap(ratelimits) - #< X-Rate-Limit-Limit: 15 - #< X-Rate-Limit-Remaining: 13 - #< X-Rate-Limit-Reset: 1357963140 / time.now() - - # https://dev.twitter.com/docs/api/1.1/get/trends/place def trends(self, irc, msg, args, getopts, optwoeid): - """ + """[--exclude] + Returns the Top 10 Twitter trends for a specific location. Use optional argument location for trends. Ex: trends Boston Use --exclude to not include #hashtags in trends data. @@ -395,7 +360,7 @@ class Tweety(callbacks.Plugin): if woeid: args['id'] = woeid - data = self.twitter.ApiCall('trends/place', parameters=args) + data = self.twitterApi.ApiCall('trends/place', parameters=args) try: data = json.loads(data) except: @@ -414,10 +379,8 @@ class Tweety(callbacks.Plugin): trends = wrap(trends, [getopts({'exclude':''}), optional('text')]) - - # https://dev.twitter.com/docs/api/1.1/get/search/tweets def tsearch(self, irc, msg, args, optlist, optterm): - """ [--num number] [--searchtype mixed,recent,popular] [--lang xx] + """[--num number] [--searchtype mixed,recent,popular] [--lang xx] Searches Twitter for the and returns the most recent results. Number is number of results. Must be a number higher than 0 and max 10. @@ -440,16 +403,11 @@ class Tweety(callbacks.Plugin): if key == 'lang': # lang . Uses ISO-639 codes like 'en' http://en.wikipedia.org/wiki/ISO_639-1 tsearchArgs['lang'] = value + data = self.twitterApi.ApiCall('search/tweets', parameters=tsearchArgs) try: - twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + data = json.loads(data) except: - irc.reply("Failed to authorize with twitter.") - return - - try: - data = twitter.ApiCall('search/tweets', parameters=tsearchArgs) - except: - irc.reply("Failed to get search results. Twitter broken?") + irc.reply("Failed to lookup Twitter search. Something might have gone wrong. DATA: %s" % data) return results = data.get('statuses', None) # data returned as a dict @@ -469,9 +427,6 @@ class Tweety(callbacks.Plugin): tsearch = wrap(tsearch, [getopts({'num':('int'), 'searchtype':('literal', ('popular', 'mixed', 'recent')), 'lang':('somethingWithoutSpaces')}), ('text')]) - # INFO https://dev.twitter.com/docs/api/1.1/get/users/show - # ID https://dev.twitter.com/docs/api/1.1/get/statuses/show/%3Aid - # TIMELINE https://dev.twitter.com/docs/api/1.1/get/statuses/user_timeline def twitter(self, irc, msg, args, optlist, optnick): """[--noreply] [--nort] [--num number] | <--id id> | [--info nick] @@ -524,16 +479,11 @@ class Tweety(callbacks.Plugin): twitterArgs['exclude_replies'] = 'false' # now with and call the api. + data = self.twitterApi.ApiCall(apiUrl, parameters=twitterArgs) try: - twitter = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) + data = json.loads(data) except: - irc.reply("Failed to authorize with twitter.") - return - - try: - data = twitter.ApiCall(apiUrl, parameters=twitterArgs) - except: - irc.reply("Failed to get user's timeline. Twitter broken?") + irc.reply("Failed to lookup Twitter data. Something might have gone wrong. DATA: %s" % data) return # process the data. @@ -545,8 +495,6 @@ class Tweety(callbacks.Plugin): tweetid = data.get('id', None) self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) return - - elif args['info']: # Works with --info to return info on a Twitter user. location = data.get('location', None) followers = data.get('followers_count', None) @@ -572,7 +520,6 @@ class Tweety(callbacks.Plugin): irc.reply(ret) return - else: # Else, its the user's timeline. Count is handled above in the GET request. if len(data) == 0: irc.reply("User: {0} has not tweeted yet.".format(optnick)) From 2396301d061242121bbaae0b7f8b78238eb8a7c4 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 12 Jan 2013 12:56:29 -0500 Subject: [PATCH 26/63] Update formatting/error messages. --- plugin.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/plugin.py b/plugin.py index a395f46..167f58b 100644 --- a/plugin.py +++ b/plugin.py @@ -180,7 +180,6 @@ class Tweety(callbacks.Plugin): return text # leave as is 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. @@ -364,7 +363,7 @@ class Tweety(callbacks.Plugin): try: data = json.loads(data) except: - irc.reply("Failed to lookup trends data. Something might have gone wrong. DATA: %s" % data) + irc.reply("Error: failed to lookup Twitter trends: %s" % data) return try: @@ -407,7 +406,7 @@ class Tweety(callbacks.Plugin): try: data = json.loads(data) except: - irc.reply("Failed to lookup Twitter search. Something might have gone wrong. DATA: %s" % data) + irc.reply("Error: %s trying to search Twitter." % data) return results = data.get('statuses', None) # data returned as a dict @@ -483,7 +482,7 @@ class Tweety(callbacks.Plugin): try: data = json.loads(data) except: - irc.reply("Failed to lookup Twitter data. Something might have gone wrong. DATA: %s" % data) + irc.reply("Failed to lookup Twitter account for @{0} ({1}) ".format(optnick, data)) return # process the data. @@ -520,11 +519,10 @@ class Tweety(callbacks.Plugin): irc.reply(ret) return - else: # Else, its the user's timeline. Count is handled above in the GET request. - if len(data) == 0: + else: + if len(data) == 0: # length handled above but user might have 0 tweets. irc.reply("User: {0} has not tweeted yet.".format(optnick)) return - for tweet in data: text = self._unescape(tweet.get('text', None)) nick = tweet["user"].get('screen_name', None) @@ -533,7 +531,7 @@ class Tweety(callbacks.Plugin): relativeTime = self._time_created_at(tweet.get('created_at', None)) self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) - twitter = wrap(twitter, [getopts({'noreply':'', 'nort': '', 'info': '', 'id': '', 'num': ('int')}), ('something')]) + twitter = wrap(twitter, [getopts({'noreply':'','nort':'','info':'','id':'','num':('int')}), ('something')]) Class = Tweety From d46967e3390aa4d07219586e281cd066d2e6b17f Mon Sep 17 00:00:00 2001 From: spline Date: Sun, 20 Jan 2013 13:31:33 -0500 Subject: [PATCH 27/63] Updating README. --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 44bb284..99006d3 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ Supybot-Tweety ====== -*Twitter client for Supybot -Overview --------- -Supply a username and get the latest tweet(s). Or an ID to a tweet. Or search on twitter. Or view latest trends. Does not require a user account or apikey or anything like that. Just point and shoot. -This plugin does NOT relay tweets in real time. It only fetches data from Twitter when commands are called. +# Twitter client for Supybot -This is forked from Hoaas' Tweety plugin at: http://github.com/Hoaas/Supybot-Plugins to support Twitter API v1.1 and add in a few features: + Description + + This is a supybot client. Instructions ------------ From 683cd3861bf281ca218efb4c6854cf881748cfd8 Mon Sep 17 00:00:00 2001 From: spline Date: Sun, 20 Jan 2013 14:12:54 -0500 Subject: [PATCH 28/63] Update some stuff in plugin. Update copyrights and ids. --- README.md | 53 +++++++++++++++++++++++------- __init__.py | 29 +--------------- config.py | 30 +---------------- plugin.py | 95 +++++++++++------------------------------------------ test.py | 29 +--------------- 5 files changed, 64 insertions(+), 172 deletions(-) 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 * From 4ee3681fe94f2328e7c6481e41d900252c6bfe22 Mon Sep 17 00:00:00 2001 From: spline Date: Sun, 20 Jan 2013 14:17:56 -0500 Subject: [PATCH 29/63] Update README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 34ff6ce..3dde2ca 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ This is a much watered down version of ProgVal's Twitter client. It only include read-only features (no risk of accidental Tweeting) that most folks use: tweet display, tweet searching and trends. +If you need to do any type of Twittering via the bot such as posting tweets, responding, +announcing of timelines, you will want his. This will never contain more than what is above. + Instructions ------------ 1.) On an up-to-date Python 2.7+ system, one dependency is needed. From 39fcb2aa65775c14711d5d54a0e0019909ef674a Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 21 Jan 2013 10:05:00 -0500 Subject: [PATCH 30/63] Change oauth class so we don't return read urllib2 object, meaning we can't grab the status code out of it. Minor structural changes. --- plugin.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/plugin.py b/plugin.py index 0e734be..b7631c5 100644 --- a/plugin.py +++ b/plugin.py @@ -19,6 +19,7 @@ import oauth2 as oauth # supybot libs import supybot.utils as utils from supybot.commands import * +import supybot.ircdb as ircdb import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks @@ -43,7 +44,7 @@ class OAuthApi: req = self._makeOAuthRequest(url, params=extra_params) opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=1)) url = req.to_url() - url_data = opener.open(url).read() + url_data = opener.open(url) opener.close() return url_data @@ -107,14 +108,11 @@ class Tweety(callbacks.Plugin): self.log.info("Got all 4 keys. Now trying to auth up with Twitter.") twitterApi = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) data = twitterApi.ApiCall('account/verify_credentials') - - try: - testjson = json.loads(data) - except: - self.log.debug("Failed logging in. Returned: %s" % data) + + if data.getcode() == "401": + self.log.error("ERROR: I could not log in using your credentials. Message: %s" % data.read()) return False - - if testjson: + else: self.log.info("I have successfully authorized and logged in to Twitter using your credentials.") self.twitterApi = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) else: @@ -260,9 +258,14 @@ class Tweety(callbacks.Plugin): Display current rate limits for your twitter API account. """ - data = self.twitterApi.ApiCall('application/rate_limit_status') #, parameters={'resources':optstatus}) + # make sure only admins can run this command. + if not ircdb.checkCapability(msg.prefix, 'admin'): + irc.reply("Only admins can run this command. Sorry.") + return + + data = self.twitterApi.ApiCall('application/rate_limit_status') try: - data = json.loads(data) + data = json.loads(data.read()) except: irc.reply("Failed to lookup ratelimit data: %s" % data) return @@ -274,14 +277,15 @@ class Tweety(callbacks.Plugin): return # we only have resources needed in here. def below works with each entry properly. - resourcelist = ['trends/place', 'search/tweets', 'users/show/:id', 'statuses/show/:id', 'statuses/user_timeline/:id'] + resourcelist = ['trends/place', 'search/tweets', 'users/show/:id', 'statuses/show/:id', 'statuses/user_timeline'] for resource in resourcelist: family, endpoint = resource.split('/', 1) # need to split each entry on /, resource family is [0], append / to entry. resourcedict = data.get(family, None) endpoint = resourcedict.get("/"+resource, None) - irc.reply("{0} :: {1}".format(resource, endpoint)) - # endpoint is {u'reset': 1351226072, u'limit': 15, u'remaining': 15} + minutes = "%sm%ss" % divmod(int(endpoint['reset'])-int(time.time()), 60) + output = "Reset in: {0} Remaining: {1}".format(minutes, endpoint['remaining']) + irc.reply("{0} :: {1}".format(ircutils.bold(resource), output)) ratelimits = wrap(ratelimits) @@ -307,7 +311,7 @@ class Tweety(callbacks.Plugin): data = self.twitterApi.ApiCall('trends/place', parameters=args) try: - data = json.loads(data) + data = json.loads(data.read()) except: irc.reply("Error: failed to lookup Twitter trends: %s" % data) return @@ -350,7 +354,7 @@ class Tweety(callbacks.Plugin): data = self.twitterApi.ApiCall('search/tweets', parameters=tsearchArgs) try: - data = json.loads(data) + data = json.loads(data.read()) except: irc.reply("Error: %s trying to search Twitter." % data) return @@ -425,7 +429,7 @@ class Tweety(callbacks.Plugin): # now with and call the api. data = self.twitterApi.ApiCall(apiUrl, parameters=twitterArgs) try: - data = json.loads(data) + data = json.loads(data.read()) except: irc.reply("Failed to lookup Twitter account for @{0} ({1}) ".format(optnick, data)) return From 2398d6c06979963547ac1dbbdd98acdcd9fc30c5 Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 21 Jan 2013 11:01:10 -0500 Subject: [PATCH 31/63] Make sure we have twitterApi object before running commands. Error if not. --- plugin.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/plugin.py b/plugin.py index b7631c5..27a2213 100644 --- a/plugin.py +++ b/plugin.py @@ -250,17 +250,14 @@ class Tweety(callbacks.Plugin): woeidlookup = wrap(woeidlookup, ['text']) - - # RATELIMITING - def ratelimits(self, irc, msg, args): """ Display current rate limits for your twitter API account. """ - - # make sure only admins can run this command. - if not ircdb.checkCapability(msg.prefix, 'admin'): - irc.reply("Only admins can run this command. Sorry.") + + # before we do anything, make sure we have a twitterApi object. + if not self.twitterApi: + irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return data = self.twitterApi.ApiCall('application/rate_limit_status') @@ -297,6 +294,11 @@ class Tweety(callbacks.Plugin): Use --exclude to not include #hashtags in trends data. """ + # before we do anything, make sure we have a twitterApi object. + if not self.twitterApi: + irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") + return + args = {'id':self.registryValue('woeid', msg.args[0]),'exclude':self.registryValue('hideHashtagsTrends', msg.args[0])} if getopts: for (key,value) in getopts: @@ -335,6 +337,11 @@ class Tweety(callbacks.Plugin): Number is number of results. Must be a number higher than 0 and max 10. searchtype being recent, popular or mixed. Popular is the default. """ + + # before we do anything, make sure we have a twitterApi object. + if not self.twitterApi: + irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") + return tsearchArgs = {'include_entities':'false', 'count': self.registryValue('defaultSearchResults', msg.args[0]), 'lang':'en', 'q':urllib.quote(optterm)} @@ -383,6 +390,11 @@ class Tweety(callbacks.Plugin): Or returns tweet with id 'id'. Or returns information on user with --info. """ + + # before we do anything, make sure we have a twitterApi object. + if not self.twitterApi: + irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") + return optnick = optnick.replace('@','') # strip @ from input if given. From 6a0253f9ec80dbc2144299014767e7e21351d478 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 2 Feb 2013 07:49:21 -0500 Subject: [PATCH 32/63] Clean up whitespace. Add in exception for protected Tweets (user account is locked). --- plugin.py | 124 +++++++++++++++++++++++++++--------------------------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/plugin.py b/plugin.py index 27a2213..1fa7049 100644 --- a/plugin.py +++ b/plugin.py @@ -34,32 +34,32 @@ class OAuthApi: token = None self._Consumer = oauth.Consumer(consumer_key, consumer_secret) self._signature_method = oauth.SignatureMethod_HMAC_SHA1() - self._access_token = token - + self._access_token = token + def _FetchUrl(self,url, parameters=None): extra_params = {} if parameters: extra_params.update(parameters) - - req = self._makeOAuthRequest(url, params=extra_params) + + req = self._makeOAuthRequest(url, params=extra_params) opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=1)) url = req.to_url() url_data = opener.open(url) opener.close() return url_data - - def _makeOAuthRequest(self, url, token=None, params=None): + + def _makeOAuthRequest(self, url, token=None, params=None): oauth_base_params = { 'oauth_version': "1.0", 'oauth_nonce': oauth.generate_nonce(), 'oauth_timestamp': int(time.time()) } - + if params: params.update(oauth_base_params) else: params = oauth_base_params - + if not token: token = self._access_token request = oauth.Request(method="GET", url=url, parameters=params) @@ -81,7 +81,7 @@ class OAuthApi: class Tweety(callbacks.Plugin): """Public Twitter class for working with the API.""" threaded = True - + def __init__(self, irc): self.__parent = super(Tweety, self) self.__parent.__init__(irc) @@ -100,11 +100,11 @@ class Tweety(callbacks.Plugin): self.log.debug("Failed checking keys. We're missing the config value for: {0}. Please set this and try again.".format(checkKey)) failTest = True break - + if failTest: self.log.error('Failed getting keys. You must set all 4 keys in config variables.') return False - + self.log.info("Got all 4 keys. Now trying to auth up with Twitter.") twitterApi = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) data = twitterApi.ApiCall('account/verify_credentials') @@ -143,7 +143,7 @@ class Tweety(callbacks.Plugin): def _time_created_at(self, s): """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: @@ -158,7 +158,7 @@ class Tweety(callbacks.Plugin): # now parse and return. if d.days: rel_time = "%sd ago" % d.days - elif d.seconds > 3600: + elif d.seconds > 3600: rel_time = "%sh ago" % (d.seconds / 3600) elif 60 <= d.seconds < 3600: rel_time = "%sm ago" % (d.seconds / 60) @@ -170,35 +170,35 @@ class Tweety(callbacks.Plugin): """ Takes a group of strings and outputs a Tweet to IRC. Used for tsearch and twitter. """ - + outputColorTweets = self.registryValue('outputColorTweets', msg.args[0]) - + if outputColorTweets: ret = ircutils.underline(ircutils.mircColor(("@" + nick), 'blue')) else: ret = ircutils.underline(ircutils.bold("@" + nick)) - + if not self.registryValue('hideRealName', msg.args[0]): # show realname in tweet output? ret += " ({0})".format(name) - + # add in the end with the text + tape if outputColorTweets: text = re.sub(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)', ircutils.mircColor(r'\1', 'red'), text) # color urls. ret += ": {0} ({1})".format(text, ircutils.mircColor(time, 'yellow')) - else: + else: ret += ": {0} ({1})".format(text, ircutils.bold(time)) - + if self.registryValue('addShortUrl', msg.args[0]): if self._createShortUrl(nick, tweetid): ret += " {0}".format(url) - + irc.reply(ret) def _createShortUrl(self, nick, tweetid): """ Takes a nick and tweetid and returns a shortened URL via is.gd service. """ - + longurl = "https://twitter.com/#!/{0}/status/{1}".format(nick, tweetid) try: req = urllib2.Request("http://is.gd/api.php?longurl=" + urllib.quote(longurl)) @@ -214,11 +214,10 @@ class Tweety(callbacks.Plugin): """ query = "select * from geo.places where text='%s'" % lookup - params = { - "q": query, - "format":"json", - "diagnostics":"false", - "env":"store://datatables.org/alltableswithkeys"} + params = {"q": query, + "format":"json", + "diagnostics":"false", + "env":"store://datatables.org/alltableswithkeys"} try: response = urllib2.urlopen("http://query.yahooapis.com/v1/public/yql",urllib.urlencode(params)) @@ -232,7 +231,7 @@ class Tweety(callbacks.Plugin): except Exception, err: self.log.error("Error looking up %s :: %s" % (lookup,err)) return None - + ########################## ### PUBLIC FUNCTIONS ##### ########################## @@ -249,7 +248,7 @@ class Tweety(callbacks.Plugin): irc.reply(("Something broke while looking up: '%s'") % (lookup)) woeidlookup = wrap(woeidlookup, ['text']) - + def ratelimits(self, irc, msg, args): """ Display current rate limits for your twitter API account. @@ -259,23 +258,23 @@ class Tweety(callbacks.Plugin): if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - + data = self.twitterApi.ApiCall('application/rate_limit_status') try: data = json.loads(data.read()) except: irc.reply("Failed to lookup ratelimit data: %s" % data) - return - + 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) return - # we only have resources needed in here. def below works with each entry properly. + # we only have resources needed in here. def below works with each entry properly. resourcelist = ['trends/place', 'search/tweets', 'users/show/:id', 'statuses/show/:id', 'statuses/user_timeline'] - + for resource in resourcelist: family, endpoint = resource.split('/', 1) # need to split each entry on /, resource family is [0], append / to entry. resourcedict = data.get(family, None) @@ -283,53 +282,53 @@ class Tweety(callbacks.Plugin): minutes = "%sm%ss" % divmod(int(endpoint['reset'])-int(time.time()), 60) output = "Reset in: {0} Remaining: {1}".format(minutes, endpoint['remaining']) irc.reply("{0} :: {1}".format(ircutils.bold(resource), output)) - + ratelimits = wrap(ratelimits) def trends(self, irc, msg, args, getopts, optwoeid): """[--exclude] - - Returns the Top 10 Twitter trends for a specific location. + + Returns the Top 10 Twitter trends for a specific location. Use optional argument location for trends. Ex: trends Boston Use --exclude to not include #hashtags in trends data. """ - + # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - + args = {'id':self.registryValue('woeid', msg.args[0]),'exclude':self.registryValue('hideHashtagsTrends', msg.args[0])} if getopts: for (key,value) in getopts: if key=='exclude': # remove hashtags from trends. args['exclude'] = 'hashtags' - + # work with woeid. 1 is world, the default. can be set via input or via config. if optwoeid: woeid = self._woeid_lookup(optwoeid) if woeid: args['id'] = woeid - + data = self.twitterApi.ApiCall('trends/place', parameters=args) try: data = json.loads(data.read()) except: irc.reply("Error: failed to lookup Twitter trends: %s" % data) return - + try: location = data[0]['locations'][0]['name'] except: 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')]) - + def tsearch(self, irc, msg, args, optlist, optterm): """[--num number] [--searchtype mixed,recent,popular] [--lang xx] @@ -342,7 +341,7 @@ class Tweety(callbacks.Plugin): if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - + tsearchArgs = {'include_entities':'false', 'count': self.registryValue('defaultSearchResults', msg.args[0]), 'lang':'en', 'q':urllib.quote(optterm)} if optlist: @@ -357,7 +356,7 @@ class Tweety(callbacks.Plugin): if key == 'searchtype': tsearchArgs['result_type'] = value # limited by getopts to valid values. if key == 'lang': # lang . Uses ISO-639 codes like 'en' http://en.wikipedia.org/wiki/ISO_639-1 - tsearchArgs['lang'] = value + tsearchArgs['lang'] = value data = self.twitterApi.ApiCall('search/tweets', parameters=tsearchArgs) try: @@ -365,8 +364,8 @@ class Tweety(callbacks.Plugin): except: irc.reply("Error: %s trying to search Twitter." % data) return - - results = data.get('statuses', None) # data returned as a dict + + results = data.get('statuses', None) # data returned as a dict if not results or len(results) == 0: irc.reply("Error: No Twitter Search results found for '{0}'".format(optterm)) @@ -386,22 +385,22 @@ class Tweety(callbacks.Plugin): """[--noreply] [--nort] [--num number] | <--id id> | [--info nick] Returns last tweet or 'number' tweets (max 10). Shows all tweets, including rt and reply. - To not display replies or RT's, use --noreply or --nort, respectively. + To not display replies or RT's, use --noreply or --nort, respectively. Or returns tweet with id 'id'. - Or returns information on user with --info. + Or returns information on user with --info. """ # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - + optnick = optnick.replace('@','') # strip @ from input if given. - + args = {'id': False, 'nort': False, 'noreply': False, 'num': self.registryValue('defaultResults', msg.args[0]), 'info': False} - # handle input optlist. - if optlist: + # handle input optlist. + if optlist: for (key, value) in optlist: if key == 'id': args['id'] = True @@ -412,14 +411,14 @@ class Tweety(callbacks.Plugin): if key == 'num': if value > self.registryValue('maxResults', msg.args[0]) or value <= 0: irc.reply("Error: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(value, max)) - return + return else: args['num'] = value if key == 'info': args['info'] = True # handle the three different rest api endpoint urls + twitterArgs dict for options. - if args['id']: + if args['id']: apiUrl = 'statuses/show' twitterArgs = {'id': optnick, 'include_entities':'false'} elif args['info']: @@ -432,7 +431,7 @@ class Tweety(callbacks.Plugin): twitterArgs['include_rts'] = 'false' else: twitterArgs['include_rts'] = 'true' - + if args['noreply']: # This parameter will prevent replies from appearing in the returned timeline. twitterArgs['exclude_replies'] = 'true' else: @@ -445,8 +444,8 @@ class Tweety(callbacks.Plugin): except: irc.reply("Failed to lookup Twitter account for @{0} ({1}) ".format(optnick, data)) return - - # process the data. + + # process the data. if args['id']: # If --id was given for a single tweet. text = self._unescape(data.get('text', None)) nick = data["user"].get('screen_name', None) @@ -464,7 +463,7 @@ class Tweety(callbacks.Plugin): name = data.get('name', None) url = data.get('url', None) - # build ret, output string + # build ret, output string ret = ircutils.underline(ircutils.bold("@" + optnick)) ret += " ({0}):".format(name.encode('utf-8')) if url: @@ -473,14 +472,17 @@ class Tweety(callbacks.Plugin): ret += " {0}".format(description.encode('utf-8')) ret += " [{0} friends,".format(ircutils.bold(friends)) ret += " {0} followers.".format(ircutils.bold(followers)) - if location: + if location: ret += " Location: {0}]".format(location.encode('utf-8')) else: ret += "]" - + irc.reply(ret) return else: + if data.has_key('error'): + irc.reply("ERROR: I cannot fetch tweets from {0}: {1}".format(optnick, data['error'])) + return if len(data) == 0: # length handled above but user might have 0 tweets. irc.reply("User: {0} has not tweeted yet.".format(optnick)) return From c278f806162a59fa2dfa7c85d3f1cdfd795f0b92 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 2 Feb 2013 16:58:40 -0500 Subject: [PATCH 33/63] Botched that error parse attempt. Fixed now for each instance in twitter command. --- plugin.py | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/plugin.py b/plugin.py index 1fa7049..969c6f5 100644 --- a/plugin.py +++ b/plugin.py @@ -144,13 +144,13 @@ class Tweety(callbacks.Plugin): def _time_created_at(self, s): """Return relative delta.""" - 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] except ValueError: - try: # search's created_at Thu, 06 Oct 2011 19:41:12 +0000 + 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" + return s # do the math d = datetime.utcnow() - datetime(*ddate, tzinfo=None) @@ -267,7 +267,7 @@ class Tweety(callbacks.Plugin): return data = data.get('resources', None) - if not data: # simple check if we have part of the json dict. + 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) return @@ -276,7 +276,7 @@ class Tweety(callbacks.Plugin): resourcelist = ['trends/place', 'search/tweets', 'users/show/:id', 'statuses/show/:id', 'statuses/user_timeline'] for resource in resourcelist: - family, endpoint = resource.split('/', 1) # need to split each entry on /, resource family is [0], append / to entry. + family, endpoint = resource.split('/', 1) # need to split each entry on /, resource family is [0], append / to entry. resourcedict = data.get(family, None) endpoint = resourcedict.get("/"+resource, None) minutes = "%sm%ss" % divmod(int(endpoint['reset'])-int(time.time()), 60) @@ -298,10 +298,10 @@ class Tweety(callbacks.Plugin): irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - args = {'id':self.registryValue('woeid', msg.args[0]),'exclude':self.registryValue('hideHashtagsTrends', msg.args[0])} + args = {'id': self.registryValue('woeid', msg.args[0]),'exclude': self.registryValue('hideHashtagsTrends', msg.args[0])} if getopts: - for (key,value) in getopts: - if key=='exclude': # remove hashtags from trends. + for (key, value) in getopts: + if key == 'exclude': # remove hashtags from trends. args['exclude'] = 'hashtags' # work with woeid. 1 is world, the default. can be set via input or via config. @@ -320,7 +320,7 @@ class Tweety(callbacks.Plugin): try: location = data[0]['locations'][0]['name'] except: - irc.reply("ERROR: Cannot load trends: {0}".format(data)) # error also throws 404. + irc.reply("ERROR: Cannot load trends: {0}".format(data)) # error also throws 404. return # package together in object and output. @@ -395,7 +395,7 @@ class Tweety(callbacks.Plugin): irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - optnick = optnick.replace('@','') # strip @ from input if given. + optnick = optnick.replace('@','') # strip @ from input if given. args = {'id': False, 'nort': False, 'noreply': False, 'num': self.registryValue('defaultResults', msg.args[0]), 'info': False} @@ -427,12 +427,12 @@ class Tweety(callbacks.Plugin): else: apiUrl = 'statuses/user_timeline' twitterArgs = {'screen_name': optnick, 'count': args['num']} - if args['nort']: # When set to false, the timeline will strip any native retweets + if args['nort']: # When set to false, the timeline will strip any native retweets twitterArgs['include_rts'] = 'false' else: twitterArgs['include_rts'] = 'true' - if args['noreply']: # This parameter will prevent replies from appearing in the returned timeline. + if args['noreply']: # This parameter will prevent replies from appearing in the returned timeline. twitterArgs['exclude_replies'] = 'true' else: twitterArgs['exclude_replies'] = 'false' @@ -446,7 +446,11 @@ class Tweety(callbacks.Plugin): return # process the data. - if args['id']: # If --id was given for a single tweet. + if args['id']: # If --id was given for a single tweet. + # first, check for errors. + if 'errors' in data: + irc.reply("ERROR: I cannot fetch Tweet ID: {0} message: {1}".format(optnick, data['errors'][0]['message'])) + return text = self._unescape(data.get('text', None)) nick = data["user"].get('screen_name', None) name = data["user"].get('name', None) @@ -454,7 +458,11 @@ class Tweety(callbacks.Plugin): tweetid = data.get('id', None) self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) return - elif args['info']: # Works with --info to return info on a Twitter user. + elif args['info']: # Works with --info to return info on a Twitter user. + # first, check for errors. + if 'errors' in data: + irc.reply("ERROR: I cannot fetch Twitter info for: {0} message: {1}".format(optnick, data['errors'][0]['message'])) + return location = data.get('location', None) followers = data.get('followers_count', None) friends = data.get('friends_count', None) @@ -480,11 +488,11 @@ class Tweety(callbacks.Plugin): irc.reply(ret) return else: - if data.has_key('error'): + if 'error' in data: irc.reply("ERROR: I cannot fetch tweets from {0}: {1}".format(optnick, data['error'])) return - if len(data) == 0: # length handled above but user might have 0 tweets. - irc.reply("User: {0} has not tweeted yet.".format(optnick)) + if len(data) == 0: # length handled above but user might have 0 tweets. + irc.reply("ERROR: {0} has not tweeted yet.".format(optnick)) return for tweet in data: text = self._unescape(tweet.get('text', None)) @@ -494,7 +502,7 @@ class Tweety(callbacks.Plugin): relativeTime = self._time_created_at(tweet.get('created_at', None)) self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) - twitter = wrap(twitter, [getopts({'noreply':'','nort':'','info':'','id':'','num':('int')}), ('something')]) + twitter = wrap(twitter, [getopts({'noreply':'', 'nort':'', 'info':'', 'id':'', 'num':('int')}), ('somethingWithoutSpaces')]) Class = Tweety From 9aa9502b36ae1d3f07cfd585eb1d014b45d09bbb Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 12 Apr 2013 18:20:28 -0400 Subject: [PATCH 34/63] Fix some commenting. Fix central error handing of messages. Still work to do. --- plugin.py | 132 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 48 deletions(-) diff --git a/plugin.py b/plugin.py index 969c6f5..2763a29 100644 --- a/plugin.py +++ b/plugin.py @@ -3,23 +3,22 @@ ### # my libs -import urllib, urllib2 +import urllib2 import json import string # libraries for time_created_at import time from datetime import tzinfo, datetime, timedelta # for unescape -import re, htmlentitydefs +import re +import htmlentitydefs # reencode import unicodedata # oauthtwitter -import urlparse import oauth2 as oauth # supybot libs import supybot.utils as utils from supybot.commands import * -import supybot.ircdb as ircdb import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks @@ -27,11 +26,8 @@ import supybot.callbacks as callbacks 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) - else: - token = None + def __init__(self, consumer_key, consumer_secret, token, token_secret): + token = oauth.Token(token, token_secret) self._Consumer = oauth.Consumer(consumer_key, consumer_secret) self._signature_method = oauth.SignatureMethod_HMAC_SHA1() self._access_token = token @@ -118,6 +114,46 @@ class Tweety(callbacks.Plugin): else: pass + ######################## + # COLOR AND FORMATTING # + ######################## + + def _red(self, string): + """Returns a red string.""" + return ircutils.mircColor(string, 'red') + + def _yellow(self, string): + """Returns a yellow string.""" + return ircutils.mircColor(string, 'yellow') + + def _green(self, string): + """Returns a green string.""" + return ircutils.mircColor(string, 'green') + + def _teal(self, string): + """Returns a teal string.""" + return ircutils.mircColor(string, 'teal') + + def _blue(self, string): + """Returns a blue string.""" + return ircutils.mircColor(string, 'blue') + + def _orange(self, string): + """Returns an orange string.""" + return ircutils.mircColor(string, 'orange') + + def _bold(self, string): + """Returns a bold string.""" + return ircutils.bold(string) + + def _ul(self, string): + """Returns an underline string.""" + return ircutils.underline(string) + + def _bu(self, string): + """Returns a bold/underline string.""" + return ircutils.bold(ircutils.underline(string)) + def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" text = text.replace("\n", " ") @@ -142,7 +178,9 @@ class Tweety(callbacks.Plugin): return re.sub("&#?\w+;", fixup, text) def _time_created_at(self, s): - """Return relative delta.""" + """ + Return relative time delta. Ex: 3m ago. + """ 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] @@ -201,7 +239,7 @@ class Tweety(callbacks.Plugin): longurl = "https://twitter.com/#!/{0}/status/{1}".format(nick, tweetid) try: - req = urllib2.Request("http://is.gd/api.php?longurl=" + urllib.quote(longurl)) + req = urllib2.Request("http://is.gd/api.php?longurl=" + utils.web.urlquote(longurl)) f = urllib2.urlopen(req) shorturl = f.read() return shorturl @@ -209,7 +247,7 @@ class Tweety(callbacks.Plugin): return False def _woeid_lookup(self, lookup): - """ + """ Use Yahoo's API to look-up a WOEID. """ @@ -220,7 +258,7 @@ class Tweety(callbacks.Plugin): "env":"store://datatables.org/alltableswithkeys"} try: - response = urllib2.urlopen("http://query.yahooapis.com/v1/public/yql",urllib.urlencode(params)) + response = urllib2.urlopen("http://query.yahooapis.com/v1/public/yql", utils.web.urlencode(params)) data = json.loads(response.read()) if data['query']['count'] > 1: @@ -243,7 +281,7 @@ class Tweety(callbacks.Plugin): woeid = self._woeid_lookup(lookup) if woeid: - irc.reply(("I found WOEID: %s while searching for: '%s'") % (ircutils.bold(woeid), lookup)) + irc.reply(("I found WOEID: %s while searching for: '%s'") % (self._bold(woeid), lookup)) else: irc.reply(("Something broke while looking up: '%s'") % (lookup)) @@ -281,7 +319,7 @@ class Tweety(callbacks.Plugin): endpoint = resourcedict.get("/"+resource, None) minutes = "%sm%ss" % divmod(int(endpoint['reset'])-int(time.time()), 60) output = "Reset in: {0} Remaining: {1}".format(minutes, endpoint['remaining']) - irc.reply("{0} :: {1}".format(ircutils.bold(resource), output)) + irc.reply("{0} :: {1}".format(self._bold(resource), output)) ratelimits = wrap(ratelimits) @@ -325,7 +363,7 @@ class Tweety(callbacks.Plugin): # 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)) + irc.reply("Top 10 Twitter Trends in {0} :: {1}".format(self._bold(location), ttrends)) trends = wrap(trends, [getopts({'exclude':''}), optional('text')]) @@ -342,7 +380,7 @@ class Tweety(callbacks.Plugin): irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - tsearchArgs = {'include_entities':'false', 'count': self.registryValue('defaultSearchResults', msg.args[0]), 'lang':'en', 'q':urllib.quote(optterm)} + tsearchArgs = {'include_entities':'false', 'count': self.registryValue('defaultSearchResults', msg.args[0]), 'lang':'en', 'q':utils.web.urlquote(optterm)} if optlist: for (key, value) in optlist: @@ -354,8 +392,8 @@ class Tweety(callbacks.Plugin): else: tsearchArgs['count'] = value if key == 'searchtype': - tsearchArgs['result_type'] = value # limited by getopts to valid values. - if key == 'lang': # lang . Uses ISO-639 codes like 'en' http://en.wikipedia.org/wiki/ISO_639-1 + tsearchArgs['result_type'] = value # limited by getopts to valid values. + if key == 'lang': # lang . Uses ISO-639 codes like 'en' http://en.wikipedia.org/wiki/ISO_639-1 tsearchArgs['lang'] = value data = self.twitterApi.ApiCall('search/tweets', parameters=tsearchArgs) @@ -368,13 +406,13 @@ class Tweety(callbacks.Plugin): results = data.get('statuses', None) # data returned as a dict if not results or len(results) == 0: - irc.reply("Error: No Twitter Search results found for '{0}'".format(optterm)) + irc.reply("ERROR: No Twitter Search results found for '{0}'".format(optterm)) return else: for result in results: nick = result['user'].get('screen_name', None) name = result["user"].get('name', None) - text = self._unescape(result.get('text', None)) # look also at the unicode strip here. + text = self._unescape(result.get('text', None)) # look also at the unicode strip here. date = self._time_created_at(result.get('created_at', None)) tweetid = result.get('id_str', None) self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), date, tweetid) @@ -418,16 +456,16 @@ class Tweety(callbacks.Plugin): args['info'] = True # handle the three different rest api endpoint urls + twitterArgs dict for options. - if args['id']: + if args['id']: # -id #. apiUrl = 'statuses/show' twitterArgs = {'id': optnick, 'include_entities':'false'} - elif args['info']: + elif args['info']: # --info. apiUrl = 'users/show' twitterArgs = {'screen_name': optnick, 'include_entities':'false'} - else: + else: # if not an --id or --info, we're printing from their timeline. apiUrl = 'statuses/user_timeline' twitterArgs = {'screen_name': optnick, 'count': args['num']} - if args['nort']: # When set to false, the timeline will strip any native retweets + if args['nort']: # When set to false, the timeline will strip any native retweets. twitterArgs['include_rts'] = 'false' else: twitterArgs['include_rts'] = 'true' @@ -442,15 +480,21 @@ class Tweety(callbacks.Plugin): try: data = json.loads(data.read()) except: - irc.reply("Failed to lookup Twitter account for @{0} ({1}) ".format(optnick, data)) + irc.reply("ERROR: Failed to lookup Twitter account for '{0}' ({1}) ".format(optnick, data)) + return + + # check for errors + if 'errors' in data: + errmsg = "" # prep string for output + if data['errors'][0]['code']: + errmsg += "{0} ".format(data['errors'][0]['code']) + if data['errors'][0]['message']: + errmsg += " {0}".format(data['errors'][0]['message']) + irc.reply("ERROR: {0}".format(errmsg)) return # process the data. if args['id']: # If --id was given for a single tweet. - # first, check for errors. - if 'errors' in data: - irc.reply("ERROR: I cannot fetch Tweet ID: {0} message: {1}".format(optnick, data['errors'][0]['message'])) - return text = self._unescape(data.get('text', None)) nick = data["user"].get('screen_name', None) name = data["user"].get('name', None) @@ -459,10 +503,6 @@ class Tweety(callbacks.Plugin): self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) return elif args['info']: # Works with --info to return info on a Twitter user. - # first, check for errors. - if 'errors' in data: - irc.reply("ERROR: I cannot fetch Twitter info for: {0} message: {1}".format(optnick, data['errors'][0]['message'])) - return location = data.get('location', None) followers = data.get('followers_count', None) friends = data.get('friends_count', None) @@ -471,30 +511,26 @@ class Tweety(callbacks.Plugin): name = data.get('name', None) url = data.get('url', None) - # build ret, output string - ret = ircutils.underline(ircutils.bold("@" + optnick)) + # build output string conditionally. + ret = self._bu("@" + screen_name.encode('utf-8')) ret += " ({0}):".format(name.encode('utf-8')) if url: - ret += " {0}".format(ircutils.underline(url.encode('utf-8'))) + ret += " {0}".format(self._ul(url.encode('utf-8'))) if description: ret += " {0}".format(description.encode('utf-8')) - ret += " [{0} friends,".format(ircutils.bold(friends)) - ret += " {0} followers.".format(ircutils.bold(followers)) + ret += " [{0} friends,".format(self._bold(friends)) + ret += " {0} followers.".format(self._bold(followers)) if location: ret += " Location: {0}]".format(location.encode('utf-8')) else: ret += "]" - - irc.reply(ret) + irc.reply(ret) # output info. return - else: - if 'error' in data: - irc.reply("ERROR: I cannot fetch tweets from {0}: {1}".format(optnick, data['error'])) + else: # this will display tweets/a user's timeline. + if len(data) == 0: # If we have no data, user has not tweeted. + irc.reply("ERROR: '{0}' has not tweeted yet.".format(optnick)) return - if len(data) == 0: # length handled above but user might have 0 tweets. - irc.reply("ERROR: {0} has not tweeted yet.".format(optnick)) - return - for tweet in data: + for tweet in data: # iterate through each tweet. text = self._unescape(tweet.get('text', None)) nick = tweet["user"].get('screen_name', None) name = tweet["user"].get('name', None) From b7bde2f906b670c4abbaf30ddb8c92f473289635 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 13 Apr 2013 18:39:18 -0400 Subject: [PATCH 35/63] Add in signup + tweets to --info. Continue to polish up and change some logic. --- plugin.py | 122 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 68 insertions(+), 54 deletions(-) diff --git a/plugin.py b/plugin.py index 2763a29..066c39d 100644 --- a/plugin.py +++ b/plugin.py @@ -5,15 +5,12 @@ # my libs import urllib2 import json -import string # libraries for time_created_at import time -from datetime import tzinfo, datetime, timedelta +from datetime import datetime # for unescape import re import htmlentitydefs -# reencode -import unicodedata # oauthtwitter import oauth2 as oauth # supybot libs @@ -26,6 +23,7 @@ import supybot.callbacks as callbacks class OAuthApi: """ OAuth class to work with Twitter v1.1 API.""" + def __init__(self, consumer_key, consumer_secret, token, token_secret): token = oauth.Token(token, token_secret) self._Consumer = oauth.Consumer(consumer_key, consumer_secret) @@ -33,6 +31,7 @@ class OAuthApi: self._access_token = token def _FetchUrl(self,url, parameters=None): + extra_params = {} if parameters: extra_params.update(parameters) @@ -45,6 +44,7 @@ class OAuthApi: return url_data def _makeOAuthRequest(self, url, token=None, params=None): + oauth_base_params = { 'oauth_version': "1.0", 'oauth_nonce': oauth.generate_nonce(), @@ -63,7 +63,7 @@ class OAuthApi: return request def ApiCall(self, call, parameters={}): - return_value = [] + try: data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", parameters) except urllib2.HTTPError, e: @@ -87,6 +87,7 @@ class Tweety(callbacks.Plugin): 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'): @@ -156,6 +157,7 @@ class Tweety(callbacks.Plugin): def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" + text = text.replace("\n", " ") def fixup(m): text = m.group(0) @@ -189,10 +191,8 @@ class Tweety(callbacks.Plugin): ddate = time.strptime(s, "%a, %d %b %Y %H:%M:%S +0000")[:-2] except ValueError: return s - # do the math d = datetime.utcnow() - datetime(*ddate, tzinfo=None) - # now parse and return. if d.days: rel_time = "%sd ago" % d.days @@ -211,21 +211,24 @@ class Tweety(callbacks.Plugin): outputColorTweets = self.registryValue('outputColorTweets', msg.args[0]) - if outputColorTweets: - ret = ircutils.underline(ircutils.mircColor(("@" + nick), 'blue')) - else: - ret = ircutils.underline(ircutils.bold("@" + nick)) + # build output string. + if outputColorTweets: # blue if color is on. + ret = "@{0}".format(self._ul(self._blue(nick))) + else: # bold otherwise. + ret = "@{0}".format(self._ul(self._bold(nick))) - if not self.registryValue('hideRealName', msg.args[0]): # show realname in tweet output? + # show real name in tweet output? + if not self.registryValue('hideRealName', msg.args[0]): ret += " ({0})".format(name) - # add in the end with the text + tape + # add in the end with the text + tape. if outputColorTweets: - text = re.sub(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)', ircutils.mircColor(r'\1', 'red'), text) # color urls. - ret += ": {0} ({1})".format(text, ircutils.mircColor(time, 'yellow')) + text = re.sub(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)', self._red(r'\1'), text) # color urls. + ret += ": {0} ({1})".format(text, self._yellow(time)) else: - ret += ": {0} ({1})".format(text, ircutils.bold(time)) + ret += ": {0} ({1})".format(text, self._bold(time)) + # short url the link to the tweet? if self.registryValue('addShortUrl', msg.args[0]): if self._createShortUrl(nick, tweetid): ret += " {0}".format(url) @@ -336,7 +339,7 @@ class Tweety(callbacks.Plugin): irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - args = {'id': self.registryValue('woeid', msg.args[0]),'exclude': self.registryValue('hideHashtagsTrends', msg.args[0])} + args = {'id': self.registryValue('woeid', msg.args[0]), 'exclude': self.registryValue('hideHashtagsTrends', msg.args[0])} if getopts: for (key, value) in getopts: if key == 'exclude': # remove hashtags from trends. @@ -362,7 +365,7 @@ class Tweety(callbacks.Plugin): return # package together in object and output. - ttrends = string.join([trend['name'].encode('utf-8') for trend in data[0]['trends']], " | ") + ttrends = " | ".join([trend['name'].encode('utf-8') for trend in data[0]['trends']]) irc.reply("Top 10 Twitter Trends in {0} :: {1}".format(self._bold(location), ttrends)) trends = wrap(trends, [getopts({'exclude':''}), optional('text')]) @@ -410,22 +413,22 @@ class Tweety(callbacks.Plugin): return else: for result in results: - nick = result['user'].get('screen_name', None) - name = result["user"].get('name', None) - text = self._unescape(result.get('text', None)) # look also at the unicode strip here. - date = self._time_created_at(result.get('created_at', None)) - tweetid = result.get('id_str', None) + nick = result['user'].get('screen_name') + name = result["user"].get('name') + text = self._unescape(result.get('text')) + date = self._time_created_at(result.get('created_at')) + tweetid = result.get('id_str') self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), date, tweetid) 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] + """[--noreply] [--nort] [--num number] | [--id id] | [--info nick] Returns last tweet or 'number' tweets (max 10). Shows all tweets, including rt and reply. To not display replies or RT's, use --noreply or --nort, respectively. - Or returns tweet with id 'id'. - Or returns information on user with --info. + Or returns specific tweet with --id 'tweet#'. + Or returns information on user with --info 'name'. """ # before we do anything, make sure we have a twitterApi object. @@ -448,7 +451,7 @@ class Tweety(callbacks.Plugin): args['noreply'] = True if key == 'num': if value > self.registryValue('maxResults', msg.args[0]) or value <= 0: - irc.reply("Error: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(value, max)) + irc.reply("ERROR: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(value, max)) return else: args['num'] = value @@ -495,47 +498,58 @@ class Tweety(callbacks.Plugin): # process the data. if args['id']: # If --id was given for a single tweet. - text = self._unescape(data.get('text', None)) - nick = data["user"].get('screen_name', None) - name = data["user"].get('name', None) - relativeTime = self._time_created_at(data.get('created_at', None)) - tweetid = data.get('id', None) + text = self._unescape(data.get('text')) + nick = data["user"].get('screen_name') + name = data["user"].get('name') + relativeTime = self._time_created_at(data.get('created_at')) + tweetid = data.get('id') + # send to outputTweet because it is an individual "tweet". self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) return - elif args['info']: # Works with --info to return info on a Twitter user. - location = data.get('location', None) - followers = data.get('followers_count', None) - friends = data.get('friends_count', None) - description = data.get('description', None) - screen_name = data.get('screen_name', None) - name = data.get('name', None) - url = data.get('url', None) - + elif args['info']: # --info to return info on a Twitter user. + location = data.get('location') + followers = data.get('followers_count') + friends = data.get('friends_count') + description = data.get('description') + screen_name = data.get('screen_name') + created_at = data.get('created_at') + listed_count = data.get('listed_count') + protected = data.get('protected') + name = data.get('name') + url = data.get('url') # build output string conditionally. - ret = self._bu("@" + screen_name.encode('utf-8')) - ret += " ({0}):".format(name.encode('utf-8')) - if url: + # we don't use outputTweet since it's not a tweet. + ret = self._bu("@{0}".format(screen_name.encode('utf-8'))) + ret += " ({0})".format(name.encode('utf-8')) + if protected: # is the account protected/locked? + ret += " [{0}]:".format(self._bu('LOCKED')) + else: # open. + ret += ":" + if url: # do they have a url? ret += " {0}".format(self._ul(url.encode('utf-8'))) - if description: + if description: # a description? ret += " {0}".format(description.encode('utf-8')) ret += " [{0} friends,".format(self._bold(friends)) - ret += " {0} followers.".format(self._bold(followers)) - if location: + ret += " {0} tweets,".format(self._bold(listed_count)) + ret += " {0} followers,".format(self._bold(followers)) + ret += " signup: {0}".format(self._bold(self._time_created_at(created_at))) + if location: # do we have location? ret += " Location: {0}]".format(location.encode('utf-8')) - else: + else: # nope. ret += "]" - irc.reply(ret) # output info. + # finally, output. + irc.reply(ret) return else: # this will display tweets/a user's timeline. if len(data) == 0: # If we have no data, user has not tweeted. irc.reply("ERROR: '{0}' has not tweeted yet.".format(optnick)) return for tweet in data: # iterate through each tweet. - text = self._unescape(tweet.get('text', None)) - nick = tweet["user"].get('screen_name', None) - name = tweet["user"].get('name', None) - tweetid = tweet.get('id', None) - relativeTime = self._time_created_at(tweet.get('created_at', None)) + text = self._unescape(tweet.get('text')) + nick = tweet["user"].get('screen_name') + name = tweet["user"].get('name') + tweetid = tweet.get('id') + relativeTime = self._time_created_at(tweet.get('created_at')) self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) twitter = wrap(twitter, [getopts({'noreply':'', 'nort':'', 'info':'', 'id':'', 'num':('int')}), ('somethingWithoutSpaces')]) From 2c146c804693a4b639c7219f45ba1f44169e58b5 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 13 Apr 2013 18:54:48 -0400 Subject: [PATCH 36/63] Ooops. I used the wrong field for tweets in --info. --- plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index 066c39d..6b7271e 100644 --- a/plugin.py +++ b/plugin.py @@ -513,7 +513,7 @@ class Tweety(callbacks.Plugin): description = data.get('description') screen_name = data.get('screen_name') created_at = data.get('created_at') - listed_count = data.get('listed_count') + statuses_count = data.get('statuses_count') protected = data.get('protected') name = data.get('name') url = data.get('url') @@ -530,7 +530,7 @@ class Tweety(callbacks.Plugin): if description: # a description? ret += " {0}".format(description.encode('utf-8')) ret += " [{0} friends,".format(self._bold(friends)) - ret += " {0} tweets,".format(self._bold(listed_count)) + ret += " {0} tweets,".format(self._bold(statuses_count)) ret += " {0} followers,".format(self._bold(followers)) ret += " signup: {0}".format(self._bold(self._time_created_at(created_at))) if location: # do we have location? From 026ca36ec78d429967a67ca9a1f3d59d1ab366d1 Mon Sep 17 00:00:00 2001 From: spline Date: Thu, 23 May 2013 16:14:09 -0400 Subject: [PATCH 37/63] Update README --- README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3dde2ca..c8603e2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Supybot-Tweety Description ----------- - + 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. @@ -14,7 +14,7 @@ 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: +read-only features (no risk of accidental Tweeting) that most folks use: tweet display, tweet searching and trends. If you need to do any type of Twittering via the bot such as posting tweets, responding, @@ -22,15 +22,15 @@ announcing of timelines, you will want his. This will never contain more than wh Instructions ------------ -1.) On an up-to-date Python 2.7+ system, one dependency is needed. +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. + 2. Go to dev.twitter.com and log in. 3. Click create an application. - 4. Fill out the information. Name does not matter. + 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 load Tweety, you need to set these keys: @@ -38,8 +38,11 @@ Instructions * /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. + 8. /msg reload Tweety - THIS IS REQUIRED. YOU MUST RELOAD BEFORE USING. + + Next, I suggest you /msg config search Tweety. There are a lot of options here. + + Things should work fine from here providing your keys are right. Examples -------- @@ -47,9 +50,9 @@ Examples 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 +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 +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. From 2312fe4199949cce6c121a06ffae2c4726e7fb3a Mon Sep 17 00:00:00 2001 From: spline Date: Sun, 26 May 2013 15:35:54 -0400 Subject: [PATCH 38/63] Implement config variable to require voice to use all commands and code in functions. --- config.py | 1 + plugin.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/config.py b/config.py index c56ed18..b0c0528 100644 --- a/config.py +++ b/config.py @@ -28,5 +28,6 @@ conf.registerChannelValue(Tweety,'defaultResults',registry.Integer(1, """Default conf.registerChannelValue(Tweety,'maxResults',registry.Integer(10, """Maximum number of results to return on timelines.""")) 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.""")) +conf.registerChannelValue(Tweety,'requireVoiceOrAbove',registry.Boolean(False, """Only allows a user with voice or above on a channel to use commands.""")) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=250: diff --git a/plugin.py b/plugin.py index 6b7271e..3cd535f 100644 --- a/plugin.py +++ b/plugin.py @@ -334,6 +334,10 @@ class Tweety(callbacks.Plugin): Use --exclude to not include #hashtags in trends data. """ + # enforce +voice or above to use command? + if self.registryValue('requireVoiceOrAbove', msg.args[0]) and not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): + irc.error("ERROR: You have to be at least voiced to use this command.") + # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") @@ -378,6 +382,10 @@ class Tweety(callbacks.Plugin): searchtype being recent, popular or mixed. Popular is the default. """ + # enforce +voice or above to use command? + if self.registryValue('requireVoiceOrAbove', msg.args[0]) and not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): + irc.error("ERROR: You have to be at least voiced to use this command.") + # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") @@ -431,6 +439,10 @@ class Tweety(callbacks.Plugin): Or returns information on user with --info 'name'. """ + # enforce +voice or above to use command? + if self.registryValue('requireVoiceOrAbove', msg.args[0]) and not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): + irc.error("ERROR: You have to be at least voiced to use this command.") + # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") From 7285b91affbea23be985d4ef5029069b01806bf5 Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 27 May 2013 10:51:54 -0400 Subject: [PATCH 39/63] Add in local stub. I assume this was missing because code was developed outside Limnoria. --- local/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 local/__init__.py diff --git a/local/__init__.py b/local/__init__.py new file mode 100644 index 0000000..e86e97b --- /dev/null +++ b/local/__init__.py @@ -0,0 +1 @@ +# Stub so local is a module, used for third-party modules From 07ba62823b65ebcec0f545a3dfa39053507e99fa Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 27 May 2013 18:05:35 -0400 Subject: [PATCH 40/63] Fix header in __init__.py --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index b0d8185..585a605 100644 --- a/__init__.py +++ b/__init__.py @@ -22,7 +22,7 @@ __author__ = supybot.Author('reticulatingspline', 'spline', 'spline') __contributors__ = {} # This is a url where the most recent plugin package can be downloaded. -__url__ = '' # 'http://supybot.com/Members/yourname/Tweety/download' +__url__ = 'http://github.com/reticulatingspline/Supybot-Tweety' import config import plugin From 5dab3507576104a0e95e072875a96e4edef9aab7 Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 27 May 2013 18:05:49 -0400 Subject: [PATCH 41/63] Update README --- README.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c8603e2..46dcdde 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,10 @@ This is a much watered down version of ProgVal's Twitter client. It only include read-only features (no risk of accidental Tweeting) that most folks use: tweet display, tweet searching and trends. -If you need to do any type of Twittering via the bot such as posting tweets, responding, -announcing of timelines, you will want his. This will never contain more than what is above. +If you need to do any type of Twittering via the bot such as posting tweets, +responding, announcing of timelines, you will want ProgVals. + +This will never contain more than what is above. Instructions ------------ @@ -47,6 +49,21 @@ Instructions Examples -------- + - Twitter Trends + trends + Top 10 Twitter Trends in United States :: #BeforeIDieIWantTo | #ThingsIMissAboutMyChildhood | Happy Memorial Day | #RG13 | #USA | #america | BBQ | WWII | God Bless | Facebook + + - Searching Twitter + tsearch news + @ray_gallego (Ray Gallego): http://t.co/ftNbDEzXaR (Researchers say Western IQs dropped 14 points over last century) (14s ago) + @surfing93 (emilyhenderson): @MariaaEveline Hay here is the Crestillion Interview. http://t.co/CEiDpboeMX (15s ago) + + - Getting tweets from someones' timeline. + twitter --num 3 @ESPNStatsInfo + @ESPNStatsInfo (ESPN Stats & Info): In 1st-round win vs Daniel Brands, Rafael Nadal lost 19 games. He lost a total of 19 games in the 1st 4 rounds at last year's French Open. (30m ago) + @ESPNStatsInfo (ESPN Stats & Info): Key stats from Miami's win yesterday. Haslem's jump shot, LeBron's post-up and more: http://t.co/a4CcUnKJMi (53m ago) + @ESPNStatsInfo (ESPN Stats & Info): Heat avoid losing consecutive games. They haven't lost 2 straight in more than 5 months (January 8-10) (1h ago) + Background ---------- Hoaas, on GitHub, started this plugin with basics for Twitter and I started to submit @@ -59,7 +76,7 @@ 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: +Much/almost all of the oAuth code came from: https://github.com/jpittman/OAuth-Python-Twitter Documentation From 9a2c05b60171583b1c78ad304799b4025175f36f Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 27 May 2013 18:06:12 -0400 Subject: [PATCH 42/63] Add in new config variable. Polish up plugin.py a bit and refactor some code. --- config.py | 1 + plugin.py | 390 +++++++++++++++++++++++++++++------------------------- 2 files changed, 211 insertions(+), 180 deletions(-) diff --git a/config.py b/config.py index b0c0528..0c1e390 100644 --- a/config.py +++ b/config.py @@ -29,5 +29,6 @@ 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.""")) conf.registerChannelValue(Tweety,'requireVoiceOrAbove',registry.Boolean(False, """Only allows a user with voice or above on a channel to use commands.""")) +conf.registerChannelValue(Tweety,'colorTweetURLs',registry.Boolean(False, """Try and color URLs (red) in Tweets?""")) # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=250: diff --git a/plugin.py b/plugin.py index 3cd535f..48e4285 100644 --- a/plugin.py +++ b/plugin.py @@ -22,7 +22,7 @@ import supybot.callbacks as callbacks class OAuthApi: - """ OAuth class to work with Twitter v1.1 API.""" + """OAuth class to work with Twitter v1.1 API.""" def __init__(self, consumer_key, consumer_secret, token, token_secret): token = oauth.Token(token, token_secret) @@ -31,19 +31,21 @@ class OAuthApi: self._access_token = token def _FetchUrl(self,url, parameters=None): + """Fetch a URL with oAuth. Returns a string containing the body of the response.""" extra_params = {} if parameters: extra_params.update(parameters) req = self._makeOAuthRequest(url, params=extra_params) - opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=1)) + opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=0)) url = req.to_url() url_data = opener.open(url) opener.close() return url_data def _makeOAuthRequest(self, url, token=None, params=None): + """Make a OAuth request from url and parameters. Returns oAuth object.""" oauth_base_params = { 'oauth_version': "1.0", @@ -63,14 +65,15 @@ class OAuthApi: return request def ApiCall(self, call, parameters={}): + """Calls the twitter API with 'call' and returns the twitter object (JSON).""" try: data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", parameters) - except urllib2.HTTPError, e: - return e - except urllib2.URLError, e: - return e - else: + except urllib2.HTTPError, e: # http error code. + return e.code + except urllib2.URLError, e: # http "reason" + return e.reason + else: # return data if good. return data @@ -88,31 +91,32 @@ class Tweety(callbacks.Plugin): def _checkAuthorization(self): """ Check if we have our keys and can auth.""" - if not self.twitterApi: - failTest = False + if not self.twitterApi: # if not set, try and auth. + failTest = False # first check that we have all 4 keys. for checkKey in ('consumerKey', 'consumerSecret', 'accessKey', 'accessSecret'): - try: + try: # try to see if each key is set. testKey = self.registryValue(checkKey) - except: + except: # a key is not set, break and error. self.log.debug("Failed checking keys. We're missing the config value for: {0}. Please set this and try again.".format(checkKey)) failTest = True break - + # if any missing, throw an error and keep twitterApi=False if failTest: - self.log.error('Failed getting keys. You must set all 4 keys in config variables.') + self.log.error('Failed getting keys. You must set all 4 keys in config variables and reload plugin.') return False - + # We have all 4 keys. Now lets see if they are valid by calling verify_credentials in the API. self.log.info("Got all 4 keys. Now trying to auth up with Twitter.") twitterApi = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) data = twitterApi.ApiCall('account/verify_credentials') - - if data.getcode() == "401": - self.log.error("ERROR: I could not log in using your credentials. Message: %s" % data.read()) - return False - else: + # check the response. if we can load json, it means we're authenticated. else, return response. + try: # if we pass, response is validated. set self.twitterApi w/object. + json.loads(data.read()) self.log.info("I have successfully authorized and logged in to Twitter using your credentials.") self.twitterApi = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) - else: + except: # response failed. Return what we got back. + self.log.error("ERROR: I could not log in using your credentials. Message: {0}".format(data)) + return False + else: # if we're already validated, pass. pass ######################## @@ -123,26 +127,10 @@ class Tweety(callbacks.Plugin): """Returns a red string.""" return ircutils.mircColor(string, 'red') - def _yellow(self, string): - """Returns a yellow string.""" - return ircutils.mircColor(string, 'yellow') - - def _green(self, string): - """Returns a green string.""" - return ircutils.mircColor(string, 'green') - - def _teal(self, string): - """Returns a teal string.""" - return ircutils.mircColor(string, 'teal') - def _blue(self, string): """Returns a blue string.""" return ircutils.mircColor(string, 'blue') - def _orange(self, string): - """Returns an orange string.""" - return ircutils.mircColor(string, 'orange') - def _bold(self, string): """Returns a bold string.""" return ircutils.bold(string) @@ -155,6 +143,10 @@ class Tweety(callbacks.Plugin): """Returns a bold/underline string.""" return ircutils.bold(ircutils.underline(string)) + ###################### + # INTERNAL FUNCTIONS # + ###################### + def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" @@ -181,7 +173,7 @@ class Tweety(callbacks.Plugin): def _time_created_at(self, s): """ - Return relative time delta. Ex: 3m ago. + Return relative time delta between now and s (dt string). """ try: # timeline's created_at Tue May 08 10:58:49 +0000 2012 @@ -206,87 +198,85 @@ class Tweety(callbacks.Plugin): 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. + Constructs string to output for Tweet. Used for tsearch and twitter. """ - outputColorTweets = self.registryValue('outputColorTweets', msg.args[0]) - # build output string. - if outputColorTweets: # blue if color is on. + if self.registryValue('outputColorTweets', msg.args[0]): ret = "@{0}".format(self._ul(self._blue(nick))) else: # bold otherwise. - ret = "@{0}".format(self._ul(self._bold(nick))) - + ret = "@{0}".format(self._bu(nick)) # show real name in tweet output? if not self.registryValue('hideRealName', msg.args[0]): ret += " ({0})".format(name) - # add in the end with the text + tape. - if outputColorTweets: - text = re.sub(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)', self._red(r'\1'), text) # color urls. - ret += ": {0} ({1})".format(text, self._yellow(time)) - else: + 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) + ret += ": {0} ({1})".format(text, self._bold(time)) + else: # only bold time. no text color. ret += ": {0} ({1})".format(text, self._bold(time)) - # short url the link to the tweet? if self.registryValue('addShortUrl', msg.args[0]): - if self._createShortUrl(nick, tweetid): + url = self._createShortUrl(nick, tweetid) + if url: # if we got a url back. ret += " {0}".format(url) - - irc.reply(ret) + # now return. + return ret def _createShortUrl(self, nick, tweetid): - """ - Takes a nick and tweetid and returns a shortened URL via is.gd service. - """ + """Shortens a tweet into a short one.""" - longurl = "https://twitter.com/#!/{0}/status/{1}".format(nick, tweetid) try: - req = urllib2.Request("http://is.gd/api.php?longurl=" + utils.web.urlquote(longurl)) - f = urllib2.urlopen(req) - shorturl = f.read() - return shorturl - except: - return False + longurl = "https://twitter.com/#!/%s/status/%s" % (nick, tweetid) + posturi = "https://www.googleapis.com/urlshortener/v1/url" + data = json.dumps({'longUrl': longurl}) + headers = {'Content-Type':'application/json'} + request = utils.web.getUrl(posturi, data=data, headers=headers) + return json.loads(request)['id'] + except Exception, err: + self.log.error("ERROR: Failed shortening url: {0} :: {1}".format(longurl, err)) + return None def _woeid_lookup(self, lookup): """ Use Yahoo's API to look-up a WOEID. """ - query = "select * from geo.places where text='%s'" % lookup + query = "SELECT * FROM geo.places WHERE text='%s'" % lookup params = {"q": query, "format":"json", "diagnostics":"false", - "env":"store://datatables.org/alltableswithkeys"} - + "env":"store://datatables.org/alltableswithkeys" } + # everything in try/except block incase it breaks. try: - response = urllib2.urlopen("http://query.yahooapis.com/v1/public/yql", utils.web.urlencode(params)) - data = json.loads(response.read()) + url = "http://query.yahooapis.com/v1/public/yql?"+utils.web.urlencode(params) + response = utils.web.getUrl(url) + data = json.loads(response) - if data['query']['count'] > 1: + if data['query']['count'] > 1: # return the "first" one. woeid = data['query']['results']['place'][0]['woeid'] - else: + else: # if one, return it. woeid = data['query']['results']['place']['woeid'] return woeid except Exception, err: - self.log.error("Error looking up %s :: %s" % (lookup,err)) + self.log.error("ERROR: Failed looking up WOEID for '{0}' :: {1}".format(lookup, err)) return None - ########################## - ### PUBLIC FUNCTIONS ##### - ########################## + #################### + # PUBLIC FUNCTIONS # + #################### def woeidlookup(self, irc, msg, args, lookup): """ Search Yahoo's WOEID DB for a location. Useful for the trends variable. + Ex: London or Boston """ woeid = self._woeid_lookup(lookup) if woeid: - irc.reply(("I found WOEID: %s while searching for: '%s'") % (self._bold(woeid), lookup)) + irc.reply("WOEID: {0} for '{1}'".format(self._bold(woeid), lookup)) else: - irc.reply(("Something broke while looking up: '%s'") % (lookup)) + irc.reply("ERROR: Something broke trying to find a WOEID for '{0}'".format(lookup)) woeidlookup = wrap(woeidlookup, ['text']) @@ -299,76 +289,92 @@ class Tweety(callbacks.Plugin): if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - - data = self.twitterApi.ApiCall('application/rate_limit_status') + # make API call. + data = self.twitterApi.ApiCall('application/rate_limit_status', parameters={'resources':'trends,search,statuses,users'}) try: data = json.loads(data.read()) except: - irc.reply("Failed to lookup ratelimit data: %s" % data) + irc.reply("ERROR: Failed to lookup ratelimit data: {0}".format(data)) return - - data = data.get('resources', None) + # parse data; + data = data.get('resources') 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) + irc.reply("ERROR: Failed to fetch application rate limit status. Something could be wrong with Twitter.") + self.log.error("ERROR: fetching rate limit data. '{0}'".format(data)) return - - # we only have resources needed in here. def below works with each entry properly. - resourcelist = ['trends/place', 'search/tweets', 'users/show/:id', 'statuses/show/:id', 'statuses/user_timeline'] - - for resource in resourcelist: - family, endpoint = resource.split('/', 1) # need to split each entry on /, resource family is [0], append / to entry. - resourcedict = data.get(family, None) - endpoint = resourcedict.get("/"+resource, None) - minutes = "%sm%ss" % divmod(int(endpoint['reset'])-int(time.time()), 60) + # dict of resources we want and how to parse. key=human name, values are for the json dict. + resources = {'trends':['trends', '/trends/place'], + 'tsearch':['search', '/search/tweets'], + 'twitter --id':['statuses', '/statuses/show/:id'], + 'twitter --info':['users', '/users/show/:id'], + 'twitter timeline':['statuses', '/statuses/user_timeline'] } + # now iterate through dict above. + for resource in resources: + rdict = resources[resource] # get value. + endpoint = data.get(rdict[0]).get(rdict[1]) # value[0], value[1] + minutes = "%sm%ss" % divmod(int(endpoint['reset'])-int(time.time()), 60) # math. output = "Reset in: {0} Remaining: {1}".format(minutes, endpoint['remaining']) irc.reply("{0} :: {1}".format(self._bold(resource), output)) ratelimits = wrap(ratelimits) def trends(self, irc, msg, args, getopts, optwoeid): - """[--exclude] + """[--exclude] [location] - Returns the Top 10 Twitter trends for a specific location. - Use optional argument location for trends. Ex: trends Boston + Returns the Top 10 Twitter trends for a specific location. Use optional argument location for trends. + Defaults to worldwide and can be set via config variable. Use --exclude to not include #hashtags in trends data. + Ex: Boston or --exclude London """ # enforce +voice or above to use command? - if self.registryValue('requireVoiceOrAbove', msg.args[0]) and not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): - irc.error("ERROR: You have to be at least voiced to use this command.") + if self.registryValue('requireVoiceOrAbove', msg.args[0]): + if irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they voice or op? + irc.error("ERROR: You have to be at least voiced to use the trends command in {0}.".format(msg.args[0])) + return # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - args = {'id': self.registryValue('woeid', msg.args[0]), 'exclude': self.registryValue('hideHashtagsTrends', msg.args[0])} + # default arguments. + args = {'id': self.registryValue('woeid', msg.args[0]), + 'exclude': self.registryValue('hideHashtagsTrends', msg.args[0])} + # handle input. if getopts: for (key, value) in getopts: if key == 'exclude': # remove hashtags from trends. args['exclude'] = 'hashtags' - # work with woeid. 1 is world, the default. can be set via input or via config. - if optwoeid: - woeid = self._woeid_lookup(optwoeid) - if woeid: - args['id'] = woeid - + if optwoeid: # if we have an input location, lookup the woeid. + if optwoeid.lower().startswith('world'): # looking for worldwide or some variation. (bypass) + args['id'] = 1 # "World Wide" is worldwide (odd bug) = 1. + else: # looking for something else. + woeid = self._woeid_lookup(optwoeid) # yahoo search for woeid. + if woeid: # if we get a returned value, set it. otherwise default value. + args['id'] = woeid + else: # location not found. + irc.reply("ERROR: I could not lookup location: {0}. Try a different location.".format(optwoeid)) + return + # now build our API call data = self.twitterApi.ApiCall('trends/place', parameters=args) try: data = json.loads(data.read()) except: - irc.reply("Error: failed to lookup Twitter trends: %s" % data) + irc.reply("ERROR: failed to lookup trends on Twitter: {0}".format(data)) return - - try: - location = data[0]['locations'][0]['name'] - except: - irc.reply("ERROR: Cannot load trends: {0}".format(data)) # error also throws 404. - return - - # package together in object and output. + # now, before processing, check for errors: + if 'errors' in data: + if data['errors'][0]['code'] == 34: # 34 means location not found. + irc.reply("ERROR: I do not have any trends for: {0}".format(optwoeid)) + return + else: # just return the message. + errmsg = data['errors'][0] + irc.reply("ERROR: Could not load trends. ({0} {1})".format(errmsg['code'], errmsg['message'])) + return + # if no error here, we found trends. prepare string and output. + location = data[0]['locations'][0]['name'] ttrends = " | ".join([trend['name'].encode('utf-8') for trend in data[0]['trends']]) irc.reply("Top 10 Twitter Trends in {0} :: {1}".format(self._bold(location), ttrends)) @@ -378,57 +384,68 @@ class Tweety(callbacks.Plugin): """[--num number] [--searchtype mixed,recent,popular] [--lang xx] Searches Twitter for the and returns the most recent results. - Number is number of results. Must be a number higher than 0 and max 10. - searchtype being recent, popular or mixed. Popular is the default. + --num is number of results. (1-10) + --searchtype being recent, popular or mixed. Popular is the default. + Ex: --num 3 breaking news """ # enforce +voice or above to use command? - if self.registryValue('requireVoiceOrAbove', msg.args[0]) and not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): - irc.error("ERROR: You have to be at least voiced to use this command.") + if self.registryValue('requireVoiceOrAbove', msg.args[0]): + if irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they voice or op? + irc.error("ERROR: You have to be at least voiced to use the tsearch command in {0}.".format(msg.args[0])) + return # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return - tsearchArgs = {'include_entities':'false', 'count': self.registryValue('defaultSearchResults', msg.args[0]), 'lang':'en', 'q':utils.web.urlquote(optterm)} - + # default arguments. + tsearchArgs = {'include_entities':'false', + 'count': self.registryValue('defaultSearchResults', msg.args[0]), + 'lang':'en', + 'q':utils.web.urlquote(optterm)} + # check input. if optlist: for (key, value) in optlist: - if key == 'num': - max = self.registryValue('maxSearchResults', msg.args[0]) - if value > max or value <= 0: - irc.reply("Error: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(value, max)) + if key == 'num': # --num + maxresults = self.registryValue('maxSearchResults', msg.args[0]) + if not (1 <= value <= maxresults): # make sure it's between what we should output. + irc.reply("ERROR: '{0}' is not a valid number of tweets. Range is between 1 and {1}.".format(value, maxresults)) return - else: + else: # change number to output. tsearchArgs['count'] = value - if key == 'searchtype': + if key == 'searchtype': # getopts limits us here. tsearchArgs['result_type'] = value # limited by getopts to valid values. if key == 'lang': # lang . Uses ISO-639 codes like 'en' http://en.wikipedia.org/wiki/ISO_639-1 tsearchArgs['lang'] = value - + # now build our API call. data = self.twitterApi.ApiCall('search/tweets', parameters=tsearchArgs) try: data = json.loads(data.read()) except: - irc.reply("Error: %s trying to search Twitter." % data) + irc.reply("ERROR: Something went wrong trying to search Twitter. ({0})".format(data)) return - - results = data.get('statuses', None) # data returned as a dict - - if not results or len(results) == 0: + # check the return data. + results = data.get('statuses') # data returned as a dict. + if not results or len(results) == 0: # found nothing or length 0. irc.reply("ERROR: No Twitter Search results found for '{0}'".format(optterm)) return - else: - for result in results: - nick = result['user'].get('screen_name') - name = result["user"].get('name') - text = self._unescape(result.get('text')) + else: # we found something. + for result in results: # iterate over each. + nick = result['user'].get('screen_name').encode('utf-8') + name = result["user"].get('name').encode('utf-8') + text = self._unescape(result.get('text')).encode('utf-8') date = self._time_created_at(result.get('created_at')) tweetid = result.get('id_str') - self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), date, tweetid) + # build output string and output. + output = self._outputTweet(irc, msg, nick, name, text, date, tweetid) + irc.reply(output) - tsearch = wrap(tsearch, [getopts({'num':('int'), 'searchtype':('literal', ('popular', 'mixed', 'recent')), 'lang':('somethingWithoutSpaces')}), ('text')]) + 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] @@ -437,21 +454,28 @@ class Tweety(callbacks.Plugin): To not display replies or RT's, use --noreply or --nort, respectively. Or returns specific tweet with --id 'tweet#'. Or returns information on user with --info 'name'. + Ex: --info @cnn OR --id 337197009729622016 OR --number 3 @drudge """ # enforce +voice or above to use command? - if self.registryValue('requireVoiceOrAbove', msg.args[0]) and not irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): - irc.error("ERROR: You have to be at least voiced to use this command.") + if self.registryValue('requireVoiceOrAbove', msg.args[0]): + if irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they voice or op? + irc.error("ERROR: You have to be at least voiced to use the twitter command in {0}.".format(msg.args[0])) + return # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: irc.reply("ERROR: Twitter is not authorized. Please check logs before running this command.") return + # now begin optnick = optnick.replace('@','') # strip @ from input if given. - - args = {'id': False, 'nort': False, 'noreply': False, 'num': self.registryValue('defaultResults', msg.args[0]), 'info': False} - + # default options. + args = {'id': False, + 'nort': False, + 'noreply': False, + 'num': self.registryValue('defaultResults', msg.args[0]), + 'info': False} # handle input optlist. if optlist: for (key, value) in optlist: @@ -462,14 +486,14 @@ class Tweety(callbacks.Plugin): if key == 'noreply': args['noreply'] = True if key == 'num': - if value > self.registryValue('maxResults', msg.args[0]) or value <= 0: - irc.reply("ERROR: '{0}' is not a valid number of tweets. Range is above 0 and max {1}.".format(value, max)) + maxresults = self.registryValue('maxResults', msg.args[0]) + if not (1 <= value <= maxresults): # make sure it's between what we should output. + irc.reply("ERROR: '{0}' is not a valid number of tweets. Range is between 1 and {1}.".format(value, maxresults)) return - else: + else: # number is valid so return this. args['num'] = value if key == 'info': args['info'] = True - # handle the three different rest api endpoint urls + twitterArgs dict for options. if args['id']: # -id #. apiUrl = 'statuses/show' @@ -480,43 +504,44 @@ class Tweety(callbacks.Plugin): else: # if not an --id or --info, we're printing from their timeline. apiUrl = 'statuses/user_timeline' twitterArgs = {'screen_name': optnick, 'count': args['num']} - if args['nort']: # When set to false, the timeline will strip any native retweets. + if args['nort']: # show retweets? twitterArgs['include_rts'] = 'false' - else: + else: # default is to show retweets. twitterArgs['include_rts'] = 'true' - - if args['noreply']: # This parameter will prevent replies from appearing in the returned timeline. + if args['noreply']: # show replies? twitterArgs['exclude_replies'] = 'true' - else: + else: # default is to NOT exclude replies. twitterArgs['exclude_replies'] = 'false' - - # now with and call the api. + # call the Twitter API with our data. data = self.twitterApi.ApiCall(apiUrl, parameters=twitterArgs) try: data = json.loads(data.read()) except: - irc.reply("ERROR: Failed to lookup Twitter account for '{0}' ({1}) ".format(optnick, data)) + irc.reply("ERROR: Failed to lookup Twitter for '{0}' ({1}) ".format(optnick, data)) return - - # check for errors + # before anything, check for errors. errmsg is conditional. if 'errors' in data: - errmsg = "" # prep string for output - if data['errors'][0]['code']: - errmsg += "{0} ".format(data['errors'][0]['code']) - if data['errors'][0]['message']: - errmsg += " {0}".format(data['errors'][0]['message']) - irc.reply("ERROR: {0}".format(errmsg)) - return - - # process the data. + if data['errors'][0]['code'] == 34: # not found. + if args['id']: # --id #. # is not found. + errmsg = "ERROR: Tweet ID '{0}' not found.".format(optnick) + else: # --info or twitter not found. + errmsg = "ERROR: Twitter user '{0}' not found.".format(optnick) + irc.reply(errmsg) # print the error and exit. + return + else: # errmsg is not 34. just return it. + errmsg = data['errors'][0] + irc.reply("ERROR: {0} {1}".format(errmsg['code'], errmsg['message'])) + return + # no errors, so we process data conditionally. if args['id']: # If --id was given for a single tweet. - text = self._unescape(data.get('text')) - nick = data["user"].get('screen_name') - name = data["user"].get('name') + text = self._unescape(data.get('text')).encode('utf-8') + nick = data["user"].get('screen_name').encode('utf-8') + name = data["user"].get('name').encode('utf-8') relativeTime = self._time_created_at(data.get('created_at')) tweetid = data.get('id') - # send to outputTweet because it is an individual "tweet". - self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) + # prepare string to output and send to irc. + output = self._outputTweet(irc, msg, nick, name, text, relativeTime, tweetid) + irc.reply(output) return elif args['info']: # --info to return info on a Twitter user. location = data.get('location') @@ -529,8 +554,7 @@ class Tweety(callbacks.Plugin): protected = data.get('protected') name = data.get('name') url = data.get('url') - # build output string conditionally. - # we don't use outputTweet since it's not a tweet. + # build output string conditionally. build string conditionally. ret = self._bu("@{0}".format(screen_name.encode('utf-8'))) ret += " ({0})".format(name.encode('utf-8')) if protected: # is the account protected/locked? @@ -552,19 +576,25 @@ class Tweety(callbacks.Plugin): # finally, output. irc.reply(ret) return - else: # this will display tweets/a user's timeline. - if len(data) == 0: # If we have no data, user has not tweeted. + else: # this will display tweets/a user's timeline. can be n+1 tweets. + if len(data) == 0: # no tweets found. irc.reply("ERROR: '{0}' has not tweeted yet.".format(optnick)) return - for tweet in data: # iterate through each tweet. - text = self._unescape(tweet.get('text')) - nick = tweet["user"].get('screen_name') - name = tweet["user"].get('name') + for tweet in data: # n+1 tweets found. iterate through each tweet. + text = self._unescape(tweet.get('text')).encode('utf-8') + nick = tweet["user"].get('screen_name').encode('utf-8') + name = tweet["user"].get('name').encode('utf-8') tweetid = tweet.get('id') relativeTime = self._time_created_at(tweet.get('created_at')) - self._outputTweet(irc, msg, nick.encode('utf-8'), name.encode('utf-8'), text.encode('utf-8'), relativeTime, tweetid) + # prepare string to output and send to irc. + output = self._outputTweet(irc, msg, nick, name, text, relativeTime, tweetid) + irc.reply(output) - twitter = wrap(twitter, [getopts({'noreply':'', 'nort':'', 'info':'', 'id':'', 'num':('int')}), ('somethingWithoutSpaces')]) + twitter = wrap(twitter, [getopts({'noreply':'', + 'nort':'', + 'info':'', + 'id':('int'), + 'num':('int')}), ('somethingWithoutSpaces')]) Class = Tweety From edbfdd526d272c4fbb898042dac875d435fdcdee Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 27 May 2013 18:12:35 -0400 Subject: [PATCH 43/63] Broke the code to check if isVoicePlus. Fixed in all 3 commands. --- plugin.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/plugin.py b/plugin.py index 48e4285..c5577a0 100644 --- a/plugin.py +++ b/plugin.py @@ -328,10 +328,11 @@ class Tweety(callbacks.Plugin): """ # enforce +voice or above to use command? - if self.registryValue('requireVoiceOrAbove', msg.args[0]): - if irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they voice or op? - irc.error("ERROR: You have to be at least voiced to use the trends command in {0}.".format(msg.args[0])) - return + if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check? + if ircutils.isChannel(msg.args[0]): # are we in a channel? + if 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])) + return # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: @@ -390,10 +391,11 @@ class Tweety(callbacks.Plugin): """ # enforce +voice or above to use command? - if self.registryValue('requireVoiceOrAbove', msg.args[0]): - if irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they voice or op? - irc.error("ERROR: You have to be at least voiced to use the tsearch command in {0}.".format(msg.args[0])) - return + if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check? + if ircutils.isChannel(msg.args[0]): # are we in a channel? + if 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 tsearchcommand in {0}.".format(msg.args[0])) + return # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: @@ -458,10 +460,11 @@ class Tweety(callbacks.Plugin): """ # enforce +voice or above to use command? - if self.registryValue('requireVoiceOrAbove', msg.args[0]): - if irc.state.channels[msg.args[0]].isVoicePlus(msg.nick): # are they voice or op? - irc.error("ERROR: You have to be at least voiced to use the twitter command in {0}.".format(msg.args[0])) - return + if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check? + if ircutils.isChannel(msg.args[0]): # are we in a channel? + if 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])) + return # before we do anything, make sure we have a twitterApi object. if not self.twitterApi: From 49ec63736e550d34f60be19d39012d5692246a30 Mon Sep 17 00:00:00 2001 From: spline Date: Tue, 28 May 2013 10:16:05 -0400 Subject: [PATCH 44/63] Ok, I botched the isVoicePlus. It's fixed now. Typo on error message fixed, also. --- plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin.py b/plugin.py index c5577a0..1e3a178 100644 --- a/plugin.py +++ b/plugin.py @@ -330,7 +330,7 @@ class Tweety(callbacks.Plugin): # enforce +voice or above to use command? if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check? if ircutils.isChannel(msg.args[0]): # are we in a channel? - if 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])) return @@ -393,8 +393,8 @@ class Tweety(callbacks.Plugin): # enforce +voice or above to use command? if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check? if ircutils.isChannel(msg.args[0]): # are we in a channel? - if 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 tsearchcommand in {0}.".format(msg.args[0])) + 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])) return # before we do anything, make sure we have a twitterApi object. @@ -462,7 +462,7 @@ class Tweety(callbacks.Plugin): # enforce +voice or above to use command? if self.registryValue('requireVoiceOrAbove', msg.args[0]): # should we check? if ircutils.isChannel(msg.args[0]): # are we in a channel? - if 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])) return From 3bbfb3884ad7b74a40a92ce377a99584f6302c3b Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 3 Jun 2013 19:08:26 -0400 Subject: [PATCH 45/63] Fix issue with getopts in Twitter and --id --- plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 1e3a178..84c205a 100644 --- a/plugin.py +++ b/plugin.py @@ -596,7 +596,7 @@ class Tweety(callbacks.Plugin): twitter = wrap(twitter, [getopts({'noreply':'', 'nort':'', 'info':'', - 'id':('int'), + 'id':'', 'num':('int')}), ('somethingWithoutSpaces')]) Class = Tweety From 53c9eaf7b802484b749cc44a3dbeb077da4f64ba Mon Sep 17 00:00:00 2001 From: spline Date: Thu, 20 Jun 2013 14:46:09 -0400 Subject: [PATCH 46/63] Fix tsearch count max because Twitters API apparently does not use count when issuing searchtype --- plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 84c205a..4aa4f35 100644 --- a/plugin.py +++ b/plugin.py @@ -434,7 +434,7 @@ class Tweety(callbacks.Plugin): irc.reply("ERROR: No Twitter Search results found for '{0}'".format(optterm)) return else: # we found something. - for result in results: # iterate over each. + for result in results[0:int(tsearchArgs['count'])]: # iterate over each. nick = result['user'].get('screen_name').encode('utf-8') name = result["user"].get('name').encode('utf-8') text = self._unescape(result.get('text')).encode('utf-8') From e366582110a6d6123dae99cc15265afba9315de4 Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 21 Jun 2013 13:32:22 -0400 Subject: [PATCH 47/63] Fix some tiny bugs with formatting, etc. I also added instructions in the README to use messageparser. --- README.md | 19 +++++++++++++++++++ plugin.py | 10 ++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 46dcdde..b714ca3 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,25 @@ Instructions Things should work fine from here providing your keys are right. +3.) EXTRAS: Want the bot to function like others do parsing out Twitter links and displaying? + + IE: + + <@snackle> https://twitter.com/JSportsnet/status/348114324004413440 + <@milo> @JSportsnet (John Shannon): Am told that Tippett's new deal is for 5 years, and he's "committed to the franchise where ever it ends up". (44m ago) + + OR: + + <@Hoaas> Should work on links to profiles aswell: https://twitter.com/EricFrancis + <@Bunisher> @EricFrancis (Eric Francis): HNIC-turned-Sportsnet analyst, Calgary Sun columnist, JACK FM morning host, author, FAN 960, + job collector, KidSport Ambassador, Ticats kicker, Dad, rum lover [513 friends, 3903 tweets, 20646 followers, signup: 1561d ago Location: Calgary] + + Load the messageparser plugin: (Thanks to Hoaas for this!) + + /msg load MessageParser + /msg messageparser add global "https?://twitter\.com/([^ \t/]+)(?:$|[ \t])" "Tweety twitter --info $1" + /msg messageparser add global "https?://twitter\.com/([A-Za-z0-9_]+)/status/([0-9]+)" "Tweety twitter --id $2" + Examples -------- diff --git a/plugin.py b/plugin.py index 4aa4f35..bc7fcb7 100644 --- a/plugin.py +++ b/plugin.py @@ -13,6 +13,8 @@ import re import htmlentitydefs # oauthtwitter import oauth2 as oauth +# extra supybot libs. +import supybot.ircmsgs as ircmsgs # supybot libs import supybot.utils as utils from supybot.commands import * @@ -150,7 +152,7 @@ class Tweety(callbacks.Plugin): def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" - text = text.replace("\n", " ") + text = text.replace('\n', ' ') def fixup(m): text = m.group(0) if text[:2] == "&#": @@ -369,7 +371,7 @@ class Tweety(callbacks.Plugin): if 'errors' in data: if data['errors'][0]['code'] == 34: # 34 means location not found. irc.reply("ERROR: I do not have any trends for: {0}".format(optwoeid)) - return + returnirc.reply(responseTxt, prefixNick=False) else: # just return the message. errmsg = data['errors'][0] irc.reply("ERROR: Could not load trends. ({0} {1})".format(errmsg['code'], errmsg['message'])) @@ -567,13 +569,13 @@ class Tweety(callbacks.Plugin): if url: # do they have a url? ret += " {0}".format(self._ul(url.encode('utf-8'))) if description: # a description? - ret += " {0}".format(description.encode('utf-8')) + ret += " {0}".format(self._unescape(description).encode('utf-8')) ret += " [{0} friends,".format(self._bold(friends)) ret += " {0} tweets,".format(self._bold(statuses_count)) ret += " {0} followers,".format(self._bold(followers)) ret += " signup: {0}".format(self._bold(self._time_created_at(created_at))) if location: # do we have location? - ret += " Location: {0}]".format(location.encode('utf-8')) + ret += " Location: {0}]".format(self._bold(location.encode('utf-8'))) else: # nope. ret += "]" # finally, output. From f96245c677c30f9002cbd8c7f56f76bd880d0449 Mon Sep 17 00:00:00 2001 From: spline Date: Fri, 21 Jun 2013 13:36:41 -0400 Subject: [PATCH 48/63] Screwed two things up in the last push. --- plugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugin.py b/plugin.py index bc7fcb7..65ba81b 100644 --- a/plugin.py +++ b/plugin.py @@ -13,8 +13,6 @@ import re import htmlentitydefs # oauthtwitter import oauth2 as oauth -# extra supybot libs. -import supybot.ircmsgs as ircmsgs # supybot libs import supybot.utils as utils from supybot.commands import * @@ -371,7 +369,7 @@ class Tweety(callbacks.Plugin): if 'errors' in data: if data['errors'][0]['code'] == 34: # 34 means location not found. irc.reply("ERROR: I do not have any trends for: {0}".format(optwoeid)) - returnirc.reply(responseTxt, prefixNick=False) + return else: # just return the message. errmsg = data['errors'][0] irc.reply("ERROR: Could not load trends. ({0} {1})".format(errmsg['code'], errmsg['message'])) From 061da46fb910c60143c66472407e0df79ebebf3b Mon Sep 17 00:00:00 2001 From: spline Date: Mon, 9 Dec 2013 19:57:45 -0500 Subject: [PATCH 49/63] Fix \r and \n in tweets that would break formatting. --- plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index 65ba81b..782bc48 100644 --- a/plugin.py +++ b/plugin.py @@ -150,7 +150,9 @@ class Tweety(callbacks.Plugin): def _unescape(self, text): """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)""" - text = text.replace('\n', ' ') + # quick dump \n and \r, usually coming from bots that autopost html. + text = text.replace('\n', ' ').replace('\r', ' ') + # now the actual unescape. def fixup(m): text = m.group(0) if text[:2] == "&#": From 900e669861b0fb0145513c9f5efc1d48285e6f49 Mon Sep 17 00:00:00 2001 From: spline Date: Wed, 3 Sep 2014 12:01:50 -0400 Subject: [PATCH 50/63] Trying out a small fix to relative time that will correct some negative dates (thx snackle). --- plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin.py b/plugin.py index 782bc48..13942e2 100644 --- a/plugin.py +++ b/plugin.py @@ -189,13 +189,13 @@ class Tweety(callbacks.Plugin): d = datetime.utcnow() - datetime(*ddate, tzinfo=None) # now parse and return. if d.days: - rel_time = "%sd ago" % d.days + rel_time = "%sd ago" % abs(d.days) elif d.seconds > 3600: - rel_time = "%sh ago" % (d.seconds / 3600) + rel_time = "%sh ago" % (abs(d.seconds) / 3600) elif 60 <= d.seconds < 3600: - rel_time = "%sm ago" % (d.seconds / 60) + rel_time = "%sm ago" % (abs(d.seconds) / 60) else: - rel_time = "%ss ago" % (d.seconds) + rel_time = "%ss ago" % (abs(d.seconds)) return rel_time def _outputTweet(self, irc, msg, nick, name, text, time, tweetid): From cdd77950a5cd37cdc82ec6dff04823c37d06871d Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 11 Oct 2014 12:45:47 -0400 Subject: [PATCH 51/63] Add in requirements. --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8e331a2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +git+https://github.com/ProgVal/Limnoria.git +oauth2==1.5.211 From a07d6b4c7512c654ada38371537b9735fa8bc756 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 11 Oct 2014 12:46:22 -0400 Subject: [PATCH 52/63] Add in travis. --- .travis.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c16270d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: python +python: + - "2.7" + - pypy +# command to install dependencies, +install: + - pip install -vr requirements.txt||true +# command to run tests, e.g. python setup.py test +script: + - echo $TRAVIS_PYTHON_VERSION + - cd .. && supybot-test Tweety +notifications: + email: false + irc: + channels: + - "irc.efnet.net#supybot" +matrix: + fast_finish: true + From 801891a7eb1fcc6c26e4c6d9a82d683496d8dadc Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 11 Oct 2014 12:56:09 -0400 Subject: [PATCH 53/63] Lets test this. --- test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test.py b/test.py index 1c7b7af..329165a 100644 --- a/test.py +++ b/test.py @@ -1,11 +1,14 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2013, spline +# Copyright (c) 2013-2014, spline ### from supybot.test import * +import os class TweetyTestCase(PluginTestCase): plugins = ('Tweety',) + def testTweety(self): + t = os.environ.get('test') + print t # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From be845e6ff22bb59890f47f3538d03ab30b860dca Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 11 Oct 2014 13:10:36 -0400 Subject: [PATCH 54/63] Lets test this. --- test.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test.py b/test.py index 329165a..94cc789 100644 --- a/test.py +++ b/test.py @@ -7,8 +7,20 @@ import os class TweetyTestCase(PluginTestCase): plugins = ('Tweety',) - def testTweety(self): - t = os.environ.get('test') - print t + def setUp(self): + PluginTestCase.setUp(self) + # get our variables via the secure environment. + consumerKey = os.environ.get('consumerKey') + consumerSecret = os.environ.get('consumerSecret') + accessKey = os.environ.get('accessKey') + accessSecret = os.environ.get('accessSecret') + # now set them. + conf.supybot.plugins.Tweety.consumerKey.setValue(consumerKey) + conf.supybot.plugins.Tweety.consumerSecret.setValue(consumerSecret) + conf.supybot.plugins.Tweety.accessKey.setValue(accessKey) + conf.supybot.plugins.Tweety.accessSecret.setValue(accessSecret) + def testTweety(self): + self.assertSnarfResponse('reload Tweety', 'The operation succeeded.') + self.assertRegex('trends', 'Top 10 Twitter Trends') # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From e7b096bf3cdbfadc3845af5fb18637497c1332bf Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 11 Oct 2014 13:12:33 -0400 Subject: [PATCH 55/63] Spelled Regexp wrong.. --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index 94cc789..370edb6 100644 --- a/test.py +++ b/test.py @@ -22,5 +22,5 @@ class TweetyTestCase(PluginTestCase): def testTweety(self): self.assertSnarfResponse('reload Tweety', 'The operation succeeded.') - self.assertRegex('trends', 'Top 10 Twitter Trends') + self.assertRegexp('trends', 'Top 10 Twitter Trends') # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From 5c0aee91dfb8323af13dd36c7accd877e5501e60 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 11 Oct 2014 13:22:02 -0400 Subject: [PATCH 56/63] Add in more tests. --- test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test.py b/test.py index 370edb6..f60e71a 100644 --- a/test.py +++ b/test.py @@ -23,4 +23,7 @@ class TweetyTestCase(PluginTestCase): def testTweety(self): self.assertSnarfResponse('reload Tweety', 'The operation succeeded.') self.assertRegexp('trends', 'Top 10 Twitter Trends') + self.assertRegexp('twitter --info CNN', 'CNN') + self.assertRegexp('twitter CNN', 'CNN') + # vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: From 060c8480b0700b541208a59bf3937a5c654f6047 Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 11 Oct 2014 13:48:24 -0400 Subject: [PATCH 57/63] Update README. --- README.md | 163 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 85 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index b714ca3..cdabc1c 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,111 @@ -Supybot-Tweety -====== +[![Build Status](https://travis-ci.org/reticulatingspline/Tweety.svg?branch=master)](https://travis-ci.org/reticulatingspline/Tweety) -# Twitter client for Supybot +# Limnoria plugin for Twitter. -Description ------------ +## Introduction -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. +This began with [Hoaas](https://github.com/Hoaas) making a slimmed down version of +ProgVal's [Twitter plugin](https://github.com/ProgVal/Supybot-Plugins/Twitter). He +was just interested in reading Tweets and showing information about the account, not +having any write cabaility. I started adding features to it and had to do an entire +rewrite after Twitter introduced the v1.1 API. -It has been updated to work with the oAuth requirement in v1.1 API along with their -updated endpoints. +This plugin is able to display information on accounts, display specific tweets, search for tweets, and display trends. -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. +If you are looking for anything outside of this, I suggest you do not run this plugin and instead install +ProgVal's version that I linked to above. -If you need to do any type of Twittering via the bot such as posting tweets, -responding, announcing of timelines, you will want ProgVals. -This will never contain more than what is above. +## Install -Instructions ------------- -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 +You will need a working Limnoria bot on Python 2.7 for this to work. -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. - 3. Click create an application. - 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 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. /msg reload Tweety - THIS IS REQUIRED. YOU MUST RELOAD BEFORE USING. +Go into your Limnoria plugin dir, usually ~/supybot/plugins and run: - Next, I suggest you /msg config search Tweety. There are a lot of options here. +``` +git clone https://github.com/reticulatingspline/Tweety +``` - Things should work fine from here providing your keys are right. +To install additional requirements, run: -3.) EXTRAS: Want the bot to function like others do parsing out Twitter links and displaying? +``` +pip install -r requirements.txt +``` - IE: +Next, load the plugin: - <@snackle> https://twitter.com/JSportsnet/status/348114324004413440 - <@milo> @JSportsnet (John Shannon): Am told that Tippett's new deal is for 5 years, and he's "committed to the franchise where ever it ends up". (44m ago) +``` +/msg bot load Tweety +``` - OR: +[Fetch the API keys for Twitter](http://dev.twitter.com) by signing up (free). +Create an application. Fill out the requested information. Name does not matter +but the name of the application must be unique. Default is read-only, which is fine. +Once complete, they'll issue you 4 different "strings" that you need to input +into the bot, matching up with the config variable names. - <@Hoaas> Should work on links to profiles aswell: https://twitter.com/EricFrancis - <@Bunisher> @EricFrancis (Eric Francis): HNIC-turned-Sportsnet analyst, Calgary Sun columnist, JACK FM morning host, author, FAN 960, - job collector, KidSport Ambassador, Ticats kicker, Dad, rum lover [513 friends, 3903 tweets, 20646 followers, signup: 1561d ago Location: Calgary] +``` +/msg config plugins.Tweety.consumerKey xxxxx +/msg config plugins.Tweety.consumerSecret xxxxx +/msg config plugins.Tweety.accessKey xxxxx +/msg config plugins.Tweety.accessSecret xxxxx +``` - Load the messageparser plugin: (Thanks to Hoaas for this!) +Now, reload the bot and you should be good to go: - /msg load MessageParser - /msg messageparser add global "https?://twitter\.com/([^ \t/]+)(?:$|[ \t])" "Tweety twitter --info $1" - /msg messageparser add global "https?://twitter\.com/([A-Za-z0-9_]+)/status/([0-9]+)" "Tweety twitter --id $2" +``` +/msg bot reload Tweety +``` -Examples --------- +Optional: There are some config variables that can be set for the bot. They mainly control output stuff. - - Twitter Trends - trends - Top 10 Twitter Trends in United States :: #BeforeIDieIWantTo | #ThingsIMissAboutMyChildhood | Happy Memorial Day | #RG13 | #USA | #america | BBQ | WWII | God Bless | Facebook +``` +/msg bot config search Tweety +``` - - Searching Twitter - tsearch news - @ray_gallego (Ray Gallego): http://t.co/ftNbDEzXaR (Researchers say Western IQs dropped 14 points over last century) (14s ago) - @surfing93 (emilyhenderson): @MariaaEveline Hay here is the Crestillion Interview. http://t.co/CEiDpboeMX (15s ago) +## Example Usage - - Getting tweets from someones' timeline. - twitter --num 3 @ESPNStatsInfo - @ESPNStatsInfo (ESPN Stats & Info): In 1st-round win vs Daniel Brands, Rafael Nadal lost 19 games. He lost a total of 19 games in the 1st 4 rounds at last year's French Open. (30m ago) - @ESPNStatsInfo (ESPN Stats & Info): Key stats from Miami's win yesterday. Haslem's jump shot, LeBron's post-up and more: http://t.co/a4CcUnKJMi (53m ago) - @ESPNStatsInfo (ESPN Stats & Info): Heat avoid losing consecutive games. They haven't lost 2 straight in more than 5 months (January 8-10) (1h ago) +``` + trends + Top 10 Twitter Trends in United States :: #BeforeIDieIWantTo | #ThingsIMissAboutMyChildhood | Happy Memorial Day | #RG13 | #USA | #america | BBQ | WWII | God Bless | Facebook -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. + tsearch news + @ray_gallego (Ray Gallego): http://t.co/ftNbDEzXaR (Researchers say Western IQs dropped 14 points over last century) (14s ago) + @surfing93 (emilyhenderson): @MariaaEveline Hay here is the Crestillion Interview. http://t.co/CEiDpboeMX (15s ago) -So, as I take over, I must acknowledge the work done by Hoaas: -http://github.com/Hoaas/ -Much/almost all of the oAuth code came from: -https://github.com/jpittman/OAuth-Python-Twitter + twitter --num 3 @ESPNStatsInfo + @ESPNStatsInfo (ESPN Stats & Info): In 1st-round win vs Daniel Brands, Rafael Nadal lost 19 games. He lost a total of 19 games in the 1st 4 rounds at last year's French Open. (30m ago) + @ESPNStatsInfo (ESPN Stats & Info): Key stats from Miami's win yesterday. Haslem's jump shot, LeBron's post-up and more: http://t.co/a4CcUnKJMi (53m ago) + @ESPNStatsInfo (ESPN Stats & Info): Heat avoid losing consecutive games. They haven't lost 2 straight in more than 5 months (January 8-10) (1h ago) +``` -Documentation -------------- +## Extras -* https://dev.twitter.com/docs/api/1.1 +Want the bot to function like others do parsing out Twitter links and displaying? (Thanks to Hoaas) + +``` +<@snackle> https://twitter.com/JSportsnet/status/348114324004413440 +<@milo> @JSportsnet (John Shannon): Am told that Tippett's new deal is for 5 years, and he's "committed to the franchise where ever it ends up". (44m ago) +``` + +``` +<@Hoaas> Should work on links to profiles aswell: https://twitter.com/EricFrancis +<@Bunisher> @EricFrancis (Eric Francis): HNIC-turned-Sportsnet analyst, Calgary Sun columnist... +``` + +Load the messageparser plugin: + +``` +/msg load MessageParser +/msg messageparser add global "https?://twitter\.com/([^ \t/]+)(?:$|[ \t])" "Tweety twitter --info $1" +/msg messageparser add global "https?://twitter\.com/([A-Za-z0-9_]+)/status/([0-9]+)" "Tweety twitter --id $2" +``` + +## About + +All of my plugins are free and open source. When I first started out, one of the main reasons I was +able to learn was due to other code out there. If you find a bug or would like an improvement, feel +free to give me a message on IRC or fork and submit a pull request. Many hours do go into each plugin, +so, if you're feeling generous, I do accept donations via Amazon or browse my [wish list](http://amzn.com/w/380JKXY7P5IKE). + +I'm always looking for work, so if you are in need of a custom feature, plugin or something bigger, contact me via GitHub or IRC. \ No newline at end of file From a312bad52ee807c87b51583be24485fd8a5ecc2e Mon Sep 17 00:00:00 2001 From: spline Date: Sat, 15 Nov 2014 09:20:08 -0500 Subject: [PATCH 58/63] Add LICENSE file via addalicense.com --- LICENSE.txt | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..41e9e07 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 spline + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 6338d0707ad41faeff6150ca78d5bee83379a831 Mon Sep 17 00:00:00 2001 From: reticulatingspline Date: Tue, 12 May 2015 07:20:13 -0400 Subject: [PATCH 59/63] Misc fixes for unicode characters in name on each function. --- plugin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugin.py b/plugin.py index 13942e2..23912e2 100644 --- a/plugin.py +++ b/plugin.py @@ -437,8 +437,8 @@ class Tweety(callbacks.Plugin): return else: # we found something. for result in results[0:int(tsearchArgs['count'])]: # iterate over each. - nick = result['user'].get('screen_name').encode('utf-8') - name = result["user"].get('name').encode('utf-8') + nick = self._unescape(result['user'].get('screen_name').encode('utf-8')) + name = self._unescape(result["user"].get('name').encode('utf-8')) text = self._unescape(result.get('text')).encode('utf-8') date = self._time_created_at(result.get('created_at')) tweetid = result.get('id_str') @@ -540,8 +540,8 @@ class Tweety(callbacks.Plugin): # no errors, so we process data conditionally. if args['id']: # If --id was given for a single tweet. text = self._unescape(data.get('text')).encode('utf-8') - nick = data["user"].get('screen_name').encode('utf-8') - name = data["user"].get('name').encode('utf-8') + nick = self._unescape(data["user"].get('screen_name').encode('utf-8')) + name = self._unescape(data["user"].get('name').encode('utf-8')) relativeTime = self._time_created_at(data.get('created_at')) tweetid = data.get('id') # prepare string to output and send to irc. @@ -552,12 +552,12 @@ class Tweety(callbacks.Plugin): location = data.get('location') followers = data.get('followers_count') friends = data.get('friends_count') - description = data.get('description') - screen_name = data.get('screen_name') + description = self._unescape(data.get('description')) + screen_name = self._unescape(data.get('screen_name')) created_at = data.get('created_at') statuses_count = data.get('statuses_count') protected = data.get('protected') - name = data.get('name') + name = self._unescape(data.get('name')) url = data.get('url') # build output string conditionally. build string conditionally. ret = self._bu("@{0}".format(screen_name.encode('utf-8'))) @@ -587,8 +587,8 @@ class Tweety(callbacks.Plugin): return for tweet in data: # n+1 tweets found. iterate through each tweet. text = self._unescape(tweet.get('text')).encode('utf-8') - nick = tweet["user"].get('screen_name').encode('utf-8') - name = tweet["user"].get('name').encode('utf-8') + nick = self._unescape(tweet["user"].get('screen_name').encode('utf-8')) + name = self._unescape(tweet["user"].get('name').encode('utf-8')) tweetid = tweet.get('id') relativeTime = self._time_created_at(tweet.get('created_at')) # prepare string to output and send to irc. From e69d5e4397ce1d008d35f03a14baabdc3abd92b6 Mon Sep 17 00:00:00 2001 From: Gordon Shumway <39967334+oddluck@users.noreply.github.com> Date: Tue, 5 Mar 2019 19:45:06 -0500 Subject: [PATCH 60/63] python3 compatible, working woeid lookup --- plugin.py | 147 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 49 deletions(-) diff --git a/plugin.py b/plugin.py index 23912e2..d428d7e 100644 --- a/plugin.py +++ b/plugin.py @@ -3,14 +3,16 @@ ### # my libs -import urllib2 +import urllib.request, urllib.error, urllib.parse import json +import requests +import urllib # libraries for time_created_at import time from datetime import datetime # for unescape import re -import htmlentitydefs +import html.entities # oauthtwitter import oauth2 as oauth # supybot libs @@ -19,7 +21,7 @@ from supybot.commands import * import supybot.plugins as plugins import supybot.ircutils as ircutils import supybot.callbacks as callbacks - +from bs4 import BeautifulSoup class OAuthApi: """OAuth class to work with Twitter v1.1 API.""" @@ -38,7 +40,7 @@ class OAuthApi: extra_params.update(parameters) req = self._makeOAuthRequest(url, params=extra_params) - opener = urllib2.build_opener(urllib2.HTTPHandler(debuglevel=0)) + opener = urllib.request.build_opener(urllib.request.HTTPHandler(debuglevel=0)) url = req.to_url() url_data = opener.open(url) opener.close() @@ -69,9 +71,9 @@ class OAuthApi: try: data = self._FetchUrl("https://api.twitter.com/1.1/" + call + ".json", parameters) - except urllib2.HTTPError, e: # http error code. + except urllib.error.HTTPError as e: # http error code. return e.code - except urllib2.URLError, e: # http "reason" + except urllib.error.URLError as e: # http "reason" return e.reason else: # return data if good. return data @@ -87,6 +89,40 @@ class Tweety(callbacks.Plugin): self.twitterApi = False if not self.twitterApi: self._checkAuthorization() + + 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.""" + + try: + if h and d: + page = utils.web.getUrl(url, headers=h, data=d) + else: + h = {"User-Agent":"Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:17.0) Gecko/20100101 Firefox/17.0"} + page = utils.web.getUrl(url, headers=h) + try: + page = page.decode() + except: + page = page.decode('iso-8859-1') + return page + except utils.web.Error as e: + self.log.error("ERROR opening {0} message: {1}".format(url, e)) + return None + + + def _shortenUrl(self, url): + """Shortens a long URL into a short one.""" + + api_key = self.registryValue('bitlyKey') + url_enc = urllib.parse.quote_plus(url) + api_url = 'https://api-ssl.bitly.com/v3/shorten?access_token={}&longUrl={}&format=txt' + + try: + url2 = requests.get(api_url.format(api_key, url_enc)) + if 'RATE_LIMIT_EXCEEDED' in url2.text.strip(): + return url + return url2.text.strip() + except: + return url def _checkAuthorization(self): """ Check if we have our keys and can auth.""" @@ -110,7 +146,7 @@ class Tweety(callbacks.Plugin): data = twitterApi.ApiCall('account/verify_credentials') # check the response. if we can load json, it means we're authenticated. else, return response. try: # if we pass, response is validated. set self.twitterApi w/object. - json.loads(data.read()) + json.loads(data.read().decode()) self.log.info("I have successfully authorized and logged in to Twitter using your credentials.") self.twitterApi = OAuthApi(self.registryValue('consumerKey'), self.registryValue('consumerSecret'), self.registryValue('accessKey'), self.registryValue('accessSecret')) except: # response failed. Return what we got back. @@ -159,15 +195,15 @@ class Tweety(callbacks.Plugin): # character reference try: if text[:3] == "&#x": - return unichr(int(text[3:-1], 16)) + return chr(int(text[3:-1], 16)) else: - return unichr(int(text[2:-1])) + return chr(int(text[2:-1])) except (ValueError, OverflowError): pass else: # named entity try: - text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) + text = chr(html.entities.name2codepoint[text[1:-1]]) except KeyError: pass return text # leave as is @@ -189,16 +225,16 @@ class Tweety(callbacks.Plugin): d = datetime.utcnow() - datetime(*ddate, tzinfo=None) # now parse and return. if d.days: - rel_time = "%sd ago" % abs(d.days) + rel_time = "{:1d}d ago".format(abs(d.days)) elif d.seconds > 3600: - rel_time = "%sh ago" % (abs(d.seconds) / 3600) + rel_time = "{:.1f}h ago".format(round((abs(d.seconds) / 3600),1)) elif 60 <= d.seconds < 3600: - rel_time = "%sm ago" % (abs(d.seconds) / 60) + rel_time = "{:.1f}m ago".format(round((abs(d.seconds) / 60),1)) else: rel_time = "%ss ago" % (abs(d.seconds)) return rel_time - def _outputTweet(self, irc, msg, nick, name, text, time, tweetid): + def _outputTweet(self, irc, msg, nick, name, verified, text, time, tweetid): """ Constructs string to output for Tweet. Used for tsearch and twitter. """ @@ -208,6 +244,9 @@ class Tweety(callbacks.Plugin): ret = "@{0}".format(self._ul(self._blue(nick))) else: # bold otherwise. ret = "@{0}".format(self._bu(nick)) + if verified: + string = self._bold(ircutils.mircColor("✓", 'white', 'blue')) + ret += "{}".format(string) # show real name in tweet output? if not self.registryValue('hideRealName', msg.args[0]): ret += " ({0})".format(name) @@ -235,7 +274,7 @@ class Tweety(callbacks.Plugin): headers = {'Content-Type':'application/json'} request = utils.web.getUrl(posturi, data=data, headers=headers) return json.loads(request)['id'] - except Exception, err: + except Exception as err: self.log.error("ERROR: Failed shortening url: {0} :: {1}".format(longurl, err)) return None @@ -251,16 +290,14 @@ class Tweety(callbacks.Plugin): "env":"store://datatables.org/alltableswithkeys" } # everything in try/except block incase it breaks. try: - url = "http://query.yahooapis.com/v1/public/yql?"+utils.web.urlencode(params) - response = utils.web.getUrl(url) - data = json.loads(response) - - if data['query']['count'] > 1: # return the "first" one. - woeid = data['query']['results']['place'][0]['woeid'] - else: # if one, return it. - woeid = data['query']['results']['place']['woeid'] + data = requests.get('http://woeid.rosselliot.co.nz/lookup/{0}'.format(lookup)) + if not data: # http fetch breaks. + irc.reply("ERROR") + return + soup = BeautifulSoup(data.text) + woeid = soup.find("td", class_='woeid').getText() return woeid - except Exception, err: + except Exception as err: self.log.error("ERROR: Failed looking up WOEID for '{0}' :: {1}".format(lookup, err)) return None @@ -294,7 +331,7 @@ class Tweety(callbacks.Plugin): # make API call. data = self.twitterApi.ApiCall('application/rate_limit_status', parameters={'resources':'trends,search,statuses,users'}) try: - data = json.loads(data.read()) + data = json.loads(data.read().decode()) except: irc.reply("ERROR: Failed to lookup ratelimit data: {0}".format(data)) return @@ -363,7 +400,7 @@ class Tweety(callbacks.Plugin): # now build our API call data = self.twitterApi.ApiCall('trends/place', parameters=args) try: - data = json.loads(data.read()) + data = json.loads(data.read().decode()) except: irc.reply("ERROR: failed to lookup trends on Twitter: {0}".format(data)) return @@ -378,7 +415,7 @@ class Tweety(callbacks.Plugin): return # if no error here, we found trends. prepare string and output. location = data[0]['locations'][0]['name'] - ttrends = " | ".join([trend['name'].encode('utf-8') for trend in data[0]['trends']]) + ttrends = " | ".join([trend['name'] for trend in data[0]['trends']]) irc.reply("Top 10 Twitter Trends in {0} :: {1}".format(self._bold(location), ttrends)) trends = wrap(trends, [getopts({'exclude':''}), optional('text')]) @@ -406,6 +443,7 @@ class Tweety(callbacks.Plugin): # default arguments. tsearchArgs = {'include_entities':'false', + 'tweet_mode': 'extended', 'count': self.registryValue('defaultSearchResults', msg.args[0]), 'lang':'en', 'q':utils.web.urlquote(optterm)} @@ -426,7 +464,7 @@ class Tweety(callbacks.Plugin): # now build our API call. data = self.twitterApi.ApiCall('search/tweets', parameters=tsearchArgs) try: - data = json.loads(data.read()) + data = json.loads(data.read().decode()) except: irc.reply("ERROR: Something went wrong trying to search Twitter. ({0})".format(data)) return @@ -437,13 +475,14 @@ class Tweety(callbacks.Plugin): return else: # we found something. for result in results[0:int(tsearchArgs['count'])]: # iterate over each. - nick = self._unescape(result['user'].get('screen_name').encode('utf-8')) - name = self._unescape(result["user"].get('name').encode('utf-8')) - text = self._unescape(result.get('text')).encode('utf-8') + nick = self._unescape(result['user'].get('screen_name')) + name = self._unescape(result["user"].get('name')) + verified = result['user'].get('verified') + text = self._unescape(result.get('full_text')) or self._unescape(result.get('text')) date = self._time_created_at(result.get('created_at')) tweetid = result.get('id_str') # build output string and output. - output = self._outputTweet(irc, msg, nick, name, text, date, tweetid) + output = self._outputTweet(irc, msg, nick, name, verified, text, date, tweetid) irc.reply(output) tsearch = wrap(tsearch, [getopts({'num':('int'), @@ -451,7 +490,7 @@ class Tweety(callbacks.Plugin): 'lang':('somethingWithoutSpaces')}), ('text')]) - def twitter(self, irc, msg, args, optlist, optnick): + def twitter(self, irc, msg, args, optlist, optnick, opturl): """[--noreply] [--nort] [--num number] | [--id id] | [--info nick] Returns last tweet or 'number' tweets (max 10). Shows all tweets, including rt and reply. @@ -479,6 +518,7 @@ class Tweety(callbacks.Plugin): args = {'id': False, 'nort': False, 'noreply': False, + 'url': False, 'num': self.registryValue('defaultResults', msg.args[0]), 'info': False} # handle input optlist. @@ -486,6 +526,8 @@ class Tweety(callbacks.Plugin): for (key, value) in optlist: if key == 'id': args['id'] = True + if key == 'url': + args['url'] = True if key == 'nort': args['nort'] = True if key == 'noreply': @@ -502,7 +544,7 @@ class Tweety(callbacks.Plugin): # handle the three different rest api endpoint urls + twitterArgs dict for options. if args['id']: # -id #. apiUrl = 'statuses/show' - twitterArgs = {'id': optnick, 'include_entities':'false'} + twitterArgs = {'id': optnick, 'include_entities':'false', 'tweet_mode': 'extended'} elif args['info']: # --info. apiUrl = 'users/show' twitterArgs = {'screen_name': optnick, 'include_entities':'false'} @@ -520,7 +562,7 @@ class Tweety(callbacks.Plugin): # call the Twitter API with our data. data = self.twitterApi.ApiCall(apiUrl, parameters=twitterArgs) try: - data = json.loads(data.read()) + data = json.loads(data.read().decode()) except: irc.reply("ERROR: Failed to lookup Twitter for '{0}' ({1}) ".format(optnick, data)) return @@ -539,13 +581,18 @@ class Tweety(callbacks.Plugin): return # no errors, so we process data conditionally. if args['id']: # If --id was given for a single tweet. - text = self._unescape(data.get('text')).encode('utf-8') - nick = self._unescape(data["user"].get('screen_name').encode('utf-8')) - name = self._unescape(data["user"].get('name').encode('utf-8')) + url = '' + if opturl: + url = ' - {}'.format(self._shortenUrl(opturl)) + text = self._unescape(data.get('full_text')) or self._unescape(data.get('text')) + nick = self._unescape(data["user"].get('screen_name')) + name = self._unescape(data["user"].get('name')) + verified = data["user"].get('verified') relativeTime = self._time_created_at(data.get('created_at')) tweetid = data.get('id') # prepare string to output and send to irc. - output = self._outputTweet(irc, msg, nick, name, text, relativeTime, tweetid) + output = self._outputTweet(irc, msg, nick, name, verified, text, relativeTime, tweetid) + output += url irc.reply(output) return elif args['info']: # --info to return info on a Twitter user. @@ -560,22 +607,22 @@ class Tweety(callbacks.Plugin): name = self._unescape(data.get('name')) url = data.get('url') # build output string conditionally. build string conditionally. - ret = self._bu("@{0}".format(screen_name.encode('utf-8'))) - ret += " ({0})".format(name.encode('utf-8')) + ret = self._bu("@{0}".format(screen_name)) + ret += " ({0})".format(name) if protected: # is the account protected/locked? ret += " [{0}]:".format(self._bu('LOCKED')) else: # open. ret += ":" if url: # do they have a url? - ret += " {0}".format(self._ul(url.encode('utf-8'))) + ret += " {0}".format(self._ul(url)) if description: # a description? - ret += " {0}".format(self._unescape(description).encode('utf-8')) + ret += " {0}".format(self._unescape(description)) ret += " [{0} friends,".format(self._bold(friends)) ret += " {0} tweets,".format(self._bold(statuses_count)) ret += " {0} followers,".format(self._bold(followers)) ret += " signup: {0}".format(self._bold(self._time_created_at(created_at))) if location: # do we have location? - ret += " Location: {0}]".format(self._bold(location.encode('utf-8'))) + ret += " Location: {0}]".format(self._bold(location)) else: # nope. ret += "]" # finally, output. @@ -586,20 +633,22 @@ class Tweety(callbacks.Plugin): irc.reply("ERROR: '{0}' has not tweeted yet.".format(optnick)) return for tweet in data: # n+1 tweets found. iterate through each tweet. - text = self._unescape(tweet.get('text')).encode('utf-8') - nick = self._unescape(tweet["user"].get('screen_name').encode('utf-8')) - name = self._unescape(tweet["user"].get('name').encode('utf-8')) + text = self._unescape(tweet.get('text')) or self._unescape(tweet.get('full_text')) + nick = self._unescape(tweet["user"].get('screen_name')) + name = self._unescape(tweet["user"].get('name')) + verified = tweet['user'].get('verified') tweetid = tweet.get('id') relativeTime = self._time_created_at(tweet.get('created_at')) # prepare string to output and send to irc. - output = self._outputTweet(irc, msg, nick, name, text, relativeTime, tweetid) + output = self._outputTweet(irc, msg, nick, name, verified, text, relativeTime, tweetid) irc.reply(output) twitter = wrap(twitter, [getopts({'noreply':'', 'nort':'', 'info':'', 'id':'', - 'num':('int')}), ('somethingWithoutSpaces')]) + 'url':'', + 'num':('int')}), ('somethingWithoutSpaces'), optional('somethingWithoutSpaces')]) Class = Tweety From afa37dfae4ebd4c790d769fbb85d4929e71bae6d Mon Sep 17 00:00:00 2001 From: Gordon Shumway <39967334+oddluck@users.noreply.github.com> Date: Tue, 5 Mar 2019 19:45:43 -0500 Subject: [PATCH 61/63] Update requirements.txt --- requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8e331a2..bf5f447 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -git+https://github.com/ProgVal/Limnoria.git -oauth2==1.5.211 +oauth2 +requests +bs4 From 2756c31bb53213379bd16d593f3b51e4b87cac6c Mon Sep 17 00:00:00 2001 From: Gordon Shumway <39967334+oddluck@users.noreply.github.com> Date: Tue, 5 Mar 2019 20:13:45 -0500 Subject: [PATCH 62/63] tweet_mode: extended --- plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index d428d7e..8340227 100644 --- a/plugin.py +++ b/plugin.py @@ -550,7 +550,7 @@ class Tweety(callbacks.Plugin): twitterArgs = {'screen_name': optnick, 'include_entities':'false'} else: # if not an --id or --info, we're printing from their timeline. apiUrl = 'statuses/user_timeline' - twitterArgs = {'screen_name': optnick, 'count': args['num']} + twitterArgs = {'screen_name': optnick, 'count': args['num'], 'tweet_mode': 'extended'} if args['nort']: # show retweets? twitterArgs['include_rts'] = 'false' else: # default is to show retweets. @@ -633,7 +633,7 @@ class Tweety(callbacks.Plugin): irc.reply("ERROR: '{0}' has not tweeted yet.".format(optnick)) return for tweet in data: # n+1 tweets found. iterate through each tweet. - text = self._unescape(tweet.get('text')) or self._unescape(tweet.get('full_text')) + text = self._unescape(tweet.get('full_text')) or self._unescape(tweet.get('text')) nick = self._unescape(tweet["user"].get('screen_name')) name = self._unescape(tweet["user"].get('name')) verified = tweet['user'].get('verified') From 9b7d3f9b4f4d54f2569f4ea9f375d92d154dcb2c Mon Sep 17 00:00:00 2001 From: Gordon Shumway <39967334+oddluck@users.noreply.github.com> Date: Tue, 5 Mar 2019 22:33:26 -0500 Subject: [PATCH 63/63] fix link shortening --- plugin.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/plugin.py b/plugin.py index 8340227..cfd9087 100644 --- a/plugin.py +++ b/plugin.py @@ -114,14 +114,17 @@ class Tweety(callbacks.Plugin): api_key = self.registryValue('bitlyKey') url_enc = urllib.parse.quote_plus(url) - api_url = 'https://api-ssl.bitly.com/v3/shorten?access_token={}&longUrl={}&format=txt' + api_url = 'https://api-ssl.bitly.com/v3/shorten?access_token={}&longUrl={}&format=json' try: - url2 = requests.get(api_url.format(api_key, url_enc)) - if 'RATE_LIMIT_EXCEEDED' in url2.text.strip(): + data = requests.get(api_url.format(api_key, url_enc)).json() + url2 = data['data'].get('url') + if url2.strip(): + return url2.strip() + else: return url - return url2.text.strip() except: + self.log.error("ERROR: Failed shortening url: {0}".format(longurl)) return url def _checkAuthorization(self): @@ -267,16 +270,20 @@ class Tweety(callbacks.Plugin): def _createShortUrl(self, nick, tweetid): """Shortens a tweet into a short one.""" + api_key = self.registryValue('bitlyKey') + longurl = "https://twitter.com/%s/status/%s" % (nick, tweetid) + api_url = 'https://api-ssl.bitly.com/v3/shorten?access_token={}&longUrl={}&format=json' + try: - longurl = "https://twitter.com/#!/%s/status/%s" % (nick, tweetid) - posturi = "https://www.googleapis.com/urlshortener/v1/url" - data = json.dumps({'longUrl': longurl}) - headers = {'Content-Type':'application/json'} - request = utils.web.getUrl(posturi, data=data, headers=headers) - return json.loads(request)['id'] - except Exception as err: - self.log.error("ERROR: Failed shortening url: {0} :: {1}".format(longurl, err)) - return None + data = requests.get(api_url.format(api_key, longurl)).json() + url2 = data['data'].get('url') + if url2.strip(): + return url2.strip() + else: + return longurl + except: + self.log.error("ERROR: Failed shortening url: {0}".format(longurl)) + return longurl def _woeid_lookup(self, lookup): """