source: trunk/amazonbot/amazonbot.py @ 25

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