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