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 = cfgs[0][7] # urgh, magic, to support magic knowledge below
319 fdb,fdbk = cfgs[1][7]
320 sdb,sdbk = cfgs[2][7]
323 bot.automsg(public,nick,"Who or what do you want to blame?")
325 cwhat=' '.join(clist[2:])
326 if clist[1]=="#last":
327 if fish.last_cfg is None:
328 bot.automsg(public,nick,"Nothing")
330 xdb,xdbk = fish.last_cfg[7]
331 ans=__getcommits(xdb,xdbk,fish.last)
332 elif clist[1]=="#trouts" or clist[1]=="#trout":
333 ans=__getcommits(tdb,tdbk,cwhat)
334 elif clist[1]=="#flirts" or clist[1]=="#flirt":
335 ans=__getcommits(fdb,fdbk,cwhat)
336 elif clist[1]=="#slashes" or clist[1]=="#slash":
337 ans=__getcommits(sdb,sdbk,cwhat)
339 cwhat=' '.join(clist[1:])
340 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
342 bot.automsg(public,nick,"No match found")
345 bot.automsg(public,nick,ans[0])
347 bot.automsg(public,nick,"Modified %s: %s" % (ans[0][2].isoformat(),ans[0][1]))
349 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
353 bot.automsg(public,nick,a)
355 bot.automsg(public,nick,"'%s' modified on %s: %s" % (a[0],a[2].isoformat(),a[1]))
357 ### say to msg/channel
358 def sayq(bot, cmd, nick, conn, public):
359 if irc_lower(nick) == irc_lower(bot.owner):
360 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
363 conn.notice(nick, "You're not my owner!")
365 ### action to msg/channel
366 def doq(bot, cmd, nick, conn, public):
367 sys.stderr.write(irc_lower(bot.owner))
368 sys.stderr.write(irc_lower(nick))
370 if irc_lower(nick) == irc_lower(bot.owner):
371 conn.action(bot.channel, string.join(cmd.split()[1:]))
373 conn.notice(nick, "You're not my owner!")
376 def disconnq(bot, cmd, nick, conn, public):
377 if cmd == "disconnect": # hop off for 60s
378 bot.disconnect(msg="Be right back.")
380 ### list keys of a dictionary
381 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
385 bot.automsg(public,nick,string.join(d))
387 ### rot13 text (yes, I could have typed out the letters....)
388 ### also "foo".encode('rot13') would have worked
389 def rot13q(bot, cmd, nick, conn, public):
390 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
392 trans=string.maketrans(a+a.upper(),b+b.upper())
393 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
395 ### URL-tracking stuff
397 ### return a easy-to-read approximation of a time period
398 def nicetime(tempus):
400 tm="%d seconds ago"%int(tempus)
402 tm="%d minutes ago"%int(tempus/60)
404 tm="%d hours ago"%int(tempus/3600)
407 ### class to store URL data
409 "contains meta-data about a URL seen on-channel"
410 def __init__(self,url,nick):
413 self.first=time.time()
414 self.localfirst=time.localtime(self.first)
416 self.lastseen=time.time()
417 self.lastasked=time.time()
418 def recenttime(self):
419 return max(self.lastseen,self.lastasked)
421 n=time.localtime(time.time())
422 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
423 if n.tm_yday != self.localfirst.tm_yday:
424 s+=time.strftime(" on %d %B", self.localfirst)
427 z=min(len(urlinfos)-1, self.count-1)
430 #(?:) is a regexp that doesn't group
431 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
432 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
433 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
434 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
435 #How long (in s) to wait since the most recent mention before commenting
436 url_repeat_time = 300
442 ### Deal with /msg bot url or ~url in channel
443 def urlq(bot, cmd, nick, conn, public,urldb):
444 if (not urlre.search(cmd)):
445 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
448 urlstring=urlre.search(cmd).group(1)
449 url=canonical_url(urlstring)
452 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
453 (T.nick,T.firstmen())
455 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
457 bot.automsg(False,nick,comment)
458 T.lastasked=time.time()
459 #URL suppressed, so mention in #urls
460 if urlstring != cmd.split()[1]: #first argument to URL was not the url
461 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
463 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
466 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
468 if urlstring != cmd.split()[1]: #first argument to URL was not the url
469 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
471 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
472 urldb[url]=UrlLog(url,nick)
474 ### Deal with URLs spotted in channel
475 def dourl(bot,conn,nick,command,urldb):
476 urlstring=urlre.search(command).group(1)
477 urlstring=canonical_url(urlstring)
479 if urlstring in urldb:
481 message="saw that URL in scrool, first mentioned by %s at %s" % \
482 (T.nick,T.firstmen())
483 if shibboleth.search(command)==None and \
484 time.time() - T.lastseen > url_repeat_time:
485 conn.action(bot.channel, message)
486 T.lastseen=time.time()
489 urldb[urlstring]=UrlLog(urlstring,nick)
492 def urlexpire(urldb,expire):
495 if time.time() - urldb[u].recenttime() > expire:
498 # canonicalise BBC URLs (internal use only)
499 def canonical_url(urlstring):
500 if "nsfw://" in urlstring or "nsfws://" in urlstring:
501 urlstring=urlstring.replace("nsfw","http",1)
502 if (urlstring.find("news.bbc.co.uk") != -1):
503 for middle in ("/low/","/mobile/"):
504 x = urlstring.find(middle)
506 urlstring.replace(middle,"/hi/")
509 # automatically make nsfw urls for you and pass them on to url
510 def nsfwq(bot,cmd,nick,conn,public,urldb):
511 if (not hturlre.search(cmd)):
512 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
514 newcmd=hturlre.sub(nsfwify,cmd)
515 urlq(bot,newcmd,nick,conn,public,urldb)
522 def twitterq(bot,cmd,nick,conn,public,twitapi):
524 if (not urlre.search(cmd)):
525 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
528 urlstring = urlre.search(cmd).group(1)
529 if (urlstring.find("twitter.com") !=-1):
530 stringsout = getTweet(urlstring,twitapi)
531 for stringout in stringsout:
532 bot.automsg(public, nick, stringout)
534 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
535 unobfuscate_urls=True
536 expand_included_tweets=True
539 path = urlparse.urlparse(urlstring).path
540 tweetID = path.split('/')[-1]
542 status = twitapi.GetStatus(tweetID)
544 return "twitapi.GetStatus returned nothing :-("
545 if status.user == None and status.text == None:
546 return "Empty status object returned :("
547 if status.retweeted_status and status.retweeted_status.text:
548 status = status.retweeted_status
549 if status.user is not None:
550 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
551 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
553 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
554 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
555 tweetText = status.full_text
557 replacements = defaultdict( list )
558 for medium in status.media:
559 replacements[medium.url].append(medium.media_url_https)
561 for k,v in replacements.items():
563 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
565 replacementstring = "[" + " ; ".join(v) +"]"
567 replacementstring = v[0]
568 tweetText = tweetText.replace(k, replacementstring)
570 for url in status.urls:
571 toReplace = url.expanded_url
575 rv = urlparse.urlparse(toReplace)
577 # sourced from http://bit.do/list-of-url-shorteners.php
578 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
579 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
580 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
581 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
582 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
583 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
584 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
585 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
586 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
590 #expand list as needed.
591 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
592 resptext = response.read()
593 if resptext.startswith('http'): # ie it looks urlish (http or https)
594 if resptext != toReplace:
596 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
598 # remove tracking utm_ query parameters, for privacy and brevity
599 # code snippet from https://gist.github.com/lepture/5997883
600 rv = urlparse.urlparse(toReplace)
602 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
604 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
606 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
608 if expand_included_tweets:
609 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
611 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
613 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
616 quotedtweet[0] = "Q{ " + quotedtweet[0]
617 quotedtweet[-1] += " }"
618 stringsout = quotedtweet + stringsout
620 tweetText = tweetText.replace(url.url, toReplace)
622 tweetText = tweetText.replace(">",">")
623 tweetText = tweetText.replace("<","<")
624 tweetText = tweetText.replace("&","&")
625 tweetText = tweetText.replace("\n"," ")
626 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
627 except twitter.TwitterError:
628 terror = sys.exc_info()
629 stringout = "Twitter error: %s" % terror[1].__str__()
631 terror = sys.exc_info()
632 stringout = "Error: %s" % terror[1].__str__()
633 stringsout = [stringout] + stringsout
635 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
637 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)