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