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