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):
62 def note_last(fishpond, msg, cfg):
63 fishpond.last.insert(0,(msg,cfg))
64 fishpond.last = fishpond.last[0:10]
66 # Check on fish stocks
69 if time.time()>=pond.quotatime:
73 if (time.time()-pond.quotatime)>pond.fish_time_inc:
74 pond.cur_fish+=(((time.time()-pond.quotatime)
75 /pond.fish_time_inc)*pond.fish_inc)
76 if pond.cur_fish>pond.max_fish:
77 pond.cur_fish=pond.max_fish
78 pond.quotatime=time.time()
80 # List of things the bot might be called to work round the self-trouting code
81 synonyms=["itself","the bot","themself"]
83 # trout someone, or flirt with them
84 def troutq(bot, cmd, nick, conn, public, cfg):
91 selftroutchance=cfg[6]
95 conn.notice(nick, quietmsg%fishpond.Boring_Git)
97 if fishpond.cur_fish<=0:
98 conn.notice(nick, nofishmsg)
100 target = string.join(cmd.split()[1:])
102 conn.notice(nick, notargetmsg)
104 me = bot.connection.get_nickname()
105 trout_msg = random.choice(fishlist)
106 fishpond.note_last(trout_msg,cfg)
107 # The bot won't trout or flirt with itself;
108 if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
110 # There's a chance the game may be given away if the request was not
113 if random.random()<=selftroutchance:
114 trout_msg=trout_msg+(selftrout%nick)
116 conn.action(bot.channel, trout_msg % target)
120 def slashq(bot, cmd, nick, conn, public, cfg):
127 selfslashchance=cfg[6]
131 conn.notice(nick, quietmsg%fishpond.Boring_Git)
133 if fishpond.cur_fish<=0:
134 conn.notice(nick, nofishmsg)
136 target = string.join(cmd.split()[1:])
137 #who = cmd.split()[1:]
138 who = ' '.join(cmd.split()[1:]).split(' / ')
140 conn.notice(nick, "it takes two to tango!")
143 conn.notice(nick, "we'll have none of that round here")
145 me = bot.connection.get_nickname()
146 slash_msg = random.choice(fishlist)
147 fishpond.note_last(slash_msg,cfg)
148 # The bot won't slash people with themselves
149 if irc_lower(who[0]) == irc_lower(who[1]):
150 conn.notice(nick, "oooooh no missus!")
152 # The bot won't slash with itself, instead slashing the requester
154 if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
156 # Perhaps someone asked to slash themselves with the bot then we get
157 if irc_lower(who[0]) == irc_lower(who[1]):
158 conn.notice(nick, "you wish!")
160 # There's a chance the game may be given away if the request was not
163 if random.random()<=selfslashchance:
164 slash_msg=slash_msg+(selfslash%nick)
166 conn.action(bot.channel, slash_msg % (who[0], who[1]))
170 def unitq(bot, cmd, nick, conn, public):
171 args = ' '.join(cmd.split()[1:]).split(' as ')
173 args = ' '.join(cmd.split()[1:]).split(' / ')
175 conn.notice(nick, "syntax: units arg1 as arg2")
178 sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
180 sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
183 #popen2 doesn't clean up the child properly. Do this by hand
185 if os.WEXITSTATUS(child[1])==0:
186 bot.automsg(public,nick,res[0].strip())
188 conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
190 # Shut up trouting for a minute
191 def nofishq(bot, cmd, nick, conn, public, fish):
195 fish.quotatime=time.time()
196 fish.quotatime+=fish.nofish_time
197 conn.notice(nick, "Fish stocks depleted, as you wish.")
200 def reloadq(bot, cmd, nick, conn, public):
201 if not public and irc_lower(nick) == irc_lower(bot.owner):
204 conn.notice(nick, "Config reloaded.")
206 conn.notice(nick, "Config reloading failed!")
208 bot.automsg(public,nick,
209 "Configuration can only be reloaded by my owner, by /msg.")
212 def quitq(bot, cmd, nick, conn, public):
213 if irc_lower(nick) == irc_lower(bot.owner):
214 bot.die(msg = "I have been chosen!")
216 conn.notice(nick, "Such aggression in public!")
218 conn.notice(nick, "You're not my owner.")
220 # google for something
221 def googleq(bot, cmd, nick, conn, public):
222 cmdrest = string.join(cmd.split()[1:])
223 # "I'm Feeling Lucky" rather than try and parse the html
224 targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
225 % urllib.quote_plus(cmdrest))
227 # get redirected and grab the resulting url for returning
228 gsearch = urllib.urlopen(targ).geturl()
229 if gsearch != targ: # we've found something
230 bot.automsg(public,nick,str(gsearch))
231 else: # we haven't found anything.
232 bot.automsg(public,nick,"No pages found.")
233 except IOError: # if the connection times out. This blocks. :(
234 bot.automsg(public,nick,"The web's broken. Waah!")
236 # Look up the definition of something using google
237 def defineq(bot, cmd, nick, conn, public):
238 #this doesn't work any more
239 bot.automsg(public,nick,"'define' is broken because google are bastards :(")
241 cmdrest = string.join(cmd.split()[1:])
242 targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
243 % urllib.quote_plus(cmdrest))
245 # Just slurp everything into a string
246 defnpage = urllib.urlopen(targ).read()
247 # For definitions we really do have to parse the HTML, sadly.
248 # This is of course going to be a bit fragile. We first look for
249 # 'Definitions of %s on the Web' -- if this isn't present we
250 # assume we have the 'no definitions found page'.
251 # The first defn starts after the following <p> tag, but as the
252 # first <li> in a <ul type="disc" class=std>
253 # Following that we assume that each definition is all the non-markup
254 # before a <br> tag. Currently we just dump out the first definition.
255 match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
257 bot.automsg(public,nick,"Some things defy definition.")
259 # We assume google has truncated the definition for us so this
260 # won't flood the channel with text...
261 defn = " ".join(match.group(1).split("\n"))
262 bot.automsg(public,nick,defn)
263 except IOError: # if the connection times out. This blocks. :(
264 bot.automsg(public,nick,"The web's broken. Waah!")
266 # Look up a currency conversion via xe.com
267 def currencyq(bot, cmd, nick, conn, public):
268 args = ' '.join(cmd.split()[1:]).split(' as ')
269 if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
270 conn.notice(nick, "syntax: currency arg1 as arg2")
272 targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
274 currencypage = urllib.urlopen(targ).read()
275 match = re.search(r"(1 %s = [\d\.]+ %s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
277 bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
279 conversion = match.group(1);
280 conversion = conversion.replace(' ',' ');
281 bot.automsg(public,nick,conversion + " (from xe.com)")
282 except IOError: # if the connection times out. This blocks. :(
283 bot.automsg(public,nick,"The web's broken. Waah!")
286 ### extract the commit message and timestamp for commit
287 def __getcommitinfo(commit):
288 cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
289 x=subprocess.Popen(cmd,
290 stdout=subprocess.PIPE,stderr=subprocess.PIPE)
291 out,err=x.communicate()
296 ts,mes=out.split('|')
298 md5mes=hashlib.md5(mes).hexdigest()
299 if bfd and md5mes in bfd:
301 when=datetime.date.fromtimestamp(float(ts))
304 ###Return an array of commit messages and timestamps for lines in db that match what
305 def __getcommits(db,keys,what):
309 ret=__getcommitinfo(db[k])
310 if len(ret)==1: #error message
311 return ["Error message from git blame: %s" % ret]
313 ans.append( (k,ret[0],ret[1]) )
316 ###search all three databases for what
317 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
320 tans=__getcommits(tdb,tdbk,what)
321 fans=__getcommits(fdb,fdbk,what)
322 sans=__getcommits(sdb,sdbk,what)
323 return tans+fans+sans
325 def blameq(bot,cmd,nick,conn,public,fishpond,cfgs):
326 tdb,tdbk,x = cfgs[0][7] # urgh, magic, to support magic knowledge below
327 fdb,fdbk,x = cfgs[1][7]
328 sdb,sdbk,x = cfgs[2][7]
331 bot.automsg(public,nick,"Who or what do you want to blame?")
333 cwhat=' '.join(clist[2:])
335 if clist[1]=="#last":
336 try: lmsg, lcfg = fishpond.last[0]
338 bot.automsg(public,nick,"Nothing")
340 xdb,xdbk,kindsfile = lcfg[7]
341 ans=__getcommits(xdb,xdbk,lmsg)
342 elif clist[1]=="#trouts" or clist[1]=="#trout":
343 ans=__getcommits(tdb,tdbk,cwhat)
344 elif clist[1]=="#flirts" or clist[1]=="#flirt":
345 ans=__getcommits(fdb,fdbk,cwhat)
346 elif clist[1]=="#slashes" or clist[1]=="#slash":
347 ans=__getcommits(sdb,sdbk,cwhat)
349 cwhat=' '.join(clist[1:])
350 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
352 bot.automsg(public,nick,"No match found")
355 bot.automsg(public,nick,ans[0])
357 bot.automsg(public,nick,"Modified %s %s: %s" % (kindsfile, ans[0][2].isoformat(),ans[0][1]))
359 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
363 bot.automsg(public,nick,a)
365 bot.automsg(public,nick,"%s '%s' modified on %s: %s" % (kindsfile, a[0],a[2].isoformat(),a[1]))
367 ### say to msg/channel
368 def sayq(bot, cmd, nick, conn, public):
369 if irc_lower(nick) == irc_lower(bot.owner):
370 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
373 conn.notice(nick, "You're not my owner!")
375 ### action to msg/channel
376 def doq(bot, cmd, nick, conn, public):
377 sys.stderr.write(irc_lower(bot.owner))
378 sys.stderr.write(irc_lower(nick))
380 if irc_lower(nick) == irc_lower(bot.owner):
381 conn.action(bot.channel, string.join(cmd.split()[1:]))
383 conn.notice(nick, "You're not my owner!")
386 def disconnq(bot, cmd, nick, conn, public):
387 if cmd == "disconnect": # hop off for 60s
388 bot.disconnect(msg="Be right back.")
390 ### list keys of a dictionary
391 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
395 bot.automsg(public,nick,string.join(d))
397 ### rot13 text (yes, I could have typed out the letters....)
398 ### also "foo".encode('rot13') would have worked
399 def rot13q(bot, cmd, nick, conn, public):
400 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
402 trans=string.maketrans(a+a.upper(),b+b.upper())
403 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
405 ### URL-tracking stuff
407 ### return a easy-to-read approximation of a time period
408 def nicetime(tempus):
410 tm="%d seconds ago"%int(tempus)
412 tm="%d minutes ago"%int(tempus/60)
414 tm="%d hours ago"%int(tempus/3600)
417 ### class to store URL data
419 "contains meta-data about a URL seen on-channel"
420 def __init__(self,url,nick):
423 self.first=time.time()
424 self.localfirst=time.localtime(self.first)
426 self.lastseen=time.time()
427 self.lastasked=time.time()
428 def recenttime(self):
429 return max(self.lastseen,self.lastasked)
431 n=time.localtime(time.time())
432 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
433 if n.tm_yday != self.localfirst.tm_yday:
434 s+=time.strftime(" on %d %B", self.localfirst)
437 z=min(len(urlinfos)-1, self.count-1)
440 #(?:) is a regexp that doesn't group
441 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
442 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
443 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
444 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
445 #How long (in s) to wait since the most recent mention before commenting
446 url_repeat_time = 300
452 ### Deal with /msg bot url or ~url in channel
453 def urlq(bot, cmd, nick, conn, public,urldb):
454 if (not urlre.search(cmd)):
455 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
458 urlstring=urlre.search(cmd).group(1)
459 url=canonical_url(urlstring)
462 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
463 (T.nick,T.firstmen())
465 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
467 bot.automsg(False,nick,comment)
468 T.lastasked=time.time()
469 #URL suppressed, so mention in #urls
470 if urlstring != cmd.split()[1]: #first argument to URL was not the url
471 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
473 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
476 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
478 if urlstring != cmd.split()[1]: #first argument to URL was not the url
479 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
481 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
482 urldb[url]=UrlLog(url,nick)
484 ### Deal with URLs spotted in channel
485 def dourl(bot,conn,nick,command,urldb):
486 urlstring=urlre.search(command).group(1)
487 urlstring=canonical_url(urlstring)
489 if urlstring in urldb:
491 message="saw that URL in scrool, first mentioned by %s at %s" % \
492 (T.nick,T.firstmen())
493 if shibboleth.search(command)==None and \
494 time.time() - T.lastseen > url_repeat_time:
495 conn.action(bot.channel, message)
496 T.lastseen=time.time()
499 urldb[urlstring]=UrlLog(urlstring,nick)
502 def urlexpire(urldb,expire):
505 if time.time() - urldb[u].recenttime() > expire:
508 # canonicalise BBC URLs (internal use only)
509 def canonical_url(urlstring):
510 if "nsfw://" in urlstring or "nsfws://" in urlstring:
511 urlstring=urlstring.replace("nsfw","http",1)
512 if (urlstring.find("news.bbc.co.uk") != -1):
513 for middle in ("/low/","/mobile/"):
514 x = urlstring.find(middle)
516 urlstring.replace(middle,"/hi/")
519 # automatically make nsfw urls for you and pass them on to url
520 def nsfwq(bot,cmd,nick,conn,public,urldb):
521 if (not hturlre.search(cmd)):
522 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
524 newcmd=hturlre.sub(nsfwify,cmd)
525 urlq(bot,newcmd,nick,conn,public,urldb)
532 def twitterq(bot,cmd,nick,conn,public,twitapi):
534 if (not urlre.search(cmd)):
535 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
538 urlstring = urlre.search(cmd).group(1)
539 if (urlstring.find("twitter.com") !=-1):
540 stringsout = getTweet(urlstring,twitapi)
541 for stringout in stringsout:
542 bot.automsg(public, nick, stringout)
544 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
545 unobfuscate_urls=True
546 expand_included_tweets=True
549 path = urlparse.urlparse(urlstring).path
550 tweetID = path.split('/')[-1]
552 status = twitapi.GetStatus(tweetID)
554 return "twitapi.GetStatus returned nothing :-("
555 if status.user == None and status.text == None:
556 return "Empty status object returned :("
557 if status.retweeted_status and status.retweeted_status.text:
558 status = status.retweeted_status
559 if status.user is not None:
560 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
561 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
563 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
564 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
565 tweetText = status.full_text
567 replacements = defaultdict( list )
568 for medium in status.media:
569 replacements[medium.url].append(medium.media_url_https)
571 for k,v in replacements.items():
573 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
575 replacementstring = "[" + " ; ".join(v) +"]"
577 replacementstring = v[0]
578 tweetText = tweetText.replace(k, replacementstring)
580 for url in status.urls:
581 toReplace = url.expanded_url
585 rv = urlparse.urlparse(toReplace)
587 # sourced from http://bit.do/list-of-url-shorteners.php
588 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
589 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
590 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
591 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
592 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
593 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
594 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
595 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
596 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
600 #expand list as needed.
601 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
602 resptext = response.read()
603 if resptext.startswith('http'): # ie it looks urlish (http or https)
604 if resptext != toReplace:
606 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
608 # remove tracking utm_ query parameters, for privacy and brevity
609 # code snippet from https://gist.github.com/lepture/5997883
610 rv = urlparse.urlparse(toReplace)
612 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
614 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
616 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
618 if expand_included_tweets:
619 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
621 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
623 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
626 quotedtweet[0] = "Q{ " + quotedtweet[0]
627 quotedtweet[-1] += " }"
628 stringsout = quotedtweet + stringsout
630 tweetText = tweetText.replace(url.url, toReplace)
632 tweetText = tweetText.replace(">",">")
633 tweetText = tweetText.replace("<","<")
634 tweetText = tweetText.replace("&","&")
635 tweetText = tweetText.replace("\n"," ")
636 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
637 except twitter.TwitterError:
638 terror = sys.exc_info()
639 stringout = "Twitter error: %s" % terror[1].__str__()
641 terror = sys.exc_info()
642 stringout = "Error: %s" % terror[1].__str__()
643 stringsout = [stringout] + stringsout
645 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
647 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)