| import time |
| import logging |
| import socket |
| import asyncore |
| import asynchat |
| import random |
| from urlparse import urljoin |
| from datetime import datetime, timedelta |
| |
| import tg |
| from paste.script import command |
| from paste.deploy.converters import asint |
| |
| from ming.orm import session, ThreadLocalORMSession |
| |
| import allura |
| from allura.command import base |
| from allura.lib import helpers as h |
| from allura.lib import search, security |
| from allura import model as M |
| |
| from forgechat import model as CM |
| |
| class IRCBotCommand(allura.command.Command): |
| min_args=1 |
| max_args=1 |
| usage = '<ini file>' |
| summary = 'Connect to all configured IRC servers and relay messages' |
| parser = command.Command.standard_parser(verbose=True) |
| parser.add_option('-c', '--context', dest='context', |
| help=('The context of the message (path to the project' |
| ' and/or tool')) |
| |
| def command(self): |
| self.basic_setup() |
| base.log.info('IRCBot starting up...') |
| while True: |
| try: |
| IRCBot( |
| tg.config.get('forgechat.host', 'irc.freenode.net'), |
| asint(tg.config.get('forgechat.port', '6667'))) |
| asyncore.loop() |
| except Exception: |
| base.log.exception('Error in ircbot asyncore.loop(), restart in 5s') |
| time.sleep(5) |
| |
| class IRCBot(asynchat.async_chat): |
| TIME_BETWEEN_CONFIGS=timedelta(minutes=1) |
| |
| def __init__(self, host, port, nick='sfbot'): |
| self.logger = logging.getLogger(__name__) |
| self.host = host |
| self.port = port |
| self.nick = nick |
| sock = socket.socket() |
| sock.connect((host, port)) |
| asynchat.async_chat.__init__(self, sock) |
| self.set_terminator('\r\n') |
| self.data = [] |
| self.channels = {} |
| self.set_nick('000') |
| self.say('USER sfbot %s %s :SFBot 0.0' % (self.host, self.host)) |
| self.configure() |
| |
| def set_nick(self, suffix=None): |
| if suffix is None: |
| suffix = '%.3d' % random.randint(0, 999) |
| nick = '%s-%s' % (self.nick, suffix) |
| self.say('NICK ' + nick) |
| |
| def collect_incoming_data(self, data): |
| self.data.append(data) |
| |
| def found_terminator(self): |
| request = ''.join(self.data) |
| self.logger.debug('RECV %s', request) |
| self.data=[] |
| if request.startswith(':'): |
| sender, cmd, rest = request[1:].split(' ', 2) |
| sender = sender.split('!', 1) |
| else: |
| sender = ('', '') |
| cmd, rest = request.split(' ', 1) |
| self.handle_command(sender, cmd, rest) |
| |
| def configure(self): |
| new_channels = dict( |
| (ch.channel, ch) for ch in CM.ChatChannel.query.find()) |
| for channel in new_channels: |
| if channel not in self.channels and channel: |
| self.say('JOIN %s' % channel) |
| for channel in self.channels: |
| if channel not in new_channels and channel: |
| self.say('LEAVE %s' % channel) |
| self.channels = new_channels |
| self.last_configured = datetime.utcnow() |
| |
| def check_configure(self): |
| if (datetime.utcnow() - self.last_configured |
| > self.TIME_BETWEEN_CONFIGS): |
| self.configure() |
| |
| def say(self, s): |
| s = s.encode('utf-8') |
| self.logger.debug('SAYING %s', s) |
| self.push(s + '\r\n') |
| |
| def notice(self, out, message): |
| self.say('NOTICE %s :%s' % (out, message)) |
| CM.ChatMessage( |
| sender=self.nick, |
| channel=out, |
| text=message) |
| ThreadLocalORMSession.flush_all() |
| |
| def handle_command(self, sender, cmd, rest): |
| if cmd == 'NOTICE': pass |
| elif cmd == '433': |
| self.set_nick() |
| self.channels = {} |
| self.configure() |
| elif cmd == 'PING': |
| self.say('PONG ' + rest) |
| elif cmd in ('NOTICE', 'PRIVMSG'): |
| rcpt, msg = rest.split(' ', 1) |
| if not self.set_context(rcpt): return |
| if msg.startswith(':'): msg = msg[1:] |
| self.log_channel(sender, cmd, rcpt, msg) |
| if cmd == 'NOTICE': return |
| for lnk in search.find_shortlinks(msg): |
| self.handle_shortlink(lnk, sender, rcpt) |
| ThreadLocalORMSession.flush_all() |
| ThreadLocalORMSession.close_all() |
| self.check_configure() |
| ThreadLocalORMSession.close_all() |
| |
| def set_context(self, rcpt): |
| if rcpt == self.nick: return False |
| chan = self.channels.get(rcpt, None) |
| if not chan: return False |
| h.set_context(chan.project_id, |
| app_config_id=chan.app_config_id) |
| return True |
| |
| def handle_shortlink(self, lnk, sender, rcpt): |
| art = lnk.ref.artifact |
| if security.has_access(art, 'read', user=M.User.anonymous())(): |
| index = art.index() |
| text = index['snippet_s'] or index['title_s'] |
| url = urljoin(tg.config.get('base_url', 'http://sourceforge.net'), index['url_s']) |
| self.notice(rcpt, '[%s] - [%s](%s)' % (lnk.link, text,url)) |
| |
| def log_channel(self, sender, cmd, rcpt, rest): |
| if cmd not in ('NOTICE', 'PRIVMSG'): |
| self.logger.debug('IGN: %s %s %s %s', sender, cmd, rcpt, rest) |
| return |
| if cmd == 'NOTICE': |
| text = '--' + rest |
| else: |
| text = rest |
| CM.ChatMessage( |
| sender='!'.join(sender), |
| channel=rcpt, |
| text=text) |
| ThreadLocalORMSession.flush_all() |
| ThreadLocalORMSession.close_all() |