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())))
56 # Check on fish stocks
59 if time.time()>=pond.quotatime:
63 if (time.time()-pond.quotatime)>pond.fish_time_inc:
64 pond.cur_fish+=(((time.time()-pond.quotatime)
65 /pond.fish_time_inc)*pond.fish_inc)
66 if pond.cur_fish>pond.max_fish:
67 pond.cur_fish=pond.max_fish
68 pond.quotatime=time.time()
70 # List of things the bot might be called to work round the self-trouting code
71 synonyms=["itself","the bot","themself"]
73 # trout someone, or flirt with them
74 def troutq(bot, cmd, nick, conn, public, cfg):
81 selftroutchance=cfg[6]
85 conn.notice(nick, quietmsg%fishpond.Boring_Git)
87 if fishpond.cur_fish<=0:
88 conn.notice(nick, nofishmsg)
90 target = string.join(cmd.split()[1:])
92 conn.notice(nick, notargetmsg)
94 me = bot.connection.get_nickname()
95 trout_msg = random.choice(fishlist)
96 fishpond.last=trout_msg
98 # The bot won't trout or flirt with itself;
99 if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
101 # There's a chance the game may be given away if the request was not
104 if random.random()<=selftroutchance:
105 trout_msg=trout_msg+(selftrout%nick)
107 conn.action(bot.channel, trout_msg % target)
111 def slashq(bot, cmd, nick, conn, public, cfg):
118 selfslashchance=cfg[6]
122 conn.notice(nick, quietmsg%fishpond.Boring_Git)
124 if fishpond.cur_fish<=0:
125 conn.notice(nick, nofishmsg)
127 target = string.join(cmd.split()[1:])
128 #who = cmd.split()[1:]
129 who = ' '.join(cmd.split()[1:]).split(' / ')
131 conn.notice(nick, "it takes two to tango!")
134 conn.notice(nick, "we'll have none of that round here")
136 me = bot.connection.get_nickname()
137 slash_msg = random.choice(fishlist)
138 fishpond.last=slash_msg
139 fishpond.last_cfg=cfg
140 # The bot won't slash people with themselves
141 if irc_lower(who[0]) == irc_lower(who[1]):
142 conn.notice(nick, "oooooh no missus!")
144 # The bot won't slash with itself, instead slashing the requester
146 if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
148 # Perhaps someone asked to slash themselves with the bot then we get
149 if irc_lower(who[0]) == irc_lower(who[1]):
150 conn.notice(nick, "you wish!")
152 # There's a chance the game may be given away if the request was not
155 if random.random()<=selfslashchance:
156 slash_msg=slash_msg+(selfslash%nick)
158 conn.action(bot.channel, slash_msg % (who[0], who[1]))
162 def unitq(bot, cmd, nick, conn, public):
163 args = ' '.join(cmd.split()[1:]).split(' as ')
165 args = ' '.join(cmd.split()[1:]).split(' / ')
167 conn.notice(nick, "syntax: units arg1 as arg2")
170 sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
172 sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
175 #popen2 doesn't clean up the child properly. Do this by hand
177 if os.WEXITSTATUS(child[1])==0:
178 bot.automsg(public,nick,res[0].strip())
180 conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
182 # Shut up trouting for a minute
183 def nofishq(bot, cmd, nick, conn, public, fish):
187 fish.quotatime=time.time()
188 fish.quotatime+=fish.nofish_time
189 conn.notice(nick, "Fish stocks depleted, as you wish.")
192 def reloadq(bot, cmd, nick, conn, public):
193 if not public and irc_lower(nick) == irc_lower(bot.owner):
196 conn.notice(nick, "Config reloaded.")
198 conn.notice(nick, "Config reloading failed!")
200 bot.automsg(public,nick,
201 "Configuration can only be reloaded by my owner, by /msg.")
204 def quitq(bot, cmd, nick, conn, public):
205 if irc_lower(nick) == irc_lower(bot.owner):
206 bot.die(msg = "I have been chosen!")
208 conn.notice(nick, "Such aggression in public!")
210 conn.notice(nick, "You're not my owner.")
212 # google for something
213 def googleq(bot, cmd, nick, conn, public):
214 cmdrest = string.join(cmd.split()[1:])
215 # "I'm Feeling Lucky" rather than try and parse the html
216 targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
217 % urllib.quote_plus(cmdrest))
219 # get redirected and grab the resulting url for returning
220 gsearch = urllib.urlopen(targ).geturl()
221 if gsearch != targ: # we've found something
222 bot.automsg(public,nick,str(gsearch))
223 else: # we haven't found anything.
224 bot.automsg(public,nick,"No pages found.")
225 except IOError: # if the connection times out. This blocks. :(
226 bot.automsg(public,nick,"The web's broken. Waah!")
228 # Look up the definition of something using google
229 def defineq(bot, cmd, nick, conn, public):
230 #this doesn't work any more
231 bot.automsg(public,nick,"'define' is broken because google are bastards :(")
233 cmdrest = string.join(cmd.split()[1:])
234 targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
235 % urllib.quote_plus(cmdrest))
237 # Just slurp everything into a string
238 defnpage = urllib.urlopen(targ).read()
239 # For definitions we really do have to parse the HTML, sadly.
240 # This is of course going to be a bit fragile. We first look for
241 # 'Definitions of %s on the Web' -- if this isn't present we
242 # assume we have the 'no definitions found page'.
243 # The first defn starts after the following <p> tag, but as the
244 # first <li> in a <ul type="disc" class=std>
245 # Following that we assume that each definition is all the non-markup
246 # before a <br> tag. Currently we just dump out the first definition.
247 match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
249 bot.automsg(public,nick,"Some things defy definition.")
251 # We assume google has truncated the definition for us so this
252 # won't flood the channel with text...
253 defn = " ".join(match.group(1).split("\n"))
254 bot.automsg(public,nick,defn)
255 except IOError: # if the connection times out. This blocks. :(
256 bot.automsg(public,nick,"The web's broken. Waah!")
258 # Look up a currency conversion via xe.com
259 def currencyq(bot, cmd, nick, conn, public):
260 args = ' '.join(cmd.split()[1:]).split(' as ')
261 if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
262 conn.notice(nick, "syntax: currency arg1 as arg2")
264 targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
266 currencypage = urllib.urlopen(targ).read()
267 match = re.search(r"(1 %s = [\d\.]+ %s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
269 bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
271 conversion = match.group(1);
272 conversion = conversion.replace(' ',' ');
273 bot.automsg(public,nick,conversion + " (from xe.com)")
274 except IOError: # if the connection times out. This blocks. :(
275 bot.automsg(public,nick,"The web's broken. Waah!")
278 ### extract the commit message and timestamp for commit
279 def __getcommitinfo(commit):
280 cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
281 x=subprocess.Popen(cmd,
282 stdout=subprocess.PIPE,stderr=subprocess.PIPE)
283 out,err=x.communicate()
288 ts,mes=out.split('|')
290 md5mes=hashlib.md5(mes).hexdigest()
291 if bfd and md5mes in bfd:
293 when=datetime.date.fromtimestamp(float(ts))
296 ###Return an array of commit messages and timestamps for lines in db that match what
297 def __getcommits(db,keys,what):
301 ret=__getcommitinfo(db[k])
302 if len(ret)==1: #error message
303 return ["Error message from git blame: %s" % ret]
305 ans.append( (k,ret[0],ret[1]) )
308 ###search all three databases for what
309 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
312 tans=__getcommits(tdb,tdbk,what)
313 fans=__getcommits(fdb,fdbk,what)
314 sans=__getcommits(sdb,sdbk,what)
315 return tans+fans+sans
317 def blameq(bot,cmd,nick,conn,public,fish,cfgs):
318 tdb,tdbk,x = cfgs[0][7] # urgh, magic, to support magic knowledge below
319 fdb,fdbk,x = cfgs[1][7]
320 sdb,sdbk,x = cfgs[2][7]
323 bot.automsg(public,nick,"Who or what do you want to blame?")
325 cwhat=' '.join(clist[2:])
327 if clist[1]=="#last":
328 if fish.last_cfg is None:
329 bot.automsg(public,nick,"Nothing")
331 xdb,xdbk,kindsfile = fish.last_cfg[7]
332 ans=__getcommits(xdb,xdbk,fish.last)
333 elif clist[1]=="#trouts" or clist[1]=="#trout":
334 ans=__getcommits(tdb,tdbk,cwhat)
335 elif clist[1]=="#flirts" or clist[1]=="#flirt":
336 ans=__getcommits(fdb,fdbk,cwhat)
337 elif clist[1]=="#slashes" or clist[1]=="#slash":
338 ans=__getcommits(sdb,sdbk,cwhat)
340 cwhat=' '.join(clist[1:])
341 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
343 bot.automsg(public,nick,"No match found")
346 bot.automsg(public,nick,ans[0])
348 bot.automsg(public,nick,"Modified %s %s: %s" % (kindsfile, ans[0][2].isoformat(),ans[0][1]))
350 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
354 bot.automsg(public,nick,a)
356 bot.automsg(public,nick,"%s '%s' modified on %s: %s" % (kindsfile, a[0],a[2].isoformat(),a[1]))
358 ### say to msg/channel
359 def sayq(bot, cmd, nick, conn, public):
360 if irc_lower(nick) == irc_lower(bot.owner):
361 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
364 conn.notice(nick, "You're not my owner!")
366 ### action to msg/channel
367 def doq(bot, cmd, nick, conn, public):
368 sys.stderr.write(irc_lower(bot.owner))
369 sys.stderr.write(irc_lower(nick))
371 if irc_lower(nick) == irc_lower(bot.owner):
372 conn.action(bot.channel, string.join(cmd.split()[1:]))
374 conn.notice(nick, "You're not my owner!")
377 def disconnq(bot, cmd, nick, conn, public):
378 if cmd == "disconnect": # hop off for 60s
379 bot.disconnect(msg="Be right back.")
381 ### list keys of a dictionary
382 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
386 bot.automsg(public,nick,string.join(d))
388 ### rot13 text (yes, I could have typed out the letters....)
389 ### also "foo".encode('rot13') would have worked
390 def rot13q(bot, cmd, nick, conn, public):
391 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
393 trans=string.maketrans(a+a.upper(),b+b.upper())
394 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
396 ### URL-tracking stuff
398 ### return a easy-to-read approximation of a time period
399 def nicetime(tempus):
401 tm="%d seconds ago"%int(tempus)
403 tm="%d minutes ago"%int(tempus/60)
405 tm="%d hours ago"%int(tempus/3600)
408 ### class to store URL data
410 "contains meta-data about a URL seen on-channel"
411 def __init__(self,url,nick):
414 self.first=time.time()
415 self.localfirst=time.localtime(self.first)
417 self.lastseen=time.time()
418 self.lastasked=time.time()
419 def recenttime(self):
420 return max(self.lastseen,self.lastasked)
422 n=time.localtime(time.time())
423 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
424 if n.tm_yday != self.localfirst.tm_yday:
425 s+=time.strftime(" on %d %B", self.localfirst)
428 z=min(len(urlinfos)-1, self.count-1)
431 #(?:) is a regexp that doesn't group
432 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
433 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
434 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
435 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
436 #How long (in s) to wait since the most recent mention before commenting
437 url_repeat_time = 300
443 ### Deal with /msg bot url or ~url in channel
444 def urlq(bot, cmd, nick, conn, public,urldb):
445 if (not urlre.search(cmd)):
446 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
449 urlstring=urlre.search(cmd).group(1)
450 url=canonical_url(urlstring)
453 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
454 (T.nick,T.firstmen())
456 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
458 bot.automsg(False,nick,comment)
459 T.lastasked=time.time()
460 #URL suppressed, so mention in #urls
461 if urlstring != cmd.split()[1]: #first argument to URL was not the url
462 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
464 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
467 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
469 if urlstring != cmd.split()[1]: #first argument to URL was not the url
470 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
472 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
473 urldb[url]=UrlLog(url,nick)
475 ### Deal with URLs spotted in channel
476 def dourl(bot,conn,nick,command,urldb):
477 urlstring=urlre.search(command).group(1)
478 urlstring=canonical_url(urlstring)
480 if urlstring in urldb:
482 message="saw that URL in scrool, first mentioned by %s at %s" % \
483 (T.nick,T.firstmen())
484 if shibboleth.search(command)==None and \
485 time.time() - T.lastseen > url_repeat_time:
486 conn.action(bot.channel, message)
487 T.lastseen=time.time()
490 urldb[urlstring]=UrlLog(urlstring,nick)
493 def urlexpire(urldb,expire):
496 if time.time() - urldb[u].recenttime() > expire:
499 # canonicalise BBC URLs (internal use only)
500 def canonical_url(urlstring):
501 if "nsfw://" in urlstring or "nsfws://" in urlstring:
502 urlstring=urlstring.replace("nsfw","http",1)
503 if (urlstring.find("news.bbc.co.uk") != -1):
504 for middle in ("/low/","/mobile/"):
505 x = urlstring.find(middle)
507 urlstring.replace(middle,"/hi/")
510 # automatically make nsfw urls for you and pass them on to url
511 def nsfwq(bot,cmd,nick,conn,public,urldb):
512 if (not hturlre.search(cmd)):
513 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
515 newcmd=hturlre.sub(nsfwify,cmd)
516 urlq(bot,newcmd,nick,conn,public,urldb)
523 def twitterq(bot,cmd,nick,conn,public,twitapi):
525 if (not urlre.search(cmd)):
526 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
529 urlstring = urlre.search(cmd).group(1)
530 if (urlstring.find("twitter.com") !=-1):
531 stringsout = getTweet(urlstring,twitapi)
532 for stringout in stringsout:
533 bot.automsg(public, nick, stringout)
535 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
536 unobfuscate_urls=True
537 expand_included_tweets=True
540 path = urlparse.urlparse(urlstring).path
541 tweetID = path.split('/')[-1]
543 status = twitapi.GetStatus(tweetID)
545 return "twitapi.GetStatus returned nothing :-("
546 if status.user == None and status.text == None:
547 return "Empty status object returned :("
548 if status.retweeted_status and status.retweeted_status.text:
549 status = status.retweeted_status
550 if status.user is not None:
551 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
552 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
554 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
555 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
556 tweetText = status.full_text
558 replacements = defaultdict( list )
559 for medium in status.media:
560 replacements[medium.url].append(medium.media_url_https)
562 for k,v in replacements.items():
564 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
566 replacementstring = "[" + " ; ".join(v) +"]"
568 replacementstring = v[0]
569 tweetText = tweetText.replace(k, replacementstring)
571 for url in status.urls:
572 toReplace = url.expanded_url
576 rv = urlparse.urlparse(toReplace)
578 # sourced from http://bit.do/list-of-url-shorteners.php
579 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
580 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
581 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
582 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
583 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
584 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
585 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
586 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
587 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
591 #expand list as needed.
592 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
593 resptext = response.read()
594 if resptext.startswith('http'): # ie it looks urlish (http or https)
595 if resptext != toReplace:
597 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
599 # remove tracking utm_ query parameters, for privacy and brevity
600 # code snippet from https://gist.github.com/lepture/5997883
601 rv = urlparse.urlparse(toReplace)
603 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
605 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
607 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
609 if expand_included_tweets:
610 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
612 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
614 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
617 quotedtweet[0] = "Q{ " + quotedtweet[0]
618 quotedtweet[-1] += " }"
619 stringsout = quotedtweet + stringsout
621 tweetText = tweetText.replace(url.url, toReplace)
623 tweetText = tweetText.replace(">",">")
624 tweetText = tweetText.replace("<","<")
625 tweetText = tweetText.replace("&","&")
626 tweetText = tweetText.replace("\n"," ")
627 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
628 except twitter.TwitterError:
629 terror = sys.exc_info()
630 stringout = "Twitter error: %s" % terror[1].__str__()
632 terror = sys.exc_info()
633 stringout = "Error: %s" % terror[1].__str__()
634 stringsout = [stringout] + stringsout
636 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
638 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)