source: trunk/amazonbot/amazonbot.py @ 28

Revision 28, 17.1 KB checked in by atzm, 16 years ago (diff)
  • change mail address
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4__version__ = '$Revision$'
5__author__ = 'Atzm WATANABE <atzm@atzm.org>'
6__date__ = '$Date$'
7__copyright__ = 'Copyright(C) 2006-2007 Atzm WATANABE, all rights reserved.'
8__license__ = 'Python'
9
10import re
11import string
12import sys
13import time
14import shlex
15import random
16import getopt
17
18import MeCab
19import nkf
20
21from ircbot import SingleServerIRCBot
22from irclib import nm_to_n
23
24try:
25    set, frozenset
26except NameError:
27    from sets import Set as set, ImmutableSet as frozenset
28
29
30import config; config.init()
31
32import my_amazon
33my_amazon.setLocale(config.get('amazon', 'locale'))
34my_amazon.setLicense(config.get('amazon', 'access_key'))
35
36
37DEBUG_MSG_TO = sys.stderr
38
39
40def uniq(sequence):
41    """リストから重耇を取り陀く (順番が狂うので泚意)
42    """
43    return list(set(sequence))
44
45def unicoding(text):
46    """text を匷制的に unicode オブゞェクトに倉換
47    """
48    if type(text) is unicode:
49        return text
50    return unicode(nkf.nkf('-w', text), 'utf-8')
51
52def ununicoding(text, encoding='iso-2022-jp'):
53    """text を指定された encoding で゚ンコヌドしraw str に匷制倉換
54    """
55    if type(text) is not unicode:
56        return unicoding(text).encode(encoding)
57    return text.encode(encoding)
58
59def mecab_parse(text):
60    """MeCab を䜿っお圢態玠解析し固有名詞ず䞀般名詞だけを抜出する
61    """
62    def choice_nominal(wlist):
63        res = []
64        for word, wtype in wlist:
65            wtypes = wtype.split('-')
66            if '固有名詞' in wtypes or ('名詞' in wtypes and '䞀般' in wtypes):
67                res.append(unicoding(word))
68        return res
69
70    text = ununicoding(text, 'utf-8')
71    result = []
72    tag = MeCab.Tagger('-Ochasen')
73    for line in tag.parse(text).split('\n'):
74        if not line or line == 'EOS':
75            break
76        words = line.split()
77        result.append((words[0], words[-1])) # word, word-type
78
79    result = uniq(choice_nominal(result))
80    return result
81
82def _debug(fmt, *args):
83    if __debug__:
84        timeline = time.strftime("%b %d %T", time.localtime())
85        try:
86            fmt = ununicoding(fmt, 'euc-jp')
87            args = list(args)
88            for i in range(len(args)):
89                if isinstance(args[i], basestring):
90                    args[i] = ununicoding(args[i], 'euc-jp')
91
92            print >> DEBUG_MSG_TO, '(%s) <DEBUG>' % timeline,
93            print >> DEBUG_MSG_TO, fmt % tuple(args)
94
95        except:
96            print >> DEBUG_MSG_TO, '(%s) <DEBUG>' % timeline,
97            print >> DEBUG_MSG_TO, '!! debug message print failed !!'
98
99class AmazonBotBase(SingleServerIRCBot):
100    """アマゟンボットのベヌスクラス
101    単䜓では受け取ったメッセヌゞの圢態玠解析ず名詞抜出たでしかやらない
102    サブクラスで process_keyword を実装しお Amazon ぞク゚リを投げるべし
103
104    サブクラスには onmsg_HOGEHOGE(self, conn, ev, to, args) メ゜ッドを䜜るこずでコマンド远加可胜
105    コマンド曞匏は !HOGEHOGE arg [, arg2, ...] ずなる
106    ヘルプはメ゜ッドに docstring を曞けば OK
107    """
108    def __init__(self):
109        _server = [(config.get('irc', 'server'), config.get('irc', 'port', 'int'))]
110        _nick = config.get('bot', 'nick')
111
112        self._current_lines = 0
113        self._prev_time = time.time() - config.get('freq', 'timeout', 'int')
114        self._silent = False
115        SingleServerIRCBot.__init__(self, _server, _nick, _nick)
116
117    def start(self):
118        try:
119            SingleServerIRCBot.start(self)
120        except KeyboardInterrupt:
121            self.die(ununicoding(config.get('bot', 'bye')))
122
123    def on_welcome(self, c, e):
124        c.join(config.get('irc', 'channel'))
125        _debug('Joined %s', config.get('irc', 'channel'))
126
127    def on_nicknameinuse(self, c, e):
128        c.nick(c.get_nickname() + '_')
129
130    def on_privmsg(self, c, e):
131        return self.on_pubmsg(c, e, to=nm_to_n(e.source()))
132
133    def on_pubmsg(self, c, e, to=config.get('irc', 'channel')):
134        msg = unicoding(e.arguments()[0])
135        _debug('pubmsg incoming "%s", should be reply to %s', msg, to)
136
137        if msg[0] == '!':
138            try:
139                words = shlex.split(ununicoding(msg, 'utf-8')[1:])
140            except:
141                return False
142            if not words:
143                return False
144            method = getattr(self, 'onmsg_%s' % words[0], lambda *arg: False)
145            return method(c, e, to, words[1:]) # words[0] == command name
146
147        self.message_action(msg, c, e, to)
148
149        # silence
150        self.silence(msg, c, e, to)
151        if self._silent:
152            return False
153
154        # freq_lines
155        self._current_lines += 1
156        _freq_lines = config.get('freq', 'lines', 'int')
157        if _freq_lines:
158            if config.get('freq', 'lines_random', 'boolean'):
159                _freq_lines = random.randint(int(_freq_lines/2)+1, _freq_lines)
160
161            _debug('Line count: now %d, next: %d', self._current_lines, _freq_lines)
162
163            if self._current_lines < _freq_lines:
164                return False
165        self._current_lines = 0
166
167        # freq
168        _current_time = time.time()
169        if _current_time < self._prev_time + config.get('freq', 'timeout', 'int'):
170            cur = time.strftime('%H:%M:%S', time.localtime(_current_time))
171            go = time.strftime('%H:%M:%S', time.localtime(
172                self._prev_time + config.get('freq', 'timeout', 'int')))
173            _debug('Not expired: now %s, be expired at: %s', cur, go)
174            return False
175        self._prev_time = _current_time
176
177        nominals = mecab_parse(msg)
178        if not nominals:
179            _debug("Couldn't find nominal words")
180            return False
181
182        title, url = self.process_keyword(' '.join(nominals))
183        if title and url:
184            content = unicoding(config.get('bot', 'content'))
185            try:
186                message = ununicoding(': '.join([content, title, url]))
187            except UnicodeError, err:
188                # なぜかたたに unicode オブゞェクトを iso-2022-jp で゚ンコヌドできない
189                _debug('%s', str(err))
190                return False
191
192            c.notice(to, message)
193            return True
194        return False
195
196    ACTIVE_PATTERN = re.compile(unicoding(config.get('bot', 'active_pattern')))
197    SILENT_PATTERN = re.compile(unicoding(config.get('bot', 'silent_pattern')))
198    def silence(self, msg, c, e, to):
199        active = self.ACTIVE_PATTERN.search(msg)
200        silent = self.SILENT_PATTERN.search(msg)
201        _debug('ACT_PATT: %s, SIL_PATT: %s', str(active), str(silent))
202
203        if active:
204            self._silent = False
205            c.notice(to, ununicoding(config.get('bot', 'thanks')))
206        elif silent:
207            self._silent = True
208            c.notice(to, ununicoding(config.get('bot', 'sorry')))
209
210    def process_keyword(self, keyword):
211        return [None, None]
212
213    def is_silent(self):
214        return self._silent
215    def get_current_lines(self):
216        return self._current_lines
217    def get_prev_time(self):
218        return self._prev_time
219
220    def message_action(self, msg, c, e, to):
221        for i in xrange(100):
222            action = 'action%d' % i
223            if not config.has_section(action):
224                break
225
226            c_stime = config.get(action, 'start_time')
227            c_etime = config.get(action, 'end_time')
228
229            try:
230                if c_stime and c_etime:
231                    now = time.time()
232                    [now_y, now_m, now_d] = time.localtime(now)[:3]
233
234                    stime = '%04d/%02d/%02d %s' % (now_y, now_m, now_d, c_stime)
235                    etime = '%04d/%02d/%02d %s' % (now_y, now_m, now_d, c_etime)
236                    stime = time.mktime(time.strptime(stime, '%Y/%m/%d %H:%M'))
237                    etime = time.mktime(time.strptime(etime, '%Y/%m/%d %H:%M'))
238
239                    if not ((stime <= now) and (now <= etime)):
240                        _debug('Out of time: %s - %s' % (c_stime, c_etime))
241                        continue
242            except:
243                _debug('Invalid time: %s - %s' % (str(c_stime), str(c_etime)))
244                continue
245
246            pattern = unicoding(config.get(action, 'input_pattern'))
247            match   = None
248            try:
249                match = re.search(pattern, msg)
250            except:
251                _debug('Invalid regexp: %s', pattern)
252                continue
253
254            if not match:
255                continue
256
257            act = config.get(action, 'action')
258            fmt = config.get(action, 'message')
259            try:
260                _from   = nm_to_n(e.source())
261                message = ununicoding(fmt % _from)
262
263                if not message:
264                    _debug('No message specified')
265                    continue
266
267                if not act:
268                    c.notice(to, message)
269                    continue
270
271                method = getattr(self, 'onact_%s' % act, lambda *arg: False)
272                method(message, c, e, to)
273
274            except:
275                _debug('Action failed: %s (%s)' % (str(act), str(fmt)))
276
277        return True
278
279class AmazonBot(AmazonBotBase):
280    """アマゟンボットの実装クラス
281    process_keyword メ゜ッドで Amazon ぞク゚リを投げお結果を返す
282    """
283    _AVAIL_PRODUCT_LINES = {
284        'books-jp': '(和曞, default)',
285        'books-us': '(掋曞)',
286        'music-jp': '(ポピュラヌ音楜)',
287        'classical-jp': '(クラシック音楜)',
288        'dvd-jp': '(DVD)',
289        'vhs-jp': '(ビデオ)',
290        'electronics-jp': '(゚レクトロニクス)',
291        'kitchen-jp': '(ホヌムキッチン)',
292        'software-jp': '(゜フトりェア)',
293        'videogames-jp': '(ゲヌム)',
294        'magazines-jp': '(雑誌)',
295        'toys-jp': '(おもちゃホビヌ)',
296    }
297
298    def __init__(self):
299        AmazonBotBase.__init__(self)
300
301    def get_version(self):
302        return 'AmazonBot by %s, based on python-irclib' % __author__
303
304    def onmsg_reload(self, c, e, to, args):
305        """Syntax: !reload
306        """
307        _debug('in reload command: %s', str(args))
308        config.read()
309        c.notice(to, 'reloaded config')
310        return True
311
312    def onmsg_lt(self, c, e, to, args): return self.onmsg_localtime(c, e, to, args)
313    def onmsg_localtime(self, c, e, to, args):
314        """Syntax: !localtime <unix time>
315        """
316        _debug('in localtime command: %s', str(args))
317
318        _from = nm_to_n(e.source())
319        try:
320            sec = float(args[0])
321            c.notice(_from, time.strftime('%b %d %T', time.localtime(sec)))
322        except ValueError, e:
323            c.notice(_from, 'Invalid argument: %s' % args[0])
324            return False
325
326        return True
327
328    def onmsg_s(self, c, e, to, args): return self.onmsg_status(c, e, to, args)
329    def onmsg_status(self, c, e, to, args):
330        """Syntax: !status
331        """
332        _debug('in status command: %s', str(args))
333
334        c.notice(to, 'silent: %s' % self.is_silent())
335        c.notice(to, 'current lines: %d' % self.get_current_lines())
336        c.notice(to, time.strftime('previous time: %b %d %T',
337                                   time.localtime(self.get_prev_time())))
338        return True
339
340    def onmsg_isbn(self, c, e, to, args):
341        """Syntax: !isbn <ISBN number>
342        """
343        return self.onmsg_asin(c, e, to, args)
344    def onmsg_asin(self, c, e, to, args):
345        """Syntax: !asin <ASIN number>
346        """
347        _debug('in asin command: %s', str(args))
348
349        try:
350            data = my_amazon.searchByASIN(args[0])
351        except my_amazon.AmazonError, err:
352            c.notice(to, ununicoding(config.get('bot', 'no_products')))
353            _debug('Caught AmazonError in onmsg_asin: %s', str(err))
354            return False
355        except IndexError, err:
356            c.notice(to, 'Please specify an argument.')
357            return False
358
359        return self._process_onmsg(c, e, to, data)
360
361    def onmsg_k(self, c, e, to, args): return self.onmsg_keyword(c, e, to, args)
362    def onmsg_keyword(self, c, e, to, args):
363        """Syntax: !keyword [-h] [-t type] <keyword1> [, keyword2, ...]
364        """
365        _debug('in keyword command: %s', str(args))
366
367        try:
368            options, rest = getopt.getopt(args, 't:h', ['type=', 'help'])
369        except getopt.GetoptError, err:
370            _debug('Caught GetoptError in onmsg_keyword: %s', str(err))
371            return False
372
373        keyword = ' '.join(rest).strip()
374        product_line = 'books-jp'
375        for opt, val in options:
376            if opt in ['-t', '--type']:
377                if val not in self._AVAIL_PRODUCT_LINES.keys():
378                    c.notice(to, 'Type "%s" is not available.' % val)
379                    return False
380
381                product_line = val
382                break
383
384            elif opt in ['-h', '--help']:
385                _from = nm_to_n(e.source()) # ログを流しおしたうのでヘルプは盎接送信元ぞ
386                c.notice(_from, ununicoding('Available types:'))
387
388                for key, val in self._AVAIL_PRODUCT_LINES.iteritems():
389                    time.sleep(1) # XXX: 連続投皿するず匟かれるこずがあるので暫定察凊
390                    c.notice(_from, ununicoding(' * %s: %s' % (key, val)))
391
392                return True
393
394        if not keyword:
395            c.notice(to, 'Please specify keywords.')
396            return False
397
398        _debug('keyword="%s", product_line=%s', keyword, product_line)
399
400        try:
401            data = my_amazon.searchByKeyword(keyword, product_line=product_line)
402        except my_amazon.AmazonError, err:
403            c.notice(to, ununicoding(config.get('bot', 'no_products')))
404            _debug('Caught AmazonError in onmsg_amazon: %s', str(err))
405            return False
406
407        return self._process_onmsg(c, e, to, data)
408
409    def onmsg_h(self, c, e, to, args): return self.onmsg_help(c, e, to, args)
410    def onmsg_help(self, c, e, to, args):
411        """Syntax: !help
412        """
413        _debug('in help command: %s', str(args))
414
415        _from = nm_to_n(e.source()) # ログを流しおしたうのでヘルプは盎接送信元ぞ
416        c.notice(_from, self.get_version())
417
418        docs = []
419        for key in dir(self):
420            val = getattr(self, key, '')
421            _debug('key=%s, val=%s', key, str(val))
422
423            if key[:6] != 'onmsg_':
424                continue
425
426            doc = val.__doc__
427            if doc:
428                doc = doc.strip()
429                if not doc:
430                    continue
431                time.sleep(1) # XXX: 連続投皿するず匟かれるっぜいので暫定察凊
432                c.notice(_from, doc)
433
434        return True
435
436    def _process_onmsg(self, c, e, to, data):
437        if type(data.Details) is not list:
438            data.Details = [data.Details]
439
440        detail = random.choice(data.Details)
441        title = ununicoding(detail.ProductName)
442        url = ununicoding(detail.URL)
443        c.notice(to, '%(title)s: %(url)s' % locals())
444
445        return True
446
447    def process_keyword(self, keyword):
448        keyword = ununicoding(keyword, 'utf-8')
449        _debug('KEYWORD: %s', keyword)
450
451        try:
452            data = my_amazon.searchByBlended(keyword)
453            if type(data.ProductLine) is not type([]):
454                data.ProductLine = [data.ProductLine]
455        except my_amazon.AmazonError, err:
456            _debug('Caught AmazonError: %s', str(err))
457            return [None, None]
458
459        product_line = random.choice(data.ProductLine)
460        detail = random.choice(product_line.ProductInfo.Details)
461
462        url = unicoding(getattr(detail, 'URL', None))
463        product_name = unicoding(getattr(detail, 'ProductName', None))
464
465        return [product_name, url]
466
467    def onact_oper(self, msg, c, e, to):
468        nick = nm_to_n(e.source())
469        _debug('in oper action: %s to %s in %s' % (msg, nick, to))
470        c.notice(to, msg)
471        c.mode(to, '+o %s' % nick)
472        return True
473
474    def onact_nooper(self, msg, c, e, to):
475        nick = nm_to_n(e.source())
476        _debug('in nooper action: %s to %s in %s' % (msg, nick, to))
477        c.notice(to, msg)
478        c.mode(to, '-o %s' % nick)
479        return True
480
481    def onact_kick(self, msg, c, e, to):
482        nick = nm_to_n(e.source())
483        _debug('in kick action: %s to %s in %s' % (msg, nick, to))
484        c.kick(to, nick, msg)
485        return True
486
487    def onact_kick_and_invite(self, msg, c, e, to):
488        nick = nm_to_n(e.source())
489        _debug('in kick_and_invite action: %s to %s in %s' % (msg, nick, to))
490        c.kick(to, nick, msg)
491        c.invite(nick, to)
492        return True
493
494    NICK_TRANS_TABLE = string.maketrans('-_i1z2o0s5bdnmft', '_-1i2z0o5sdbmntf')
495    def onact_nick(self, msg, c, e, to):
496        nick   = nm_to_n(e.source())
497        nickto = nick[0] + nick[1:].translate(self.NICK_TRANS_TABLE)
498        if nick == nickto:
499            nickto = nick + '_'
500        _debug('in nick action: %s to %s in %s (%s)' % (msg, nick, to, nickto))
501        c.notice(to, msg)
502        c.nick(nickto)
503        return True
504
505    def onact_topic(self, msg, c, e, to):
506        nick = nm_to_n(e.source())
507        _debug('in topic action: %s to %s in %s' % (msg, nick, to))
508        c.topic(to, msg)
509        return True
510
511if __name__ == '__main__':
512    bot = AmazonBot()
513    bot.start()
514    print '> Bye ;)'
Note: See TracBrowser for help on using the repository browser.