chiark / gitweb /
slash from rjk, patch from col [to note bishops can be of either gender]
[irc.git] / Servus-chiark.py
1 # This file is part of Acrobat.
2 #
3 # Acrobat is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published
5 # by the Free Software Foundation; either version 2 of the License,
6 # or (at your option) any later version.
7 #
8 # Acrobat is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with Acrobat; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
16 # USA.
17
18 # Andrew Walkingshaw <andrew@lexical.org.uk>
19 # Peter Corbett <ptc24@cam.ac.uk>
20 # Matthew Vernon <matthew@debian.org>
21 # Stephen Early <steve@greenend.org.uk>
22 # Richard Kettlewell <rjk@greenend.org.uk
23
24 # Acrobat configuration file
25
26 # The following definitions are required to be present in this module:
27 # You can also override them on the command-line
28 # e.g. python acrobat.py Servus-chiark nickname=testbot channel=\#test owner=MyNick
29 server = "chiark"
30 port = 6667
31 nickname = "Servus"
32 channel = "#chiark"
33 owner = "Emperor"
34 # Also a function called "command"; see later.
35
36 # Everything else in this file is configuration-specific.
37
38 import os, time, re, twitter, subprocess, sys, os.path
39
40 # Most command implementations are stored in a separate module.
41 import commands as c
42
43 # This fishpond is shared between trouts and flirts.  It doesn't have to be;
44 # you can define as many ponds as you like.
45 class fish:
46         cur_fish=5
47         max_fish=5
48         nofish_time=60
49         fish_time_inc=60
50         fish_inc=2
51         DoS=0
52         Boring_Git='Nobody'
53         quotatime=0
54         last=""
55
56 # load the "blame" details for a file
57 def loadblame(filename):
58     p=subprocess.Popen(["git","blame","-s",filename],
59                        stdout=subprocess.PIPE,stderr=subprocess.PIPE)
60     out,err=p.communicate()
61     if len(err)>0:
62         sys.exit("git blame failure: %s" % err)
63     bdb={}
64     lines=out.split("\n")
65     for line in lines:
66         l=line.split()
67         if len(line.strip())>0:
68             commit=l[0]
69             thing=' '.join(l[2:])
70             bdb[thing]=commit
71     keys=bdb.keys()
72     return bdb,keys
73     
74 #set up blame dbs for trouts/flirts/slashes
75 tbdb,tbdbk=loadblame("trouts")
76 fbdb,fbdbk=loadblame("flirts")
77 sbdb,sbdbk=loadblame("slashes")
78
79 # load a file full of flirts or trouts
80 def __load(filename):
81     try:
82         f = open(filename, "r")
83         r = [l.strip() for l in f.readlines() if l.find("%s") != -1]
84         f.close()
85     except IOError:
86         r = [ "doesn't know what to do about %s." ]
87     return r
88
89 # (troutlist,selftroutmsg,DoSmsg,notargetmsg,nofishmsg,fishpond,selftroutprob)
90 troutcfg = (
91         __load("trouts"),
92         ' (at the instigation of %s)',
93         "Sorry, but %s is being a spoilsport.",
94         "Who do you wish me to trout?",
95         "Fish stocks exhausted.",
96         fish,
97         0.1)
98
99 flirtcfg = (
100         __load("flirts"),
101         ' (but %s is their secret admirer)',
102         "Sorry, but %s made me take Holy Orders.",
103         "Who do you wish me to flirt with?",
104         "My libido is over-used!",
105         fish,
106         0.1)
107
108 slashcfg= ( 
109         __load("slashes"),
110         ' (while %s watches)',
111         "Sorry, but %s stole my pen.",
112         "Who do you want to slash?",
113         "I have writer's block!",
114         fish,
115         0.1)
116
117 # Hacky command to output the current fishpond state
118 def fishq(bot, cmd, nick, conn, public,f):
119         from irclib import irc_lower
120         if not public and irc_lower(nick) == irc_lower(bot.owner):
121                 state=("Fishpond state: cur_fish=%d, max_fish=%d, nofish_time=%d, "
122                        +"fish_time_inc=%d, fish_inc=%d, DoS=%d, Boring_Git=%s, "
123                        +"quotatime=%d")%(f.cur_fish,f.max_fish,f.nofish_time,
124                                          f.fish_time_inc,f.fish_inc,f.DoS,f.Boring_Git,
125                                          f.quotatime)
126                 bot.automsg(public,nick,state)
127                     
128 # Karma implementation
129 import cPickle
130 karmafilename = "chiark-karma-"+channel
131 # load the karma db
132 try:
133     f = open(karmafilename, "r")
134     karmadb = cPickle.load(f)
135     f.close()
136 except IOError:
137     karmadb = {}
138 # Modify karma
139 def karma(cmd, amount):
140     thing=cmd.split()[0][:-2].lower()
141     if karmadb.has_key(thing):
142         karmadb[thing] += amount
143     else:
144         karmadb[thing] = amount
145     savekarma()
146 def savekarma():
147     tmp = "%s.tmp" % karmafilename
148     try:
149         f = open(tmp, "w")
150         cPickle.dump(karmadb, f)
151         f.close()
152         os.rename(tmp, karmafilename)
153     except IOError, e:
154         sys.stderr.write("error writing karma: %s" % e)
155
156 def quit(bot,cmd,nick,conn,public):
157     c.quitq(bot,cmd,nick,conn,public)
158 def reload(bot,cmd,nick,conn,public):
159     c.reloadq(bot,cmd,nick,conn,public)
160
161 # initialise the urldb on startup
162 urldb={}
163 lastexp=time.time()
164 #expire urls if not asked about or seen for >71 hours
165 expirelen=71*60*60
166 #do an expiry run every hour
167 expirevery=60*60
168
169
170 #path where Oauth details are kept
171 twioauthpath=os.path.expanduser("~/private/servus_twapi_oauth.txt")
172
173 try:
174   f=open(twioauthpath,"r")
175   for line in f:
176     if line[0]=='#':
177       continue
178     key,val=map(str.strip,line.split(':'))
179     if key == "consumer_key":
180       twoaapck = val
181     elif key == "consumer_secret":
182       twoaapcs = val
183     elif key == "access_token":
184       twoapat = val
185     elif key == "access_token_secret":
186       twoapats = val
187     else:
188       raise ValueError, "Invalid line in twitter auth details file %s" % line
189   f.close()
190   twitapi = twitter.Api(consumer_key = twoaapck,
191                         consumer_secret = twoaapcs,
192                         access_token_key = twoapat,
193                         access_token_secret = twoapats)
194 except IOError:
195 # non-authenticated twitter api instance
196   twitapi = twitter.Api()
197
198 # Command processing: whenever something is said that the bot can hear,
199 # "command" is invoked and must decide what to do.  This configuration
200 # defines a couple of special cases (for karma) but is otherwise driven
201 # by a dictionary of commands.
202
203 commands = {"karma": (c.karmaq,karmadb),
204             "karmalist": (c.listkeysq,karmadb),
205             "karmadel": (c.karmadelq,karmadb),
206             "info": (c.infoq,karmadb),
207             "trout": (c.troutq,troutcfg),
208             "slash": (c.slashq, slashcfg),
209             "rot13": c.rot13q,
210             "fish": (fishq,fish),
211             "flirt": (c.troutq,flirtcfg),
212             "quiet": (c.nofishq,fish),
213             "reload": reload,
214             "quit": quit,
215             "die": quit,
216             "define": c.defineq,
217             "google": c.googleq,
218             "url": (c.urlq,urldb),
219             "nsfw": (c.nsfwq,urldb),
220             "nws": (c.nsfwq,urldb),
221             "units": c.unitq,
222             "currency":c.currencyq,
223             "blame": (c.blameq,fish,tbdb,tbdbk,fbdb,fbdbk,sbdb,sbdbk),
224             "help": c.helpq,
225             "say": c.sayq,
226             "do": c.doq, 
227             "twit": (c.twitterq,twitapi) }
228 # disconnect and hop annoy people
229 #            "disconnect": c.disconnq,
230 #            "hop": c.disconnq }
231 commands["list"]=(c.listkeysq,commands,True)
232
233 triggers = ("!", "~") # what character should the bot be invoked by:
234                       # eg !trout, ~trout etc.
235
236 def command(bot, cmd, nick, conn, public):
237     global urldb,lastexp,expirelen,expirevery,twitapi
238     ours=0
239     try:
240             if public and cmd[0] in triggers:
241                     ours=1
242                     cmd=cmd[1:]
243             if not public:
244                     ours=1
245             command = cmd.split()[0]
246     except IndexError:
247             command=""
248
249     t=time.time()
250     if t - lastexp > expirevery:
251             c.urlexpire(urldb,expirelen)
252             lastexp=t
253
254     if public:
255       if c.urlre.search(cmd) and command.lower()!="url":
256         c.dourl(bot,conn,nick,cmd,urldb)
257
258     # karma: up
259     if command.endswith("++"):
260         karma(cmd,1)
261     # karma: down
262     if command.endswith("--"):
263         karma(cmd,-1)
264
265     if ours and command.lower() in commands.keys():
266         e=commands[command.lower()]
267         if callable(e):
268             e(bot,cmd,nick,conn,public)
269         else:
270             e[0](bot,cmd,nick,conn,public,*e[1:])