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