2 import string, cPickle, random, urllib, sys, time, re, os, twitter, subprocess, datetime, urlparse
3 from collections import defaultdict
4 from irclib import irc_lower, nm_to_n
7 def karmaq(bot, cmd, nick, conn, public, karma):
9 item=cmd.split()[1].lower()
13 bot.automsg(public,nick,"I have karma on %s items." %
15 elif karma.has_key(item):
16 bot.automsg(public,nick,"%s has karma %s."
19 bot.automsg(public,nick, "%s has no karma set." % item)
22 def karmadelq(bot, cmd, nick, conn, public, karma):
24 item=cmd.split()[1].lower()
26 conn.notice(nick, "What should I delete?")
29 conn.notice(nick, "You are not my owner.")
31 if karma.has_key(item):
33 conn.notice(nick, "Item %s deleted."%item)
35 conn.notice(nick, "There is no karma stored for %s."%item)
37 # help - provides the URL of the help file
38 def helpq(bot, cmd, nick, conn, public):
39 bot.automsg(public,nick,
40 "For help see http://www.chiark.greenend.org.uk/~matthewv/irc/servus.html")
44 def infoq(bot, cmd, nick, conn, public, karma):
45 bot.automsg(public,nick,
46 ("I am Acrobat %s, on %s, as nick %s. "+
47 "My owner is %s; I have karma on %s items.") %
48 (bot.revision.split()[1], bot.channel, conn.get_nickname(),
49 bot.owner, len(karma.keys())))
51 # Check on fish stocks
54 if time.time()>=pond.quotatime:
58 if (time.time()-pond.quotatime)>pond.fish_time_inc:
59 pond.cur_fish+=(((time.time()-pond.quotatime)
60 /pond.fish_time_inc)*pond.fish_inc)
61 if pond.cur_fish>pond.max_fish:
62 pond.cur_fish=pond.max_fish
63 pond.quotatime=time.time()
65 # List of things the bot might be called to work round the self-trouting code
66 synonyms=["itself","the bot","themself"]
68 # trout someone, or flirt with them
69 def troutq(bot, cmd, nick, conn, public, cfg):
76 selftroutchance=cfg[6]
80 conn.notice(nick, quietmsg%fishpond.Boring_Git)
82 if fishpond.cur_fish<=0:
83 conn.notice(nick, nofishmsg)
85 target = string.join(cmd.split()[1:])
87 conn.notice(nick, notargetmsg)
89 me = bot.connection.get_nickname()
90 trout_msg = random.choice(fishlist)
91 fishpond.last=trout_msg
92 # The bot won't trout or flirt with itself;
93 if irc_lower(me) == irc_lower(target) or irc_lower(target) in synonyms:
95 # There's a chance the game may be given away if the request was not
98 if random.random()<=selftroutchance:
99 trout_msg=trout_msg+(selftrout%nick)
101 conn.action(bot.channel, trout_msg % target)
105 def slashq(bot, cmd, nick, conn, public, cfg):
112 selfslashchance=cfg[6]
116 conn.notice(nick, quietmsg%fishpond.Boring_Git)
118 if fishpond.cur_fish<=0:
119 conn.notice(nick, nofishmsg)
121 target = string.join(cmd.split()[1:])
122 #who = cmd.split()[1:]
123 who = ' '.join(cmd.split()[1:]).split(' / ')
125 conn.notice(nick, "it takes two to tango!")
128 conn.notice(nick, "we'll have none of that round here")
130 me = bot.connection.get_nickname()
131 slash_msg = random.choice(fishlist)
132 fishpond.last=slash_msg
133 # The bot won't slash people with themselves
134 if irc_lower(who[0]) == irc_lower(who[1]):
135 conn.notice(nick, "oooooh no missus!")
137 # The bot won't slash with itself, instead slashing the requester
139 if irc_lower(me) == irc_lower(who[n]) or irc_lower(who[n]) in synonyms:
141 # Perhaps someone asked to slash themselves with the bot then we get
142 if irc_lower(who[0]) == irc_lower(who[1]):
143 conn.notice(nick, "you wish!")
145 # There's a chance the game may be given away if the request was not
148 if random.random()<=selfslashchance:
149 slash_msg=slash_msg+(selfslash%nick)
151 conn.action(bot.channel, slash_msg % (who[0], who[1]))
155 def unitq(bot, cmd, nick, conn, public):
156 args = ' '.join(cmd.split()[1:]).split(' as ')
158 args = ' '.join(cmd.split()[1:]).split(' / ')
160 conn.notice(nick, "syntax: units arg1 as arg2")
163 sin,sout=os.popen4(["units","--verbose","--",args[0]],"r")
165 sin,sout=os.popen4(["units","--verbose","--",args[0],args[1]],"r")
168 #popen2 doesn't clean up the child properly. Do this by hand
170 if os.WEXITSTATUS(child[1])==0:
171 bot.automsg(public,nick,res[0].strip())
173 conn.notice(nick,'; '.join(map(lambda x: x.strip(),res)))
175 # Shut up trouting for a minute
176 def nofishq(bot, cmd, nick, conn, public, fish):
180 fish.quotatime=time.time()
181 fish.quotatime+=fish.nofish_time
182 conn.notice(nick, "Fish stocks depleted, as you wish.")
185 def reloadq(bot, cmd, nick, conn, public):
186 if not public and irc_lower(nick) == irc_lower(bot.owner):
189 conn.notice(nick, "Config reloaded.")
191 conn.notice(nick, "Config reloading failed!")
193 bot.automsg(public,nick,
194 "Configuration can only be reloaded by my owner, by /msg.")
197 def quitq(bot, cmd, nick, conn, public):
198 if irc_lower(nick) == irc_lower(bot.owner):
199 bot.die(msg = "I have been chosen!")
201 conn.notice(nick, "Such aggression in public!")
203 conn.notice(nick, "You're not my owner.")
205 # google for something
206 def googleq(bot, cmd, nick, conn, public):
207 cmdrest = string.join(cmd.split()[1:])
208 # "I'm Feeling Lucky" rather than try and parse the html
209 targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
210 % urllib.quote_plus(cmdrest))
212 # get redirected and grab the resulting url for returning
213 gsearch = urllib.urlopen(targ).geturl()
214 if gsearch != targ: # we've found something
215 bot.automsg(public,nick,str(gsearch))
216 else: # we haven't found anything.
217 bot.automsg(public,nick,"No pages found.")
218 except IOError: # if the connection times out. This blocks. :(
219 bot.automsg(public,nick,"The web's broken. Waah!")
221 # Look up the definition of something using google
222 def defineq(bot, cmd, nick, conn, public):
223 #this doesn't work any more
224 bot.automsg(public,nick,"'define' is broken because google are bastards :(")
226 cmdrest = string.join(cmd.split()[1:])
227 targ = ("http://www.google.co.uk/search?q=define%%3A%s&ie=utf-8&oe=utf-8"
228 % urllib.quote_plus(cmdrest))
230 # Just slurp everything into a string
231 defnpage = urllib.urlopen(targ).read()
232 # For definitions we really do have to parse the HTML, sadly.
233 # This is of course going to be a bit fragile. We first look for
234 # 'Definitions of %s on the Web' -- if this isn't present we
235 # assume we have the 'no definitions found page'.
236 # The first defn starts after the following <p> tag, but as the
237 # first <li> in a <ul type="disc" class=std>
238 # Following that we assume that each definition is all the non-markup
239 # before a <br> tag. Currently we just dump out the first definition.
240 match = re.search(r"Definitions of <b>.*?</b> on the Web.*?<li>\s*([^>]*)((<br>)|(<li>))",defnpage,re.MULTILINE)
242 bot.automsg(public,nick,"Some things defy definition.")
244 # We assume google has truncated the definition for us so this
245 # won't flood the channel with text...
246 defn = " ".join(match.group(1).split("\n"))
247 bot.automsg(public,nick,defn)
248 except IOError: # if the connection times out. This blocks. :(
249 bot.automsg(public,nick,"The web's broken. Waah!")
251 # Look up a currency conversion via xe.com
252 def currencyq(bot, cmd, nick, conn, public):
253 args = ' '.join(cmd.split()[1:]).split(' as ')
254 if len(args) != 2 or len(args[0]) != 3 or len(args[1]) != 3:
255 conn.notice(nick, "syntax: currency arg1 as arg2")
257 targ = ("http://www.xe.com/ucc/convert.cgi?From=%s&To=%s" % (args[0], args[1]))
259 currencypage = urllib.urlopen(targ).read()
260 match = re.search(r"(1 %s = [\d\.]+ %s)" % (args[0].upper(),args[1].upper()),currencypage,re.MULTILINE)
262 bot.automsg(public,nick,"Dear Chief Secretary, there is no money.")
264 conversion = match.group(1);
265 conversion = conversion.replace(' ',' ');
266 bot.automsg(public,nick,conversion + " (from xe.com)")
267 except IOError: # if the connection times out. This blocks. :(
268 bot.automsg(public,nick,"The web's broken. Waah!")
271 ### extract the commit message and timestamp for commit
272 def __getcommitinfo(commit):
273 cmd=["git","log","-n","1","--pretty=format:%ct|%s",commit]
274 x=subprocess.Popen(cmd,
275 stdout=subprocess.PIPE,stderr=subprocess.PIPE)
276 out,err=x.communicate()
281 ts,mes=out.split('|')
282 when=datetime.date.fromtimestamp(float(ts))
283 return mes.strip(), when
285 ###Return an array of commit messages and timestamps for lines in db that match what
286 def __getcommits(db,keys,what):
290 ret=__getcommitinfo(db[k])
291 if len(ret)==1: #error message
292 return ["Error message from git blame: %s" % ret]
294 ans.append( (k,ret[0],ret[1]) )
297 ###search all three databases for what
298 def __getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,what):
301 tans=__getcommits(tdb,tdbk,what)
302 fans=__getcommits(fdb,fdbk,what)
303 sans=__getcommits(sdb,sdbk,what)
304 return tans+fans+sans
306 def blameq(bot,cmd,nick,conn,public,fish,tdb,tdbk,fdb,fdbk,sdb,sdbk):
309 bot.automsg(public,nick,"Who or what do you want to blame?")
311 cwhat=' '.join(clist[2:])
312 if clist[1]=="#last":
313 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,fish.last)
314 elif clist[1]=="#trouts" or clist[1]=="#trout":
315 ans=__getcommits(tdb,tdbk,cwhat)
316 elif clist[1]=="#flirts" or clist[1]=="#flirt":
317 ans=__getcommits(fdb,fdbk,cwhat)
318 elif clist[1]=="#slashes" or clist[1]=="#slash":
319 ans=__getcommits(sdb,sdbk,cwhat)
321 cwhat=' '.join(clist[1:])
322 ans=__getall(tdb,tdbk,fdb,fdbk,sdb,sdbk,cwhat)
324 bot.automsg(public,nick,"No match found")
327 bot.automsg(public,nick,ans[0])
329 bot.automsg(public,nick,"Modified %s: %s" % (ans[0][2].isoformat(),ans[0][1]))
331 bot.automsg(public,nick,"I found %d matches, which is too many. Please be more specific!" % (len(ans)) )
335 bot.automsg(public,nick,a)
337 bot.automsg(public,nick,"'%s' modified on %s: %s" % (a[0],a[2].isoformat(),a[1]))
339 ### say to msg/channel
340 def sayq(bot, cmd, nick, conn, public):
341 if irc_lower(nick) == irc_lower(bot.owner):
342 conn.privmsg(bot.channel, string.join(cmd.split()[1:]))
345 conn.notice(nick, "You're not my owner!")
347 ### action to msg/channel
348 def doq(bot, cmd, nick, conn, public):
349 sys.stderr.write(irc_lower(bot.owner))
350 sys.stderr.write(irc_lower(nick))
352 if irc_lower(nick) == irc_lower(bot.owner):
353 conn.action(bot.channel, string.join(cmd.split()[1:]))
355 conn.notice(nick, "You're not my owner!")
358 def disconnq(bot, cmd, nick, conn, public):
359 if cmd == "disconnect": # hop off for 60s
360 bot.disconnect(msg="Be right back.")
362 ### list keys of a dictionary
363 def listkeysq(bot, cmd, nick, conn, public, dict, sort=False):
367 bot.automsg(public,nick,string.join(d))
369 ### rot13 text (yes, I could have typed out the letters....)
370 ### also "foo".encode('rot13') would have worked
371 def rot13q(bot, cmd, nick, conn, public):
372 a=''.join(map(chr,range((ord('a')),(ord('z')+1))))
374 trans=string.maketrans(a+a.upper(),b+b.upper())
375 conn.notice(nick, string.join(cmd.split()[1:]).translate(trans))
377 ### URL-tracking stuff
379 ### return a easy-to-read approximation of a time period
380 def nicetime(tempus):
382 tm="%d seconds ago"%int(tempus)
384 tm="%d minutes ago"%int(tempus/60)
386 tm="%d hours ago"%int(tempus/3600)
389 ### class to store URL data
391 "contains meta-data about a URL seen on-channel"
392 def __init__(self,url,nick):
395 self.first=time.time()
396 self.localfirst=time.localtime(self.first)
398 self.lastseen=time.time()
399 self.lastasked=time.time()
400 def recenttime(self):
401 return max(self.lastseen,self.lastasked)
403 n=time.localtime(time.time())
404 s="%02d:%02d" % (self.localfirst.tm_hour,self.localfirst.tm_min)
405 if n.tm_yday != self.localfirst.tm_yday:
406 s+=time.strftime(" on %d %B", n)
409 z=min(len(urlinfos)-1, self.count-1)
412 #(?:) is a regexp that doesn't group
413 urlre = re.compile(r"((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
414 hturlre= re.compile(r"(http)(s?://[^ ]+)( |$)")
415 #matches \bre\:?\s+ before a regexp; (?i)==case insensitive match
416 shibboleth = re.compile(r"(?i)\bre\:?\s+((?:(?:http)|(?:nsfw))s?://[^ ]+)( |$)")
417 #How long (in s) to wait since the most recent mention before commenting
418 url_repeat_time = 300
424 ### Deal with /msg bot url or ~url in channel
425 def urlq(bot, cmd, nick, conn, public,urldb):
426 if (not urlre.search(cmd)):
427 bot.automsg(False,nick,"Please use 'url' only with http, https, nsfw, or nsfws URLs")
430 urlstring=urlre.search(cmd).group(1)
431 url=canonical_url(urlstring)
434 comment="I saw that URL in scrool, first mentioned by %s at %s" % \
435 (T.nick,T.firstmen())
437 comment=comment+". Furthermore it defeats the point of this command to use it other than via /msg."
439 bot.automsg(False,nick,comment)
440 T.lastasked=time.time()
441 #URL suppressed, so mention in #urls
442 if urlstring != cmd.split()[1]: #first argument to URL was not the url
443 conn.privmsg("#urls","%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
445 conn.privmsg("#urls","(via %s) %s"%(nick," ".join(cmd.split()[1:])))
448 bot.automsg(False,nick,"That URL was unique. There is little point in using !url out loud; please use it via /msg")
450 if urlstring != cmd.split()[1]: #first argument to URL was not the url
451 conn.privmsg(bot.channel,"%s remarks: %s" % (nick," ".join(cmd.split()[1:])))
453 conn.privmsg(bot.channel,"(via %s) %s"%(nick," ".join(cmd.split()[1:])))
454 urldb[url]=UrlLog(url,nick)
456 ### Deal with URLs spotted in channel
457 def dourl(bot,conn,nick,command,urldb):
458 urlstring=urlre.search(command).group(1)
459 urlstring=canonical_url(urlstring)
461 if urlstring in urldb:
463 message="saw that URL in scrool, first mentioned by %s at %s" % \
464 (T.nick,T.firstmen())
465 if shibboleth.search(command)==None and \
466 time.time() - T.lastseen > url_repeat_time:
467 conn.action(bot.channel, message)
468 T.lastseen=time.time()
471 urldb[urlstring]=UrlLog(urlstring,nick)
474 def urlexpire(urldb,expire):
477 if time.time() - urldb[u].recenttime() > expire:
480 # canonicalise BBC URLs (internal use only)
481 def canonical_url(urlstring):
482 if "nsfw://" in urlstring or "nsfws://" in urlstring:
483 urlstring=urlstring.replace("nsfw","http",1)
484 if (urlstring.find("news.bbc.co.uk") != -1):
485 for middle in ("/low/","/mobile/"):
486 x = urlstring.find(middle)
488 urlstring.replace(middle,"/hi/")
491 # automatically make nsfw urls for you and pass them on to url
492 def nsfwq(bot,cmd,nick,conn,public,urldb):
493 if (not hturlre.search(cmd)):
494 bot.automsg(False,nick,"Please use 'nsfw' only with http or https URLs")
496 newcmd=hturlre.sub(nsfwify,cmd)
497 urlq(bot,newcmd,nick,conn,public,urldb)
504 def twitterq(bot,cmd,nick,conn,public,twitapi):
506 if (not urlre.search(cmd)):
507 bot.automsg(False,nick,"Please use 'twit' only with http or https URLs")
510 urlstring = urlre.search(cmd).group(1)
511 if (urlstring.find("twitter.com") !=-1):
512 stringsout = getTweet(urlstring,twitapi)
513 for stringout in stringsout:
514 bot.automsg(public, nick, stringout)
516 def getTweet(urlstring,twitapi,inclusion=False,recurlvl=0):
517 unobfuscate_urls=True
518 expand_included_tweets=True
521 parts = string.split(urlstring,'/')
524 status = twitapi.GetStatus(tweetID)
526 return "twitapi.GetStatus returned nothing :-("
527 if status.user == None and status.text == None:
528 return "Empty status object returned :("
529 if status.retweeted_status and status.retweeted_status.text:
530 status = status.retweeted_status
531 if status.user is not None:
532 tweeter_screen = status.user.screen_name #.encode('UTF-8', 'replace')
533 tweeter_name = status.user.name #.encode('UTF-8', 'replace')
535 tweeter_screen = "[not returned]" ; tweeter_name = "[not returned]"
536 tweeter_name = tweeter_name + " RTing " + status.user.name #.encode('UTF-8', 'replace')
537 tweetText = status.full_text
539 replacements = defaultdict( list )
540 for medium in status.media:
541 replacements[medium.url].append(medium.media_url_https)
543 for k,v in replacements.items():
545 v = [re.sub(r"/tweet_video_thumb/([\w\-]+).jpg", r"/tweet_video/\1.mp4", link) for link in v]
547 replacementstring = "[" + " ; ".join(v) +"]"
549 replacementstring = v[0]
550 tweetText = tweetText.replace(k, replacementstring)
552 for url in status.urls:
553 toReplace = url.expanded_url
557 rv = urlparse.urlparse(toReplace)
559 # sourced from http://bit.do/list-of-url-shorteners.php
560 "bit.do", "t.co", "lnkd.in", "db.tt", "qr.ae", "adf.ly",
561 "goo.gl", "bitly.com", "cur.lv", "tinyurl.com", "ow.ly",
562 "bit.ly", "adcrun.ch", "ity.im", "q.gs", "viralurl.com",
563 "is.gd", "po.st", "vur.me", "bc.vc", "twitthis.com", "u.to",
564 "j.mp", "buzurl.com", "cutt.us", "u.bb", "yourls.org",
565 "crisco.com", "x.co", "prettylinkpro.com", "viralurl.biz",
566 "adcraft.co", "virl.ws", "scrnch.me", "filoops.info", "vurl.bz",
567 "vzturl.com", "lemde.fr", "qr.net", "1url.com", "tweez.me",
568 "7vd.cn", "v.gd", "dft.ba", "aka.gr", "tr.im",
572 #expand list as needed.
573 response = urllib.urlopen('http://urlex.org/txt/' + toReplace)
574 resptext = response.read()
575 if resptext.startswith('http'): # ie it looks urlish (http or https)
576 if resptext != toReplace:
578 # maybe make a note of the domain of the original URL to compile list of shortenable domains?
580 # remove tracking utm_ query parameters, for privacy and brevity
581 # code snippet from https://gist.github.com/lepture/5997883
582 rv = urlparse.urlparse(toReplace)
584 query = re.sub(r'utm_\w+=[^&]+&?', '', rv.query)
586 toReplace = '%s://%s%s?%s' % (rv.scheme, rv.hostname, rv.path, query)
588 toReplace = '%s://%s%s' % (rv.scheme, rv.hostname, rv.path) # leave off the final '?'
590 if expand_included_tweets:
591 if rv.hostname == 'twitter.com' and re.search(r'status/\d+',rv.path):
593 stringsout = [ "{{ Recursion level too high }}" ] + stringsout
595 quotedtweet = getTweet(toReplace, twitapi, inclusion=True, recurlvl=recurlvl+1) # inclusion parameter limits recursion.
598 quotedtweet[0] = "Q{ " + quotedtweet[0]
599 quotedtweet[-1] += " }"
600 stringsout = quotedtweet + stringsout
602 tweetText = tweetText.replace(url.url, toReplace)
604 tweetText = tweetText.replace(">",">")
605 tweetText = tweetText.replace("<","<")
606 tweetText = tweetText.replace("&","&")
607 tweetText = tweetText.replace("\n"," ")
608 stringout = "tweet by %s (%s): %s" %(tweeter_screen,tweeter_name,tweetText)
609 except twitter.TwitterError:
610 terror = sys.exc_info()
611 stringout = "Twitter error: %s" % terror[1].__str__()
613 terror = sys.exc_info()
614 stringout = "Error: %s" % terror[1].__str__()
615 stringsout = [stringout] + stringsout
617 return stringsout # don't want to double-encode it, so just pass it on for now and encode later
619 return map(lambda x: x.encode('UTF-8', 'replace'), stringsout)