chiark / gitweb /
blame: Allow `~blame #last 2' etc.
[irc.git] / commands.py
1 # Part of Acrobat.
2 import string, cPickle, random, urllib, sys, time, re, os, twitter, subprocess, datetime, urlparse, hashlib
3 from collections import defaultdict
4 from irclib import irc_lower, nm_to_n
5
6 try:
7     from blame_filter import bfd
8 except ImportError:
9     bfd = None
10
11 # query karma
12 def karmaq(bot, cmd, nick, conn, public, karma):
13     try:
14         item=cmd.split()[1].lower()
15     except IndexError:
16         item=None
17     if item==None:
18         bot.automsg(public,nick,"I have karma on %s items." %
19                          len(karma.keys()))
20     elif karma.has_key(item):
21         bot.automsg(public,nick,"%s has karma %s."
22                      %(item,karma[item]))
23     else:
24         bot.automsg(public,nick, "%s has no karma set." % item)
25
26 # delete karma
27 def karmadelq(bot, cmd, nick, conn, public, karma):
28     try:
29         item=cmd.split()[1].lower()
30     except IndexError:
31         conn.notice(nick, "What should I delete?")
32         return
33     if nick != bot.owner:
34         conn.notice(nick, "You are not my owner.")
35         return
36     if karma.has_key(item):
37         del karma[item]
38         conn.notice(nick, "Item %s deleted."%item)
39     else:
40         conn.notice(nick, "There is no karma stored for %s."%item)
41
42 # help - provides the URL of the help file
43 def helpq(bot, cmd, nick, conn, public):
44     bot.automsg(public,nick,
45                 "For help see http://www.chiark.greenend.org.uk/~matthewv/irc/servus.html")
46
47
48 # query bot status
49 def infoq(bot, cmd, nick, conn, public, karma):
50     bot.automsg(public,nick,
51         ("I am Acrobat %s, on %s, as nick %s.  "+
52         "My owner is %s; I have karma on %s items.") %
53         (bot.revision.split()[1], bot.channel, conn.get_nickname(),
54          bot.owner, len(karma.keys())))
55
56 class FishPond:
57     def __init__(fishpond):
58         fishpond.last=[]
59         fishpond.DoS=0
60         fishpond.quotatime=0
61
62     def note_last(fishpond, msg, cfg):
63         fishpond.last.insert(0,(msg,cfg))
64         fishpond.last = fishpond.last[0:10]
65
66 # Check on fish stocks
67 def fish_quota(pond):
68     if pond.DoS:
69         if time.time()>=pond.quotatime:
70             pond.DoS=0
71         else:
72             return
73     if (time.time()-pond.quotatime)>pond.fish_time_inc:
74         pond.cur_fish+=(((time.time()-pond.quotatime)
75                          /pond.fish_time_inc)*pond.fish_inc)
76         if pond.cur_fish>pond.max_fish:
77             pond.cur_fish=pond.max_fish
78         pond.quotatime=time.time()
79
80 # List of things the bot might be called to work round the self-trouting code
81 synonyms=["itself","the bot","themself"]
82
83 # trout someone, or flirt with them
84 def troutq(bot, cmd, nick, conn, public, cfg):
85     fishlist=cfg[0]
86     selftrout=cfg[1]
87     quietmsg=cfg[2]
88     notargetmsg=cfg[3]
89     nofishmsg=cfg[4]
90     fishpond=cfg[5]
91     selftroutchance=cfg[6]
92
93     fish_quota(fishpond)
94     if fishpond.DoS:
95         conn.notice(nick, quietmsg%fishpond.Boring_Git)
96         return
97     if fishpond.cur_fish<=0:
98         conn.notice(nick, nofishmsg)
99         return
100     target = string.join(cmd.split()[1:])
101     if len(target)==0:
102         conn.notice(nick, notargetmsg)
103         return
104     me = bot.connection.get_nickname()
105     trout_msg = random.choice(fishlist)
106     fishpond.note_last(trout_msg,cfg)
107     # The bot won't trout or flirt with itself;
108     if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
109         target = nick
110     # There's a chance the game may be given away if the request was not
111     # public...
112     if not public:
113         if random.random()<=selftroutchance:
114             trout_msg=trout_msg+(selftrout%nick)
115
116     conn.action(bot.channel, trout_msg % target)
117     fishpond.cur_fish-=1
118
119 # slash a pair
120 def slashq(bot, cmd, nick, conn, public, cfg):
121     fishlist=cfg[0]
122     selfslash=cfg[1]
123     quietmsg=cfg[2]
124     notargetmsg=cfg[3]
125     nofishmsg=cfg[4]
126     fishpond=cfg[5]
127     selfslashchance=cfg[6]
128
129     fish_quota(fishpond)
130     if fishpond.DoS:
131         conn.notice(nick, quietmsg%fishpond.Boring_Git)
132         return
133     if fishpond.cur_fish<=0:
134         conn.notice(nick, nofishmsg)
135         return
136     target = string.join(cmd.split()[1:])
137     #who = cmd.split()[1:]
138     who = ' '.join(cmd.split()[1:]).split(' / ')
139     if len(who) < 2:
140         conn.notice(nick, "it takes two to tango!")
141         return
142     elif len(who) > 2:
143         conn.notice(nick, "we'll have none of that round here")
144         return
145     me = bot.connection.get_nickname()
146     slash_msg = random.choice(fishlist)
147     fishpond.note_last(slash_msg,cfg)
148     # The bot won't slash people with themselves
149     if irc_lower(who[0]) == irc_lower(who[1]):
150         conn.notice(nick, "oooooh no missus!")
151         return
152     # The bot won't slash with itself, instead slashing the requester
153     for n in [0,1]:
154         if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
155             who[n] = nick
156     # Perhaps someone asked to slash themselves with the bot then we get
157     if irc_lower(who[0]) == irc_lower(who[1]):
158         conn.notice(nick, "you wish!")
159         return
160     # There's a chance the game may be given away if the request was not
161     # public...
162     if not public:
163         if random.random()<=selfslashchance:
164             slash_msg=slash_msg+(selfslash%nick)
165
166     conn.action(bot.channel, slash_msg % (who[0], who[1]))
167     fishpond.cur_fish-=1
168
169 #query units
170 def unitq(bot, cmd, nick, conn, public):
171     args = ' '.join(cmd.split()[1:]).split(' as ')
172     if len(args) != 2:
173         args = ' '.join(cmd.split()[1:]).split(' / ')
174         if len(args) != 2:
175             conn.notice(nick, "syntax: units arg1 as arg2")
176             return
177     if args[1]=='?':
178         sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
179     else:
180         sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
181     sin.close()
182     res=sout.readlines()
183     #popen2 doesn't clean up the child properly. Do this by hand
184     child=os.wait()
185     if os.WEXITSTATUS(child[1])==0:
186         bot.automsg(public,nick,res[0].strip())
187     else:
188         conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
189
190 # Shut up trouting for a minute
191 def nofishq(bot, cmd, nick, conn, public, fish):
192     fish.cur_fish=0
193     fish.DoS=1
194     fish.Boring_Git=nick
195     fish.quotatime=time.time()
196     fish.quotatime+=fish.nofish_time
197     conn.notice(nick, "Fish stocks depleted, as you wish.")
198
199 # rehash bot config
200 def reloadq(bot, cmd, nick, conn, public):
201     if not public and irc_lower(nick) == irc_lower(bot.owner):
202         try:
203             reload(bot.config)
204             conn.notice(nick, "Config reloaded.")
205         except ImportError:
206             conn.notice(nick, "Config reloading failed!")
207     else:
208         bot.automsg(public,nick,
209                 "Configuration can only be reloaded by my owner, by /msg.")
210
211 # quit irc
212 def quitq(bot, cmd, nick, conn, public):
213     if irc_lower(nick) == irc_lower(bot.owner):
214         bot.die(msg = "I have been chosen!")
215     elif public:
216         conn.notice(nick, "Such aggression in public!")
217     else:
218         conn.notice(nick, "You're not my owner.")
219
220 # google for something
221 def googleq(bot, cmd, nick, conn, public):
222     cmdrest = string.join(cmd.split()[1:])
223     # "I'm Feeling Lucky" rather than try and parse the html
224     targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
225             % urllib.quote_plus(cmdrest))
226     try:
227         # get redirected and grab the resulting url for returning
228         gsearch = urllib.urlopen(targ).geturl()
229         if gsearch != targ: # we've found something
230             bot.automsg(public,nick,str(gsearch))
231         else: # we haven't found anything.
232             bot.automsg(public,nick,"No pages found.")
233     except IOError: # if the connection times out. This blocks. :(
234         bot.automsg(public,nick,"The web's broken. Waah!")
235
236 # Look up the definition of something using google
237 def defineq(bot, cmd, nick, conn, public):
238     #this doesn't work any more
239     bot.automsg(public,nick,"'define' is broken because google are bastards :(")
240     return
241     cmdrest = string.join(cmd.split()[1:])
242     targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
243             % urllib.quote_plus(cmdrest))
244     try:
245         # Just slurp everything into a string
246         defnpage = urllib.urlopen(targ).read()
247         # For definitions we really do have to parse the HTML, sadly.
248         # This is of course going to be a bit fragile. We first look for
249         # 'Definitions of %s on the Web' -- if this isn't present we
250         # assume we have the 'no definitions found page'.
251         # The first defn starts after the following <p> tag, but as the
252         # first <li> in a <ul type="disc" class=std>
253         # Following that we assume that each definition is all the non-markup
254         # before a <br> tag. Currently we just dump out the first definition.
255         match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
256         if match == None:
257            bot.automsg(public,nick,"Some things defy definition.")
258         else:
259            # We assume google has truncated the definition for us so this
260            # won't flood the channel with text...
261            defn = " ".join(match.group(1).split("\n"))
262            bot.automsg(public,nick,defn)
263     except IOError: # if the connection times out. This blocks. :(
264          bot.automsg(public,nick,"The web's broken. Waah!")
265
266 # Look up a currency conversion via xe.com
267 def currencyq(bot, cmd, nick, conn, public):
268     args = ' '.join(cmd.split()[1:]).split(' as ')
269     if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
270         conn.notice(nick, "syntax: currency arg1 as arg2")
271         return
272     targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
273     try:
274         currencypage = urllib.urlopen(targ).read()
275         match = re.search(r"(1&nbsp;%s&nbsp;=&nbsp;[\d\.]+&nbsp;%s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
276         if match == None:
277             bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
278         else:
279             conversion = match.group(1);
280             conversion = conversion.replace('&nbsp;',' ');
281             bot.automsg(public,nick,conversion + " (from xe.com)")
282     except IOError: # if the connection times out. This blocks. :(
283         bot.automsg(public,nick,"The web's broken. Waah!")
284                  
285
286 ### extract the commit message and timestamp for commit 
287 def __getcommitinfo(commit):
288     cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
289     x=subprocess.Popen(cmd,
290                        stdout=subprocess.PIPE,stderr=subprocess.PIPE)
291     out,err=x.communicate()
292
293     if len(err):
294         return(err)
295
296     ts,mes=out.split('|')
297     mes=mes.strip()
298     md5mes=hashlib.md5(mes).hexdigest()
299     if bfd and md5mes in bfd:
300         mes=bfd[md5mes]
301     when=datetime.date.fromtimestamp(float(ts))
302     return mes, when
303
304 ###Return an array of commit messages and timestamps for lines in db that match what
305 def __getcommits(db,keys,what):
306     ans=[]
307     for k in keys:
308         if what in k:
309             ret=__getcommitinfo(db[k])
310             if len(ret)==1: #error message
311                 return ["Error message from git blame: %s" % ret]
312             else:
313                 ans.append( (k,ret[0],ret[1]) )
314     return ans
315
316 ###search all three databases for what
317 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
318     if what.strip()=="":
319         return []
320     tans=__getcommits(tdb,tdbk,what)
321     fans=__getcommits(fdb,fdbk,what)
322     sans=__getcommits(sdb,sdbk,what)
323     return tans+fans+sans
324
325 def blameq(bot,cmd,nick,conn,public,fishpond,cfgs):
326     tdb,tdbk,x = cfgs[0][7] # urgh, magic, to support magic knowledge below
327     fdb,fdbk,x = cfgs[1][7]
328     sdb,sdbk,x = cfgs[2][7]
329     clist=cmd.split()
330     if len(clist) < 2:
331         bot.automsg(public,nick,"Who or what do you want to blame?")
332         return
333     cwhat=' '.join(clist[2:])
334     kindsfile = "fish?"
335     if clist[1]=="#last":
336         try:
337             n = abs(int(clist[2]))-1
338             if n < 0: raise ValueError
339         except IndexError: n = 0
340         except ValueError:
341             bot.automsg(public,nick,"Huh?")
342             return
343         try: lmsg, lcfg = fishpond.last[n]
344         except IndexError:
345             bot.automsg(public,nick,"Nothing")
346             return
347         xdb,xdbk,kindsfile = lcfg[7]
348         ans=__getcommits(xdb,xdbk,lmsg)
349     elif clist[1]=="#trouts" or clist[1]=="#trout":
350         ans=__getcommits(tdb,tdbk,cwhat)
351     elif clist[1]=="#flirts" or clist[1]=="#flirt":
352         ans=__getcommits(fdb,fdbk,cwhat)
353     elif clist[1]=="#slashes" or clist[1]=="#slash":
354         ans=__getcommits(sdb,sdbk,cwhat)
355     else:
356         cwhat=' '.join(clist[1:])
357         ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
358     if len(ans)==0:
359         bot.automsg(public,nick,"No match found")
360     elif len(ans)==1:
361         if len(ans[0])==1:
362             bot.automsg(public,nick,ans[0])
363         else:
364             bot.automsg(public,nick,"Modified %s %s: %s" % (kindsfile, ans[0][2].isoformat(),ans[0][1]))
365     elif len(ans)>4:
366         bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
367     else:
368         for a in ans:
369             if len(a)==1:
370                 bot.automsg(public,nick,a)
371             else:
372                 bot.automsg(public,nick,"%s '%s' modified on %s: %s" % (kindsfile, a[0],a[2].isoformat(),a[1]))
373
374 ### say to msg/channel            
375 def sayq(bot, cmd, nick, conn, public):
376     if irc_lower(nick) == irc_lower(bot.owner):
377         conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
378     else:
379         if not public:
380             conn.notice(nick, "You're not my owner!")
381
382 ### action to msg/channel
383 def doq(bot, cmd, nick, conn, public):
384     sys.stderr.write(irc_lower(bot.owner))
385     sys.stderr.write(irc_lower(nick))
386     if not public:
387         if irc_lower(nick) == irc_lower(bot.owner):
388             conn.action(bot.channel, string.join(cmd.split()[1:]))
389         else:
390             conn.notice(nick, "You're not my owner!")
391
392 ###disconnect
393 def disconnq(bot, cmd, nick, conn, public):
394     if cmd == "disconnect": # hop off for 60s
395         bot.disconnect(msg="Be right back.")
396
397 ### list keys of a dictionary
398 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
399     d=dict.keys()
400     if sort:
401         d.sort()
402     bot.automsg(public,nick,string.join(d))
403
404 ### rot13 text (yes, I could have typed out the letters....)
405 ### also "foo".encode('rot13') would have worked
406 def rot13q(bot, cmd, nick, conn, public):
407     a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
408     b=a[13:]+a[:13]
409     trans=string.maketrans(a+a.upper(),b+b.upper())
410     conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
411
412 ### URL-tracking stuff
413
414 ### return a easy-to-read approximation of a time period
415 def nicetime(tempus):
416   if (tempus<120):
417     tm="%d seconds ago"%int(tempus)
418   elif (tempus<7200):
419     tm="%d minutes ago"%int(tempus/60)
420   if (tempus>7200):
421     tm="%d hours ago"%int(tempus/3600)
422   return tm
423
424 ### class to store URL data
425 class UrlLog:
426     "contains meta-data about a URL seen on-channel"
427     def __init__(self,url,nick):
428         self.nick=nick
429         self.url=url
430         self.first=time.time()
431         self.localfirst=time.localtime(self.first)
432         self.count=1
433         self.lastseen=time.time()
434         self.lastasked=time.time()
435     def recenttime(self):
436         return max(self.lastseen,self.lastasked)
437     def firstmen(self):
438         n=time.localtime(time.time())
439         s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
440         if n.tm_yday != self.localfirst.tm_yday:
441             s+=time.strftime(" on %d %B", self.localfirst)
442         return s
443     def urltype(self):
444         z=min(len(urlinfos)-1, self.count-1)
445         return urlinfos[z]
446
447 #(?:) is a regexp that doesn't group        
448 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
449 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
450 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
451 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
452 #How long (in s) to wait since the most recent mention before commenting
453 url_repeat_time = 300
454 urlinfos = ["a new",
455             "a fascinating",
456             "an interesting",
457             "a popular"]
458
459 ### Deal with /msg bot url or ~url in channel
460 def urlq(bot, cmd, nick, conn, public,urldb):
461   if (not urlre.search(cmd)):
462     bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
463     return
464
465   urlstring=urlre.search(cmd).group(1)
466   url=canonical_url(urlstring)
467   if (url in urldb):
468     T = urldb[url]
469     comment="I saw that URL in scrool, first mentioned by %s at %s" % \
470                (T.nick,T.firstmen())
471     if (public):
472       comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
473       T.count+=1
474     bot.automsg(False,nick,comment)
475     T.lastasked=time.time()
476     #URL suppressed, so mention in #urls
477     if urlstring != cmd.split()[1]: #first argument to URL was not the url
478       conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
479     else:
480       conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
481   else:
482     if (public):
483       bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
484     else:
485       if urlstring != cmd.split()[1]: #first argument to URL was not the url
486         conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
487       else:
488         conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
489     urldb[url]=UrlLog(url,nick)
490
491 ### Deal with URLs spotted in channel
492 def dourl(bot,conn,nick,command,urldb):
493   urlstring=urlre.search(command).group(1)
494   urlstring=canonical_url(urlstring)
495
496   if urlstring in urldb:
497     T=urldb[urlstring]
498     message="saw that URL in scrool, first mentioned by %s at %s" % \
499              (T.nick,T.firstmen())
500     if shibboleth.search(command)==None and \
501        time.time() - T.lastseen > url_repeat_time:
502         conn.action(bot.channel, message)
503     T.lastseen=time.time()
504     T.count+=1
505   else:
506     urldb[urlstring]=UrlLog(urlstring,nick)
507
508 ### Expire old urls
509 def urlexpire(urldb,expire):
510     urls=urldb.keys()
511     for u in urls:
512         if time.time() - urldb[u].recenttime() > expire:
513             del urldb[u]
514
515 # canonicalise BBC URLs (internal use only)
516 def canonical_url(urlstring):
517   if "nsfw://" in urlstring or "nsfws://" in urlstring:
518       urlstring=urlstring.replace("nsfw","http",1)
519   if (urlstring.find("news.bbc.co.uk") != -1):
520     for middle in ("/low/","/mobile/"):
521       x = urlstring.find(middle)
522       if (x != -1):
523         urlstring.replace(middle,"/hi/")
524   return urlstring
525
526 # automatically make nsfw urls for you and pass them on to url
527 def nsfwq(bot,cmd,nick,conn,public,urldb):
528   if (not hturlre.search(cmd)):
529     bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
530     return
531   newcmd=hturlre.sub(nsfwify,cmd)
532   urlq(bot,newcmd,nick,conn,public,urldb)
533
534 def nsfwify(match):
535     a,b,c=match.groups()
536     return 'nsfw'+b+c
537
538 #get tweet text
539 def twitterq(bot,cmd,nick,conn,public,twitapi):
540   
541   if (not urlre.search(cmd)):
542     bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
543     return
544
545   urlstring = urlre.search(cmd).group(1)
546   if (urlstring.find("twitter.com") !=-1):
547     stringsout = getTweet(urlstring,twitapi)
548     for stringout in stringsout:
549         bot.automsg(public, nick, stringout)
550   
551 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
552   unobfuscate_urls=True
553   expand_included_tweets=True
554   stringsout=[]
555
556   path = urlparse.urlparse(urlstring).path
557   tweetID = path.split('/')[-1]
558   try:
559     status = twitapi.GetStatus(tweetID)
560     if status == {}:
561         return "twitapi.GetStatus returned nothing :-("
562     if status.user == None and status.text == None:
563         return "Empty status object returned :("
564     if status.retweeted_status and status.retweeted_status.text:
565         status = status.retweeted_status
566     if status.user is not None:
567         tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
568         tweeter_name = status.user.name #.encode('UTF-8', 'replace')
569     else:
570         tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
571         tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
572     tweetText = status.full_text
573     if status.media:
574         replacements = defaultdict( list )
575         for medium in status.media:
576             replacements[medium.url].append(medium.media_url_https)
577
578         for k,v in replacements.items():
579
580             v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
581             if len(v) > 1:
582                 replacementstring = "[" +  " ; ".join(v) +"]"
583             else:
584                 replacementstring = v[0]
585             tweetText = tweetText.replace(k, replacementstring)
586
587     for url in status.urls:
588         toReplace = url.expanded_url
589
590         if unobfuscate_urls:
591             import urllib
592             rv = urlparse.urlparse(toReplace)
593             if rv.hostname in {
594                 # sourced from http://bit.do/list-of-url-shorteners.php
595                 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
596                 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
597                 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
598                 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
599                 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
600                 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
601                 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
602                 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
603                 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
604                  # added by ASB:
605                  "trib.al", "dlvr.it"
606                                }:
607                 #expand list as needed.
608                 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
609                 resptext = response.read()
610                 if resptext.startswith('http'): # ie it looks urlish (http or https)
611                     if resptext != toReplace:
612                         toReplace = resptext
613                     # maybe make a note of the domain of the original URL to compile list of shortenable domains?
614
615         # remove tracking utm_ query parameters, for privacy and brevity
616         # code snippet from https://gist.github.com/lepture/5997883
617         rv = urlparse.urlparse(toReplace)
618         if rv.query:
619             query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
620             if query:
621                 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
622             else:
623                 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
624
625         if expand_included_tweets:
626             if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
627                 if recurlvl > 2:
628                   stringsout = [ "{{ Recursion level too high }}" ] + stringsout
629                 else:
630                   quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
631                   if not quotedtweet:
632                       quotedtweet = [""]
633                   quotedtweet[0] = "Q{ " + quotedtweet[0]
634                   quotedtweet[-1] += " }"
635                   stringsout = quotedtweet + stringsout
636
637         tweetText = tweetText.replace(url.url, toReplace)
638
639     tweetText = tweetText.replace("&gt;",">")
640     tweetText = tweetText.replace("&lt;","<")
641     tweetText = tweetText.replace("&amp;","&")
642     tweetText = tweetText.replace("\n"," ")
643     stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
644   except twitter.TwitterError:
645     terror = sys.exc_info()
646     stringout = "Twitter error: %s" % terror[1].__str__()
647   except Exception:
648     terror = sys.exc_info()
649     stringout = "Error: %s" % terror[1].__str__()
650   stringsout = [stringout] + stringsout
651   if inclusion:
652       return stringsout # don't want to double-encode it, so just pass it on for now and encode later
653
654   return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)