chiark / gitweb /
edit ascii-art trout by popular request
[irc.git] / acrobat-chiark-0.2.py
1 #!/usr/bin/env python2
2 # $Id$
3 #
4 # Joel Rosdahl <joel@rosdahl.net>
5 # Andrew Walkingshaw <andrew@lexical.org.uk>
6 # Peter Corbett <ptc24@cam.ac.uk>
7 # Matthew Vernon <matthew@debian.org>
8
9 # This file is part of Acrobat.
10 #
11 # Acrobat is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published
13 # by the Free Software Foundation; either version 2 of the License,
14 # or (at your option) any later version.
15 #
16 # Acrobat is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20
21 # You should have received a copy of the GNU General Public License
22 # along with Acrobat; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
24 # USA.
25
26 """
27     disconnect -- Disconnect the bot.  The bot will try to reconnect
28                   after 60 seconds.
29
30     die -- Let the bot cease to exist.
31
32     google -- search, 'I'm Feeling Lucky', and notice the user who searches
33               back with the url.
34 """
35
36 import string, urllib, sys, cPickle, os, random, re, time
37 from ircbot import SingleServerIRCBot
38 from irclib import nm_to_n, irc_lower
39
40
41
42 class Karma:
43     def __init__(self):
44         self.dict = {}
45     
46 class Acrobat(SingleServerIRCBot):
47     def __init__(self, channel, nickname, server, owner, port=6667):
48         SingleServerIRCBot.__init__(self,
49                                     [(server, port)], nickname, nickname)
50         self.channel = channel
51         self.owner = owner
52         self.quotatime = time.time()
53         #List of known !commands we respond to
54         self.known =['karma','trout','info','die','quiet','list','google','say','do','reload','flirt']
55         #Configurable stuff - how often do we add how many fish?
56         self.cur_fish=5
57         self.max_fish=5       #Maximum of 5 fish
58         self.fish_time_inc=60  #Add fish with 20s granularity
59         self.fish_inc=2        #Rate of increase is 2 fish per 60s
60         self.DoS=0             #Have we been told to shut up?
61         self.Boring_Git='Nobody' #Who told us to shut up?
62         # load the karma db
63         try:
64             f = open("karmadump", "r")
65             self.karma = cPickle.load(f)
66             f.close()
67         except IOError:
68             self.karma = Karma()
69         try:
70             f = open("trouts", "r")
71             self.trouts = [l.strip() for l in f.readlines() if l.find("%s") != -1]
72             f.close()
73         except IOError:
74             self.trouts = [ "hits %s with a wet trout.", "thwaps %s.", "questions %s's parentage.", "pokes its tounge out at %s.", "bites its thumb at %s."]
75         try:
76             f = open("flirts", "r")
77             self.flirts = [l.strip() for l in f.readlines() if l.find("%s") != -1]
78             f.close()
79         except IOError:
80             self.flirts = [ "falls madly in love with %s", "blows kisses at %s"]
81
82     ## EVENT HANDLERS
83             
84     def on_welcome(self, conn, evt):
85         conn.join(self.channel)
86
87     def on_privmsg(self, conn, evt):
88         self.do_command(nm_to_n(evt.source()), evt.arguments()[0])
89         
90     def on_pubmsg(self, conn, evt):
91         payload = evt.arguments()[0]
92         a = string.split(evt.arguments()[0], " ", 1)
93         if len(a) > 1 \
94            and (irc_lower(a[0]) == irc_lower(self.connection.get_nickname())
95                 or irc_lower(a[0])[:-1] == irc_lower(self.connection.get_nickname())):
96
97             self.do_command(nm_to_n(evt.source()), string.strip(a[1]), public = 1)
98         if a[0].endswith("++"):
99             self.karmaup(a[0])
100         if a[0].endswith("--"):
101             self.karmadown(a[0])
102         if payload[0] == "!" and len(payload)>1:
103             self.do_command(nm_to_n(evt.source()), string.strip(payload[1:]), public=1)
104         if payload[0] == "~" and len(payload)>1:
105             self.do_command(nm_to_n(evt.source()), string.strip(payload[1:]), public=1)
106
107     # And now bot commands;
108
109     # increment karma
110     def karmaup(self, cmd):
111         if self.karma.dict.has_key(cmd.split()[0][:-2]):
112             self.karma.dict[cmd.split()[0][:-2]] += 1
113         else:
114             self.karma.dict[cmd.split()[0][:-2]] = 1
115
116     #decrement karma
117     def karmadown(self, cmd):
118         if self.karma.dict.has_key(cmd.split()[0][:-2]):
119             self.karma.dict[cmd.split()[0][:-2]] -= 1
120         else:
121             self.karma.dict[cmd.split()[0][:-2]] = -1
122
123     # query karma
124     def karmaq(self, cmd, conn, nick, public):
125         # in public
126         if public == 1:
127             try:
128                 if self.karma.dict.has_key(cmd.split()[1]):
129                     conn.privmsg(self.channel, "%s has karma %s."
130                                  %(cmd.split()[1],
131                                        self.karma.dict[cmd.split()[1]]))
132                 else:
133                     conn.privmsg(self.channel, "%s has no karma set." %
134                                  cmd.split()[1])
135             except IndexError:
136                 conn.privmsg(self.channel, "I have karma on %s items." %
137                              len(self.karma.dict.keys()))
138         # in private
139         else:
140             try:
141                 if self.karma.dict.has_key(cmd.split()[1]):
142                     conn.notice(nick, "%s has karma %s." %
143                                 (cmd.split()[1],
144                                  self.karma.dict[cmd.split()[1]]))
145                 else:
146                     conn.notice(nick, "I have karma on %s items." %
147                                 len(self.karma.dict.keys()))
148             except IndexError:
149                 conn.notice(nick, "I have karma on %s items." %
150                             len(self.karma.dict.keys()))
151     # query bot status
152     def infoq(self, cmd, nick, conn, public):
153         # version control magic
154         acrorevision="$Revision$"
155         acrorev1=re.sub(r'\$Revision: (.*)',r'\1',acrorevision)
156         acroversion=re.sub(r'(.*) \$',r'\1',acrorev1)
157         if public == 1:
158             conn.privmsg(self.channel,
159                          "I am Acrobat %s, on %s, as nick %s." %
160                          (acroversion, self.channel, self.connection.get_nickname()))
161             conn.privmsg(self.channel,
162                          "My owner is %s; I have karma on %s items." %
163                          (self.owner, len(self.karma.dict.keys())))
164         else:
165             conn.notice(nick, "I am Acrobat %s, on %s, as nick %s." %
166                         (acroversion, self.channel, self.connection.get_nickname()))
167             conn.notice(nick, "My owner is %s; I have karma on %s items." %
168                         (self.owner, len(self.karma.dict.keys())))
169
170     # list know commands
171     def listq(self, cmd, nick, conn, public):
172         conn.notice(nick, "%s" % string.join(self.known))
173     # flirt with someone
174     def flirtq(self, cmd, nick, conn, public):
175         self.fish_quota()
176         if self.DoS == 1:
177             conn.notice(nick, "Sorry, but %s made me take Holy Orders." %
178                         self.Boring_Git)
179             return
180         if self.cur_fish <= 0:
181             conn.notice(nick, "My libido is over-used!")
182         else:
183             self.cur_fish -=1
184             try:
185                 target = string.join(cmd.split()[1:])
186                 me = self.connection.get_nickname()
187                 trout_msg = random.choice(self.flirts)
188                 # ...and touchy.
189                 if me.lower() == target.lower():
190                     target = nick
191                 if public == 0:
192                     if random.random() <= 0.1:
193                         trout_msg+= ' (but %s is their secret admirer)' % nick
194                 conn.action(self.channel, trout_msg % target)
195             except IndexError:
196                 conn.notice(nick, "Who do you wish me to flirt with?")
197     
198     # trout someone
199     def troutq(self, cmd, nick, conn, public):
200         self.fish_quota()
201         if self.DoS == 1:
202             conn.notice(nick, "Sorry, but %s is being a spoilsport." %
203                         self.Boring_Git)
204             return
205         if self.cur_fish <= 0:
206             conn.notice(nick, "Fish stocks exhausted.")
207         else:
208             self.cur_fish -=1
209             try:
210                 target = string.join(cmd.split()[1:])
211                 me = self.connection.get_nickname()
212                 trout_msg = random.choice(self.trouts)
213 #                # The bot is loyal(ish)...
214 #                if target.lower() == self.owner.lower():
215 #                    target = nick
216                 # ...and touchy.
217                 if me.lower() == target.lower():
218                     target = nick
219                 if public == 0:
220                     if random.random() <= 0.1:
221                         trout_msg+= ' (at the instigation of %s)' % nick
222                 conn.action(self.channel, trout_msg % target)
223             except IndexError:
224                 conn.notice(nick, "Who do you wish me to trout?")
225
226     # stock up on trouts
227     def reloadq(self, cmd, nick, conn, public):
228         if irc_lower(nick) == irc_lower(self.owner):
229             tback = self.trouts
230             fback = self.flirts
231             try:
232                 f = open("trouts", "r")
233                 self.trouts = [l.strip() for l in f.readlines() if l.find("%s") != -1]
234                 f.close()
235                 f = open("flirts", "r")
236                 self.flirts = [l.strip() for l in f.readlines() if l.find("%s") != -1]
237                 conn.notice(nick, "I am re-armed!")
238             except IOError:
239                 conn.notice(nick, "Trout re-arming failed!")
240                 self.trouts = tback
241                 self.flirts = fback
242         else:
243             conn.notice(nick, "This command can only be invoked by my owner.")
244     # Shut up trouting for a minute
245     def nofish(self, cmd, nick, conn, public):
246         self.cur_fish=0
247         self.DoS=1
248         self.Boring_Git=nick
249         self.quotatime=time.time()
250         self.quotatime+=60 #60 seconds of no fishing
251         conn.notice(nick, "Fish stocks depleted, as you wish.")
252     # Check on fish stocks
253     def fish_quota(self):
254         if self.DoS==1:
255             if time.time()>=self.quotatime:
256                 self.DoS=0
257             else:
258                 return
259         if self.DoS==0:
260             if (time.time()-self.quotatime)>self.fish_time_inc:
261                 self.cur_fish+=(((time.time()-self.quotatime)/self.fish_time_inc)*self.fish_inc)
262                 if self.cur_fish>self.max_fish:
263                     self.cur_fish=self.max_fish
264                 self.quotatime=time.time()
265
266     # quit irc
267     def quit(self, cmd, nick, conn, public):
268         if irc_lower(nick) == irc_lower(self.owner):
269             f = open("karmadump", "w")
270             cPickle.dump(self.karma, f)
271             f.close()
272             self.die(msg="I have been chosen!")
273         elif public == 1:
274             conn.privmsg(nick, "Such aggression in public!")
275         else:
276             conn.notice(nick, "You're not my owner.")
277             
278     # google for something
279     def googleq(self, cmd, nick, conn, public):
280         cmdrest = string.join(cmd.split()[1:])
281         # "I'm Feeling Lucky" rather than try and parse the html
282         targ = ("http://www.google.com/search?q=%s&btnI=I'm+Feeling+Lucky"
283                 % urllib.quote_plus(cmdrest))
284         try:
285             # get redirected and grab the resulting url for returning
286             gsearch = urllib.urlopen(targ).geturl()
287             if gsearch != targ: # we've found something
288                 if public == 0:
289                     conn.notice(nick, str(gsearch))
290                 else: # we haven't found anything.
291                     conn.privmsg(self.channel, str(gsearch))
292             else:
293                 if public == 0:
294                     conn.notice(nick, "No pages found.")
295                 else:
296                     conn.privmsg(self.channel, "No pages found.")
297         except IOError: # if the connection times out. This blocks. :(
298             if public == 0:
299                 conn,notice(nick, "The web's broken. Waah!")
300             else:
301                 conn.privmsg(self.channel, "The web's broken. Waah!")
302                 
303     # General query handler
304     def do_command(self, nick, cmd, public=0):
305         conn = self.connection
306         # karma: up
307         if cmd.split()[0].endswith("++"):
308             self.karmaup(cmd)
309         
310         # karma: down
311         if cmd.split()[0].endswith("--"):
312             self.karmadown(cmd)
313             
314         # karma: query
315         if cmd.split()[0] == "karma" or cmd.split()[0] == "Karma":
316             self.karmaq(cmd, conn, nick, public)
317             
318         # bot's vital statistics
319         if cmd == "info":
320             self.infoq(cmd, nick, conn, public)
321
322         # Known commands
323         if cmd == "list":
324             self.listq(cmd, nick, conn, public)
325         # weaponry
326         if cmd.split()[0] == "trout":
327             self.troutq(cmd, nick, conn, public)
328         if cmd.split()[0] == "flirt":
329             self.flirtq(cmd, nick, conn, public)
330         if cmd == "reload":
331             self.reloadq(cmd, nick, conn, public)
332
333 #Disconnect disabled 'cos people hated it    
334 #        #disconnect
335 #        if cmd == "disconnect": # hop off for 60s
336 #            self.disconnect(msg="Be right back.")
337
338         # No more trout
339         if cmd.split()[0] == "quiet":
340             self.nofish(cmd,nick,conn, public)
341         
342         # say to msg/channel
343         elif cmd.split()[0] == "say" \
344              and irc_lower(nick) == irc_lower(self.owner):
345             conn.privmsg(self.channel, string.join(cmd.split()[1:]))
346
347         # action to msg/channel
348         elif cmd.split()[0] == "do" \
349              and irc_lower(nick) == irc_lower(self.owner):
350             conn.action(self.channel, string.join(cmd.split()[1:]))
351
352         # quit IRC
353         elif cmd == "die":
354             self.quit(cmd, nick, conn, public)
355
356         # Google!
357         elif (cmd.split()[0] == "google" or cmd.split()[0] == "Google"):
358             self.googleq(cmd, nick, conn, public)            
359
360 def main():
361     if len(sys.argv) != 5: # insufficient arguments
362         print "Usage: acrobat <server[:port]> <channel> <nickname> owner"
363         sys.exit(1)
364     sv_port = string.split(sys.argv[1], ":", 1) # tuple; (server, port)
365     server = sv_port[0]
366     if len(sv_port) == 2:
367         try:
368             port = int(sv_port[1])
369         except ValueError:
370             print "Error: Erroneous port."
371             sys.exit(1)
372     else:
373         port = 6667 # default irc port
374     channel = sys.argv[2]
375     nickname = sys.argv[3]
376     owner = sys.argv[4]
377     # initialize the bot
378     bot = Acrobat(channel, nickname, server, owner, port)
379     sys.stderr.write("Trying to connect...\n")
380     # and the event loop
381     bot.start()
382
383 if __name__ == "__main__":
384     main()