Browse Source

Convert frontend and watcher into classes

tags/v0.1^2
Ben Kurtovic 12 years ago
parent
commit
551e1e8a9c
11 changed files with 323 additions and 308 deletions
  1. +0
    -3
      earwigbot/classes/__init__.py
  2. +37
    -0
      earwigbot/commands/restart.py
  3. +0
    -137
      earwigbot/frontend.py
  4. +27
    -0
      earwigbot/irc/__init__.py
  5. +60
    -35
      earwigbot/irc/connection.py
  6. +0
    -0
      earwigbot/irc/data.py
  7. +99
    -0
      earwigbot/irc/frontend.py
  8. +0
    -0
      earwigbot/irc/rc.py
  9. +88
    -0
      earwigbot/irc/watcher.py
  10. +12
    -19
      earwigbot/main.py
  11. +0
    -114
      earwigbot/watcher.py

+ 0
- 3
earwigbot/classes/__init__.py View File

@@ -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 *

+ 37
- 0
earwigbot/commands/restart.py View File

@@ -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

+ 0
- 137
earwigbot/frontend.py View File

@@ -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)

+ 27
- 0
earwigbot/irc/__init__.py View File

@@ -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 *

earwigbot/classes/connection.py → earwigbot/irc/connection.py View File

@@ -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

earwigbot/classes/data.py → earwigbot/irc/data.py View File


+ 99
- 0
earwigbot/irc/frontend.py View File

@@ -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)

earwigbot/classes/rc.py → earwigbot/irc/rc.py View File


+ 88
- 0
earwigbot/irc/watcher.py View File

@@ -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)

+ 12
- 19
earwigbot/main.py View File

@@ -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:


+ 0
- 114
earwigbot/watcher.py View File

@@ -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)

Loading…
Cancel
Save