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