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,tdb,tdbk,fdb,fdbk,sdb,sdbk):
320 bot.automsg(public,nick,"Who or what do you want to blame?")
322 cwhat=' '.join(clist[2:])
323 if clist[1]=="#last":
324 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,fish.last)
325 elif clist[1]=="#trouts" or clist[1]=="#trout":
326 ans=__getcommits(tdb,tdbk,cwhat)
327 elif clist[1]=="#flirts" or clist[1]=="#flirt":
328 ans=__getcommits(fdb,fdbk,cwhat)
329 elif clist[1]=="#slashes" or clist[1]=="#slash":
330 ans=__getcommits(sdb,sdbk,cwhat)
332 cwhat=' '.join(clist[1:])
333 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
335 bot.automsg(public,nick,"No match found")
338 bot.automsg(public,nick,ans[0])
340 bot.automsg(public,nick,"Modified %s: %s" % (ans[0][2].isoformat(),ans[0][1]))
342 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
346 bot.automsg(public,nick,a)
348 bot.automsg(public,nick,"'%s' modified on %s: %s" % (a[0],a[2].isoformat(),a[1]))
350 ### say to msg/channel
351 def sayq(bot, cmd, nick, conn, public):
352 if irc_lower(nick) == irc_lower(bot.owner):
353 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
356 conn.notice(nick, "You're not my owner!")
358 ### action to msg/channel
359 def doq(bot, cmd, nick, conn, public):
360 sys.stderr.write(irc_lower(bot.owner))
361 sys.stderr.write(irc_lower(nick))
363 if irc_lower(nick) == irc_lower(bot.owner):
364 conn.action(bot.channel, string.join(cmd.split()[1:]))
366 conn.notice(nick, "You're not my owner!")
369 def disconnq(bot, cmd, nick, conn, public):
370 if cmd == "disconnect": # hop off for 60s
371 bot.disconnect(msg="Be right back.")
373 ### list keys of a dictionary
374 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
378 bot.automsg(public,nick,string.join(d))
380 ### rot13 text (yes, I could have typed out the letters....)
381 ### also "foo".encode('rot13') would have worked
382 def rot13q(bot, cmd, nick, conn, public):
383 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
385 trans=string.maketrans(a+a.upper(),b+b.upper())
386 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
388 ### URL-tracking stuff
390 ### return a easy-to-read approximation of a time period
391 def nicetime(tempus):
393 tm="%d seconds ago"%int(tempus)
395 tm="%d minutes ago"%int(tempus/60)
397 tm="%d hours ago"%int(tempus/3600)
400 ### class to store URL data
402 "contains meta-data about a URL seen on-channel"
403 def __init__(self,url,nick):
406 self.first=time.time()
407 self.localfirst=time.localtime(self.first)
409 self.lastseen=time.time()
410 self.lastasked=time.time()
411 def recenttime(self):
412 return max(self.lastseen,self.lastasked)
414 n=time.localtime(time.time())
415 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
416 if n.tm_yday != self.localfirst.tm_yday:
417 s+=time.strftime(" on %d %B", self.localfirst)
420 z=min(len(urlinfos)-1, self.count-1)
423 #(?:) is a regexp that doesn't group
424 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
425 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
426 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
427 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
428 #How long (in s) to wait since the most recent mention before commenting
429 url_repeat_time = 300
435 ### Deal with /msg bot url or ~url in channel
436 def urlq(bot, cmd, nick, conn, public,urldb):
437 if (not urlre.search(cmd)):
438 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
441 urlstring=urlre.search(cmd).group(1)
442 url=canonical_url(urlstring)
445 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
446 (T.nick,T.firstmen())
448 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
450 bot.automsg(False,nick,comment)
451 T.lastasked=time.time()
452 #URL suppressed, so mention in #urls
453 if urlstring != cmd.split()[1]: #first argument to URL was not the url
454 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
456 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
459 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
461 if urlstring != cmd.split()[1]: #first argument to URL was not the url
462 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
464 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
465 urldb[url]=UrlLog(url,nick)
467 ### Deal with URLs spotted in channel
468 def dourl(bot,conn,nick,command,urldb):
469 urlstring=urlre.search(command).group(1)
470 urlstring=canonical_url(urlstring)
472 if urlstring in urldb:
474 message="saw that URL in scrool, first mentioned by %s at %s" % \
475 (T.nick,T.firstmen())
476 if shibboleth.search(command)==None and \
477 time.time() - T.lastseen > url_repeat_time:
478 conn.action(bot.channel, message)
479 T.lastseen=time.time()
482 urldb[urlstring]=UrlLog(urlstring,nick)
485 def urlexpire(urldb,expire):
488 if time.time() - urldb[u].recenttime() > expire:
491 # canonicalise BBC URLs (internal use only)
492 def canonical_url(urlstring):
493 if "nsfw://" in urlstring or "nsfws://" in urlstring:
494 urlstring=urlstring.replace("nsfw","http",1)
495 if (urlstring.find("news.bbc.co.uk") != -1):
496 for middle in ("/low/","/mobile/"):
497 x = urlstring.find(middle)
499 urlstring.replace(middle,"/hi/")
502 # automatically make nsfw urls for you and pass them on to url
503 def nsfwq(bot,cmd,nick,conn,public,urldb):
504 if (not hturlre.search(cmd)):
505 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
507 newcmd=hturlre.sub(nsfwify,cmd)
508 urlq(bot,newcmd,nick,conn,public,urldb)
515 def twitterq(bot,cmd,nick,conn,public,twitapi):
517 if (not urlre.search(cmd)):
518 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
521 urlstring = urlre.search(cmd).group(1)
522 if (urlstring.find("twitter.com") !=-1):
523 stringsout = getTweet(urlstring,twitapi)
524 for stringout in stringsout:
525 bot.automsg(public, nick, stringout)
527 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
528 unobfuscate_urls=True
529 expand_included_tweets=True
532 path = urlparse.urlparse(urlstring).path
533 tweetID = path.split('/')[-1]
535 status = twitapi.GetStatus(tweetID)
537 return "twitapi.GetStatus returned nothing :-("
538 if status.user == None and status.text == None:
539 return "Empty status object returned :("
540 if status.retweeted_status and status.retweeted_status.text:
541 status = status.retweeted_status
542 if status.user is not None:
543 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
544 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
546 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
547 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
548 tweetText = status.full_text
550 replacements = defaultdict( list )
551 for medium in status.media:
552 replacements[medium.url].append(medium.media_url_https)
554 for k,v in replacements.items():
556 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
558 replacementstring = "[" + " ; ".join(v) +"]"
560 replacementstring = v[0]
561 tweetText = tweetText.replace(k, replacementstring)
563 for url in status.urls:
564 toReplace = url.expanded_url
568 rv = urlparse.urlparse(toReplace)
570 # sourced from http://bit.do/list-of-url-shorteners.php
571 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
572 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
573 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
574 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
575 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
576 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
577 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
578 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
579 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
583 #expand list as needed.
584 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
585 resptext = response.read()
586 if resptext.startswith('http'): # ie it looks urlish (http or https)
587 if resptext != toReplace:
589 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
591 # remove tracking utm_ query parameters, for privacy and brevity
592 # code snippet from https://gist.github.com/lepture/5997883
593 rv = urlparse.urlparse(toReplace)
595 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
597 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
599 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
601 if expand_included_tweets:
602 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
604 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
606 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
609 quotedtweet[0] = "Q{ " + quotedtweet[0]
610 quotedtweet[-1] += " }"
611 stringsout = quotedtweet + stringsout
613 tweetText = tweetText.replace(url.url, toReplace)
615 tweetText = tweetText.replace(">",">")
616 tweetText = tweetText.replace("<","<")
617 tweetText = tweetText.replace("&","&")
618 tweetText = tweetText.replace("\n"," ")
619 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
620 except twitter.TwitterError:
621 terror = sys.exc_info()
622 stringout = "Twitter error: %s" % terror[1].__str__()
624 terror = sys.exc_info()
625 stringout = "Error: %s" % terror[1].__str__()
626 stringsout = [stringout] + stringsout
628 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
630 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)