root/fuktommy.com/trunk/niconico/nicolivealertirc.py

Revision 259, 11.4 KB (checked in by fuktommy, 5 months ago)

投稿したあと時間を置く。

  • Property svn:keywords set to Id Revision
Line 
1#!/usr/bin/python
2"""Nico Live Alert to IRC.
3
4Help:
5    nicolivealertirc.py --help
6
7Require:
8    nicolivealert.py
9    python-irclib
10"""
11#
12# Copyright (c) 2009 Satoshi Fukutomi <info@fuktommy.com>.
13# All rights reserved.
14#
15# Redistribution and use in source and binary forms, with or without
16# modification, are permitted provided that the following conditions
17# are met:
18# 1. Redistributions of source code must retain the above copyright
19#    notice, this list of conditions and the following disclaimer.
20# 2. Redistributions in binary form must reproduce the above copyright
21#    notice, this list of conditions and the following disclaimer in the
22#    documentation and/or other materials provided with the distribution.
23#
24# THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS ``AS IS'' AND
25# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE
28# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
30# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
33# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
34# SUCH DAMAGE.
35#
36# $Id$
37#
38
39import optparse
40import random
41import re
42import sqlite3
43import time
44import traceback
45from threading import Thread
46
47import ircbot
48import irclib
49
50import nicolivealert
51
52
53__version__ = "$Revision$"
54__all__ = []
55VERSION = __version__[11:-1].strip()
56
57
58class NicoAlertIRCBot(ircbot.SingleServerIRCBot):
59    """Nico Live Alert to IRC Bot.
60    """
61    channel = None
62    encoding = 'utf8'
63    filter = None
64    recommend = None
65    alert = None
66    actions = [
67        'add',
68        'delete',
69        'list',
70        'rate_set',
71        'rate_show',
72        'reconnect',
73        'help',
74    ]
75
76    def on_welcome(self, irc, e):
77        irc.join(self.channel)
78        self.post("Hi. I'm Nico Live Alert Bot.")
79
80    def on_pubmsg(self, irc, e):
81        try:
82            msg = e.arguments()[0]
83            for act in self.actions:
84                if getattr(self, 'do_' + act)(msg):
85                    return
86        except:
87            self.post('[error]')
88            traceback.print_exc()
89
90    def do_add(self, msg):
91        found = re.search(r'^add\s+(c[ho]\d+)', msg)
92        if not found:
93            return False
94        self.filter.add(found.group(1))
95        return True
96
97    def do_delete(self, msg):
98        found = re.search(r'^delete\s+(c[ho]\d+)', msg)
99        if not found:
100            return False
101        self.filter.delete(found.group(1))
102        return True
103
104    def do_list(self, msg):
105        found = re.search(r'^list', msg)
106        if not found:
107            return False
108        self.filter.list()
109        return True
110
111    def do_rate_set(self, msg):
112        found = re.search(r'^rate\s+(\d+[.]\d+)', msg)
113        if not found:
114            return False
115        self.recommend.set_random_rate(float(found.group(1)))
116        return True
117
118    def do_rate_show(self, msg):
119        if msg != 'rate':
120            return False
121        self.post('[rate] %f' % self.recommend.random_rate)
122
123    def do_reconnect(self, msg):
124        if msg != 'reconnect':
125            return False
126        self.alert.reconnect()
127
128    def do_help(self, msg):
129        if msg != 'help':
130            return False
131        self.post('[help begin]')
132        self.post('[help] random recommend rate is %f'
133                  % self.recommend.random_rate)
134        self.post('[help] "list" to list community id filter')
135        self.post('[help] "add co123" to add community id to filter')
136        self.post('[help] "delete co123" to delete community id from filter')
137        self.post('[help] "rate 0.5" to set random recommend rate')
138        self.post('[help] "reconnect" to reconnect')
139        self.post('[help end]')
140
141    def post(self, message):
142        """Post message.
143        """
144        self.connection.notice(
145            self.channel,
146            message.encode(self.encoding, 'replace'))
147        time.sleep(0.5)
148
149    def post_event(self, event):
150        """Post Nico Live Alert event.
151        """
152        if not event.is_new_stream:
153            self.post(str(event))
154            return
155        self.post('%s %s from %s' %
156                  (event['url'], event['title'], event['communityname']))
157
158
159def table_exists(db, table):
160    cursor = db.cursor()
161    cursor.execute(
162        '''SELECT COUNT(*) FROM `sqlite_master`
163             WHERE `type` = 'table' AND `name` = ?;''',
164        (table,))
165    return int(cursor.fetchone()[0]) > 0
166
167
168class Filter:
169    """Commmunity ID filter using SQLite DB.
170    """
171
172    def __init__(self, db):
173        self.db = db
174        self.add_queue = []
175        self.delete_queue = []
176        self.list_queue = False
177
178    def open(self):
179        if table_exists(self.db, 'filter'):
180            return self
181        self.db.executescript(
182            '''CREATE TABLE `filter` (
183                 `id` PRIMARY KEY,
184                 `update_time`
185               );''')
186        return self
187
188    def includes(self, id):
189        cursor = self.db.cursor()
190        cursor.execute(
191            "SELECT COUNT(*) FROM `filter` WHERE `id` = ?;", (id, ))
192        return int(cursor.fetchone()[0]) > 0
193
194    def list(self):
195        self.list_queue = True
196
197    def add(self, id):
198        self.add_queue.append(id)
199
200    def delete(self, id):
201        self.delete_queue.append(id)
202
203    def flush_queue(self, bot):
204        try:
205            while self.delete_queue:
206                id = self.delete_queue.pop(0)
207                self.delet_from_table(id)
208                bot.post('[delete] %s' % id)
209            while self.add_queue:
210                id = self.add_queue.pop(0)
211                if not self.includes(id):
212                    self.add_to_table(id)
213                bot.post('[add] %s' % id)
214            if self.list_queue:
215                self.list_queue = False
216                list_thread = Thread(target=self.post_list,
217                                     args=(bot, self.list_table()))
218                list_thread.setDaemon(True)
219                list_thread.start()
220        except:
221            bot.post('[error]')
222            traceback.print_exc()
223
224    def post_list(self, bot, lines):
225        bot.post('[list begin]')
226        for line in lines:
227            bot.post(line)
228        bot.post('[list end]')
229
230    def add_to_table(self, id):
231        cursor = self.db.cursor()
232        cursor.execute(
233            '''INSERT INTO `filter`
234                 (`id`, `update_time`)
235                 VALUES (?, DATETIME())''',
236            (id, ))
237
238    def delet_from_table(self, id):
239        cursor = self.db.cursor()
240        cursor.execute(
241            "DELETE FROM `filter` WHERE `id` = ?;", (id, ))
242
243    def list_table(self):
244        ret = []
245        cursor = self.db.cursor()
246        cursor.execute("SELECT `id` FROM `filter`")
247        for row in cursor:
248            communityid = row[0]
249            if communityid.startswith('ch'):
250                url = 'http://ch.nicovideo.jp/channel/'
251            else:
252                url = 'http://ch.nicovideo.jp/community/'
253            ret.append('[list] %s %s%s' % (communityid, url, communityid))
254        return ret
255
256
257class Recommend:
258    """Recommend rule.
259    """
260
261    def __init__(self, db):
262        self.db = db
263        self.new_rate = None
264        self.random_rate = None
265
266    def open(self):
267        self.create_table()
268        self.read_random_rate()
269        if self.random_rate is None:
270            self.create_random_rate()
271        return self
272
273    def create_table(self):
274        if table_exists(self.db, 'recommend'):
275            return
276        self.db.executescript(
277            '''CREATE TABLE `recommend` (
278                 `key` PRIMARY KEY,
279                 `value`
280               );''')
281
282    def create_random_rate(self):
283        self.random_rate = 0.1
284        self.db.cursor().execute(
285            '''INSERT INTO `recommend` (`key`, `value`)
286                 VALUES ('random', ?)''',
287            (self.random_rate, ))
288
289    def read_random_rate(self):
290        cursor = self.db.cursor()
291        cursor.execute(
292            "SELECT `value` FROM `recommend` WHERE `key` = 'random';")
293        rows = cursor.fetchall()
294        if rows:
295            self.random_rate = float(rows[0][0])
296        else:
297            self.random_rate = None
298
299    def set_random_rate(self, rate):
300        self.new_rate = rate
301
302    def flush_queue(self, bot):
303        try:
304            if self.new_rate is not None:
305                self.store_rate(self.new_rate)
306                self.random_rate = self.new_rate
307                bot.post('[rate] %f' % self.new_rate)
308                self.new_rate = None
309        except:
310            bot.post('[error]')
311            traceback.print_exc()
312
313    def store_rate(self, rate):
314        self.db.cursor().execute(
315            "UPDATE `recommend` SET `value` = ? WHERE `key` = 'random'",
316            (rate,))
317
318
319def parse_args():
320    """Parse command line argments.
321    """
322    usage = 'usage: %prog [options]'
323    parser = optparse.OptionParser(usage=usage)
324    parser.add_option('-s', '--server', dest='server', default='localhost',
325                      help='irc server host name')
326    parser.add_option('-p', '--port', dest='port', type='int', default=6667,
327                      help='irc server port')
328    parser.add_option('-c', '--channel', dest='channel', default='#nicolive',
329                      help='irc channel')
330    parser.add_option('-n', '--nick', dest='nick', default='nicolive',
331                      help='irc nickname')
332    parser.add_option('-r', '--realname', dest='real',
333                      default='Nico Live Alert Bot',
334                      help='irc realname')
335    parser.add_option('-e', '--encoding', dest='encoding', default='utf8',
336                      help='irc encoding')
337    parser.add_option('-d', '--db', dest='dbpath', metavar='PATH',
338                      default=':memory:', help='sqlite db')
339    return parser.parse_args()
340
341
342def event_is_to_post(event, filter, rate):
343    if not event.is_new_stream:
344        return True
345    if event['communityid'] == 'official':
346        return True
347    if filter.includes(event['communityid']):
348        return True
349    if random.random() <= rate:
350        return True
351    return False
352
353
354def main():
355    options, argv = parse_args()
356
357    db = sqlite3.connect(options.dbpath)
358    db.isolation_level = None
359
360    filter = Filter(db)
361    filter.open()
362
363    recommend = Recommend(db)
364    recommend.open()
365
366    bot = NicoAlertIRCBot(
367            [(options.server, options.port)],
368            options.nick,
369            options.real)
370    bot.channel = options.channel
371    bot.encoding = options.encoding
372    bot.filter = filter
373    bot.recommend = recommend
374
375    bot_thread = Thread(target=bot.start)
376    bot_thread.setDaemon(True)
377    bot_thread.start()
378
379    time.sleep(1)
380    alert = nicolivealert.connect()
381    bot.alert = alert
382    try:
383        for event in alert:
384            filter.flush_queue(bot)
385            recommend.flush_queue(bot)
386            if event_is_to_post(event, filter, recommend.random_rate):
387                post_thread = Thread(target=bot.post_event, args=(event,))
388                post_thread.setDaemon(True)
389                post_thread.start()
390    except KeyboardInterrupt:
391        pass
392
393    alert.close()
394    bot.disconnect('bye')
395
396
397if __name__ == '__main__':
398    main()
Note: See TracBrowser for help on using the browser.