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())))
57 def __init__(fishpond):
59 fishpond.last_cfg=None
63 # Check on fish stocks
66 if time.time()>=pond.quotatime:
70 if (time.time()-pond.quotatime)>pond.fish_time_inc:
71 pond.cur_fish+=(((time.time()-pond.quotatime)
72 /pond.fish_time_inc)*pond.fish_inc)
73 if pond.cur_fish>pond.max_fish:
74 pond.cur_fish=pond.max_fish
75 pond.quotatime=time.time()
77 # List of things the bot might be called to work round the self-trouting code
78 synonyms=["itself","the bot","themself"]
80 # trout someone, or flirt with them
81 def troutq(bot, cmd, nick, conn, public, cfg):
88 selftroutchance=cfg[6]
92 conn.notice(nick, quietmsg%fishpond.Boring_Git)
94 if fishpond.cur_fish<=0:
95 conn.notice(nick, nofishmsg)
97 target = string.join(cmd.split()[1:])
99 conn.notice(nick, notargetmsg)
101 me = bot.connection.get_nickname()
102 trout_msg = random.choice(fishlist)
103 fishpond.last=trout_msg
104 fishpond.last_cfg=cfg
105 # The bot won't trout or flirt with itself;
106 if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
108 # There's a chance the game may be given away if the request was not
111 if random.random()<=selftroutchance:
112 trout_msg=trout_msg+(selftrout%nick)
114 conn.action(bot.channel, trout_msg % target)
118 def slashq(bot, cmd, nick, conn, public, cfg):
125 selfslashchance=cfg[6]
129 conn.notice(nick, quietmsg%fishpond.Boring_Git)
131 if fishpond.cur_fish<=0:
132 conn.notice(nick, nofishmsg)
134 target = string.join(cmd.split()[1:])
135 #who = cmd.split()[1:]
136 who = ' '.join(cmd.split()[1:]).split(' / ')
138 conn.notice(nick, "it takes two to tango!")
141 conn.notice(nick, "we'll have none of that round here")
143 me = bot.connection.get_nickname()
144 slash_msg = random.choice(fishlist)
145 fishpond.last=slash_msg
146 fishpond.last_cfg=cfg
147 # The bot won't slash people with themselves
148 if irc_lower(who[0]) == irc_lower(who[1]):
149 conn.notice(nick, "oooooh no missus!")
151 # The bot won't slash with itself, instead slashing the requester
153 if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
155 # Perhaps someone asked to slash themselves with the bot then we get
156 if irc_lower(who[0]) == irc_lower(who[1]):
157 conn.notice(nick, "you wish!")
159 # There's a chance the game may be given away if the request was not
162 if random.random()<=selfslashchance:
163 slash_msg=slash_msg+(selfslash%nick)
165 conn.action(bot.channel, slash_msg % (who[0], who[1]))
169 def unitq(bot, cmd, nick, conn, public):
170 args = ' '.join(cmd.split()[1:]).split(' as ')
172 args = ' '.join(cmd.split()[1:]).split(' / ')
174 conn.notice(nick, "syntax: units arg1 as arg2")
177 sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
179 sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
182 #popen2 doesn't clean up the child properly. Do this by hand
184 if os.WEXITSTATUS(child[1])==0:
185 bot.automsg(public,nick,res[0].strip())
187 conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
189 # Shut up trouting for a minute
190 def nofishq(bot, cmd, nick, conn, public, fish):
194 fish.quotatime=time.time()
195 fish.quotatime+=fish.nofish_time
196 conn.notice(nick, "Fish stocks depleted, as you wish.")
199 def reloadq(bot, cmd, nick, conn, public):
200 if not public and irc_lower(nick) == irc_lower(bot.owner):
203 conn.notice(nick, "Config reloaded.")
205 conn.notice(nick, "Config reloading failed!")
207 bot.automsg(public,nick,
208 "Configuration can only be reloaded by my owner, by /msg.")
211 def quitq(bot, cmd, nick, conn, public):
212 if irc_lower(nick) == irc_lower(bot.owner):
213 bot.die(msg = "I have been chosen!")
215 conn.notice(nick, "Such aggression in public!")
217 conn.notice(nick, "You're not my owner.")
219 # google for something
220 def googleq(bot, cmd, nick, conn, public):
221 cmdrest = string.join(cmd.split()[1:])
222 # "I'm Feeling Lucky" rather than try and parse the html
223 targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
224 % urllib.quote_plus(cmdrest))
226 # get redirected and grab the resulting url for returning
227 gsearch = urllib.urlopen(targ).geturl()
228 if gsearch != targ: # we've found something
229 bot.automsg(public,nick,str(gsearch))
230 else: # we haven't found anything.
231 bot.automsg(public,nick,"No pages found.")
232 except IOError: # if the connection times out. This blocks. :(
233 bot.automsg(public,nick,"The web's broken. Waah!")
235 # Look up the definition of something using google
236 def defineq(bot, cmd, nick, conn, public):
237 #this doesn't work any more
238 bot.automsg(public,nick,"'define' is broken because google are bastards :(")
240 cmdrest = string.join(cmd.split()[1:])
241 targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
242 % urllib.quote_plus(cmdrest))
244 # Just slurp everything into a string
245 defnpage = urllib.urlopen(targ).read()
246 # For definitions we really do have to parse the HTML, sadly.
247 # This is of course going to be a bit fragile. We first look for
248 # 'Definitions of %s on the Web' -- if this isn't present we
249 # assume we have the 'no definitions found page'.
250 # The first defn starts after the following <p> tag, but as the
251 # first <li> in a <ul type="disc" class=std>
252 # Following that we assume that each definition is all the non-markup
253 # before a <br> tag. Currently we just dump out the first definition.
254 match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
256 bot.automsg(public,nick,"Some things defy definition.")
258 # We assume google has truncated the definition for us so this
259 # won't flood the channel with text...
260 defn = " ".join(match.group(1).split("\n"))
261 bot.automsg(public,nick,defn)
262 except IOError: # if the connection times out. This blocks. :(
263 bot.automsg(public,nick,"The web's broken. Waah!")
265 # Look up a currency conversion via xe.com
266 def currencyq(bot, cmd, nick, conn, public):
267 args = ' '.join(cmd.split()[1:]).split(' as ')
268 if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
269 conn.notice(nick, "syntax: currency arg1 as arg2")
271 targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
273 currencypage = urllib.urlopen(targ).read()
274 match = re.search(r"(1 %s = [\d\.]+ %s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
276 bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
278 conversion = match.group(1);
279 conversion = conversion.replace(' ',' ');
280 bot.automsg(public,nick,conversion + " (from xe.com)")
281 except IOError: # if the connection times out. This blocks. :(
282 bot.automsg(public,nick,"The web's broken. Waah!")
285 ### extract the commit message and timestamp for commit
286 def __getcommitinfo(commit):
287 cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
288 x=subprocess.Popen(cmd,
289 stdout=subprocess.PIPE,stderr=subprocess.PIPE)
290 out,err=x.communicate()
295 ts,mes=out.split('|')
297 md5mes=hashlib.md5(mes).hexdigest()
298 if bfd and md5mes in bfd:
300 when=datetime.date.fromtimestamp(float(ts))
303 ###Return an array of commit messages and timestamps for lines in db that match what
304 def __getcommits(db,keys,what):
308 ret=__getcommitinfo(db[k])
309 if len(ret)==1: #error message
310 return ["Error message from git blame: %s" % ret]
312 ans.append( (k,ret[0],ret[1]) )
315 ###search all three databases for what
316 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
319 tans=__getcommits(tdb,tdbk,what)
320 fans=__getcommits(fdb,fdbk,what)
321 sans=__getcommits(sdb,sdbk,what)
322 return tans+fans+sans
324 def blameq(bot,cmd,nick,conn,public,fish,cfgs):
325 tdb,tdbk,x = cfgs[0][7] # urgh, magic, to support magic knowledge below
326 fdb,fdbk,x = cfgs[1][7]
327 sdb,sdbk,x = cfgs[2][7]
330 bot.automsg(public,nick,"Who or what do you want to blame?")
332 cwhat=' '.join(clist[2:])
334 if clist[1]=="#last":
335 if fish.last_cfg is None:
336 bot.automsg(public,nick,"Nothing")
338 xdb,xdbk,kindsfile = fish.last_cfg[7]
339 ans=__getcommits(xdb,xdbk,fish.last)
340 elif clist[1]=="#trouts" or clist[1]=="#trout":
341 ans=__getcommits(tdb,tdbk,cwhat)
342 elif clist[1]=="#flirts" or clist[1]=="#flirt":
343 ans=__getcommits(fdb,fdbk,cwhat)
344 elif clist[1]=="#slashes" or clist[1]=="#slash":
345 ans=__getcommits(sdb,sdbk,cwhat)
347 cwhat=' '.join(clist[1:])
348 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
350 bot.automsg(public,nick,"No match found")
353 bot.automsg(public,nick,ans[0])
355 bot.automsg(public,nick,"Modified %s %s: %s" % (kindsfile, ans[0][2].isoformat(),ans[0][1]))
357 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
361 bot.automsg(public,nick,a)
363 bot.automsg(public,nick,"%s '%s' modified on %s: %s" % (kindsfile, a[0],a[2].isoformat(),a[1]))
365 ### say to msg/channel
366 def sayq(bot, cmd, nick, conn, public):
367 if irc_lower(nick) == irc_lower(bot.owner):
368 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
371 conn.notice(nick, "You're not my owner!")
373 ### action to msg/channel
374 def doq(bot, cmd, nick, conn, public):
375 sys.stderr.write(irc_lower(bot.owner))
376 sys.stderr.write(irc_lower(nick))
378 if irc_lower(nick) == irc_lower(bot.owner):
379 conn.action(bot.channel, string.join(cmd.split()[1:]))
381 conn.notice(nick, "You're not my owner!")
384 def disconnq(bot, cmd, nick, conn, public):
385 if cmd == "disconnect": # hop off for 60s
386 bot.disconnect(msg="Be right back.")
388 ### list keys of a dictionary
389 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
393 bot.automsg(public,nick,string.join(d))
395 ### rot13 text (yes, I could have typed out the letters....)
396 ### also "foo".encode('rot13') would have worked
397 def rot13q(bot, cmd, nick, conn, public):
398 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
400 trans=string.maketrans(a+a.upper(),b+b.upper())
401 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
403 ### URL-tracking stuff
405 ### return a easy-to-read approximation of a time period
406 def nicetime(tempus):
408 tm="%d seconds ago"%int(tempus)
410 tm="%d minutes ago"%int(tempus/60)
412 tm="%d hours ago"%int(tempus/3600)
415 ### class to store URL data
417 "contains meta-data about a URL seen on-channel"
418 def __init__(self,url,nick):
421 self.first=time.time()
422 self.localfirst=time.localtime(self.first)
424 self.lastseen=time.time()
425 self.lastasked=time.time()
426 def recenttime(self):
427 return max(self.lastseen,self.lastasked)
429 n=time.localtime(time.time())
430 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
431 if n.tm_yday != self.localfirst.tm_yday:
432 s+=time.strftime(" on %d %B", self.localfirst)
435 z=min(len(urlinfos)-1, self.count-1)
438 #(?:) is a regexp that doesn't group
439 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
440 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
441 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
442 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
443 #How long (in s) to wait since the most recent mention before commenting
444 url_repeat_time = 300
450 ### Deal with /msg bot url or ~url in channel
451 def urlq(bot, cmd, nick, conn, public,urldb):
452 if (not urlre.search(cmd)):
453 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
456 urlstring=urlre.search(cmd).group(1)
457 url=canonical_url(urlstring)
460 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
461 (T.nick,T.firstmen())
463 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
465 bot.automsg(False,nick,comment)
466 T.lastasked=time.time()
467 #URL suppressed, so mention in #urls
468 if urlstring != cmd.split()[1]: #first argument to URL was not the url
469 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
471 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
474 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
476 if urlstring != cmd.split()[1]: #first argument to URL was not the url
477 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
479 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
480 urldb[url]=UrlLog(url,nick)
482 ### Deal with URLs spotted in channel
483 def dourl(bot,conn,nick,command,urldb):
484 urlstring=urlre.search(command).group(1)
485 urlstring=canonical_url(urlstring)
487 if urlstring in urldb:
489 message="saw that URL in scrool, first mentioned by %s at %s" % \
490 (T.nick,T.firstmen())
491 if shibboleth.search(command)==None and \
492 time.time() - T.lastseen > url_repeat_time:
493 conn.action(bot.channel, message)
494 T.lastseen=time.time()
497 urldb[urlstring]=UrlLog(urlstring,nick)
500 def urlexpire(urldb,expire):
503 if time.time() - urldb[u].recenttime() > expire:
506 # canonicalise BBC URLs (internal use only)
507 def canonical_url(urlstring):
508 if "nsfw://" in urlstring or "nsfws://" in urlstring:
509 urlstring=urlstring.replace("nsfw","http",1)
510 if (urlstring.find("news.bbc.co.uk") != -1):
511 for middle in ("/low/","/mobile/"):
512 x = urlstring.find(middle)
514 urlstring.replace(middle,"/hi/")
517 # automatically make nsfw urls for you and pass them on to url
518 def nsfwq(bot,cmd,nick,conn,public,urldb):
519 if (not hturlre.search(cmd)):
520 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
522 newcmd=hturlre.sub(nsfwify,cmd)
523 urlq(bot,newcmd,nick,conn,public,urldb)
530 def twitterq(bot,cmd,nick,conn,public,twitapi):
532 if (not urlre.search(cmd)):
533 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
536 urlstring = urlre.search(cmd).group(1)
537 if (urlstring.find("twitter.com") !=-1):
538 stringsout = getTweet(urlstring,twitapi)
539 for stringout in stringsout:
540 bot.automsg(public, nick, stringout)
542 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
543 unobfuscate_urls=True
544 expand_included_tweets=True
547 path = urlparse.urlparse(urlstring).path
548 tweetID = path.split('/')[-1]
550 status = twitapi.GetStatus(tweetID)
552 return "twitapi.GetStatus returned nothing :-("
553 if status.user == None and status.text == None:
554 return "Empty status object returned :("
555 if status.retweeted_status and status.retweeted_status.text:
556 status = status.retweeted_status
557 if status.user is not None:
558 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
559 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
561 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
562 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
563 tweetText = status.full_text
565 replacements = defaultdict( list )
566 for medium in status.media:
567 replacements[medium.url].append(medium.media_url_https)
569 for k,v in replacements.items():
571 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
573 replacementstring = "[" + " ; ".join(v) +"]"
575 replacementstring = v[0]
576 tweetText = tweetText.replace(k, replacementstring)
578 for url in status.urls:
579 toReplace = url.expanded_url
583 rv = urlparse.urlparse(toReplace)
585 # sourced from http://bit.do/list-of-url-shorteners.php
586 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
587 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
588 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
589 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
590 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
591 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
592 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
593 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
594 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
598 #expand list as needed.
599 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
600 resptext = response.read()
601 if resptext.startswith('http'): # ie it looks urlish (http or https)
602 if resptext != toReplace:
604 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
606 # remove tracking utm_ query parameters, for privacy and brevity
607 # code snippet from https://gist.github.com/lepture/5997883
608 rv = urlparse.urlparse(toReplace)
610 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
612 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
614 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
616 if expand_included_tweets:
617 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
619 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
621 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
624 quotedtweet[0] = "Q{ " + quotedtweet[0]
625 quotedtweet[-1] += " }"
626 stringsout = quotedtweet + stringsout
628 tweetText = tweetText.replace(url.url, toReplace)
630 tweetText = tweetText.replace(">",">")
631 tweetText = tweetText.replace("<","<")
632 tweetText = tweetText.replace("&","&")
633 tweetText = tweetText.replace("\n"," ")
634 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
635 except twitter.TwitterError:
636 terror = sys.exc_info()
637 stringout = "Twitter error: %s" % terror[1].__str__()
639 terror = sys.exc_info()
640 stringout = "Error: %s" % terror[1].__str__()
641 stringsout = [stringout] + stringsout
643 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
645 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)