diff --git a/earwigbot/classes/__init__.py b/earwigbot/classes/__init__.py index 88adf84..3373679 100644 --- a/earwigbot/classes/__init__.py +++ b/earwigbot/classes/__init__.py @@ -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 * diff --git a/earwigbot/commands/restart.py b/earwigbot/commands/restart.py new file mode 100644 index 0000000..0f47f49 --- /dev/null +++ b/earwigbot/commands/restart.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2012 by Ben Kurtovic +# +# 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 diff --git a/earwigbot/frontend.py b/earwigbot/frontend.py deleted file mode 100644 index f249c70..0000000 --- a/earwigbot/frontend.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009-2012 by Ben Kurtovic -# -# 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) diff --git a/earwigbot/irc/__init__.py b/earwigbot/irc/__init__.py new file mode 100644 index 0000000..c889289 --- /dev/null +++ b/earwigbot/irc/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2012 by Ben Kurtovic +# +# 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 * diff --git a/earwigbot/classes/connection.py b/earwigbot/irc/connection.py similarity index 56% rename from earwigbot/classes/connection.py rename to earwigbot/irc/connection.py index 5a45145..3fdb6d3 100644 --- a/earwigbot/classes/connection.py +++ b/earwigbot/irc/connection.py @@ -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 diff --git a/earwigbot/classes/data.py b/earwigbot/irc/data.py similarity index 100% rename from earwigbot/classes/data.py rename to earwigbot/irc/data.py diff --git a/earwigbot/irc/frontend.py b/earwigbot/irc/frontend.py new file mode 100644 index 0000000..e17513a --- /dev/null +++ b/earwigbot/irc/frontend.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2012 by Ben Kurtovic +# +# 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) diff --git a/earwigbot/classes/rc.py b/earwigbot/irc/rc.py similarity index 100% rename from earwigbot/classes/rc.py rename to earwigbot/irc/rc.py diff --git a/earwigbot/irc/watcher.py b/earwigbot/irc/watcher.py new file mode 100644 index 0000000..020b209 --- /dev/null +++ b/earwigbot/irc/watcher.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009-2012 by Ben Kurtovic +# +# 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) diff --git a/earwigbot/main.py b/earwigbot/main.py index a269e34..e264ac7 100644 --- a/earwigbot/main.py +++ b/earwigbot/main.py @@ -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: diff --git a/earwigbot/watcher.py b/earwigbot/watcher.py deleted file mode 100644 index 670a7aa..0000000 --- a/earwigbot/watcher.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009-2012 by Ben Kurtovic -# -# 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)