bricker !
authorDana Jansens <danakj@orodu.net>
Tue, 14 Sep 2010 09:48:03 +0000 (05:48 -0400)
committerDana Jansens <danakj@orodu.net>
Tue, 14 Sep 2010 09:48:19 +0000 (05:48 -0400)
bricker.py [new file with mode: 0755]
config.py [new file with mode: 0644]

diff --git a/bricker.py b/bricker.py
new file mode 100755 (executable)
index 0000000..33596aa
--- /dev/null
@@ -0,0 +1,352 @@
+#!/usr/bin/python
+
+# if you brick and have 0 -> kick from chan
+# brick quantity must be queried, not automatically given
+# if A bricks B, A--, B++
+# cant give them nicely
+# new person gets 5 bricks
+
+import irclib
+
+import config
+
+class Bricker(object):
+
+  LISTEN_EVENTS = [
+    'welcome', 'disconnect', 'nicknameinuse', 'kick', 'privmsg', 'privnotice',
+    'bannedfromchan', 'erroneusnickname', 'join', 'namreply', 'nick', 'quit',
+    'part', 'userhost'
+    ]
+
+  OVERBRICKED_MESSAGE = 'Overbricked, haha!'
+  BRICKED_MESSAGE = '*** %s was bricked.'
+  PLEASE_REGISTER_MESSAGE = 'You need to register. Use register <password>.'
+  BAD_PASSWORD_MESSAGE = 'Bad password.'
+  REGISTERED_MESSAGE = 'Registered successfully.'
+  SHARED_CHANNEL_MESSAGE = 'Both you and your target must be in the channel.'
+  COUNT_MESSAGE = 'You have %d bricks remaining.'
+  HELP_MESSAGE = (
+    'Available commands are:\n'
+    '  register <password> - register yourself, or create a new password\n'
+    '  brick <nick> <channel> - brick someone in a channel\n'
+    '      (we must all be in the channel)\n'
+    '  count - tells you how many bricks you have left\n'
+    '  help - show this text\n')
+
+  def __init__(self, confignetwork, irc):
+    """A brickbot instance, which exists for an irc network..
+
+    Args:
+      confignetwork: A ConfigNetwork object.
+      irc: An irclib.IRC instance.
+    """
+    self.irc = irc
+    self.config = confignetwork
+    self._server_index = 0
+    self._nick = None
+    self._channels = {}
+    self.state = State(self.config.statefile)
+    self.state.load()
+
+  def run(self):
+    self._reconnect()
+
+  def _on_event(self, connection, event):
+    print 'event %s' % event.eventtype()
+
+  def _brick(self, connection, nick, target, channel):
+    if channel in self._channels:
+      fromuser = self.state.user(nick)
+      touser = self.state.user(target)
+      if not fromuser.registered:
+        connection.notice(nick, self.PLEASE_REGISTER_MESSAGE)
+      elif channel not in fromuser.visible or channel not in touser.visible:
+        connection.notice(nick, self.SHARED_CHANNEL_MESSAGE)
+      elif fromuser.bricks < 1:
+        # uh oh ! overbricked !
+        connection.kick(channel, nick, self.OVERBRICKED_MESSAGE)
+      else:
+        connection.privmsg(channel, self.BRICKED_MESSAGE % target)
+        fromuser.bricks -= 1
+        touser.bricks += 1
+        self.state.save()
+
+  def _register(self, connection, nick, password):
+    user = self.state.user(nick)
+    if not password or (user.password and password != user.password):
+      connection.notice(nick, self.BAD_PASSWORD_MESSAGE)
+      return
+    if not user.password:
+      user.password = password
+      self.state.save()
+    user.registered = True
+    user.lasthost = user.onhost
+    self.state.save()
+    connection.notice(nick, self.REGISTERED_MESSAGE)
+
+  def _count(self, connection, nick):
+    user = self.state.user(nick)
+    if not user.registered:
+      connection.notice(nick, self.PLEASE_REGISTER_MESSAGE)
+    else:
+      connection.notice(nick, self.COUNT_MESSAGE % user.bricks)
+
+  def _help(self, connection, nick):
+    for line in self.HELP_MESSAGE.split('\n'):
+      connection.notice(nick, line)
+
+  def _reconnect(self):
+    try:
+      server = self.config.servers[self._server_index]
+      self._nick = self.config.nickname
+      print 'Connecting to %s:%s' % (server.host, server.port)
+      conn = self.irc.server().connect(server.host, server.port, self._nick)
+      for s in self.LISTEN_EVENTS:
+        conn.add_global_handler(s, getattr(self, '_on_%s' % s), -10)
+      conn.add_global_handler('all_events', self._on_event, -30)
+    except irclib.ServerConnectionError:
+      self._server_index = (self._server_index + 1) % len(self.config.servers)
+      net.connection.execute_delayed(net.reconnect_delay,
+                                     self._reconnect)
+
+  def _rejoin(self, connection, channel):
+    password = ''
+    for c in self.config.channels:
+      if channel == c.name:
+        password = c.password
+        break
+    print 'rejoining %s with password %s' % (channel, password)
+    connection.join(channel, password)
+
+  def _on_welcome(self, connection, event):
+    print 'welcome %s' % event
+    for s in self.config.autocommands:
+      connection.send_raw(s)
+    if self.config.nickservpw:
+      connection.privmsg('nickserv', 'identify %s %s' % (
+        self.config.nickservpw, self.config.nickname))
+    for c in self.config.channels:
+      connection.join(c.name, c.password)
+  
+  def _on_disconnect(self, connection, event):
+    print 'disconnect %s' % event
+    connection.execute_delayed(self.config.reconnect_delay,
+                               self._reconnect)
+
+  def _on_erroneusnickname(self, connection, event):
+    print 'unable to use nickname'
+                               
+  def _on_nicknameinuse(self, connection, event):
+    print 'nicknameinuse %s' % event.target()
+    if not self.config.nickservpw:
+      self._nick += '_'
+      print 'using nickname %s' % self._nick 
+      connection.nick(self._nick)
+
+  def _on_kick(self, connection, event):
+    nick = event.arguments()[0]
+    channel = event.target()
+    print 'kick %s from %s' % (nick, channel)
+    if nick == self._nick:
+      self._channels[channel] = False
+      connection.execute_delayed(self.config.rejoin_delay, self._rejoin,
+                                 (connection, event.target()))
+    else:
+      self._rm_nick(connection, channel, nick)
+
+  def _on_bannedfromchan(self, connection, event):
+    print 'bannedfromchan %s' % event.arguments()[0]
+    connection.execute_delayed(self.config.rejoin_delay, self._rejoin,
+                               (connection, event.arguments()[0]))
+
+  def _on_privmsg(self, connection, event):
+    print 'privmsg %s' % event.target()
+    print '  %s' % event.source()
+    print '  %s' % event.arguments()
+    data = event.arguments()[0].split(' ')
+    nick = irclib.nm_to_n(event.source())
+    cmd = data[0]
+    args = data[1:]
+    if cmd == 'brick' and len(args) == 2:
+      target = args[0]
+      channel = args[1]
+      self._brick(connection, nick, target, channel)
+    if cmd == 'register' and len(args) == 1:
+      password = args[0]
+      self._register(connection, nick, password)
+    if cmd == 'count':
+      self._count(connection, nick)
+    if cmd == 'help':
+      self._help(connection, nick)
+
+  def _on_privnotice(self, connection, event):
+    print 'privnotice %s' % event.target()
+    print '  %s' % event.source()
+    print '  %s' % event.arguments()
+    data = event.arguments()[0].split(' ')
+    cmd = data[0]
+    args = data[1:]
+
+  def _add_nick(self, connection, channel, nick, host=None):
+    print 'adding nick %s (%s)' % (nick, host)
+    user = self.state.user(nick)
+    user.visible[channel] = True
+    user.onhost = host
+    if host:
+      if host == user.lasthost:
+        user.registered = True
+        print 'registered %s by host %s' % (nick, host)
+      user.ignore_userhost_reply = True
+    else:
+      connection.userhost([nick])  # get their userhost
+      user.ignore_userhost_reply = False
+    self.state.save()
+
+  def _rm_nick(self, connection, channel, nick):
+    print 'removing nick %s' % nick
+    user = self.state.user(nick)
+    del user.visible[channel]
+    if not user.visible:
+      user.registered = False
+      user.onhost = None
+
+  def _on_join(self, connection, event):
+    nick, host = event.source().split('!', 1)
+    channel = event.target()
+    print 'join %s (%s) on %s' % (nick, host, channel)
+    if nick == self._nick:
+      self._channels[channel] = True
+    else:
+      self._add_nick(connection, channel, nick, host=host)
+
+  def _on_namreply(self, connection, event):
+    nicks = event.arguments()[2].split(' ')
+    channel = event.arguments()[1]
+    print 'namreply found nicks %s' % nicks
+    nicks = [n.strip('@+ ') for n in nicks]
+    for nick in nicks:
+      if nick != self._nick:
+        self._add_nick(connection, channel, nick)
+
+  def _on_nick(self, connection, event):
+    tonick = event.target()
+    fromnick = irclib.nm_to_n(event.source())
+    host = irclib.nm_to_uh(event.source())
+    print 'nick %s -> %s' % fromnick, tonick
+    if tonick != self._nick:
+      for c in self._channels:
+        user = self.state.user(fromnick)
+        if c in user.visible:
+          self._rm_nick(connection, c, fromnick)
+          self._add_nick(connection, c, tonick, host=host)
+
+  def _on_userhost(self, connection, event):
+    if not event.arguments()[0]: return
+    nick, host = event.arguments()[0].split('=', 1)
+    nick = nick.strip('* ')
+    host = host.strip('+- ')
+    print 'userhost for %s is %s' % (nick, host)
+    user = self.state.user(nick)
+    if not user.ignore_userhost_reply:
+      for c in user.visible:
+        self._add_nick(connection, c, nick, host=host)
+
+  def _on_quit(self, connection, event):
+    nick = irclib.nm_to_n(event.source())
+    print 'quit %s' % nick
+    if nick == self._nick:
+      self._channels[channel] = False
+    else:
+      user = self.state.user(nick)
+      for c in user.visible:
+        self._rm_nick(connection, c, nick)
+
+  def _on_part(self, connection, event):
+    nick = irclib.nm_to_n(event.source())
+    channel = event.target()
+    print 'part %s from %s' % (nick, channel)
+    if nick == self._nick:
+      self._channels[channel] = False
+    else:
+      self._rm_nick(connection, channel, nick)
+
+
+class State(object):
+
+  def __init__(self, filename):
+    # a dictionary of User objects keyed by their nicknames
+    self._users = {}
+    self._filename = filename
+
+  def load(self):
+    if not self._filename:
+      print 'WARNING: not using a state file'
+      return
+    try:
+      f = open(self._filename, 'r+')
+    except IOError, e:
+      print 'IOError: %s' % e
+      print 'WARNING: not using a state file'
+      return
+
+    curnick = None
+    for line in f:
+      data = line.strip().split(' ', 1)
+      if data[0] == 'nick' and len(data) > 1:
+        curnick = data[1]
+      if curnick and data[0]  == 'lasthost' and len(data) > 1:
+        self.user(curnick).lasthost = data[1]
+      if curnick and data[0]  == 'bricks' and len(data) > 1:
+        self.user(curnick).bricks = int(data[1])
+      if curnick and data[0]  == 'password' and len(data) > 1:
+        self.user(curnick).password = data[1]
+    f.close()
+
+  def save(self):
+    if not self._filename: return
+    try:
+      f = open(self._filename, 'w')
+    except IOError, e:
+      print 'IOError: %s' % e
+      print 'WARNING: failed to write state file'
+      return
+    for u in self._users.values():
+      f.write('nick %s\n' % u.nick)
+      if u.lasthost: f.write('lasthost %s\n' % u.lasthost)
+      if u.password: f.write('password %s\n' % u.password)
+      f.write('bricks %s\n' % u.bricks)
+    f.close()
+
+  def user(self, nick):
+    nick = irclib.irc_lower(nick)
+    if nick not in self._users:
+      user = State.User(self, nick)
+      self._users[nick] = user
+      self.save()
+    else:
+      user = self._users[nick]
+    return user
+  
+  class User(object):
+
+    def __init__(self, state, nick):
+      self.state = state
+      self.nick = nick
+      self.onhost = None
+      self.lasthost = None
+      self.registered = False
+      self.visible = {}
+      self.ignore_userhost_reply = True
+      self.bricks = 5
+      self.password = None
+
+
+def main():
+  irc = irclib.IRC()
+  for n in config.Config.networks:
+    b = Bricker(n, irc)
+    b.run()
+  irc.process_forever()
+
+if __name__ == '__main__':
+  main()
diff --git a/config.py b/config.py
new file mode 100644 (file)
index 0000000..b23f58f
--- /dev/null
+++ b/config.py
@@ -0,0 +1,68 @@
+class ConfigChannel(object):
+
+  def __init__(self, name, password=''):
+    self.name = name
+    self.password = password
+
+class ConfigServer(object):
+  
+  def __init__(self, host, port=6667):
+    """Create a server for the config, which is part of a network.
+
+    Args:
+      host: The host to connect to for the server
+      port: The port to connect to on the host
+    """
+    self.host = host
+    self.port = port
+
+class ConfigNetwork(object):
+  
+  def __init__(self, statefile, servers, channels, nickname, realname,
+               nickservpw=None, autocommands=[], reconnect=None, rejoin=None):
+    """Create a network for the config.
+
+    Args:
+      statefile: The name of the file to load/save state
+      servers: A list of ConfigServer objects
+      channels: A list of ConfigChannel objects
+      nickname: A string to use as a nickname
+      realname: A string to use as a real name
+      nickservpw: A string password for the nickserv service, if available
+      autocommands: A list of strings, which are commands to send to the server
+        on connect.
+      reconnect: An optional value to specify the number of seconds to wait
+        before reconnecting.  If not set, it will not reconnect.
+      rejoin: An optional value to specify the number of seconds to wait
+        before rejoining a channel.  If not set, it will not rejoin.
+    """
+    self.statefile = statefile
+    self.servers = servers
+    self.channels = channels
+    self.nickname = nickname
+    self.realname = realname
+    self.nickservpw = nickservpw
+    self.autocommands = autocommands
+    self.reconnect_delay = reconnect
+    self.rejoin_delay = rejoin
+
+class Config:
+  """Configuration options for the brick bot."""
+
+  networks = [
+      ConfigNetwork(
+          statefile='bricker-oftc.state',
+          servers=[
+              ConfigServer('irc.oftc.net'),
+          ],
+          channels=[
+              ConfigChannel('#bling'),
+          ],
+          nickname='bricker',
+          realname='brickerbot',
+          nickservpw='_br1ck3r_1z_m1n3_',
+          autocommands=[
+          ],
+          reconnect=10,
+          rejoin=5),
+      ]