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