chiark / gitweb /
blame: Move blame db information into the cfg
[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         ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,fish.last)
328     elif clist[1]=="#trouts" or clist[1]=="#trout":
329         ans=__getcommits(tdb,tdbk,cwhat)
330     elif clist[1]=="#flirts" or clist[1]=="#flirt":
331         ans=__getcommits(fdb,fdbk,cwhat)
332     elif clist[1]=="#slashes" or clist[1]=="#slash":
333         ans=__getcommits(sdb,sdbk,cwhat)
334     else:
335         cwhat=' '.join(clist[1:])
336         ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
337     if len(ans)==0:
338         bot.automsg(public,nick,"No match found")
339     elif len(ans)==1:
340         if len(ans[0])==1:
341             bot.automsg(public,nick,ans[0])
342         else:
343             bot.automsg(public,nick,"Modified %s: %s" % (ans[0][2].isoformat(),ans[0][1]))
344     elif len(ans)>4:
345         bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
346     else:
347         for a in ans:
348             if len(a)==1:
349                 bot.automsg(public,nick,a)
350             else:
351                 bot.automsg(public,nick,"'%s' modified on %s: %s" % (a[0],a[2].isoformat(),a[1]))
352
353 ### say to msg/channel            
354 def sayq(bot, cmd, nick, conn, public):
355     if irc_lower(nick) == irc_lower(bot.owner):
356         conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
357     else:
358         if not public:
359             conn.notice(nick, "You're not my owner!")
360
361 ### action to msg/channel
362 def doq(bot, cmd, nick, conn, public):
363     sys.stderr.write(irc_lower(bot.owner))
364     sys.stderr.write(irc_lower(nick))
365     if not public:
366         if irc_lower(nick) == irc_lower(bot.owner):
367             conn.action(bot.channel, string.join(cmd.split()[1:]))
368         else:
369             conn.notice(nick, "You're not my owner!")
370
371 ###disconnect
372 def disconnq(bot, cmd, nick, conn, public):
373     if cmd == "disconnect": # hop off for 60s
374         bot.disconnect(msg="Be right back.")
375
376 ### list keys of a dictionary
377 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
378     d=dict.keys()
379     if sort:
380         d.sort()
381     bot.automsg(public,nick,string.join(d))
382
383 ### rot13 text (yes, I could have typed out the letters....)
384 ### also "foo".encode('rot13') would have worked
385 def rot13q(bot, cmd, nick, conn, public):
386     a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
387     b=a[13:]+a[:13]
388     trans=string.maketrans(a+a.upper(),b+b.upper())
389     conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
390
391 ### URL-tracking stuff
392
393 ### return a easy-to-read approximation of a time period
394 def nicetime(tempus):
395   if (tempus<120):
396     tm="%d seconds ago"%int(tempus)
397   elif (tempus<7200):
398     tm="%d minutes ago"%int(tempus/60)
399   if (tempus>7200):
400     tm="%d hours ago"%int(tempus/3600)
401   return tm
402
403 ### class to store URL data
404 class UrlLog:
405     "contains meta-data about a URL seen on-channel"
406     def __init__(self,url,nick):
407         self.nick=nick
408         self.url=url
409         self.first=time.time()
410         self.localfirst=time.localtime(self.first)
411         self.count=1
412         self.lastseen=time.time()
413         self.lastasked=time.time()
414     def recenttime(self):
415         return max(self.lastseen,self.lastasked)
416     def firstmen(self):
417         n=time.localtime(time.time())
418         s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
419         if n.tm_yday != self.localfirst.tm_yday:
420             s+=time.strftime(" on %d %B", self.localfirst)
421         return s
422     def urltype(self):
423         z=min(len(urlinfos)-1, self.count-1)
424         return urlinfos[z]
425
426 #(?:) is a regexp that doesn't group        
427 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
428 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
429 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
430 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
431 #How long (in s) to wait since the most recent mention before commenting
432 url_repeat_time = 300
433 urlinfos = ["a new",
434             "a fascinating",
435             "an interesting",
436             "a popular"]
437
438 ### Deal with /msg bot url or ~url in channel
439 def urlq(bot, cmd, nick, conn, public,urldb):
440   if (not urlre.search(cmd)):
441     bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
442     return
443
444   urlstring=urlre.search(cmd).group(1)
445   url=canonical_url(urlstring)
446   if (url in urldb):
447     T = urldb[url]
448     comment="I saw that URL in scrool, first mentioned by %s at %s" % \
449                (T.nick,T.firstmen())
450     if (public):
451       comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
452       T.count+=1
453     bot.automsg(False,nick,comment)
454     T.lastasked=time.time()
455     #URL suppressed, so mention in #urls
456     if urlstring != cmd.split()[1]: #first argument to URL was not the url
457       conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
458     else:
459       conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
460   else:
461     if (public):
462       bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
463     else:
464       if urlstring != cmd.split()[1]: #first argument to URL was not the url
465         conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
466       else:
467         conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
468     urldb[url]=UrlLog(url,nick)
469
470 ### Deal with URLs spotted in channel
471 def dourl(bot,conn,nick,command,urldb):
472   urlstring=urlre.search(command).group(1)
473   urlstring=canonical_url(urlstring)
474
475   if urlstring in urldb:
476     T=urldb[urlstring]
477     message="saw that URL in scrool, first mentioned by %s at %s" % \
478              (T.nick,T.firstmen())
479     if shibboleth.search(command)==None and \
480        time.time() - T.lastseen > url_repeat_time:
481         conn.action(bot.channel, message)
482     T.lastseen=time.time()
483     T.count+=1
484   else:
485     urldb[urlstring]=UrlLog(urlstring,nick)
486
487 ### Expire old urls
488 def urlexpire(urldb,expire):
489     urls=urldb.keys()
490     for u in urls:
491         if time.time() - urldb[u].recenttime() > expire:
492             del urldb[u]
493
494 # canonicalise BBC URLs (internal use only)
495 def canonical_url(urlstring):
496   if "nsfw://" in urlstring or "nsfws://" in urlstring:
497       urlstring=urlstring.replace("nsfw","http",1)
498   if (urlstring.find("news.bbc.co.uk") != -1):
499     for middle in ("/low/","/mobile/"):
500       x = urlstring.find(middle)
501       if (x != -1):
502         urlstring.replace(middle,"/hi/")
503   return urlstring
504
505 # automatically make nsfw urls for you and pass them on to url
506 def nsfwq(bot,cmd,nick,conn,public,urldb):
507   if (not hturlre.search(cmd)):
508     bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
509     return
510   newcmd=hturlre.sub(nsfwify,cmd)
511   urlq(bot,newcmd,nick,conn,public,urldb)
512
513 def nsfwify(match):
514     a,b,c=match.groups()
515     return 'nsfw'+b+c
516
517 #get tweet text
518 def twitterq(bot,cmd,nick,conn,public,twitapi):
519   
520   if (not urlre.search(cmd)):
521     bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
522     return
523
524   urlstring = urlre.search(cmd).group(1)
525   if (urlstring.find("twitter.com") !=-1):
526     stringsout = getTweet(urlstring,twitapi)
527     for stringout in stringsout:
528         bot.automsg(public, nick, stringout)
529   
530 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
531   unobfuscate_urls=True
532   expand_included_tweets=True
533   stringsout=[]
534
535   path = urlparse.urlparse(urlstring).path
536   tweetID = path.split('/')[-1]
537   try:
538     status = twitapi.GetStatus(tweetID)
539     if status == {}:
540         return "twitapi.GetStatus returned nothing :-("
541     if status.user == None and status.text == None:
542         return "Empty status object returned :("
543     if status.retweeted_status and status.retweeted_status.text:
544         status = status.retweeted_status
545     if status.user is not None:
546         tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
547         tweeter_name = status.user.name #.encode('UTF-8', 'replace')
548     else:
549         tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
550         tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
551     tweetText = status.full_text
552     if status.media:
553         replacements = defaultdict( list )
554         for medium in status.media:
555             replacements[medium.url].append(medium.media_url_https)
556
557         for k,v in replacements.items():
558
559             v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
560             if len(v) > 1:
561                 replacementstring = "[" +  " ; ".join(v) +"]"
562             else:
563                 replacementstring = v[0]
564             tweetText = tweetText.replace(k, replacementstring)
565
566     for url in status.urls:
567         toReplace = url.expanded_url
568
569         if unobfuscate_urls:
570             import urllib
571             rv = urlparse.urlparse(toReplace)
572             if rv.hostname in {
573                 # sourced from http://bit.do/list-of-url-shorteners.php
574                 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
575                 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
576                 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
577                 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
578                 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
579                 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
580                 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
581                 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
582                 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
583                  # added by ASB:
584                  "trib.al", "dlvr.it"
585                                }:
586                 #expand list as needed.
587                 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
588                 resptext = response.read()
589                 if resptext.startswith('http'): # ie it looks urlish (http or https)
590                     if resptext != toReplace:
591                         toReplace = resptext
592                     # maybe make a note of the domain of the original URL to compile list of shortenable domains?
593
594         # remove tracking utm_ query parameters, for privacy and brevity
595         # code snippet from https://gist.github.com/lepture/5997883
596         rv = urlparse.urlparse(toReplace)
597         if rv.query:
598             query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
599             if query:
600                 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
601             else:
602                 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
603
604         if expand_included_tweets:
605             if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
606                 if recurlvl > 2:
607                   stringsout = [ "{{ Recursion level too high }}" ] + stringsout
608                 else:
609                   quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
610                   if not quotedtweet:
611                       quotedtweet = [""]
612                   quotedtweet[0] = "Q{ " + quotedtweet[0]
613                   quotedtweet[-1] += " }"
614                   stringsout = quotedtweet + stringsout
615
616         tweetText = tweetText.replace(url.url, toReplace)
617
618     tweetText = tweetText.replace("&gt;",">")
619     tweetText = tweetText.replace("&lt;","<")
620     tweetText = tweetText.replace("&amp;","&")
621     tweetText = tweetText.replace("\n"," ")
622     stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
623   except twitter.TwitterError:
624     terror = sys.exc_info()
625     stringout = "Twitter error: %s" % terror[1].__str__()
626   except Exception:
627     terror = sys.exc_info()
628     stringout = "Error: %s" % terror[1].__str__()
629   stringsout = [stringout] + stringsout
630   if inclusion:
631       return stringsout # don't want to double-encode it, so just pass it on for now and encode later
632
633   return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)