chiark / gitweb /
blame: Remember last 10 trouts, not just 1
[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: lmsg, lcfg = fishpond.last[0]
337         except IndexError:
338             bot.automsg(public,nick,"Nothing")
339             return
340         xdb,xdbk,kindsfile = lcfg[7]
341         ans=__getcommits(xdb,xdbk,lmsg)
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)