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":
337 n = abs(int(clist[2]))-1
338 if n < 0: raise ValueError
339 except IndexError: n = 0
341 bot.automsg(public,nick,"Huh?")
343 try: lmsg, lcfg = fishpond.last[n]
345 bot.automsg(public,nick,"Nothing")
347 xdb,xdbk,kindsfile = lcfg[7]
348 ans=__getcommits(xdb,xdbk,lmsg)
349 elif clist[1]=="#trouts" or clist[1]=="#trout":
350 ans=__getcommits(tdb,tdbk,cwhat)
351 elif clist[1]=="#flirts" or clist[1]=="#flirt":
352 ans=__getcommits(fdb,fdbk,cwhat)
353 elif clist[1]=="#slashes" or clist[1]=="#slash":
354 ans=__getcommits(sdb,sdbk,cwhat)
356 cwhat=' '.join(clist[1:])
357 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
359 bot.automsg(public,nick,"No match found")
362 bot.automsg(public,nick,ans[0])
364 bot.automsg(public,nick,"Modified %s %s: %s" % (kindsfile, ans[0][2].isoformat(),ans[0][1]))
366 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
370 bot.automsg(public,nick,a)
372 bot.automsg(public,nick,"%s '%s' modified on %s: %s" % (kindsfile, a[0],a[2].isoformat(),a[1]))
374 ### say to msg/channel
375 def sayq(bot, cmd, nick, conn, public):
376 if irc_lower(nick) == irc_lower(bot.owner):
377 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
380 conn.notice(nick, "You're not my owner!")
382 ### action to msg/channel
383 def doq(bot, cmd, nick, conn, public):
384 sys.stderr.write(irc_lower(bot.owner))
385 sys.stderr.write(irc_lower(nick))
387 if irc_lower(nick) == irc_lower(bot.owner):
388 conn.action(bot.channel, string.join(cmd.split()[1:]))
390 conn.notice(nick, "You're not my owner!")
393 def disconnq(bot, cmd, nick, conn, public):
394 if cmd == "disconnect": # hop off for 60s
395 bot.disconnect(msg="Be right back.")
397 ### list keys of a dictionary
398 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
402 bot.automsg(public,nick,string.join(d))
404 ### rot13 text (yes, I could have typed out the letters....)
405 ### also "foo".encode('rot13') would have worked
406 def rot13q(bot, cmd, nick, conn, public):
407 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
409 trans=string.maketrans(a+a.upper(),b+b.upper())
410 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
412 ### URL-tracking stuff
414 ### return a easy-to-read approximation of a time period
415 def nicetime(tempus):
417 tm="%d seconds ago"%int(tempus)
419 tm="%d minutes ago"%int(tempus/60)
421 tm="%d hours ago"%int(tempus/3600)
424 ### class to store URL data
426 "contains meta-data about a URL seen on-channel"
427 def __init__(self,url,nick):
430 self.first=time.time()
431 self.localfirst=time.localtime(self.first)
433 self.lastseen=time.time()
434 self.lastasked=time.time()
435 def recenttime(self):
436 return max(self.lastseen,self.lastasked)
438 n=time.localtime(time.time())
439 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
440 if n.tm_yday != self.localfirst.tm_yday:
441 s+=time.strftime(" on %d %B", self.localfirst)
444 z=min(len(urlinfos)-1, self.count-1)
447 #(?:) is a regexp that doesn't group
448 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
449 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
450 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
451 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
452 #How long (in s) to wait since the most recent mention before commenting
453 url_repeat_time = 300
459 ### Deal with /msg bot url or ~url in channel
460 def urlq(bot, cmd, nick, conn, public,urldb):
461 if (not urlre.search(cmd)):
462 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
465 urlstring=urlre.search(cmd).group(1)
466 url=canonical_url(urlstring)
469 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
470 (T.nick,T.firstmen())
472 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
474 bot.automsg(False,nick,comment)
475 T.lastasked=time.time()
476 #URL suppressed, so mention in #urls
477 if urlstring != cmd.split()[1]: #first argument to URL was not the url
478 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
480 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
483 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
485 if urlstring != cmd.split()[1]: #first argument to URL was not the url
486 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
488 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
489 urldb[url]=UrlLog(url,nick)
491 ### Deal with URLs spotted in channel
492 def dourl(bot,conn,nick,command,urldb):
493 urlstring=urlre.search(command).group(1)
494 urlstring=canonical_url(urlstring)
496 if urlstring in urldb:
498 message="saw that URL in scrool, first mentioned by %s at %s" % \
499 (T.nick,T.firstmen())
500 if shibboleth.search(command)==None and \
501 time.time() - T.lastseen > url_repeat_time:
502 conn.action(bot.channel, message)
503 T.lastseen=time.time()
506 urldb[urlstring]=UrlLog(urlstring,nick)
509 def urlexpire(urldb,expire):
512 if time.time() - urldb[u].recenttime() > expire:
515 # canonicalise BBC URLs (internal use only)
516 def canonical_url(urlstring):
517 if "nsfw://" in urlstring or "nsfws://" in urlstring:
518 urlstring=urlstring.replace("nsfw","http",1)
519 if (urlstring.find("news.bbc.co.uk") != -1):
520 for middle in ("/low/","/mobile/"):
521 x = urlstring.find(middle)
523 urlstring.replace(middle,"/hi/")
526 # automatically make nsfw urls for you and pass them on to url
527 def nsfwq(bot,cmd,nick,conn,public,urldb):
528 if (not hturlre.search(cmd)):
529 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
531 newcmd=hturlre.sub(nsfwify,cmd)
532 urlq(bot,newcmd,nick,conn,public,urldb)
539 def twitterq(bot,cmd,nick,conn,public,twitapi):
541 if (not urlre.search(cmd)):
542 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
545 urlstring = urlre.search(cmd).group(1)
546 if (urlstring.find("twitter.com") !=-1):
547 stringsout = getTweet(urlstring,twitapi)
548 for stringout in stringsout:
549 bot.automsg(public, nick, stringout)
551 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
552 unobfuscate_urls=True
553 expand_included_tweets=True
556 path = urlparse.urlparse(urlstring).path
557 tweetID = path.split('/')[-1]
559 status = twitapi.GetStatus(tweetID)
561 return "twitapi.GetStatus returned nothing :-("
562 if status.user == None and status.text == None:
563 return "Empty status object returned :("
564 if status.retweeted_status and status.retweeted_status.text:
565 status = status.retweeted_status
566 if status.user is not None:
567 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
568 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
570 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
571 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
572 tweetText = status.full_text
574 replacements = defaultdict(list)
575 for medium in status.media:
576 replacements[medium.url].append(medium.media_url_https)
578 for k,v in replacements.items():
579 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
581 replacementstring = "[" + " ; ".join(v) +"]"
583 replacementstring = v[0]
584 tweetText = tweetText.replace(k, replacementstring)
586 for url in status.urls:
587 toReplace = url.expanded_url
591 rv = urlparse.urlparse(toReplace)
593 # sourced from http://bit.do/list-of-url-shorteners.php
594 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
595 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
596 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
597 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
598 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
599 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
600 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
601 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
602 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
606 #expand list as needed.
607 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
608 resptext = response.read()
609 if resptext.startswith('http'): # ie it looks urlish (http or https)
610 if resptext != toReplace:
612 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
614 # remove tracking utm_ query parameters, for privacy and brevity
615 # code snippet from https://gist.github.com/lepture/5997883
616 rv = urlparse.urlparse(toReplace)
618 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
620 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
622 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
624 if expand_included_tweets:
625 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
627 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
629 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
632 quotedtweet[0] = "Q{ " + quotedtweet[0]
633 quotedtweet[-1] += " }"
634 stringsout = quotedtweet + stringsout
636 tweetText = tweetText.replace(url.url, toReplace)
638 tweetText = tweetText.replace(">",">")
639 tweetText = tweetText.replace("<","<")
640 tweetText = tweetText.replace("&","&")
641 tweetText = tweetText.replace("\n"," ")
642 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
643 except twitter.TwitterError:
644 terror = sys.exc_info()
645 stringout = "Twitter error: %s" % terror[1].__str__()
647 terror = sys.exc_info()
648 stringout = "Error: %s" % terror[1].__str__()
649 stringsout = [stringout] + stringsout
651 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
653 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)