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
97 # The bot won't trout or flirt with itself;
98 if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
100 # There's a chance the game may be given away if the request was not
103 if random.random()<=selftroutchance:
104 trout_msg=trout_msg+(selftrout%nick)
106 conn.action(bot.channel, trout_msg % target)
110 def slashq(bot, cmd, nick, conn, public, cfg):
117 selfslashchance=cfg[6]
121 conn.notice(nick, quietmsg%fishpond.Boring_Git)
123 if fishpond.cur_fish<=0:
124 conn.notice(nick, nofishmsg)
126 target = string.join(cmd.split()[1:])
127 #who = cmd.split()[1:]
128 who = ' '.join(cmd.split()[1:]).split(' / ')
130 conn.notice(nick, "it takes two to tango!")
133 conn.notice(nick, "we'll have none of that round here")
135 me = bot.connection.get_nickname()
136 slash_msg = random.choice(fishlist)
137 fishpond.last=slash_msg
138 # The bot won't slash people with themselves
139 if irc_lower(who[0]) == irc_lower(who[1]):
140 conn.notice(nick, "oooooh no missus!")
142 # The bot won't slash with itself, instead slashing the requester
144 if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
146 # Perhaps someone asked to slash themselves with the bot then we get
147 if irc_lower(who[0]) == irc_lower(who[1]):
148 conn.notice(nick, "you wish!")
150 # There's a chance the game may be given away if the request was not
153 if random.random()<=selfslashchance:
154 slash_msg=slash_msg+(selfslash%nick)
156 conn.action(bot.channel, slash_msg % (who[0], who[1]))
160 def unitq(bot, cmd, nick, conn, public):
161 args = ' '.join(cmd.split()[1:]).split(' as ')
163 args = ' '.join(cmd.split()[1:]).split(' / ')
165 conn.notice(nick, "syntax: units arg1 as arg2")
168 sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
170 sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
173 #popen2 doesn't clean up the child properly. Do this by hand
175 if os.WEXITSTATUS(child[1])==0:
176 bot.automsg(public,nick,res[0].strip())
178 conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
180 # Shut up trouting for a minute
181 def nofishq(bot, cmd, nick, conn, public, fish):
185 fish.quotatime=time.time()
186 fish.quotatime+=fish.nofish_time
187 conn.notice(nick, "Fish stocks depleted, as you wish.")
190 def reloadq(bot, cmd, nick, conn, public):
191 if not public and irc_lower(nick) == irc_lower(bot.owner):
194 conn.notice(nick, "Config reloaded.")
196 conn.notice(nick, "Config reloading failed!")
198 bot.automsg(public,nick,
199 "Configuration can only be reloaded by my owner, by /msg.")
202 def quitq(bot, cmd, nick, conn, public):
203 if irc_lower(nick) == irc_lower(bot.owner):
204 bot.die(msg = "I have been chosen!")
206 conn.notice(nick, "Such aggression in public!")
208 conn.notice(nick, "You're not my owner.")
210 # google for something
211 def googleq(bot, cmd, nick, conn, public):
212 cmdrest = string.join(cmd.split()[1:])
213 # "I'm Feeling Lucky" rather than try and parse the html
214 targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
215 % urllib.quote_plus(cmdrest))
217 # get redirected and grab the resulting url for returning
218 gsearch = urllib.urlopen(targ).geturl()
219 if gsearch != targ: # we've found something
220 bot.automsg(public,nick,str(gsearch))
221 else: # we haven't found anything.
222 bot.automsg(public,nick,"No pages found.")
223 except IOError: # if the connection times out. This blocks. :(
224 bot.automsg(public,nick,"The web's broken. Waah!")
226 # Look up the definition of something using google
227 def defineq(bot, cmd, nick, conn, public):
228 #this doesn't work any more
229 bot.automsg(public,nick,"'define' is broken because google are bastards :(")
231 cmdrest = string.join(cmd.split()[1:])
232 targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
233 % urllib.quote_plus(cmdrest))
235 # Just slurp everything into a string
236 defnpage = urllib.urlopen(targ).read()
237 # For definitions we really do have to parse the HTML, sadly.
238 # This is of course going to be a bit fragile. We first look for
239 # 'Definitions of %s on the Web' -- if this isn't present we
240 # assume we have the 'no definitions found page'.
241 # The first defn starts after the following <p> tag, but as the
242 # first <li> in a <ul type="disc" class=std>
243 # Following that we assume that each definition is all the non-markup
244 # before a <br> tag. Currently we just dump out the first definition.
245 match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
247 bot.automsg(public,nick,"Some things defy definition.")
249 # We assume google has truncated the definition for us so this
250 # won't flood the channel with text...
251 defn = " ".join(match.group(1).split("\n"))
252 bot.automsg(public,nick,defn)
253 except IOError: # if the connection times out. This blocks. :(
254 bot.automsg(public,nick,"The web's broken. Waah!")
256 # Look up a currency conversion via xe.com
257 def currencyq(bot, cmd, nick, conn, public):
258 args = ' '.join(cmd.split()[1:]).split(' as ')
259 if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
260 conn.notice(nick, "syntax: currency arg1 as arg2")
262 targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
264 currencypage = urllib.urlopen(targ).read()
265 match = re.search(r"(1 %s = [\d\.]+ %s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
267 bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
269 conversion = match.group(1);
270 conversion = conversion.replace(' ',' ');
271 bot.automsg(public,nick,conversion + " (from xe.com)")
272 except IOError: # if the connection times out. This blocks. :(
273 bot.automsg(public,nick,"The web's broken. Waah!")
276 ### extract the commit message and timestamp for commit
277 def __getcommitinfo(commit):
278 cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
279 x=subprocess.Popen(cmd,
280 stdout=subprocess.PIPE,stderr=subprocess.PIPE)
281 out,err=x.communicate()
286 ts,mes=out.split('|')
288 md5mes=hashlib.md5(mes).hexdigest()
289 if bfd and md5mes in bfd:
291 when=datetime.date.fromtimestamp(float(ts))
294 ###Return an array of commit messages and timestamps for lines in db that match what
295 def __getcommits(db,keys,what):
299 ret=__getcommitinfo(db[k])
300 if len(ret)==1: #error message
301 return ["Error message from git blame: %s" % ret]
303 ans.append( (k,ret[0],ret[1]) )
306 ###search all three databases for what
307 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
310 tans=__getcommits(tdb,tdbk,what)
311 fans=__getcommits(fdb,fdbk,what)
312 sans=__getcommits(sdb,sdbk,what)
313 return tans+fans+sans
315 def blameq(bot,cmd,nick,conn,public,fish,tdb,tdbk,fdb,fdbk,sdb,sdbk):
318 bot.automsg(public,nick,"Who or what do you want to blame?")
320 cwhat=' '.join(clist[2:])
321 if clist[1]=="#last":
322 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,fish.last)
323 elif clist[1]=="#trouts" or clist[1]=="#trout":
324 ans=__getcommits(tdb,tdbk,cwhat)
325 elif clist[1]=="#flirts" or clist[1]=="#flirt":
326 ans=__getcommits(fdb,fdbk,cwhat)
327 elif clist[1]=="#slashes" or clist[1]=="#slash":
328 ans=__getcommits(sdb,sdbk,cwhat)
330 cwhat=' '.join(clist[1:])
331 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
333 bot.automsg(public,nick,"No match found")
336 bot.automsg(public,nick,ans[0])
338 bot.automsg(public,nick,"Modified %s: %s" % (ans[0][2].isoformat(),ans[0][1]))
340 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
344 bot.automsg(public,nick,a)
346 bot.automsg(public,nick,"'%s' modified on %s: %s" % (a[0],a[2].isoformat(),a[1]))
348 ### say to msg/channel
349 def sayq(bot, cmd, nick, conn, public):
350 if irc_lower(nick) == irc_lower(bot.owner):
351 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
354 conn.notice(nick, "You're not my owner!")
356 ### action to msg/channel
357 def doq(bot, cmd, nick, conn, public):
358 sys.stderr.write(irc_lower(bot.owner))
359 sys.stderr.write(irc_lower(nick))
361 if irc_lower(nick) == irc_lower(bot.owner):
362 conn.action(bot.channel, string.join(cmd.split()[1:]))
364 conn.notice(nick, "You're not my owner!")
367 def disconnq(bot, cmd, nick, conn, public):
368 if cmd == "disconnect": # hop off for 60s
369 bot.disconnect(msg="Be right back.")
371 ### list keys of a dictionary
372 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
376 bot.automsg(public,nick,string.join(d))
378 ### rot13 text (yes, I could have typed out the letters....)
379 ### also "foo".encode('rot13') would have worked
380 def rot13q(bot, cmd, nick, conn, public):
381 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
383 trans=string.maketrans(a+a.upper(),b+b.upper())
384 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
386 ### URL-tracking stuff
388 ### return a easy-to-read approximation of a time period
389 def nicetime(tempus):
391 tm="%d seconds ago"%int(tempus)
393 tm="%d minutes ago"%int(tempus/60)
395 tm="%d hours ago"%int(tempus/3600)
398 ### class to store URL data
400 "contains meta-data about a URL seen on-channel"
401 def __init__(self,url,nick):
404 self.first=time.time()
405 self.localfirst=time.localtime(self.first)
407 self.lastseen=time.time()
408 self.lastasked=time.time()
409 def recenttime(self):
410 return max(self.lastseen,self.lastasked)
412 n=time.localtime(time.time())
413 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
414 if n.tm_yday != self.localfirst.tm_yday:
415 s+=time.strftime(" on %d %B", n)
418 z=min(len(urlinfos)-1, self.count-1)
421 #(?:) is a regexp that doesn't group
422 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
423 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
424 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
425 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
426 #How long (in s) to wait since the most recent mention before commenting
427 url_repeat_time = 300
433 ### Deal with /msg bot url or ~url in channel
434 def urlq(bot, cmd, nick, conn, public,urldb):
435 if (not urlre.search(cmd)):
436 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
439 urlstring=urlre.search(cmd).group(1)
440 url=canonical_url(urlstring)
443 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
444 (T.nick,T.firstmen())
446 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
448 bot.automsg(False,nick,comment)
449 T.lastasked=time.time()
450 #URL suppressed, so mention in #urls
451 if urlstring != cmd.split()[1]: #first argument to URL was not the url
452 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
454 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
457 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
459 if urlstring != cmd.split()[1]: #first argument to URL was not the url
460 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
462 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
463 urldb[url]=UrlLog(url,nick)
465 ### Deal with URLs spotted in channel
466 def dourl(bot,conn,nick,command,urldb):
467 urlstring=urlre.search(command).group(1)
468 urlstring=canonical_url(urlstring)
470 if urlstring in urldb:
472 message="saw that URL in scrool, first mentioned by %s at %s" % \
473 (T.nick,T.firstmen())
474 if shibboleth.search(command)==None and \
475 time.time() - T.lastseen > url_repeat_time:
476 conn.action(bot.channel, message)
477 T.lastseen=time.time()
480 urldb[urlstring]=UrlLog(urlstring,nick)
483 def urlexpire(urldb,expire):
486 if time.time() - urldb[u].recenttime() > expire:
489 # canonicalise BBC URLs (internal use only)
490 def canonical_url(urlstring):
491 if "nsfw://" in urlstring or "nsfws://" in urlstring:
492 urlstring=urlstring.replace("nsfw","http",1)
493 if (urlstring.find("news.bbc.co.uk") != -1):
494 for middle in ("/low/","/mobile/"):
495 x = urlstring.find(middle)
497 urlstring.replace(middle,"/hi/")
500 # automatically make nsfw urls for you and pass them on to url
501 def nsfwq(bot,cmd,nick,conn,public,urldb):
502 if (not hturlre.search(cmd)):
503 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
505 newcmd=hturlre.sub(nsfwify,cmd)
506 urlq(bot,newcmd,nick,conn,public,urldb)
513 def twitterq(bot,cmd,nick,conn,public,twitapi):
515 if (not urlre.search(cmd)):
516 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
519 urlstring = urlre.search(cmd).group(1)
520 if (urlstring.find("twitter.com") !=-1):
521 stringsout = getTweet(urlstring,twitapi)
522 for stringout in stringsout:
523 bot.automsg(public, nick, stringout)
525 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
526 unobfuscate_urls=True
527 expand_included_tweets=True
530 path = urlparse.urlparse(urlstring).path
531 tweetID = path.split('/')[-1]
533 status = twitapi.GetStatus(tweetID)
535 return "twitapi.GetStatus returned nothing :-("
536 if status.user == None and status.text == None:
537 return "Empty status object returned :("
538 if status.retweeted_status and status.retweeted_status.text:
539 status = status.retweeted_status
540 if status.user is not None:
541 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
542 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
544 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
545 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
546 tweetText = status.full_text
548 replacements = defaultdict( list )
549 for medium in status.media:
550 replacements[medium.url].append(medium.media_url_https)
552 for k,v in replacements.items():
554 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
556 replacementstring = "[" + " ; ".join(v) +"]"
558 replacementstring = v[0]
559 tweetText = tweetText.replace(k, replacementstring)
561 for url in status.urls:
562 toReplace = url.expanded_url
566 rv = urlparse.urlparse(toReplace)
568 # sourced from http://bit.do/list-of-url-shorteners.php
569 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
570 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
571 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
572 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
573 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
574 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
575 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
576 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
577 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
581 #expand list as needed.
582 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
583 resptext = response.read()
584 if resptext.startswith('http'): # ie it looks urlish (http or https)
585 if resptext != toReplace:
587 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
589 # remove tracking utm_ query parameters, for privacy and brevity
590 # code snippet from https://gist.github.com/lepture/5997883
591 rv = urlparse.urlparse(toReplace)
593 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
595 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
597 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
599 if expand_included_tweets:
600 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
602 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
604 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
607 quotedtweet[0] = "Q{ " + quotedtweet[0]
608 quotedtweet[-1] += " }"
609 stringsout = quotedtweet + stringsout
611 tweetText = tweetText.replace(url.url, toReplace)
613 tweetText = tweetText.replace(">",">")
614 tweetText = tweetText.replace("<","<")
615 tweetText = tweetText.replace("&","&")
616 tweetText = tweetText.replace("\n"," ")
617 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
618 except twitter.TwitterError:
619 terror = sys.exc_info()
620 stringout = "Twitter error: %s" % terror[1].__str__()
622 terror = sys.exc_info()
623 stringout = "Error: %s" % terror[1].__str__()
624 stringsout = [stringout] + stringsout
626 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
628 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)