bricker !
[mikachu/bricker.git] / bricker.py
1 #!/usr/bin/python
2
3 # if you brick and have 0 -> kick from chan
4 # brick quantity must be queried, not automatically given
5 # if A bricks B, A--, B++
6 # cant give them nicely
7 # new person gets 5 bricks
8
9 import irclib
10
11 import config
12
13 class Bricker(object):
14
15   LISTEN_EVENTS = [
16     'welcome', 'disconnect', 'nicknameinuse', 'kick', 'privmsg', 'privnotice',
17     'bannedfromchan', 'erroneusnickname', 'join', 'namreply', 'nick', 'quit',
18     'part', 'userhost'
19     ]
20
21   OVERBRICKED_MESSAGE = 'Overbricked, haha!'
22   BRICKED_MESSAGE = '*** %s was bricked.'
23   PLEASE_REGISTER_MESSAGE = 'You need to register. Use register <password>.'
24   BAD_PASSWORD_MESSAGE = 'Bad password.'
25   REGISTERED_MESSAGE = 'Registered successfully.'
26   SHARED_CHANNEL_MESSAGE = 'Both you and your target must be in the channel.'
27   COUNT_MESSAGE = 'You have %d bricks remaining.'
28   HELP_MESSAGE = (
29     'Available commands are:\n'
30     '  register <password> - register yourself, or create a new password\n'
31     '  brick <nick> <channel> - brick someone in a channel\n'
32     '      (we must all be in the channel)\n'
33     '  count - tells you how many bricks you have left\n'
34     '  help - show this text\n')
35
36   def __init__(self, confignetwork, irc):
37     """A brickbot instance, which exists for an irc network..
38
39     Args:
40       confignetwork: A ConfigNetwork object.
41       irc: An irclib.IRC instance.
42     """
43     self.irc = irc
44     self.config = confignetwork
45     self._server_index = 0
46     self._nick = None
47     self._channels = {}
48     self.state = State(self.config.statefile)
49     self.state.load()
50
51   def run(self):
52     self._reconnect()
53
54   def _on_event(self, connection, event):
55     print 'event %s' % event.eventtype()
56
57   def _brick(self, connection, nick, target, channel):
58     if channel in self._channels:
59       fromuser = self.state.user(nick)
60       touser = self.state.user(target)
61       if not fromuser.registered:
62         connection.notice(nick, self.PLEASE_REGISTER_MESSAGE)
63       elif channel not in fromuser.visible or channel not in touser.visible:
64         connection.notice(nick, self.SHARED_CHANNEL_MESSAGE)
65       elif fromuser.bricks < 1:
66         # uh oh ! overbricked !
67         connection.kick(channel, nick, self.OVERBRICKED_MESSAGE)
68       else:
69         connection.privmsg(channel, self.BRICKED_MESSAGE % target)
70         fromuser.bricks -= 1
71         touser.bricks += 1
72         self.state.save()
73
74   def _register(self, connection, nick, password):
75     user = self.state.user(nick)
76     if not password or (user.password and password != user.password):
77       connection.notice(nick, self.BAD_PASSWORD_MESSAGE)
78       return
79     if not user.password:
80       user.password = password
81       self.state.save()
82     user.registered = True
83     user.lasthost = user.onhost
84     self.state.save()
85     connection.notice(nick, self.REGISTERED_MESSAGE)
86
87   def _count(self, connection, nick):
88     user = self.state.user(nick)
89     if not user.registered:
90       connection.notice(nick, self.PLEASE_REGISTER_MESSAGE)
91     else:
92       connection.notice(nick, self.COUNT_MESSAGE % user.bricks)
93
94   def _help(self, connection, nick):
95     for line in self.HELP_MESSAGE.split('\n'):
96       connection.notice(nick, line)
97
98   def _reconnect(self):
99     try:
100       server = self.config.servers[self._server_index]
101       self._nick = self.config.nickname
102       print 'Connecting to %s:%s' % (server.host, server.port)
103       conn = self.irc.server().connect(server.host, server.port, self._nick)
104       for s in self.LISTEN_EVENTS:
105         conn.add_global_handler(s, getattr(self, '_on_%s' % s), -10)
106       conn.add_global_handler('all_events', self._on_event, -30)
107     except irclib.ServerConnectionError:
108       self._server_index = (self._server_index + 1) % len(self.config.servers)
109       net.connection.execute_delayed(net.reconnect_delay,
110                                      self._reconnect)
111
112   def _rejoin(self, connection, channel):
113     password = ''
114     for c in self.config.channels:
115       if channel == c.name:
116         password = c.password
117         break
118     print 'rejoining %s with password %s' % (channel, password)
119     connection.join(channel, password)
120
121   def _on_welcome(self, connection, event):
122     print 'welcome %s' % event
123     for s in self.config.autocommands:
124       connection.send_raw(s)
125     if self.config.nickservpw:
126       connection.privmsg('nickserv', 'identify %s %s' % (
127         self.config.nickservpw, self.config.nickname))
128     for c in self.config.channels:
129       connection.join(c.name, c.password)
130   
131   def _on_disconnect(self, connection, event):
132     print 'disconnect %s' % event
133     connection.execute_delayed(self.config.reconnect_delay,
134                                self._reconnect)
135
136   def _on_erroneusnickname(self, connection, event):
137     print 'unable to use nickname'
138                                
139   def _on_nicknameinuse(self, connection, event):
140     print 'nicknameinuse %s' % event.target()
141     if not self.config.nickservpw:
142       self._nick += '_'
143       print 'using nickname %s' % self._nick 
144       connection.nick(self._nick)
145
146   def _on_kick(self, connection, event):
147     nick = event.arguments()[0]
148     channel = event.target()
149     print 'kick %s from %s' % (nick, channel)
150     if nick == self._nick:
151       self._channels[channel] = False
152       connection.execute_delayed(self.config.rejoin_delay, self._rejoin,
153                                  (connection, event.target()))
154     else:
155       self._rm_nick(connection, channel, nick)
156
157   def _on_bannedfromchan(self, connection, event):
158     print 'bannedfromchan %s' % event.arguments()[0]
159     connection.execute_delayed(self.config.rejoin_delay, self._rejoin,
160                                (connection, event.arguments()[0]))
161
162   def _on_privmsg(self, connection, event):
163     print 'privmsg %s' % event.target()
164     print '  %s' % event.source()
165     print '  %s' % event.arguments()
166     data = event.arguments()[0].split(' ')
167     nick = irclib.nm_to_n(event.source())
168     cmd = data[0]
169     args = data[1:]
170     if cmd == 'brick' and len(args) == 2:
171       target = args[0]
172       channel = args[1]
173       self._brick(connection, nick, target, channel)
174     if cmd == 'register' and len(args) == 1:
175       password = args[0]
176       self._register(connection, nick, password)
177     if cmd == 'count':
178       self._count(connection, nick)
179     if cmd == 'help':
180       self._help(connection, nick)
181
182   def _on_privnotice(self, connection, event):
183     print 'privnotice %s' % event.target()
184     print '  %s' % event.source()
185     print '  %s' % event.arguments()
186     data = event.arguments()[0].split(' ')
187     cmd = data[0]
188     args = data[1:]
189
190   def _add_nick(self, connection, channel, nick, host=None):
191     print 'adding nick %s (%s)' % (nick, host)
192     user = self.state.user(nick)
193     user.visible[channel] = True
194     user.onhost = host
195     if host:
196       if host == user.lasthost:
197         user.registered = True
198         print 'registered %s by host %s' % (nick, host)
199       user.ignore_userhost_reply = True
200     else:
201       connection.userhost([nick])  # get their userhost
202       user.ignore_userhost_reply = False
203     self.state.save()
204
205   def _rm_nick(self, connection, channel, nick):
206     print 'removing nick %s' % nick
207     user = self.state.user(nick)
208     del user.visible[channel]
209     if not user.visible:
210       user.registered = False
211       user.onhost = None
212
213   def _on_join(self, connection, event):
214     nick, host = event.source().split('!', 1)
215     channel = event.target()
216     print 'join %s (%s) on %s' % (nick, host, channel)
217     if nick == self._nick:
218       self._channels[channel] = True
219     else:
220       self._add_nick(connection, channel, nick, host=host)
221
222   def _on_namreply(self, connection, event):
223     nicks = event.arguments()[2].split(' ')
224     channel = event.arguments()[1]
225     print 'namreply found nicks %s' % nicks
226     nicks = [n.strip('@+ ') for n in nicks]
227     for nick in nicks:
228       if nick != self._nick:
229         self._add_nick(connection, channel, nick)
230
231   def _on_nick(self, connection, event):
232     tonick = event.target()
233     fromnick = irclib.nm_to_n(event.source())
234     host = irclib.nm_to_uh(event.source())
235     print 'nick %s -> %s' % fromnick, tonick
236     if tonick != self._nick:
237       for c in self._channels:
238         user = self.state.user(fromnick)
239         if c in user.visible:
240           self._rm_nick(connection, c, fromnick)
241           self._add_nick(connection, c, tonick, host=host)
242
243   def _on_userhost(self, connection, event):
244     if not event.arguments()[0]: return
245     nick, host = event.arguments()[0].split('=', 1)
246     nick = nick.strip('* ')
247     host = host.strip('+- ')
248     print 'userhost for %s is %s' % (nick, host)
249     user = self.state.user(nick)
250     if not user.ignore_userhost_reply:
251       for c in user.visible:
252         self._add_nick(connection, c, nick, host=host)
253
254   def _on_quit(self, connection, event):
255     nick = irclib.nm_to_n(event.source())
256     print 'quit %s' % nick
257     if nick == self._nick:
258       self._channels[channel] = False
259     else:
260       user = self.state.user(nick)
261       for c in user.visible:
262         self._rm_nick(connection, c, nick)
263
264   def _on_part(self, connection, event):
265     nick = irclib.nm_to_n(event.source())
266     channel = event.target()
267     print 'part %s from %s' % (nick, channel)
268     if nick == self._nick:
269       self._channels[channel] = False
270     else:
271       self._rm_nick(connection, channel, nick)
272
273
274 class State(object):
275
276   def __init__(self, filename):
277     # a dictionary of User objects keyed by their nicknames
278     self._users = {}
279     self._filename = filename
280
281   def load(self):
282     if not self._filename:
283       print 'WARNING: not using a state file'
284       return
285     try:
286       f = open(self._filename, 'r+')
287     except IOError, e:
288       print 'IOError: %s' % e
289       print 'WARNING: not using a state file'
290       return
291
292     curnick = None
293     for line in f:
294       data = line.strip().split(' ', 1)
295       if data[0] == 'nick' and len(data) > 1:
296         curnick = data[1]
297       if curnick and data[0]  == 'lasthost' and len(data) > 1:
298         self.user(curnick).lasthost = data[1]
299       if curnick and data[0]  == 'bricks' and len(data) > 1:
300         self.user(curnick).bricks = int(data[1])
301       if curnick and data[0]  == 'password' and len(data) > 1:
302         self.user(curnick).password = data[1]
303     f.close()
304
305   def save(self):
306     if not self._filename: return
307     try:
308       f = open(self._filename, 'w')
309     except IOError, e:
310       print 'IOError: %s' % e
311       print 'WARNING: failed to write state file'
312       return
313     for u in self._users.values():
314       f.write('nick %s\n' % u.nick)
315       if u.lasthost: f.write('lasthost %s\n' % u.lasthost)
316       if u.password: f.write('password %s\n' % u.password)
317       f.write('bricks %s\n' % u.bricks)
318     f.close()
319
320   def user(self, nick):
321     nick = irclib.irc_lower(nick)
322     if nick not in self._users:
323       user = State.User(self, nick)
324       self._users[nick] = user
325       self.save()
326     else:
327       user = self._users[nick]
328     return user
329   
330   class User(object):
331
332     def __init__(self, state, nick):
333       self.state = state
334       self.nick = nick
335       self.onhost = None
336       self.lasthost = None
337       self.registered = False
338       self.visible = {}
339       self.ignore_userhost_reply = True
340       self.bricks = 5
341       self.password = None
342
343
344 def main():
345   irc = irclib.IRC()
346   for n in config.Config.networks:
347     b = Bricker(n, irc)
348     b.run()
349   irc.process_forever()
350
351 if __name__ == '__main__':
352   main()