summaryrefslogtreecommitdiff
path: root/moo.py
blob: b0f76291b863e7ea9b9194cb3bbf32a19e4b8cb3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
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()