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())))
62 # Check on fish stocks
65 if time.time()>=pond.quotatime:
69 if (time.time()-pond.quotatime)>pond.fish_time_inc:
70 pond.cur_fish+=(((time.time()-pond.quotatime)
71 /pond.fish_time_inc)*pond.fish_inc)
72 if pond.cur_fish>pond.max_fish:
73 pond.cur_fish=pond.max_fish
74 pond.quotatime=time.time()
76 # List of things the bot might be called to work round the self-trouting code
77 synonyms=["itself","the bot","themself"]
79 # trout someone, or flirt with them
80 def troutq(bot, cmd, nick, conn, public, cfg):
87 selftroutchance=cfg[6]
91 conn.notice(nick, quietmsg%fishpond.Boring_Git)
93 if fishpond.cur_fish<=0:
94 conn.notice(nick, nofishmsg)
96 target = string.join(cmd.split()[1:])
98 conn.notice(nick, notargetmsg)
100 me = bot.connection.get_nickname()
101 trout_msg = random.choice(fishlist)
102 fishpond.last=trout_msg
103 fishpond.last_cfg=cfg
104 # The bot won't trout or flirt with itself;
105 if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
107 # There's a chance the game may be given away if the request was not
110 if random.random()<=selftroutchance:
111 trout_msg=trout_msg+(selftrout%nick)
113 conn.action(bot.channel, trout_msg % target)
117 def slashq(bot, cmd, nick, conn, public, cfg):
124 selfslashchance=cfg[6]
128 conn.notice(nick, quietmsg%fishpond.Boring_Git)
130 if fishpond.cur_fish<=0:
131 conn.notice(nick, nofishmsg)
133 target = string.join(cmd.split()[1:])
134 #who = cmd.split()[1:]
135 who = ' '.join(cmd.split()[1:]).split(' / ')
137 conn.notice(nick, "it takes two to tango!")
140 conn.notice(nick, "we'll have none of that round here")
142 me = bot.connection.get_nickname()
143 slash_msg = random.choice(fishlist)
144 fishpond.last=slash_msg
145 fishpond.last_cfg=cfg
146 # The bot won't slash people with themselves
147 if irc_lower(who[0]) == irc_lower(who[1]):
148 conn.notice(nick, "oooooh no missus!")
150 # The bot won't slash with itself, instead slashing the requester
152 if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
154 # Perhaps someone asked to slash themselves with the bot then we get
155 if irc_lower(who[0]) == irc_lower(who[1]):
156 conn.notice(nick, "you wish!")
158 # There's a chance the game may be given away if the request was not
161 if random.random()<=selfslashchance:
162 slash_msg=slash_msg+(selfslash%nick)
164 conn.action(bot.channel, slash_msg % (who[0], who[1]))
168 def unitq(bot, cmd, nick, conn, public):
169 args = ' '.join(cmd.split()[1:]).split(' as ')
171 args = ' '.join(cmd.split()[1:]).split(' / ')
173 conn.notice(nick, "syntax: units arg1 as arg2")
176 sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
178 sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
181 #popen2 doesn't clean up the child properly. Do this by hand
183 if os.WEXITSTATUS(child[1])==0:
184 bot.automsg(public,nick,res[0].strip())
186 conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
188 # Shut up trouting for a minute
189 def nofishq(bot, cmd, nick, conn, public, fish):
193 fish.quotatime=time.time()
194 fish.quotatime+=fish.nofish_time
195 conn.notice(nick, "Fish stocks depleted, as you wish.")
198 def reloadq(bot, cmd, nick, conn, public):
199 if not public and irc_lower(nick) == irc_lower(bot.owner):
202 conn.notice(nick, "Config reloaded.")
204 conn.notice(nick, "Config reloading failed!")
206 bot.automsg(public,nick,
207 "Configuration can only be reloaded by my owner, by /msg.")
210 def quitq(bot, cmd, nick, conn, public):
211 if irc_lower(nick) == irc_lower(bot.owner):
212 bot.die(msg = "I have been chosen!")
214 conn.notice(nick, "Such aggression in public!")
216 conn.notice(nick, "You're not my owner.")
218 # google for something
219 def googleq(bot, cmd, nick, conn, public):
220 cmdrest = string.join(cmd.split()[1:])
221 # "I'm Feeling Lucky" rather than try and parse the html
222 targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
223 % urllib.quote_plus(cmdrest))
225 # get redirected and grab the resulting url for returning
226 gsearch = urllib.urlopen(targ).geturl()
227 if gsearch != targ: # we've found something
228 bot.automsg(public,nick,str(gsearch))
229 else: # we haven't found anything.
230 bot.automsg(public,nick,"No pages found.")
231 except IOError: # if the connection times out. This blocks. :(
232 bot.automsg(public,nick,"The web's broken. Waah!")
234 # Look up the definition of something using google
235 def defineq(bot, cmd, nick, conn, public):
236 #this doesn't work any more
237 bot.automsg(public,nick,"'define' is broken because google are bastards :(")
239 cmdrest = string.join(cmd.split()[1:])
240 targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
241 % urllib.quote_plus(cmdrest))
243 # Just slurp everything into a string
244 defnpage = urllib.urlopen(targ).read()
245 # For definitions we really do have to parse the HTML, sadly.
246 # This is of course going to be a bit fragile. We first look for
247 # 'Definitions of %s on the Web' -- if this isn't present we
248 # assume we have the 'no definitions found page'.
249 # The first defn starts after the following <p> tag, but as the
250 # first <li> in a <ul type="disc" class=std>
251 # Following that we assume that each definition is all the non-markup
252 # before a <br> tag. Currently we just dump out the first definition.
253 match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
255 bot.automsg(public,nick,"Some things defy definition.")
257 # We assume google has truncated the definition for us so this
258 # won't flood the channel with text...
259 defn = " ".join(match.group(1).split("\n"))
260 bot.automsg(public,nick,defn)
261 except IOError: # if the connection times out. This blocks. :(
262 bot.automsg(public,nick,"The web's broken. Waah!")
264 # Look up a currency conversion via xe.com
265 def currencyq(bot, cmd, nick, conn, public):
266 args = ' '.join(cmd.split()[1:]).split(' as ')
267 if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
268 conn.notice(nick, "syntax: currency arg1 as arg2")
270 targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
272 currencypage = urllib.urlopen(targ).read()
273 match = re.search(r"(1 %s = [\d\.]+ %s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
275 bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
277 conversion = match.group(1);
278 conversion = conversion.replace(' ',' ');
279 bot.automsg(public,nick,conversion + " (from xe.com)")
280 except IOError: # if the connection times out. This blocks. :(
281 bot.automsg(public,nick,"The web's broken. Waah!")
284 ### extract the commit message and timestamp for commit
285 def __getcommitinfo(commit):
286 cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
287 x=subprocess.Popen(cmd,
288 stdout=subprocess.PIPE,stderr=subprocess.PIPE)
289 out,err=x.communicate()
294 ts,mes=out.split('|')
296 md5mes=hashlib.md5(mes).hexdigest()
297 if bfd and md5mes in bfd:
299 when=datetime.date.fromtimestamp(float(ts))
302 ###Return an array of commit messages and timestamps for lines in db that match what
303 def __getcommits(db,keys,what):
307 ret=__getcommitinfo(db[k])
308 if len(ret)==1: #error message
309 return ["Error message from git blame: %s" % ret]
311 ans.append( (k,ret[0],ret[1]) )
314 ###search all three databases for what
315 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
318 tans=__getcommits(tdb,tdbk,what)
319 fans=__getcommits(fdb,fdbk,what)
320 sans=__getcommits(sdb,sdbk,what)
321 return tans+fans+sans
323 def blameq(bot,cmd,nick,conn,public,fish,cfgs):
324 tdb,tdbk,x = cfgs[0][7] # urgh, magic, to support magic knowledge below
325 fdb,fdbk,x = cfgs[1][7]
326 sdb,sdbk,x = cfgs[2][7]
329 bot.automsg(public,nick,"Who or what do you want to blame?")
331 cwhat=' '.join(clist[2:])
333 if clist[1]=="#last":
334 if fish.last_cfg is None:
335 bot.automsg(public,nick,"Nothing")
337 xdb,xdbk,kindsfile = fish.last_cfg[7]
338 ans=__getcommits(xdb,xdbk,fish.last)
339 elif clist[1]=="#trouts" or clist[1]=="#trout":
340 ans=__getcommits(tdb,tdbk,cwhat)
341 elif clist[1]=="#flirts" or clist[1]=="#flirt":
342 ans=__getcommits(fdb,fdbk,cwhat)
343 elif clist[1]=="#slashes" or clist[1]=="#slash":
344 ans=__getcommits(sdb,sdbk,cwhat)
346 cwhat=' '.join(clist[1:])
347 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
349 bot.automsg(public,nick,"No match found")
352 bot.automsg(public,nick,ans[0])
354 bot.automsg(public,nick,"Modified %s %s: %s" % (kindsfile, ans[0][2].isoformat(),ans[0][1]))
356 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
360 bot.automsg(public,nick,a)
362 bot.automsg(public,nick,"%s '%s' modified on %s: %s" % (kindsfile, a[0],a[2].isoformat(),a[1]))
364 ### say to msg/channel
365 def sayq(bot, cmd, nick, conn, public):
366 if irc_lower(nick) == irc_lower(bot.owner):
367 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
370 conn.notice(nick, "You're not my owner!")
372 ### action to msg/channel
373 def doq(bot, cmd, nick, conn, public):
374 sys.stderr.write(irc_lower(bot.owner))
375 sys.stderr.write(irc_lower(nick))
377 if irc_lower(nick) == irc_lower(bot.owner):
378 conn.action(bot.channel, string.join(cmd.split()[1:]))
380 conn.notice(nick, "You're not my owner!")
383 def disconnq(bot, cmd, nick, conn, public):
384 if cmd == "disconnect": # hop off for 60s
385 bot.disconnect(msg="Be right back.")
387 ### list keys of a dictionary
388 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
392 bot.automsg(public,nick,string.join(d))
394 ### rot13 text (yes, I could have typed out the letters....)
395 ### also "foo".encode('rot13') would have worked
396 def rot13q(bot, cmd, nick, conn, public):
397 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
399 trans=string.maketrans(a+a.upper(),b+b.upper())
400 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
402 ### URL-tracking stuff
404 ### return a easy-to-read approximation of a time period
405 def nicetime(tempus):
407 tm="%d seconds ago"%int(tempus)
409 tm="%d minutes ago"%int(tempus/60)
411 tm="%d hours ago"%int(tempus/3600)
414 ### class to store URL data
416 "contains meta-data about a URL seen on-channel"
417 def __init__(self,url,nick):
420 self.first=time.time()
421 self.localfirst=time.localtime(self.first)
423 self.lastseen=time.time()
424 self.lastasked=time.time()
425 def recenttime(self):
426 return max(self.lastseen,self.lastasked)
428 n=time.localtime(time.time())
429 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
430 if n.tm_yday != self.localfirst.tm_yday:
431 s+=time.strftime(" on %d %B", self.localfirst)
434 z=min(len(urlinfos)-1, self.count-1)
437 #(?:) is a regexp that doesn't group
438 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
439 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
440 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
441 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
442 #How long (in s) to wait since the most recent mention before commenting
443 url_repeat_time = 300
449 ### Deal with /msg bot url or ~url in channel
450 def urlq(bot, cmd, nick, conn, public,urldb):
451 if (not urlre.search(cmd)):
452 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
455 urlstring=urlre.search(cmd).group(1)
456 url=canonical_url(urlstring)
459 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
460 (T.nick,T.firstmen())
462 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
464 bot.automsg(False,nick,comment)
465 T.lastasked=time.time()
466 #URL suppressed, so mention in #urls
467 if urlstring != cmd.split()[1]: #first argument to URL was not the url
468 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
470 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
473 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
475 if urlstring != cmd.split()[1]: #first argument to URL was not the url
476 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
478 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
479 urldb[url]=UrlLog(url,nick)
481 ### Deal with URLs spotted in channel
482 def dourl(bot,conn,nick,command,urldb):
483 urlstring=urlre.search(command).group(1)
484 urlstring=canonical_url(urlstring)
486 if urlstring in urldb:
488 message="saw that URL in scrool, first mentioned by %s at %s" % \
489 (T.nick,T.firstmen())
490 if shibboleth.search(command)==None and \
491 time.time() - T.lastseen > url_repeat_time:
492 conn.action(bot.channel, message)
493 T.lastseen=time.time()
496 urldb[urlstring]=UrlLog(urlstring,nick)
499 def urlexpire(urldb,expire):
502 if time.time() - urldb[u].recenttime() > expire:
505 # canonicalise BBC URLs (internal use only)
506 def canonical_url(urlstring):
507 if "nsfw://" in urlstring or "nsfws://" in urlstring:
508 urlstring=urlstring.replace("nsfw","http",1)
509 if (urlstring.find("news.bbc.co.uk") != -1):
510 for middle in ("/low/","/mobile/"):
511 x = urlstring.find(middle)
513 urlstring.replace(middle,"/hi/")
516 # automatically make nsfw urls for you and pass them on to url
517 def nsfwq(bot,cmd,nick,conn,public,urldb):
518 if (not hturlre.search(cmd)):
519 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
521 newcmd=hturlre.sub(nsfwify,cmd)
522 urlq(bot,newcmd,nick,conn,public,urldb)
529 def twitterq(bot,cmd,nick,conn,public,twitapi):
531 if (not urlre.search(cmd)):
532 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
535 urlstring = urlre.search(cmd).group(1)
536 if (urlstring.find("twitter.com") !=-1):
537 stringsout = getTweet(urlstring,twitapi)
538 for stringout in stringsout:
539 bot.automsg(public, nick, stringout)
541 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
542 unobfuscate_urls=True
543 expand_included_tweets=True
546 path = urlparse.urlparse(urlstring).path
547 tweetID = path.split('/')[-1]
549 status = twitapi.GetStatus(tweetID)
551 return "twitapi.GetStatus returned nothing :-("
552 if status.user == None and status.text == None:
553 return "Empty status object returned :("
554 if status.retweeted_status and status.retweeted_status.text:
555 status = status.retweeted_status
556 if status.user is not None:
557 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
558 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
560 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
561 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
562 tweetText = status.full_text
564 replacements = defaultdict( list )
565 for medium in status.media:
566 replacements[medium.url].append(medium.media_url_https)
568 for k,v in replacements.items():
570 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
572 replacementstring = "[" + " ; ".join(v) +"]"
574 replacementstring = v[0]
575 tweetText = tweetText.replace(k, replacementstring)
577 for url in status.urls:
578 toReplace = url.expanded_url
582 rv = urlparse.urlparse(toReplace)
584 # sourced from http://bit.do/list-of-url-shorteners.php
585 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
586 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
587 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
588 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
589 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
590 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
591 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
592 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
593 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
597 #expand list as needed.
598 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
599 resptext = response.read()
600 if resptext.startswith('http'): # ie it looks urlish (http or https)
601 if resptext != toReplace:
603 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
605 # remove tracking utm_ query parameters, for privacy and brevity
606 # code snippet from https://gist.github.com/lepture/5997883
607 rv = urlparse.urlparse(toReplace)
609 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
611 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
613 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
615 if expand_included_tweets:
616 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
618 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
620 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
623 quotedtweet[0] = "Q{ " + quotedtweet[0]
624 quotedtweet[-1] += " }"
625 stringsout = quotedtweet + stringsout
627 tweetText = tweetText.replace(url.url, toReplace)
629 tweetText = tweetText.replace(">",">")
630 tweetText = tweetText.replace("<","<")
631 tweetText = tweetText.replace("&","&")
632 tweetText = tweetText.replace("\n"," ")
633 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
634 except twitter.TwitterError:
635 terror = sys.exc_info()
636 stringout = "Twitter error: %s" % terror[1].__str__()
638 terror = sys.exc_info()
639 stringout = "Error: %s" % terror[1].__str__()
640 stringsout = [stringout] + stringsout
642 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
644 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)