@@ -22,6 +22,3 @@ | |||||
from earwigbot.classes.base_command import * | from earwigbot.classes.base_command import * | ||||
from earwigbot.classes.base_task 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 socket | ||||
import threading | import threading | ||||
__all__ = ["BrokenSocketException", "Connection"] | |||||
__all__ = ["BrokenSocketException", "IRCConnection"] | |||||
class BrokenSocketException(Exception): | 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 | pass | ||||
class Connection(object): | |||||
class IRCConnection(object): | |||||
"""A class to interface with IRC.""" | """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.host = host | ||||
self.port = port | self.port = port | ||||
self.nick = nick | self.nick = nick | ||||
self.ident = ident | self.ident = ident | ||||
self.realname = realname | self.realname = realname | ||||
self.logger = logger | self.logger = logger | ||||
self.is_running = False | |||||
# A lock to prevent us from sending two messages at once: | # 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.""" | """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: | try: | ||||
self.sock.connect((self.host, self.port)) | |||||
self._sock.connect((self.host, self.port)) | |||||
except socket.error: | except socket.error: | ||||
self.logger.critical("Couldn't connect to IRC server", exc_info=1) | self.logger.critical("Couldn't connect to IRC server", exc_info=1) | ||||
exit(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.""" | """Close our connection with the IRC server.""" | ||||
try: | try: | ||||
self.sock.shutdown(socket.SHUT_RDWR) # shut down connection first | |||||
self._sock.shutdown(socket.SHUT_RDWR) # Shut down connection first | |||||
except socket.error: | 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.""" | """Receive (i.e. get) data from the server.""" | ||||
data = self.sock.recv(4096) | |||||
data = self._sock.recv(4096) | |||||
if not data: | if not data: | ||||
# Socket isn't giving us any data, so it is dead or broken: | # Socket isn't giving us any data, so it is dead or broken: | ||||
raise BrokenSocketException() | raise BrokenSocketException() | ||||
return data | return data | ||||
def send(self, msg): | |||||
def _send(self, msg): | |||||
"""Send data to the server.""" | """Send data to the server.""" | ||||
# Ensure that we only send one message at a time with a blocking lock: | # 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) | self.logger.debug(msg) | ||||
def say(self, target, msg): | def say(self, target, msg): | ||||
"""Send a private message to a target on the server.""" | """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): | def reply(self, data, msg): | ||||
"""Send a private message as a reply to a user on the server.""" | """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): | def action(self, target, msg): | ||||
"""Send a private message to a target on the server as an action.""" | """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): | def notice(self, target, msg): | ||||
"""Send a notice to a target on the server.""" | """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): | def join(self, chan): | ||||
"""Join a channel on the server.""" | """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): | def part(self, chan): | ||||
"""Part from a channel on the server.""" | """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): | def mode(self, chan, level, msg): | ||||
"""Send a mode message to the server.""" | """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 threading | ||||
import time | import time | ||||
from earwigbot import frontend | |||||
from earwigbot import tasks | from earwigbot import tasks | ||||
from earwigbot import watcher | |||||
from earwigbot.irc import Frontend, Watcher | |||||
from earwigbot.config import config | from earwigbot.config import config | ||||
logger = logging.getLogger("earwigbot") | 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 | """Function to handle the IRC watcher as another thread (if frontend and/or | ||||
scheduler is enabled), otherwise run as the main thread.""" | 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: | try: | ||||
watcher.main(w_conn, f_conn) | |||||
watcher.loop() | |||||
except: | except: | ||||
logger.exception("Watcher had an error") | 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") | logger.warn("Watcher has stopped; restarting component") | ||||
def wiki_scheduler(): | def wiki_scheduler(): | ||||
@@ -83,17 +78,15 @@ def wiki_scheduler(): | |||||
time_end = time.time() | time_end = time.time() | ||||
time_diff = time_start - time_end | 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) | time.sleep(60 - time_diff) | ||||
def irc_frontend(): | def irc_frontend(): | ||||
"""If the IRC frontend is enabled, make it run on our primary thread, and | """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 | enable the wiki scheduler and IRC watcher on new threads if they are | ||||
enabled.""" | enabled.""" | ||||
global f_conn | |||||
logger.info("Starting IRC frontend") | logger.info("Starting IRC frontend") | ||||
f_conn = frontend.get_connection() | |||||
frontend.startup(f_conn) | |||||
frontend = Frontend() | |||||
if "wiki_schedule" in config.components: | if "wiki_schedule" in config.components: | ||||
logger.info("Starting wiki scheduler") | logger.info("Starting wiki scheduler") | ||||
@@ -105,12 +98,12 @@ def irc_frontend(): | |||||
if "irc_watcher" in config.components: | if "irc_watcher" in config.components: | ||||
logger.info("Starting IRC watcher") | 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.name = "irc-watcher" | ||||
t_watcher.daemon = True | t_watcher.daemon = True | ||||
t_watcher.start() | t_watcher.start() | ||||
frontend.main() | |||||
frontend.loop() | |||||
if "irc_watcher" in config.components: | if "irc_watcher" in config.components: | ||||
w_conn.close() | w_conn.close() | ||||
@@ -119,12 +112,12 @@ def irc_frontend(): | |||||
def main(): | def main(): | ||||
if "irc_frontend" in config.components: | if "irc_frontend" in config.components: | ||||
# Make the frontend run on our primary thread if enabled, and enable | # Make the frontend run on our primary thread if enabled, and enable | ||||
# additional components through that function | |||||
# additional components through that function: | |||||
irc_frontend() | irc_frontend() | ||||
elif "wiki_schedule" in config.components: | elif "wiki_schedule" in config.components: | ||||
# Run the scheduler on the main thread, but also run the IRC watcher on | # 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") | logger.info("Starting wiki scheduler") | ||||
tasks.load() | tasks.load() | ||||
if "irc_watcher" in enabled: | 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) |