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