summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xmoo.py324
-rw-r--r--mootest.yml19
2 files changed, 343 insertions, 0 deletions
diff --git a/moo.py b/moo.py
new file mode 100755
index 0000000..b0f7629
--- /dev/null
+++ b/moo.py
@@ -0,0 +1,324 @@
+#!/usr/bin/env python
+# coding: utf8
+
+from twisted.words.protocols import irc
+from twisted.internet import stdio, reactor, protocol
+from twisted.internet.error import ConnectionDone
+from twisted.python import log
+from twisted.protocols.basic import LineReceiver
+
+import yaml
+from sqlalchemy import *
+from sqlalchemy import exceptions
+
+import time, sys, os
+import random
+import datetime
+import re
+
+def load_config():
+ content = file(sys.argv[1]).read()
+ return yaml.load(content)
+
+def add_network(network, network_conf):
+ f = MooBotFactory(network, network_conf, console)
+ reactor.connectTCP(network_conf["host"], int(network_conf["port"]), f)
+
+def parse_channel (channel):
+ """Returns (channel, keyword)"""
+ if ":" in channel:
+ return channel.split (":", 1)
+ else:
+ return channel, None
+
+class MooBotConsole(LineReceiver):
+
+ delimiter = '\n'
+ prompt_string = '>>> '
+ bindings = None
+ networks = None
+
+ def __init__(self):
+ self.bindings = {}
+ self.networks = {}
+
+ def prompt(self):
+ self.transport.write(self.prompt_string)
+
+ def connectionMade(self):
+ self.sendLine('MooBot console')
+ self.prompt()
+
+ def output_data(self, network, data):
+ network_name, nick = network.get_network_info()
+ self.sendLine(("\n(%s@%s) " % (nick, network_name)) + data)
+ self.prompt()
+
+ def reload_config(self):
+ global conf
+ old_conf = conf
+ old_networks = old_conf['networks']
+ conf = load_config()
+ networks = conf['networks']
+ for network in networks:
+ if network in old_networks:
+ network_obj = self.networks[network]
+ if networks[network] == old_networks[network]:
+ continue
+ self.unregister_network(network_obj)
+ # Host/port changes are ignored
+ network_obj.factory.conf = networks[network]
+ channels = networks[network]['channels']
+ old_channels = old_networks[network]['channels']
+ for channel in channels:
+ if channel not in old_channels:
+ channel, keyword = parse_channel (channel)
+ network_obj.join(channel, key = keyword)
+ for channel in old_channels:
+ if channel not in channels:
+ channel, keyword = parse_channel (channel)
+ network_obj.leave(channel)
+ self.register_network(network_obj)
+ else:
+ add_network(network, networks[network])
+ for network in old_networks:
+ if network not in networks:
+ self.networks[network].quit()
+
+ def lineReceived(self, line):
+ if not line:
+ self.prompt()
+ return
+ elif line == "quit":
+ self.transport.loseConnection()
+ return
+ elif line == "reload":
+ self.reload_config()
+ self.prompt()
+ return
+ elif line == "show":
+ for binding in self.bindings:
+ network, nick, channel = binding
+ try:
+ event = self.bindings[binding].schedules[channel]
+ date = datetime.datetime.fromtimestamp(event.time)
+ self.sendLine("%s:%s@%s : Call scheduled at %s" \
+ % (nick, channel, network, date))
+ except:
+ pass
+ self.prompt()
+ return
+ elif line == "stats":
+ for binding in self.bindings:
+ network, nick, channel = binding
+ stats = self.bindings[binding].get_channel_stats(channel)
+ rank = 1
+ for user, kills, misses in stats:
+ self.sendLine("%s:%s@%s : %d. %s (%d kills, %d misses)"\
+ % (nick, channel, network, rank, user,
+ kills, misses))
+ rank += 1
+ self.prompt()
+ return
+ try:
+ bits = line.split(" ")
+ assert len(bits) == 5
+ assert bits[0] == "schedule"
+ network_name = bits[1]
+ nick = bits[2]
+ channel = bits[3]
+ delay = float(bits[4])
+ network = self.bindings[network_name, nick, channel]
+ network.schedule_call(channel, delay)
+ except:
+ exctype, value = sys.exc_info ()[:2]
+ print "Error: %s : %s" % (exctype, value)
+ self.show_help()
+
+ def show_help(self):
+ self.sendLine('Usage : - schedule network nick channel delay_minutes')
+ self.sendLine(' - show')
+ self.sendLine(' - stats')
+ self.sendLine(' - reload')
+ self.sendLine(' - quit')
+ self.prompt()
+
+ def register_network(self, network):
+ name, nick = network.get_network_info()
+ self.networks[name] = network
+ for channel in network.factory.conf['channels']:
+ channel, keyword = parse_channel (channel)
+ self.bindings[(name, nick, "#" + channel)] = network
+
+ def unregister_network(self, network):
+ name, nick = network.get_network_info()
+ del self.networks[name]
+ for channel in network.factory.conf['channels']:
+ channel, keyword = parse_channel (channel)
+ del self.bindings[(name, nick, "#" + channel)]
+
+ def connectionLost(self, reason):
+ reactor.stop()
+
+class MooBot(irc.IRCClient):
+
+ can_be_killed = None
+ schedules = None
+
+ def connectionMade(self):
+ self.can_be_killed = {}
+ self.schedules = {}
+ self.nickname = self.factory.conf['nick']
+ self.db = create_engine('sqlite:///' + self.factory.conf['kills_db'])
+ self.metadata = MetaData(self.db)
+ self.bot_column = Column('bot', String(20))
+ self.nickname_column = Column('nickname', String(20))
+ self.kills_table = Table('kills', self.metadata,
+ self.bot_column,
+ self.nickname_column,
+ Column('kills', Integer),
+ Column('misses', Integer))
+ self.kills_table.create(checkfirst = True)
+ self.factory.console.register_network(self)
+ irc.IRCClient.connectionMade(self)
+
+ def connectionLost(self, reason):
+ self.factory.console.unregister_network(self)
+ irc.IRCClient.connectionLost(self, reason)
+
+ def privmsg(self, user, channel, msg):
+ user = user.split('!', 1)[0]
+ msg = msg.lower()
+ if channel not in self.can_be_killed \
+ or not self.can_be_killed[channel] \
+ or not any([word in msg \
+ for word in self.factory.conf['kill_words']]):
+ return
+ p = random.random()
+ if p > float(self.factory.conf['kill_min_rand']):
+ self.log_kill(channel, user)
+ self.say(channel, "I was slain by %s." % user)
+ self.output("Kill by %s in %s " % (user, channel))
+ self.schedule_call_rand(channel)
+ else:
+ self.log_miss(channel, user)
+ self.say(channel, "I avoided a bad shot from %s !!!" % user)
+ self.output("Miss by %s in %s" % (user, channel))
+
+ def get_network_info(self):
+ return self.factory.network, self.factory.conf['nick']
+
+ def log_kill(self, channel, user):
+ network_name, bot_nick = self.get_network_info()
+ bot = "%s:%s@%s" % (bot_nick, channel, network_name)
+ row = self.get_db_row(bot, user)
+ if not row:
+ i = self.kills_table.insert()
+ i.execute(bot = bot, nickname = user,
+ kills = 1, misses = 0)
+ else:
+ u = self.kills_table.update()
+ u = u.where(self.bot_column == bot \
+ and self.nickname_column == user)
+ u.values(kills = row.kills + 1).execute()
+
+ def log_miss(self, channel, user):
+ network_name, bot_nick = self.get_network_info()
+ bot = "%s:%s@%s" % (bot_nick, channel, network_name)
+ row = self.get_db_row(bot, user)
+ if not row:
+ i = self.kills_table.insert()
+ i.execute(bot = bot, nickname = user,
+ kills = 0, misses = 1)
+ else:
+ u = self.kills_table.update()
+ u = u.where(self.bot_column == bot \
+ and self.nickname_column == user)
+ u.values(misses = row.misses + 1).execute()
+
+ def get_channel_stats(self, channel):
+ network_name, bot_nick = self.get_network_info()
+ bot = "%s:%s@%s" % (bot_nick, channel, network_name)
+ s = self.kills_table.select(self.bot_column == bot)
+ s = s.order_by(self.kills_table.c.kills.desc())
+ return [(str(row.nickname), row.kills, row.misses)
+ for row in s.execute()]
+
+ def get_db_row(self, bot, user):
+ s = self.kills_table.select(self.bot_column == bot \
+ and self.nickname_column == user)
+ return s.execute().fetchone()
+
+ def signedOn(self):
+ for channel in self.factory.conf["channels"]:
+ channel, keyword = parse_channel (channel)
+ self.join(channel, key = keyword)
+
+ def joined(self, channel):
+ self.schedule_call_rand(channel)
+
+ def leave(self, channel):
+ if channel in self.can_be_killed:
+ del self.can_be_killed[channel]
+ if channel in self.schedules:
+ self.schedules[channel].cancel()
+ del self.schedules[channel]
+ irc.IRCClient.leave(self, channel)
+
+ def call(self, channel):
+ self.output("Called in %s" % channel)
+ self.can_be_killed[channel] = True
+ del self.schedules[channel]
+ call_word = random.choice(self.factory.conf['call_words']).rstrip()
+ if call_word[0] == "\"":
+ call_word = call_word[1:-1]
+ call_word += "\n"
+ for line in call_word.split("\n"):
+ self.say(channel, str(line).rstrip())
+
+ def schedule_call_rand(self, channel):
+ delay = random.random() * float(self.factory.conf['call_max_delay'])
+ self.schedule_call(channel, delay)
+
+ def schedule_call(self, channel, delay):
+ self.can_be_killed[channel] = False
+ if channel in self.schedules:
+ self.schedules[channel].cancel()
+ event = reactor.callLater(60 * delay, self.call, channel)
+ self.schedules[channel] = event
+ date = datetime.datetime.now() + datetime.timedelta(0, 60 * delay)
+ self.output("Call in %s scheduled in %.02f minutes (@ %s)" \
+ % (channel, delay, date))
+
+ def output(self, data):
+ self.factory.console.output_data(self, data)
+
+class MooBotFactory(protocol.ClientFactory):
+
+ protocol = MooBot
+
+ def __init__(self, network, conf, console):
+ self.network = network
+ self.conf = conf
+ self.console = console
+
+ def clientConnectionLost(self, connector, reason):
+ if not isinstance(reason.value, ConnectionDone):
+ connector.connect()
+
+ def clientConnectionFailed(self, connector, reason):
+ print "Connection failed :", reason
+ connector.connect() # Try to reconnect
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ print "Usage : %s <moo.yml>" % sys.argv[0]
+ sys.exit()
+ conf = load_config()
+ console = MooBotConsole()
+ stdio.StandardIO(console)
+ networks = conf['networks']
+ for network in networks:
+ network_conf = networks[network]
+ add_network(network, network_conf)
+ reactor.run()
diff --git a/mootest.yml b/mootest.yml
new file mode 100644
index 0000000..6048a2c
--- /dev/null
+++ b/mootest.yml
@@ -0,0 +1,19 @@
+networks:
+ ulminfo_cochon:
+ host: irc.bla.fr
+ port: 6667
+ nick: cochon
+ channels:
+ - bla
+ call_max_delay: 240
+ call_words:
+ - Gruiiiiiiik
+ - Gruik Gruik !
+ - |
+ " (\____/)
+ / @__@ \
+ ( (oo) )
+ `-.~~.-'"
+ kill_words: [swift, shlak, zing]
+ kills_db: 'kills.db'
+ kill_min_rand: 0.33