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