diff options
author | Guillaume Seguin <guillaume@segu.in> | 2010-07-29 19:39:10 -0400 |
---|---|---|
committer | Guillaume Seguin <guillaume@segu.in> | 2010-07-29 19:39:10 -0400 |
commit | 4c5b1dd9505a3c10937cc9d6bcb4d33b5f512227 (patch) | |
tree | dc6492db1bcf0afd358a1b0a2a15aea2f66a41e7 | |
download | moobot-master.tar.gz moobot-master.tar.bz2 |
-rwxr-xr-x | moo.py | 324 | ||||
-rw-r--r-- | mootest.yml | 19 |
2 files changed, 343 insertions, 0 deletions
@@ -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 |