| @@ -22,6 +22,3 @@ | |||
| from earwigbot.classes.base_command import * | |||
| from earwigbot.classes.base_task import * | |||
| from earwigbot.classes.connection import * | |||
| from earwigbot.classes.data import * | |||
| from earwigbot.classes.rc import * | |||
| @@ -0,0 +1,37 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.classes import BaseCommand | |||
| from earwigbot.config import config | |||
| class Command(BaseCommand): | |||
| """Restart the bot. Only the owner can do this.""" | |||
| name = "restart" | |||
| def process(self, data): | |||
| if data.host not in config.irc["permissions"]["owners"]: | |||
| msg = "you must be a bot owner to use this command." | |||
| self.connection.reply(data, msg) | |||
| return | |||
| self.connection.logger.info("Restarting bot per owner request") | |||
| self.connection.is_running = False | |||
| @@ -1,137 +0,0 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| """ | |||
| EarwigBot's IRC Frontend Component | |||
| The IRC frontend runs on a normal IRC server and expects users to interact with | |||
| it and give it commands. Commands are stored as "command classes", subclasses | |||
| of BaseCommand in irc/base_command.py. All command classes are automatically | |||
| imported by irc/command_handler.py if they are in irc/commands. | |||
| """ | |||
| import logging | |||
| import re | |||
| from earwigbot import commands | |||
| from earwigbot.classes import Connection, Data, BrokenSocketException | |||
| from earwigbot.config import config | |||
| __all__ = ["get_connection", "startup", "main"] | |||
| connection = None | |||
| logger = logging.getLogger("earwigbot.frontend") | |||
| sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") | |||
| def get_connection(): | |||
| """Return a new Connection() instance with information about our server | |||
| connection, but don't actually connect yet.""" | |||
| cf = config.irc["frontend"] | |||
| connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||
| cf["realname"], logger) | |||
| return connection | |||
| def startup(conn): | |||
| """Accept a single arg, a Connection() object, and set our global variable | |||
| 'connection' to it. Load all command classes in irc/commands with | |||
| command_handler, and then establish a connection with the IRC server.""" | |||
| global connection | |||
| connection = conn | |||
| commands.load(connection) | |||
| connection.connect() | |||
| def main(): | |||
| """Main loop for the frontend component. | |||
| get_connection() and startup() should have already been called before this. | |||
| """ | |||
| read_buffer = str() | |||
| while 1: | |||
| try: | |||
| read_buffer = read_buffer + connection.get() | |||
| except BrokenSocketException: | |||
| logger.warn("Socket has broken on front-end; restarting bot") | |||
| return | |||
| lines = read_buffer.split("\n") | |||
| read_buffer = lines.pop() | |||
| for line in lines: | |||
| ret = _process_message(line) | |||
| if ret: | |||
| return | |||
| def _process_message(line): | |||
| """Process a single message from IRC.""" | |||
| line = line.strip().split() | |||
| data = Data(line) # new Data instance to store info about this line | |||
| if line[1] == "JOIN": | |||
| data.nick, data.ident, data.host = sender_regex.findall(line[0])[0] | |||
| data.chan = line[2] | |||
| # Check for 'join' hooks in our commands: | |||
| commands.check("join", data) | |||
| elif line[1] == "PRIVMSG": | |||
| data.nick, data.ident, data.host = sender_regex.findall(line[0])[0] | |||
| data.msg = ' '.join(line[3:])[1:] | |||
| data.chan = line[2] | |||
| if data.chan == config.irc["frontend"]["nick"]: | |||
| # This is a privmsg to us, so set 'chan' as the nick of the, sender | |||
| # then check for private-only command hooks: | |||
| data.chan = data.nick | |||
| commands.check("msg_private", data) | |||
| else: | |||
| # Check for public-only command hooks: | |||
| commands.check("msg_public", data) | |||
| # Check for command hooks that apply to all messages: | |||
| commands.check("msg", data) | |||
| # Hardcode the !restart command (we can't restart from within an | |||
| # ordinary command): | |||
| if data.msg in ["!restart", ".restart"]: | |||
| if data.host in config.irc["permissions"]["owners"]: | |||
| logger.info("Restarting bot per owner request") | |||
| return True | |||
| # If we are pinged, pong back: | |||
| elif line[0] == "PING": | |||
| msg = " ".join(("PONG", line[1])) | |||
| connection.send(msg) | |||
| # On successful connection to the server: | |||
| elif line[1] == "376": | |||
| # If we're supposed to auth to NickServ, do that: | |||
| try: | |||
| username = config.irc["frontend"]["nickservUsername"] | |||
| password = config.irc["frontend"]["nickservPassword"] | |||
| except KeyError: | |||
| pass | |||
| else: | |||
| msg = " ".join(("IDENTIFY", username, password)) | |||
| connection.say("NickServ", msg) | |||
| # Join all of our startup channels: | |||
| for chan in config.irc["frontend"]["channels"]: | |||
| connection.join(chan) | |||
| @@ -0,0 +1,27 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| from earwigbot.irc.connection import * | |||
| from earwigbot.irc.data import * | |||
| from earwigbot.irc.frontend import * | |||
| from earwigbot.irc.rc import * | |||
| from earwigbot.irc.watcher import * | |||
| @@ -23,93 +23,118 @@ | |||
| import socket | |||
| import threading | |||
| __all__ = ["BrokenSocketException", "Connection"] | |||
| __all__ = ["BrokenSocketException", "IRCConnection"] | |||
| class BrokenSocketException(Exception): | |||
| """A socket has broken, because it is not sending data. Raised by | |||
| Connection.get().""" | |||
| """A socket has broken, because it is not sending data. | |||
| Raised by IRCConnection()._get(). | |||
| """ | |||
| pass | |||
| class Connection(object): | |||
| class IRCConnection(object): | |||
| """A class to interface with IRC.""" | |||
| def __init__(self, host=None, port=None, nick=None, ident=None, | |||
| realname=None, logger=None): | |||
| def __init__(self, host, port, nick, ident, realname, logger): | |||
| self.host = host | |||
| self.port = port | |||
| self.nick = nick | |||
| self.ident = ident | |||
| self.realname = realname | |||
| self.logger = logger | |||
| self.is_running = False | |||
| # A lock to prevent us from sending two messages at once: | |||
| self.lock = threading.Lock() | |||
| self._lock = threading.Lock() | |||
| def connect(self): | |||
| def _connect(self): | |||
| """Connect to our IRC server.""" | |||
| self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |||
| try: | |||
| self.sock.connect((self.host, self.port)) | |||
| self._sock.connect((self.host, self.port)) | |||
| except socket.error: | |||
| self.logger.critical("Couldn't connect to IRC server", exc_info=1) | |||
| exit(1) | |||
| self.send("NICK %s" % self.nick) | |||
| self.send("USER %s %s * :%s" % (self.ident, self.host, self.realname)) | |||
| self._send("NICK {0}".format(self.nick)) | |||
| self._send("USER {0} {1} * :{2}".format(self.ident, self.host, self.realname)) | |||
| def close(self): | |||
| def _close(self): | |||
| """Close our connection with the IRC server.""" | |||
| try: | |||
| self.sock.shutdown(socket.SHUT_RDWR) # shut down connection first | |||
| self._sock.shutdown(socket.SHUT_RDWR) # Shut down connection first | |||
| except socket.error: | |||
| pass # ignore if the socket is already down | |||
| self.sock.close() | |||
| pass # Ignore if the socket is already down | |||
| self._sock.close() | |||
| def get(self, size=4096): | |||
| def _get(self, size=4096): | |||
| """Receive (i.e. get) data from the server.""" | |||
| data = self.sock.recv(4096) | |||
| data = self._sock.recv(4096) | |||
| if not data: | |||
| # Socket isn't giving us any data, so it is dead or broken: | |||
| raise BrokenSocketException() | |||
| return data | |||
| def send(self, msg): | |||
| def _send(self, msg): | |||
| """Send data to the server.""" | |||
| # Ensure that we only send one message at a time with a blocking lock: | |||
| with self.lock: | |||
| self.sock.sendall(msg + "\r\n") | |||
| with self._lock: | |||
| self._sock.sendall(msg + "\r\n") | |||
| self.logger.debug(msg) | |||
| def say(self, target, msg): | |||
| """Send a private message to a target on the server.""" | |||
| message = "".join(("PRIVMSG ", target, " :", msg)) | |||
| self.send(message) | |||
| msg = "PRIVMSG {0} :{1}".format(target, msg) | |||
| self._send(msg) | |||
| def reply(self, data, msg): | |||
| """Send a private message as a reply to a user on the server.""" | |||
| message = "".join((chr(2), data.nick, chr(0x0f), ": ", msg)) | |||
| self.say(data.chan, message) | |||
| msg = "\x02{0}\x0f: {1}".format(data.nick, msg) | |||
| self.say(data.chan, msg) | |||
| def action(self, target, msg): | |||
| """Send a private message to a target on the server as an action.""" | |||
| message = "".join((chr(1), "ACTION ", msg, chr(1))) | |||
| self.say(target, message) | |||
| msg = "\x01ACTION {0}\x01".format(msg) | |||
| self.say(target, msg) | |||
| def notice(self, target, msg): | |||
| """Send a notice to a target on the server.""" | |||
| message = "".join(("NOTICE ", target, " :", msg)) | |||
| self.send(message) | |||
| msg = "NOTICE {0} :{1}".format(target, msg) | |||
| self._send(msg) | |||
| def join(self, chan): | |||
| """Join a channel on the server.""" | |||
| message = " ".join(("JOIN", chan)) | |||
| self.send(message) | |||
| msg = "JOIN {0}".format(chan) | |||
| self._send(msg) | |||
| def part(self, chan): | |||
| """Part from a channel on the server.""" | |||
| message = " ".join(("PART", chan)) | |||
| self.send(message) | |||
| msg = "PART {0}".format(chan) | |||
| self._send(msg) | |||
| def mode(self, chan, level, msg): | |||
| """Send a mode message to the server.""" | |||
| message = " ".join(("MODE", chan, level, msg)) | |||
| self.send(message) | |||
| msg = "MODE {0} {1} {2}".format(chan, level, msg) | |||
| self._send(msg) | |||
| def pong(self, target): | |||
| """Pong another entity on the server.""" | |||
| msg = "PONG {0}".format(target) | |||
| self._send(msg) | |||
| def loop(self): | |||
| """Main loop for the IRC connection.""" | |||
| self.is_running = True | |||
| read_buffer = "" | |||
| while 1: | |||
| try: | |||
| read_buffer += self._get() | |||
| except BrokenSocketException: | |||
| self.is_running = False | |||
| break | |||
| lines = read_buffer.split("\n") | |||
| read_buffer = lines.pop() | |||
| for line in lines: | |||
| self._process_message(line) | |||
| if not self.is_running: | |||
| break | |||
| @@ -0,0 +1,99 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| import logging | |||
| import re | |||
| from earwigbot import commands | |||
| from earwigbot.irc import IRCConnection, Data, BrokenSocketException | |||
| from earwigbot.config import config | |||
| __all__ = ["Frontend"] | |||
| class Frontend(IRCConnection): | |||
| """ | |||
| EarwigBot's IRC Frontend Component | |||
| The IRC frontend runs on a normal IRC server and expects users to interact | |||
| with it and give it commands. Commands are stored as "command classes", | |||
| subclasses of BaseCommand in classes/base_command.py. All command classes | |||
| are automatically imported by commands/__init__.py if they are in | |||
| commands/. | |||
| """ | |||
| sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") | |||
| def __init__(self): | |||
| self.logger = logging.getLogger("earwigbot.frontend") | |||
| cf = config.irc["frontend"] | |||
| base = super(Frontend, self) | |||
| base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||
| cf["realname"], self.logger) | |||
| commands.load(self) | |||
| self._connect() | |||
| def _process_message(self, line): | |||
| """Process a single message from IRC.""" | |||
| line = line.strip().split() | |||
| data = Data(line) # New Data instance to store info about this line | |||
| if line[1] == "JOIN": | |||
| data.nick, data.ident, data.host = sender_regex.findall(line[0])[0] | |||
| data.chan = line[2] | |||
| # Check for 'join' hooks in our commands: | |||
| commands.check("join", data) | |||
| elif line[1] == "PRIVMSG": | |||
| data.nick, data.ident, data.host = sender_regex.findall(line[0])[0] | |||
| data.msg = " ".join(line[3:])[1:] | |||
| data.chan = line[2] | |||
| if data.chan == config.irc["frontend"]["nick"]: | |||
| # This is a privmsg to us, so set 'chan' as the nick of the | |||
| # sender, then check for private-only command hooks: | |||
| data.chan = data.nick | |||
| commands.check("msg_private", data) | |||
| else: | |||
| # Check for public-only command hooks: | |||
| commands.check("msg_public", data) | |||
| # Check for command hooks that apply to all messages: | |||
| commands.check("msg", data) | |||
| # If we are pinged, pong back: | |||
| elif line[0] == "PING": | |||
| self.pong(line[1]) | |||
| # On successful connection to the server: | |||
| elif line[1] == "376": | |||
| # If we're supposed to auth to NickServ, do that: | |||
| try: | |||
| username = config.irc["frontend"]["nickservUsername"] | |||
| password = config.irc["frontend"]["nickservPassword"] | |||
| except KeyError: | |||
| pass | |||
| else: | |||
| msg = "IDENTIFY {0} {1}".format(username, password) | |||
| self.say("NickServ", msg) | |||
| # Join all of our startup channels: | |||
| for chan in config.irc["frontend"]["channels"]: | |||
| self.join(chan) | |||
| @@ -0,0 +1,88 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| import logging | |||
| from earwigbot import rules | |||
| from earwigbot.irc import IRCConnection, RC, BrokenSocketException | |||
| from earwigbot.config import config | |||
| __all__ = ["Watcher"] | |||
| class Watcher(IRCConnection): | |||
| """ | |||
| EarwigBot's IRC Watcher Component | |||
| The IRC watcher runs on a wiki recent-changes server and listens for | |||
| edits. Users cannot interact with this part of the bot. When an event | |||
| occurs, we run it through rules.py's process() function, which can result | |||
| in wiki bot tasks being started (located in tasks/) or messages being sent | |||
| to channels on the IRC frontend. | |||
| """ | |||
| def __init__(self, frontend=None): | |||
| logger = logging.getLogger("earwigbot.watcher") | |||
| cf = config.irc["watcher"] | |||
| base = super(Frontend, self) | |||
| base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||
| cf["realname"], self.logger) | |||
| self._connect() | |||
| self.frontend = frontend | |||
| def _process_message(self, line): | |||
| """Process a single message from IRC.""" | |||
| line = line.strip().split() | |||
| if line[1] == "PRIVMSG": | |||
| chan = line[2] | |||
| # Ignore messages originating from channels not in our list, to | |||
| # prevent someone PMing us false data: | |||
| if chan not in config.irc["watcher"]["channels"]: | |||
| return | |||
| msg = " ".join(line[3:])[1:] | |||
| rc = RC(msg) # New RC object to store this event's data | |||
| rc.parse() # Parse a message into pagenames, usernames, etc. | |||
| self._process_rc(rc) # Report to frontend channels or start tasks | |||
| # If we are pinged, pong back: | |||
| elif line[0] == "PING": | |||
| self.pong(line[1]) | |||
| # When we've finished starting up, join all watcher channels: | |||
| elif line[1] == "376": | |||
| for chan in config.irc["watcher"]["channels"]: | |||
| self.join(chan) | |||
| def _process_rc(self, rc): | |||
| """Process a recent change event from IRC (or, an RC object). | |||
| The actual processing is configurable, so we don't have that hard-coded | |||
| here. We simply call rules's process() function and expect a list of | |||
| channels back, which we report the event data to. | |||
| """ | |||
| chans = rules.process(rc) | |||
| if chans and self.frontend: | |||
| pretty = rc.prettify() | |||
| for chan in chans: | |||
| self.frontend.say(chan, pretty) | |||
| @@ -49,27 +49,22 @@ import logging | |||
| import threading | |||
| import time | |||
| from earwigbot import frontend | |||
| from earwigbot import tasks | |||
| from earwigbot import watcher | |||
| from earwigbot.irc import Frontend, Watcher | |||
| from earwigbot.config import config | |||
| logger = logging.getLogger("earwigbot") | |||
| f_conn = None | |||
| w_conn = None | |||
| def irc_watcher(f_conn=None): | |||
| def irc_watcher(frontend=None): | |||
| """Function to handle the IRC watcher as another thread (if frontend and/or | |||
| scheduler is enabled), otherwise run as the main thread.""" | |||
| global w_conn | |||
| while 1: # restart the watcher component if it breaks (and nothing else) | |||
| w_conn = watcher.get_connection() | |||
| w_conn.connect() | |||
| while 1: # Restart the watcher component if it breaks (and nothing else) | |||
| watcher = Watcher(frontend) | |||
| try: | |||
| watcher.main(w_conn, f_conn) | |||
| watcher.loop() | |||
| except: | |||
| logger.exception("Watcher had an error") | |||
| time.sleep(5) # sleep a bit before restarting watcher | |||
| time.sleep(5) # Sleep a bit before restarting watcher | |||
| logger.warn("Watcher has stopped; restarting component") | |||
| def wiki_scheduler(): | |||
| @@ -83,17 +78,15 @@ def wiki_scheduler(): | |||
| time_end = time.time() | |||
| time_diff = time_start - time_end | |||
| if time_diff < 60: # sleep until the next minute | |||
| if time_diff < 60: # Sleep until the next minute | |||
| time.sleep(60 - time_diff) | |||
| def irc_frontend(): | |||
| """If the IRC frontend is enabled, make it run on our primary thread, and | |||
| enable the wiki scheduler and IRC watcher on new threads if they are | |||
| enabled.""" | |||
| global f_conn | |||
| logger.info("Starting IRC frontend") | |||
| f_conn = frontend.get_connection() | |||
| frontend.startup(f_conn) | |||
| frontend = Frontend() | |||
| if "wiki_schedule" in config.components: | |||
| logger.info("Starting wiki scheduler") | |||
| @@ -105,12 +98,12 @@ def irc_frontend(): | |||
| if "irc_watcher" in config.components: | |||
| logger.info("Starting IRC watcher") | |||
| t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) | |||
| t_watcher = threading.Thread(target=irc_watcher, args=(frontend,)) | |||
| t_watcher.name = "irc-watcher" | |||
| t_watcher.daemon = True | |||
| t_watcher.start() | |||
| frontend.main() | |||
| frontend.loop() | |||
| if "irc_watcher" in config.components: | |||
| w_conn.close() | |||
| @@ -119,12 +112,12 @@ def irc_frontend(): | |||
| def main(): | |||
| if "irc_frontend" in config.components: | |||
| # Make the frontend run on our primary thread if enabled, and enable | |||
| # additional components through that function | |||
| # additional components through that function: | |||
| irc_frontend() | |||
| elif "wiki_schedule" in config.components: | |||
| # Run the scheduler on the main thread, but also run the IRC watcher on | |||
| # another thread iff it is enabled | |||
| # another thread iff it is enabled: | |||
| logger.info("Starting wiki scheduler") | |||
| tasks.load() | |||
| if "irc_watcher" in enabled: | |||
| @@ -1,114 +0,0 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # | |||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||
| # | |||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| # of this software and associated documentation files (the "Software"), to deal | |||
| # in the Software without restriction, including without limitation the rights | |||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| # copies of the Software, and to permit persons to whom the Software is | |||
| # furnished to do so, subject to the following conditions: | |||
| # | |||
| # The above copyright notice and this permission notice shall be included in | |||
| # all copies or substantial portions of the Software. | |||
| # | |||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| """ | |||
| EarwigBot's IRC Watcher Component | |||
| The IRC watcher runs on a wiki recent-changes server and listens for edits. | |||
| Users cannot interact with this part of the bot. When an event occurs, we run | |||
| it through rules.py's process() function, which can result in wiki bot tasks | |||
| being started (located in tasks/) or messages being sent to channels on the IRC | |||
| frontend. | |||
| """ | |||
| import logging | |||
| from earwigbot import rules | |||
| from earwigbot.classes import Connection, RC, BrokenSocketException | |||
| from earwigbot.config import config | |||
| frontend_conn = None | |||
| logger = logging.getLogger("earwigbot.watcher") | |||
| def get_connection(): | |||
| """Return a new Connection() instance with connection information. | |||
| Don't actually connect yet. | |||
| """ | |||
| cf = config.irc["watcher"] | |||
| connection = Connection(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||
| cf["realname"], logger) | |||
| return connection | |||
| def main(connection, f_conn=None): | |||
| """Main loop for the Watcher IRC Bot component. | |||
| get_connection() should have already been called and the connection should | |||
| have been started with connection.connect(). Accept the frontend connection | |||
| as well as an optional parameter in order to send messages directly to | |||
| frontend IRC channels. | |||
| """ | |||
| global frontend_conn | |||
| frontend_conn = f_conn | |||
| read_buffer = str() | |||
| while 1: | |||
| try: | |||
| read_buffer = read_buffer + connection.get() | |||
| except BrokenSocketException: | |||
| return | |||
| lines = read_buffer.split("\n") | |||
| read_buffer = lines.pop() | |||
| for line in lines: | |||
| _process_message(connection, line) | |||
| def _process_message(connection, line): | |||
| """Process a single message from IRC.""" | |||
| line = line.strip().split() | |||
| if line[1] == "PRIVMSG": | |||
| chan = line[2] | |||
| # Ignore messages originating from channels not in our list, to prevent | |||
| # someone PMing us false data: | |||
| if chan not in config.irc["watcher"]["channels"]: | |||
| return | |||
| msg = ' '.join(line[3:])[1:] | |||
| rc = RC(msg) # new RC object to store this event's data | |||
| rc.parse() # parse a message into pagenames, usernames, etc. | |||
| process_rc(rc) # report to frontend channels or start tasks | |||
| # If we are pinged, pong back to the server: | |||
| elif line[0] == "PING": | |||
| msg = " ".join(("PONG", line[1])) | |||
| connection.send(msg) | |||
| # When we've finished starting up, join all watcher channels: | |||
| elif line[1] == "376": | |||
| for chan in config.irc["watcher"]["channels"]: | |||
| connection.join(chan) | |||
| def process_rc(rc): | |||
| """Process a recent change event from IRC (or, an RC object). | |||
| The actual processing is configurable, so we don't have that hard-coded | |||
| here. We simply call rules's process() function and expect a list of | |||
| channels back, which we report the event data to. | |||
| """ | |||
| chans = rules.process(rc) | |||
| if chans and frontend_conn: | |||
| pretty = rc.prettify() | |||
| for chan in chans: | |||
| frontend_conn.say(chan, pretty) | |||