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
8 from blame_filter import bfd
13 def karmaq(bot, cmd, nick, conn, public, karma):
15 item=cmd.split()[1].lower()
19 bot.automsg(public,nick,"I have karma on %s items." %
21 elif karma.has_key(item):
22 bot.automsg(public,nick,"%s has karma %s."
25 bot.automsg(public,nick, "%s has no karma set." % item)
28 def karmadelq(bot, cmd, nick, conn, public, karma):
30 item=cmd.split()[1].lower()
32 conn.notice(nick, "What should I delete?")
35 conn.notice(nick, "You are not my owner.")
37 if karma.has_key(item):
39 conn.notice(nick, "Item %s deleted."%item)
41 conn.notice(nick, "There is no karma stored for %s."%item)
43 # help - provides the URL of the help file
44 def helpq(bot, cmd, nick, conn, public):
45 bot.automsg(public,nick,
46 "For help see http://www.chiark.greenend.org.uk/~matthewv/irc/servus.html")
50 def infoq(bot, cmd, nick, conn, public, karma):
51 bot.automsg(public,nick,
52 ("I am Acrobat %s, on %s, as nick %s. "+
53 "My owner is %s; I have karma on %s items.") %
54 (bot.revision.split()[1], bot.channel, conn.get_nickname(),
55 bot.owner, len(karma.keys())))
58 def __init__(fishpond):
63 def note_last(fishpond, msg, cfg):
64 fishpond.last.insert(0,(msg,cfg))
65 fishpond.last = fishpond.last[0:10]
67 # Check on fish stocks
70 if time.time()>=pond.quotatime:
74 if (time.time()-pond.quotatime)>pond.fish_time_inc:
75 pond.cur_fish+=(((time.time()-pond.quotatime)
76 /pond.fish_time_inc)*pond.fish_inc)
77 if pond.cur_fish>pond.max_fish:
78 pond.cur_fish=pond.max_fish
79 pond.quotatime=time.time()
81 # List of things the bot might be called to work round the self-trouting code
82 synonyms=["itself","the bot","themself"]
84 # trout someone, or flirt with them
85 def troutq(bot, cmd, nick, conn, public, cfg):
92 selftroutchance=cfg[6]
96 conn.notice(nick, quietmsg%fishpond.Boring_Git)
98 if fishpond.cur_fish<=0:
99 conn.notice(nick, nofishmsg)
101 target = string.join(cmd.split()[1:])
103 conn.notice(nick, notargetmsg)
105 me = bot.connection.get_nickname()
106 trout_msg = random.choice(fishlist)
107 fishpond.note_last(trout_msg,cfg)
108 # The bot won't trout or flirt with itself;
109 if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
111 # There's a chance the game may be given away if the request was not
114 if random.random()<=selftroutchance:
115 trout_msg=trout_msg+(selftrout%nick)
117 conn.action(bot.channel, trout_msg % target)
121 def slashq(bot, cmd, nick, conn, public, cfg):
128 selfslashchance=cfg[6]
132 conn.notice(nick, quietmsg%fishpond.Boring_Git)
134 if fishpond.cur_fish<=0:
135 conn.notice(nick, nofishmsg)
137 target = string.join(cmd.split()[1:])
138 #who = cmd.split()[1:]
139 who = ' '.join(cmd.split()[1:]).split(' / ')
141 conn.notice(nick, "it takes two to tango!")
144 conn.notice(nick, "we'll have none of that round here")
146 me = bot.connection.get_nickname()
147 slash_msg = random.choice(fishlist)
148 fishpond.note_last(slash_msg,cfg)
149 # The bot won't slash people with themselves
150 if irc_lower(who[0]) == irc_lower(who[1]):
151 conn.notice(nick, "oooooh no missus!")
153 # The bot won't slash with itself, instead slashing the requester
155 if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
157 # Perhaps someone asked to slash themselves with the bot then we get
158 if irc_lower(who[0]) == irc_lower(who[1]):
159 conn.notice(nick, "you wish!")
161 # There's a chance the game may be given away if the request was not
164 if random.random()<=selfslashchance:
165 slash_msg=slash_msg+(selfslash%nick)
167 conn.action(bot.channel, slash_msg % (who[0], who[1]))
171 def unitq(bot, cmd, nick, conn, public):
172 args = ' '.join(cmd.split()[1:]).split(' as ')
174 args = ' '.join(cmd.split()[1:]).split(' / ')
176 conn.notice(nick, "syntax: units arg1 as arg2")
179 sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
181 sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
184 #popen2 doesn't clean up the child properly. Do this by hand
186 if os.WEXITSTATUS(child[1])==0:
187 bot.automsg(public,nick,res[0].strip())
189 conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
191 # Shut up trouting for a minute
192 def nofishq(bot, cmd, nick, conn, public, fish):
196 fish.quotatime=time.time()
197 fish.quotatime+=fish.nofish_time
198 conn.notice(nick, "Fish stocks depleted, as you wish.")
201 def reloadq(bot, cmd, nick, conn, public):
202 if not public and irc_lower(nick) == irc_lower(bot.owner):
205 conn.notice(nick, "Config reloaded.")
207 conn.notice(nick, "Config reloading failed!")
209 bot.automsg(public,nick,
210 "Configuration can only be reloaded by my owner, by /msg.")
213 def quitq(bot, cmd, nick, conn, public):
214 if irc_lower(nick) == irc_lower(bot.owner):
215 bot.die(msg = "I have been chosen!")
217 conn.notice(nick, "Such aggression in public!")
219 conn.notice(nick, "You're not my owner.")
221 # google for something
222 def googleq(bot, cmd, nick, conn, public):
223 cmdrest = string.join(cmd.split()[1:])
224 # "I'm Feeling Lucky" rather than try and parse the html
225 targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
226 % urllib.quote_plus(cmdrest))
228 # get redirected and grab the resulting url for returning
229 gsearch = urllib.urlopen(targ).geturl()
230 if gsearch != targ: # we've found something
231 bot.automsg(public,nick,str(gsearch))
232 else: # we haven't found anything.
233 bot.automsg(public,nick,"No pages found.")
234 except IOError: # if the connection times out. This blocks. :(
235 bot.automsg(public,nick,"The web's broken. Waah!")
237 # Look up the definition of something using google
238 def defineq(bot, cmd, nick, conn, public):
239 #this doesn't work any more
240 bot.automsg(public,nick,"'define' is broken because google are bastards :(")
242 cmdrest = string.join(cmd.split()[1:])
243 targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
244 % urllib.quote_plus(cmdrest))
246 # Just slurp everything into a string
247 defnpage = urllib.urlopen(targ).read()
248 # For definitions we really do have to parse the HTML, sadly.
249 # This is of course going to be a bit fragile. We first look for
250 # 'Definitions of %s on the Web' -- if this isn't present we
251 # assume we have the 'no definitions found page'.
252 # The first defn starts after the following <p> tag, but as the
253 # first <li> in a <ul type="disc" class=std>
254 # Following that we assume that each definition is all the non-markup
255 # before a <br> tag. Currently we just dump out the first definition.
256 match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
258 bot.automsg(public,nick,"Some things defy definition.")
260 # We assume google has truncated the definition for us so this
261 # won't flood the channel with text...
262 defn = " ".join(match.group(1).split("\n"))
263 bot.automsg(public,nick,defn)
264 except IOError: # if the connection times out. This blocks. :(
265 bot.automsg(public,nick,"The web's broken. Waah!")
267 # Look up a currency conversion via xe.com
268 def currencyq(bot, cmd, nick, conn, public):
269 args = ' '.join(cmd.split()[1:]).split(' as ')
270 if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
271 conn.notice(nick, "syntax: currency arg1 as arg2")
273 targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
275 currencypage = urllib.urlopen(targ).read()
276 match = re.search(r"(1 %s = [\d\.]+ %s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
278 bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
280 conversion = match.group(1);
281 conversion = conversion.replace(' ',' ');
282 bot.automsg(public,nick,conversion + " (from xe.com)")
283 except IOError: # if the connection times out. This blocks. :(
284 bot.automsg(public,nick,"The web's broken. Waah!")
287 ### extract the commit message and timestamp for commit
288 def __getcommitinfo(commit):
289 cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
290 x=subprocess.Popen(cmd,
291 stdout=subprocess.PIPE,stderr=subprocess.PIPE)
292 out,err=x.communicate()
297 ts,mes=out.split('|')
299 md5mes=hashlib.md5(mes).hexdigest()
300 if bfd and md5mes in bfd:
302 when=datetime.date.fromtimestamp(float(ts))
305 ###Return an array of commit messages and timestamps for lines in db that match what
306 def __getcommits(db,keys,what):
310 ret=__getcommitinfo(db[k])
311 if len(ret)==1: #error message
312 return ["Error message from git blame: %s" % ret]
314 ans.append( (k,ret[0],ret[1]) )
317 ###search all three databases for what
318 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
321 tans=__getcommits(tdb,tdbk,what)
322 fans=__getcommits(fdb,fdbk,what)
323 sans=__getcommits(sdb,sdbk,what)
324 return tans+fans+sans
326 def blameq(bot,cmd,nick,conn,public,fishpond,cfgs):
327 tdb,tdbk,x = cfgs[0][7] # urgh, magic, to support magic knowledge below
328 fdb,fdbk,x = cfgs[1][7]
329 sdb,sdbk,x = cfgs[2][7]
332 bot.automsg(public,nick,"Who or what do you want to blame?")
334 cwhat=' '.join(clist[2:])
336 if clist[1]=="#last":
338 n = abs(int(clist[2]))-1
339 if n < 0: raise ValueError
340 except IndexError: n = 0
342 bot.automsg(public,nick,"Huh?")
344 try: lmsg, lcfg = fishpond.last[n]
346 bot.automsg(public,nick,"Nothing")
348 xdb,xdbk,kindsfile = lcfg[7]
349 ans=__getcommits(xdb,xdbk,lmsg)
350 elif clist[1]=="#trouts" or clist[1]=="#trout":
351 ans=__getcommits(tdb,tdbk,cwhat)
352 elif clist[1]=="#flirts" or clist[1]=="#flirt":
353 ans=__getcommits(fdb,fdbk,cwhat)
354 elif clist[1]=="#slashes" or clist[1]=="#slash":
355 ans=__getcommits(sdb,sdbk,cwhat)
357 cwhat=' '.join(clist[1:])
358 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
360 bot.automsg(public,nick,"No match found")
363 bot.automsg(public,nick,ans[0])
365 bot.automsg(public,nick,"Modified %s %s: %s" % (kindsfile, ans[0][2].isoformat(),ans[0][1]))
367 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
371 bot.automsg(public,nick,a)
373 bot.automsg(public,nick,"%s '%s' modified on %s: %s" % (kindsfile, a[0],a[2].isoformat(),a[1]))
375 ### say to msg/channel
376 def sayq(bot, cmd, nick, conn, public):
377 if irc_lower(nick) == irc_lower(bot.owner):
378 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
381 conn.notice(nick, "You're not my owner!")
383 ### action to msg/channel
384 def doq(bot, cmd, nick, conn, public):
385 sys.stderr.write(irc_lower(bot.owner))
386 sys.stderr.write(irc_lower(nick))
388 if irc_lower(nick) == irc_lower(bot.owner):
389 conn.action(bot.channel, string.join(cmd.split()[1:]))
391 conn.notice(nick, "You're not my owner!")
394 def disconnq(bot, cmd, nick, conn, public):
395 if cmd == "disconnect": # hop off for 60s
396 bot.disconnect(msg="Be right back.")
398 ### list keys of a dictionary
399 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
403 bot.automsg(public,nick,string.join(d))
405 ### rot13 text (yes, I could have typed out the letters....)
406 ### also "foo".encode('rot13') would have worked
407 def rot13q(bot, cmd, nick, conn, public):
408 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
410 trans=string.maketrans(a+a.upper(),b+b.upper())
411 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
413 ### URL-tracking stuff
415 ### return a easy-to-read approximation of a time period
416 def nicetime(tempus):
418 tm="%d seconds ago"%int(tempus)
420 tm="%d minutes ago"%int(tempus/60)
422 tm="%d hours ago"%int(tempus/3600)
425 ### class to store URL data
427 "contains meta-data about a URL seen on-channel"
428 def __init__(self,url,nick):
431 self.first=time.time()
432 self.localfirst=time.localtime(self.first)
434 self.lastseen=time.time()
435 self.lastasked=time.time()
436 def recenttime(self):
437 return max(self.lastseen,self.lastasked)
439 n=time.localtime(time.time())
440 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
441 if n.tm_yday != self.localfirst.tm_yday:
442 s+=time.strftime(" on %d %B", self.localfirst)
445 z=min(len(urlinfos)-1, self.count-1)
448 #(?:) is a regexp that doesn't group
449 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
450 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
451 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
452 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
453 #How long (in s) to wait since the most recent mention before commenting
454 url_repeat_time = 300
460 ### Deal with /msg bot url or ~url in channel
461 def urlq(bot, cmd, nick, conn, public,urldb):
462 if (not urlre.search(cmd)):
463 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
466 urlstring=urlre.search(cmd).group(1)
467 url=canonical_url(urlstring)
470 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
471 (T.nick,T.firstmen())
473 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
475 bot.automsg(False,nick,comment)
476 T.lastasked=time.time()
477 #URL suppressed, so mention in #urls
478 if urlstring != cmd.split()[1]: #first argument to URL was not the url
479 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
481 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
484 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
486 if urlstring != cmd.split()[1]: #first argument to URL was not the url
487 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
489 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
490 urldb[url]=UrlLog(url,nick)
492 ### Deal with URLs spotted in channel
493 def dourl(bot,conn,nick,command,urldb):
494 urlstring=urlre.search(command).group(1)
495 urlstring=canonical_url(urlstring)
497 if urlstring in urldb:
499 message="saw that URL in scrool, first mentioned by %s at %s" % \
500 (T.nick,T.firstmen())
501 if shibboleth.search(command)==None and \
502 time.time() - T.lastseen > url_repeat_time:
503 conn.action(bot.channel, message)
504 T.lastseen=time.time()
507 urldb[urlstring]=UrlLog(urlstring,nick)
510 def urlexpire(urldb,expire):
513 if time.time() - urldb[u].recenttime() > expire:
516 # canonicalise BBC URLs (internal use only)
517 def canonical_url(urlstring):
518 if "nsfw://" in urlstring or "nsfws://" in urlstring:
519 urlstring=urlstring.replace("nsfw","http",1)
520 if (urlstring.find("news.bbc.co.uk") != -1):
521 for middle in ("/low/","/mobile/"):
522 x = urlstring.find(middle)
524 urlstring.replace(middle,"/hi/")
527 # automatically make nsfw urls for you and pass them on to url
528 def nsfwq(bot,cmd,nick,conn,public,urldb):
529 if (not hturlre.search(cmd)):
530 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
532 newcmd=hturlre.sub(nsfwify,cmd)
533 urlq(bot,newcmd,nick,conn,public,urldb)
540 def twitterq(bot,cmd,nick,conn,public,twitapi):
542 if (not urlre.search(cmd)):
543 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
546 urlstring = urlre.search(cmd).group(1)
547 if (urlstring.find("twitter.com") !=-1):
548 stringsout = getTweet(urlstring,twitapi)
549 for stringout in stringsout:
550 bot.automsg(public, nick, stringout)
552 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
553 unobfuscate_urls=True
554 expand_included_tweets=True
557 path = urlparse.urlparse(urlstring).path
558 tweetID = path.split('/')[-1]
560 status = twitapi.GetStatus(tweetID)
562 return "twitapi.GetStatus returned nothing :-("
563 if status.user == None and status.text == None:
564 return "Empty status object returned :("
565 if status.retweeted_status and status.retweeted_status.text:
566 status = status.retweeted_status
567 if status.user is not None:
568 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
569 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
571 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
572 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
573 tweetText = status.full_text
575 replacements = defaultdict(list)
577 for medium in status.media:
578 replacements[medium.url].append(medium.media_url_https)
580 # The twitter-api 'conveniently' parses this for you and
581 # throws away the actual video URLs, so we have to take the
582 # JSON and reparse it :sadpanda:
583 # This is particularly annoying because we don't know
584 # for sure that status.media and the JSON 'media' entry
585 # have the same elements in the same order. Probably they
586 # do but maybe twitter-api randomly reorganised things or
587 # filtered the list or something. So instead we go through
588 # the JSON and handle the media urls, discarding whatever
589 # unfortunate thing we have put in replacements already.
590 parsed_tweet = json.loads(status.AsJsonString())
591 for medium in parsed_tweet.get('media', []):
592 if medium['type'] == 'video':
593 best = { 'bitrate': -1 }
594 for vt in medium['video_info']['variants']:
595 if (vt.get('content_type') == 'video/mp4' and
596 vt.get('bitrate', -1) > best['bitrate']):
599 video_url = best['url'].split('?',1)[0]
600 duration = medium['video_info']['duration_millis']
601 # ^ duration_millis is a string
602 duration = "%.1f" % (float(duration)/1000.)
603 video_desc = "%s (%ss)" % (video_url, duration)
604 replacements[medium['url']] = [video_desc]
606 for k,v in replacements.items():
608 replacementstring = "[" + " ; ".join(v) +"]"
610 replacementstring = v[0]
611 tweetText = tweetText.replace(k, replacementstring)
613 for url in status.urls:
614 toReplace = url.expanded_url
618 rv = urlparse.urlparse(toReplace)
620 # sourced from http://bit.do/list-of-url-shorteners.php
621 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
622 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
623 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
624 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
625 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
626 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
627 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
628 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
629 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
633 #expand list as needed.
634 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
635 resptext = response.read()
636 if resptext.startswith('http'): # ie it looks urlish (http or https)
637 if resptext != toReplace:
639 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
641 # remove tracking utm_ query parameters, for privacy and brevity
642 # code snippet from https://gist.github.com/lepture/5997883
643 rv = urlparse.urlparse(toReplace)
645 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
647 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
649 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
651 if expand_included_tweets:
652 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
654 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
656 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
659 quotedtweet[0] = "Q{ " + quotedtweet[0]
660 quotedtweet[-1] += " }"
661 stringsout = quotedtweet + stringsout
663 tweetText = tweetText.replace(url.url, toReplace)
665 tweetText = tweetText.replace(">",">")
666 tweetText = tweetText.replace("<","<")
667 tweetText = tweetText.replace("&","&")
668 tweetText = tweetText.replace("\n"," ")
669 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
670 except twitter.TwitterError:
671 terror = sys.exc_info()
672 stringout = "Twitter error: %s" % terror[1].__str__()
674 terror = sys.exc_info()
675 stringout = "Error: %s" % terror[1].__str__()
676 stringsout = [stringout] + stringsout
678 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
680 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)