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 def note_last(fishpond, msg, cfg):
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,fish,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":
337 if fish.last_cfg is None:
338 bot.automsg(public,nick,"Nothing")
340 xdb,xdbk,kindsfile = fish.last_cfg[7]
341 ans=__getcommits(xdb,xdbk,fish.last)
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)