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
7 from blame_filter import bfd
12 def karmaq(bot, cmd, nick, conn, public, karma):
14 item=cmd.split()[1].lower()
18 bot.automsg(public,nick,"I have karma on %s items." %
20 elif karma.has_key(item):
21 bot.automsg(public,nick,"%s has karma %s."
24 bot.automsg(public,nick, "%s has no karma set." % item)
27 def karmadelq(bot, cmd, nick, conn, public, karma):
29 item=cmd.split()[1].lower()
31 conn.notice(nick, "What should I delete?")
34 conn.notice(nick, "You are not my owner.")
36 if karma.has_key(item):
38 conn.notice(nick, "Item %s deleted."%item)
40 conn.notice(nick, "There is no karma stored for %s."%item)
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")
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())))
56 # Check on fish stocks
59 if time.time()>=pond.quotatime:
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()
70 # List of things the bot might be called to work round the self-trouting code
71 synonyms=["itself","the bot","themself"]
73 # trout someone, or flirt with them
74 def troutq(bot, cmd, nick, conn, public, cfg):
81 selftroutchance=cfg[6]
85 conn.notice(nick, quietmsg%fishpond.Boring_Git)
87 if fishpond.cur_fish<=0:
88 conn.notice(nick, nofishmsg)
90 target = string.join(cmd.split()[1:])
92 conn.notice(nick, notargetmsg)
94 me = bot.connection.get_nickname()
95 trout_msg = random.choice(fishlist)
96 fishpond.last=trout_msg
98 # The bot won't trout or flirt with itself;
99 if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
101 # There's a chance the game may be given away if the request was not
104 if random.random()<=selftroutchance:
105 trout_msg=trout_msg+(selftrout%nick)
107 conn.action(bot.channel, trout_msg % target)
111 def slashq(bot, cmd, nick, conn, public, cfg):
118 selfslashchance=cfg[6]
122 conn.notice(nick, quietmsg%fishpond.Boring_Git)
124 if fishpond.cur_fish<=0:
125 conn.notice(nick, nofishmsg)
127 target = string.join(cmd.split()[1:])
128 #who = cmd.split()[1:]
129 who = ' '.join(cmd.split()[1:]).split(' / ')
131 conn.notice(nick, "it takes two to tango!")
134 conn.notice(nick, "we'll have none of that round here")
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!")
144 # The bot won't slash with itself, instead slashing the requester
146 if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
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!")
152 # There's a chance the game may be given away if the request was not
155 if random.random()<=selfslashchance:
156 slash_msg=slash_msg+(selfslash%nick)
158 conn.action(bot.channel, slash_msg % (who[0], who[1]))
162 def unitq(bot, cmd, nick, conn, public):
163 args = ' '.join(cmd.split()[1:]).split(' as ')
165 args = ' '.join(cmd.split()[1:]).split(' / ')
167 conn.notice(nick, "syntax: units arg1 as arg2")
170 sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
172 sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
175 #popen2 doesn't clean up the child properly. Do this by hand
177 if os.WEXITSTATUS(child[1])==0:
178 bot.automsg(public,nick,res[0].strip())
180 conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
182 # Shut up trouting for a minute
183 def nofishq(bot, cmd, nick, conn, public, fish):
187 fish.quotatime=time.time()
188 fish.quotatime+=fish.nofish_time
189 conn.notice(nick, "Fish stocks depleted, as you wish.")
192 def reloadq(bot, cmd, nick, conn, public):
193 if not public and irc_lower(nick) == irc_lower(bot.owner):
196 conn.notice(nick, "Config reloaded.")
198 conn.notice(nick, "Config reloading failed!")
200 bot.automsg(public,nick,
201 "Configuration can only be reloaded by my owner, by /msg.")
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!")
208 conn.notice(nick, "Such aggression in public!")
210 conn.notice(nick, "You're not my owner.")
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))
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!")
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 :(")
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))
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)
249 bot.automsg(public,nick,"Some things defy definition.")
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!")
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")
264 targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
266 currencypage = urllib.urlopen(targ).read()
267 match = re.search(r"(1 %s = [\d\.]+ %s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
269 bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
271 conversion = match.group(1);
272 conversion = conversion.replace(' ',' ');
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!")
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()
288 ts,mes=out.split('|')
290 md5mes=hashlib.md5(mes).hexdigest()
291 if bfd and md5mes in bfd:
293 when=datetime.date.fromtimestamp(float(ts))
296 ###Return an array of commit messages and timestamps for lines in db that match what
297 def __getcommits(db,keys,what):
301 ret=__getcommitinfo(db[k])
302 if len(ret)==1: #error message
303 return ["Error message from git blame: %s" % ret]
305 ans.append( (k,ret[0],ret[1]) )
308 ###search all three databases for what
309 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
312 tans=__getcommits(tdb,tdbk,what)
313 fans=__getcommits(fdb,fdbk,what)
314 sans=__getcommits(sdb,sdbk,what)
315 return tans+fans+sans
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]
323 bot.automsg(public,nick,"Who or what do you want to blame?")
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)
335 cwhat=' '.join(clist[1:])
336 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
338 bot.automsg(public,nick,"No match found")
341 bot.automsg(public,nick,ans[0])
343 bot.automsg(public,nick,"Modified %s: %s" % (ans[0][2].isoformat(),ans[0][1]))
345 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
349 bot.automsg(public,nick,a)
351 bot.automsg(public,nick,"'%s' modified on %s: %s" % (a[0],a[2].isoformat(),a[1]))
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:]))
359 conn.notice(nick, "You're not my owner!")
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))
366 if irc_lower(nick) == irc_lower(bot.owner):
367 conn.action(bot.channel, string.join(cmd.split()[1:]))
369 conn.notice(nick, "You're not my owner!")
372 def disconnq(bot, cmd, nick, conn, public):
373 if cmd == "disconnect": # hop off for 60s
374 bot.disconnect(msg="Be right back.")
376 ### list keys of a dictionary
377 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
381 bot.automsg(public,nick,string.join(d))
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))))
388 trans=string.maketrans(a+a.upper(),b+b.upper())
389 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
391 ### URL-tracking stuff
393 ### return a easy-to-read approximation of a time period
394 def nicetime(tempus):
396 tm="%d seconds ago"%int(tempus)
398 tm="%d minutes ago"%int(tempus/60)
400 tm="%d hours ago"%int(tempus/3600)
403 ### class to store URL data
405 "contains meta-data about a URL seen on-channel"
406 def __init__(self,url,nick):
409 self.first=time.time()
410 self.localfirst=time.localtime(self.first)
412 self.lastseen=time.time()
413 self.lastasked=time.time()
414 def recenttime(self):
415 return max(self.lastseen,self.lastasked)
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)
423 z=min(len(urlinfos)-1, self.count-1)
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
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")
444 urlstring=urlre.search(cmd).group(1)
445 url=canonical_url(urlstring)
448 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
449 (T.nick,T.firstmen())
451 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
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:])))
459 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
462 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
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:])))
467 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
468 urldb[url]=UrlLog(url,nick)
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)
475 if urlstring in urldb:
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()
485 urldb[urlstring]=UrlLog(urlstring,nick)
488 def urlexpire(urldb,expire):
491 if time.time() - urldb[u].recenttime() > expire:
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)
502 urlstring.replace(middle,"/hi/")
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")
510 newcmd=hturlre.sub(nsfwify,cmd)
511 urlq(bot,newcmd,nick,conn,public,urldb)
518 def twitterq(bot,cmd,nick,conn,public,twitapi):
520 if (not urlre.search(cmd)):
521 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
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)
530 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
531 unobfuscate_urls=True
532 expand_included_tweets=True
535 path = urlparse.urlparse(urlstring).path
536 tweetID = path.split('/')[-1]
538 status = twitapi.GetStatus(tweetID)
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')
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
553 replacements = defaultdict( list )
554 for medium in status.media:
555 replacements[medium.url].append(medium.media_url_https)
557 for k,v in replacements.items():
559 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
561 replacementstring = "[" + " ; ".join(v) +"]"
563 replacementstring = v[0]
564 tweetText = tweetText.replace(k, replacementstring)
566 for url in status.urls:
567 toReplace = url.expanded_url
571 rv = urlparse.urlparse(toReplace)
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",
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:
592 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
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)
598 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
600 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
602 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
604 if expand_included_tweets:
605 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
607 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
609 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
612 quotedtweet[0] = "Q{ " + quotedtweet[0]
613 quotedtweet[-1] += " }"
614 stringsout = quotedtweet + stringsout
616 tweetText = tweetText.replace(url.url, toReplace)
618 tweetText = tweetText.replace(">",">")
619 tweetText = tweetText.replace("<","<")
620 tweetText = tweetText.replace("&","&")
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__()
627 terror = sys.exc_info()
628 stringout = "Error: %s" % terror[1].__str__()
629 stringsout = [stringout] + stringsout
631 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
633 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)