Skip to content
Snippets Groups Projects
Commit 79a38e60 authored by Eric S. Raymond's avatar Eric S. Raymond
Browse files

Eliminate the dependency on irclib.

parent c30b9acf
No related branches found
No related tags found
No related merge requests found
......@@ -46,7 +46,7 @@ clean:
rm -f irkerd.8 irkerhook.1 irker-*.tar.gz *~
rm -f SHIPPER.* *.html
PYLINTOPTS = --rcfile=/dev/null --reports=n --include-ids=y --disable="C0103,C0111,C0301,R0201,R0902,R0903,R0912,R0914,R0915,E1101,W0201,W0212,W0621,W0702,F0401"
PYLINTOPTS = --rcfile=/dev/null --reports=n --include-ids=y --disable="C0103,C0111,C0301,R0201,R0902,R0903,R0912,R0913,R0914,R0915,E1101,W0201,W0212,W0621,W0702,F0401"
pylint:
@pylint --output-format=parseable $(PYLINTOPTS) irkerd
@pylint --output-format=parseable $(PYLINTOPTS) irkerhook.py
......
irker history
1.21 @
2.0 @
An email delivery method, suitable for use on SourceForge.
irkerhook can now be used as a hg changegroup hook.
Prevent misbehavior un UTF-8 in commit metadata.
The dependency on irclib is gone.
1.20 @ 2013-05-17
Compatibility back to Python 2.4 (provided simplejson is present).
......
......@@ -38,9 +38,6 @@ to show all traffic with IRC servers.
You should *not* make irker visible from outside the site firewall, as
it can be used to spam IRC channels while masking the source address.
You will need to have Jason Coombs's irc library where Python can see
it. See <http://pypi.python.org/pypi/irc/>; use version 3.4 or later.
Some irclib versions after 5.0 may produce problems if you try to ship
non-ASCII Unicode through them; this is not an irker bug, and should be
kicked upstrean to the irclib maintainer.
......@@ -48,6 +45,8 @@ kicked upstrean to the irclib maintainer.
The file org.catb.irkerd.plist is a Mac OS/X plist that can be
installed to launch irkerd as a boot-time service on that system.
irkerd no longer requires irclib as it did in the 1.x versions.
== Installing irkerhook.py ==
Under git, a call to irkerhook.py should be installed in the update
......
......@@ -20,11 +20,9 @@ option prints the program version and exits.
Design and code by Eric S. Raymond <esr@thyrsus.com>. See the project
resource page at <http://www.catb.org/~esr/irker/>.
Requires Python 2.6 or 2.5 with the simplejson library installed, and
the irc client library at version >= 3.4 which requires 2.6: see
http://pypi.python.org/pypi/irc/
Requires Python 2.6 or 2.5 with the simplejson library installed.
"""
from __future__ import with_statement
# These things might need tuning
......@@ -48,8 +46,8 @@ CONNECTION_MAX = 200 # To avoid hitting a thread limit
version = "1.20"
import sys, getopt, urlparse, time, random, socket, signal, re
import threading, Queue, SocketServer
import irc.client, logging
import threading, Queue, SocketServer, select, itertools
import logging
try:
import simplejson as json # Faster, also makes us Python-2.4-compatible
except ImportError:
......@@ -88,14 +86,280 @@ except ImportError:
# same problem - there is little point in reliable delivery to a relay
# that is down or unreliable.
#
# This code uses only NICK, JOIN, PART, MODE, and PRIVMSG. It is strictly
# compliant to RFC1459, except for the interpretation and use of the
# DEAF and CHANLIMIT and (obsolete) MAXCHANNELS features. CHANLIMIT
# is as described in the Internet RFC draft
# This code uses only NICK, JOIN, PART, MODE, PRIVMSG, USER, and QUIT.
# It is strictly compliant to RFC1459, except for the interpretation and
# use of the DEAF and CHANLIMIT and (obsolete) MAXCHANNELS features.
#
# CHANLIMIT is as described in the Internet RFC draft
# draft-brocklesby-irc-isupport-03 at <http://www.mirc.com/isupport.html>.
# The ",isnick" feature is as described in
# <http://ftp.ics.uci.edu/pub/ietf/uri/draft-mirashi-url-irc-01.txt>.
# Historical note: the IRCClient and IRCServerConnection classes
# (~270LOC) replace the overweight, overcomplicated 3KLOC mass of
# irclib code that irker formerly used as a service library. They
# still look similar to parts of irclib because I contributed to that
# code before giving up on it.
class IRCError(Exception):
"An IRC exception"
pass
class IRCClient():
"An IRC client session to one or more servers."
def __init__(self):
self.mutex = threading.RLock()
self.server_connections = []
self.event_handlers = {}
self.add_event_handler("ping",
lambda c, e: c.ship("PONG %s" % e.target))
def newserver(self):
"Initialize a new server-connection object."
conn = IRCServerConnection(self)
with self.mutex:
self.server_connections.append(conn)
return conn
def spin(self, timeout=0.2):
"Spin processing data from connections forever."
# Outer loop should specifically *not* be mutex-locked.
# Otherwise no other thread would ever be able to change
# the shared state of an IRC object running this function.
while True:
with self.mutex:
sockets = [x.socket for x in self.server_connections if x is not None]
sockets = [x for x in sockets if x is not None]
if sockets:
(insocks, _o, _e) = select.select(sockets, [], [], timeout)
with self.mutex:
for s, c in itertools.product(insocks, self.server_connections):
if s == c.socket:
c.consume()
else:
time.sleep(timeout)
def add_event_handler(self, event, handler):
"Set a handler to be called later."
with self.mutex:
event_handlers = self.event_handlers.setdefault(event, [])
event_handlers.append(handler)
def handle_event(self, connection, event):
with self.mutex:
h = self.event_handlers
th = sorted(h.get("all_events", []) + h.get(event.type, []))
for handler in th:
handler(connection, event)
def drop_connection(self, connection):
with self.mutex:
self.server_connections.remove(connection)
class LineBufferedStream():
"Line-buffer a read stream."
crlf_re = re.compile(b'\r?\n')
def __init__(self):
self.buffer = ''
def append(self, newbytes):
self.buffer += newbytes
def lines(self):
"Iterate over lines in the buffer."
lines = LineBufferedStream.crlf_re.split(self.buffer)
self.buffer = lines.pop()
return iter(lines)
def __iter__(self):
return self.lines()
class IRCServerConnectionError(IRCError):
pass
class IRCServerConnection():
command_re = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?")
# The full list of numeric-to-event mappings is in Perl's Net::IRC.
# We only need to ensure that if some ancient server throws numerics
# for the ones we actually want to catch, they're mapped.
codemap = {
"001": "welcome",
"005": "featurelist",
"432": "erroneusnickname",
"433": "nicknameinuse",
"436": "nickcollision",
"437": "unavailresource",
}
def __init__(self, master):
self.master = master
self.socket = None
def connect(self, server, port, nickname,
password=None, username=None, ircname=None):
log.debug("connect(server=%r, port=%r, nickname=%r, ...)",
server, port, nickname)
if self.socket is not None:
self.disconnect("Changing servers")
self.buffer = LineBufferedStream()
self.event_handlers = {}
self.real_server_name = ""
self.server = server
self.port = port
self.server_address = (server, port)
self.nickname = nickname
self.username = username or nickname
self.ircname = ircname or nickname
self.password = password
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.bind(('', 0))
self.socket.connect(self.server_address)
except socket.error as err:
raise IRCServerConnectionError("Couldn't connect to socket: %s" % err)
if self.password:
self.ship("PASS " + self.password)
self.nick(self.nickname)
self.user(self.username, self.ircname)
return self
def close(self):
# Without this thread lock, there is a window during which
# select() can find a closed socket, leading to an EBADF error.
with self.master.mutex:
self.disconnect("Closing object")
self.master.drop_connection(self)
def consume(self):
try:
incoming = self.socket.recv(16384)
except socket.error:
# Server hung up on us.
self.disconnect("Connection reset by peer")
return
if not incoming:
# Dead air also indicates a connection reset.
self.disconnect("Connection reset by peer")
return
self.buffer.append(incoming)
for line in self.buffer:
log.debug("FROM: %s", line)
if not line:
continue
prefix = None
command = None
arguments = None
self.handle_event(Event("every_raw_message",
self.real_server_name,
None,
[line]))
m = IRCServerConnection.command_re.match(line)
if m.group("prefix"):
prefix = m.group("prefix")
if not self.real_server_name:
self.real_server_name = prefix
if m.group("command"):
command = m.group("command").lower()
if m.group("argument"):
a = m.group("argument").split(" :", 1)
arguments = a[0].split()
if len(a) == 2:
arguments.append(a[1])
command = IRCServerConnection.codemap.get(command, command)
if command in ["privmsg", "notice"]:
target = arguments.pop(0)
else:
target = None
if command == "quit":
arguments = [arguments[0]]
elif command == "ping":
target = arguments[0]
else:
target = arguments[0]
arguments = arguments[1:]
log.debug("command: %s, source: %s, target: %s, "
"arguments: %s", command, prefix, target, arguments)
self.handle_event(Event(command, prefix, target, arguments))
def handle_event(self, event):
self.master.handle_event(self, event)
if event.type in self.event_handlers:
for fn in self.event_handlers[event.type]:
fn(self, event)
def is_connected(self):
return self.socket is not None
def disconnect(self, message=""):
if self.socket is None:
return
self.quit(message)
try:
self.socket.shutdown(socket.SHUT_WR)
self.socket.close()
except socket.error:
pass
del self.socket
self.socket = None
self.handle_event(Event("disconnect", self.server, "", [message]))
def join(self, channel, key=""):
self.ship("JOIN %s%s" % (channel, (key and (" " + key))))
def mode(self, target, command):
self.ship("MODE %s %s" % (target, command))
def nick(self, newnick):
self.ship("NICK " + newnick)
def part(self, channel, message=""):
cmd_parts = ['PART', channel]
if message:
cmd_parts.append(message)
self.ship(' '.join(cmd_parts))
def privmsg(self, target, text):
self.ship("PRIVMSG %s :%s" % (target, text))
def quit(self, message=""):
# Triggers an error that forces a disconnect.
self.ship("QUIT" + (message and (" :" + message)))
def user(self, username, realname):
self.ship("USER %s 0 * :%s" % (username, realname))
def ship(self, string):
"Ship a command to the server, appending CR/LF"
try:
self.socket.send(string + b'\r\n')
log.debug("TO: %s", string)
except socket.error:
self.disconnect("Connection reset by peer.")
class Event(object):
def __init__(self, evtype, source, target, arguments=None):
self.type = evtype
self.source = source
self.target = target
if arguments is None:
arguments = []
self.arguments = arguments
def is_channel(string):
return string and string[0] in "#&+!"
class Connection:
def __init__(self, irkerd, servername, port):
self.irker = irkerd
......@@ -213,14 +477,14 @@ class Connection:
elif not self.connection:
# Queue is nonempty but server isn't connected.
with self.irker.irc.mutex:
self.connection = self.irker.irc.server()
self.connection = self.irker.irc.newserver()
self.connection.context = self
# Try to avoid colliding with other instances
self.nick_trial = random.randint(1, 990)
self.channels_joined = {}
try:
# This will throw
# irc.client.ServerConnectionError on failure
# IRCServerConnectionError on failure
self.connection.connect(self.servername,
self.port,
nickname=self.nickname(),
......@@ -232,7 +496,7 @@ class Connection:
self.irker.debug(1, "XMIT_TTL bump (%s connection) at %s" % (self.servername, time.asctime()))
self.last_xmit = time.time()
self.last_ping = time.time()
except irc.client.ServerConnectionError:
except IRCServerConnectionError:
self.status = "disconnected"
elif self.status == "handshaking":
if time.time() > self.last_xmit + HANDSHAKE_TTL:
......@@ -401,18 +665,18 @@ class Irker:
"Persistent IRC multiplexer."
def __init__(self, debuglevel=0):
self.debuglevel = debuglevel
self.irc = irc.client.IRC()
self.irc.add_global_handler("ping", self._handle_ping)
self.irc.add_global_handler("welcome", self._handle_welcome)
self.irc.add_global_handler("erroneusnickname", self._handle_badnick)
self.irc.add_global_handler("nicknameinuse", self._handle_badnick)
self.irc.add_global_handler("nickcollision", self._handle_badnick)
self.irc.add_global_handler("unavailresource", self._handle_badnick)
self.irc.add_global_handler("featurelist", self._handle_features)
self.irc.add_global_handler("disconnect", self._handle_disconnect)
self.irc.add_global_handler("kick", self._handle_kick)
self.irc.add_global_handler("all_raw_messages", self._handle_all_raw_messages)
thread = threading.Thread(target=self.irc.process_forever)
self.irc = IRCClient()
self.irc.add_event_handler("ping", self._handle_ping)
self.irc.add_event_handler("welcome", self._handle_welcome)
self.irc.add_event_handler("erroneusnickname", self._handle_badnick)
self.irc.add_event_handler("nicknameinuse", self._handle_badnick)
self.irc.add_event_handler("nickcollision", self._handle_badnick)
self.irc.add_event_handler("unavailresource", self._handle_badnick)
self.irc.add_event_handler("featurelist", self._handle_features)
self.irc.add_event_handler("disconnect", self._handle_disconnect)
self.irc.add_event_handler("kick", self._handle_kick)
self.irc.add_event_handler("every_raw_message", self._handle_every_raw_message)
thread = threading.Thread(target=self.irc.spin)
thread.setDaemon(True)
self.irc._thread = thread
thread.start()
......@@ -482,7 +746,7 @@ class Irker:
self.debug(1, "irker has been kicked from %s on %s" % (target, connection.server))
if connection.context:
connection.context.handle_kick(target)
def _handle_all_raw_messages(self, _connection, event):
def _handle_every_raw_message(self, _connection, event):
"Log all messages when in watcher mode."
if logfile:
with open(logfile, "a") as logfp:
......@@ -572,6 +836,7 @@ Options
""")
if __name__ == '__main__':
log = logging.getLogger(__name__)
debuglvl = 0
namestyle = "irker%03d"
password = None
......
......@@ -154,8 +154,7 @@ discarded. </para>
project page at <ulink
url='http://www.catb.org/~esr/irker'>http://www.catb.org/~esr/irker</ulink>
for updates and other resources, including an installable repository
hook script. The implementation uses the Python IRC library by Joe
Rosdahl and Jason R. Coombs.</para>
hook script.</para>
</refsect1>
</refentry>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment