2016-07-25 18:01:51 +00:00
|
|
|
# Copyright (C) 1999--2002 Joel Rosdahl
|
|
|
|
#
|
|
|
|
# This library is free software; you can redistribute it and/or
|
|
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
|
|
# License as published by the Free Software Foundation; either
|
|
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This library is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
# Lesser General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
|
|
# License along with this library; if not, write to the Free Software
|
|
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
#
|
|
|
|
# keltus <keltus@users.sourceforge.net>
|
|
|
|
#
|
|
|
|
# $Id: irclib.py,v 1.43 2005/12/24 22:12:40 keltus Exp $
|
|
|
|
|
|
|
|
"""irclib -- Internet Relay Chat (IRC) protocol client library.
|
|
|
|
|
|
|
|
This library is intended to encapsulate the IRC protocol at a quite
|
|
|
|
low level. It provides an event-driven IRC client framework. It has
|
|
|
|
a fairly thorough support for the basic IRC protocol, CTCP, DCC chat,
|
|
|
|
but DCC file transfers is not yet supported.
|
|
|
|
|
|
|
|
In order to understand how to make an IRC client, I'm afraid you more
|
|
|
|
or less must understand the IRC specifications. They are available
|
|
|
|
here: [IRC specifications].
|
|
|
|
|
|
|
|
The main features of the IRC client framework are:
|
|
|
|
|
|
|
|
* Abstraction of the IRC protocol.
|
|
|
|
* Handles multiple simultaneous IRC server connections.
|
|
|
|
* Handles server PONGing transparently.
|
|
|
|
* Messages to the IRC server are done by calling methods on an IRC
|
|
|
|
connection object.
|
|
|
|
* Messages from an IRC server triggers events, which can be caught
|
|
|
|
by event handlers.
|
|
|
|
* Reading from and writing to IRC server sockets are normally done
|
|
|
|
by an internal select() loop, but the select()ing may be done by
|
|
|
|
an external main loop.
|
|
|
|
* Functions can be registered to execute at specified times by the
|
|
|
|
event-loop.
|
|
|
|
* Decodes CTCP tagging correctly (hopefully); I haven't seen any
|
|
|
|
other IRC client implementation that handles the CTCP
|
|
|
|
specification subtilties.
|
|
|
|
* A kind of simple, single-server, object-oriented IRC client class
|
|
|
|
that dispatches events to instance methods is included.
|
|
|
|
|
|
|
|
Current limitations:
|
|
|
|
|
|
|
|
* The IRC protocol shines through the abstraction a bit too much.
|
|
|
|
* Data is not written asynchronously to the server, i.e. the write()
|
|
|
|
may block if the TCP buffers are stuffed.
|
|
|
|
* There are no support for DCC file transfers.
|
|
|
|
* The author haven't even read RFC 2810, 2811, 2812 and 2813.
|
|
|
|
* Like most projects, documentation is lacking...
|
|
|
|
|
|
|
|
.. [IRC specifications] http://www.irchelp.org/irchelp/rfc/
|
|
|
|
"""
|
|
|
|
|
|
|
|
import bisect
|
|
|
|
import re
|
|
|
|
import select
|
|
|
|
import socket
|
|
|
|
import string
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
import types
|
|
|
|
|
|
|
|
VERSION = 0, 4, 6
|
|
|
|
DEBUG = 0
|
|
|
|
|
|
|
|
# TODO
|
|
|
|
# ----
|
|
|
|
# (maybe) thread safety
|
|
|
|
# (maybe) color parser convenience functions
|
|
|
|
# documentation (including all event types)
|
|
|
|
# (maybe) add awareness of different types of ircds
|
|
|
|
# send data asynchronously to the server (and DCC connections)
|
|
|
|
# (maybe) automatically close unused, passive DCC connections after a while
|
|
|
|
|
|
|
|
# NOTES
|
|
|
|
# -----
|
|
|
|
# connection.quit() only sends QUIT to the server.
|
|
|
|
# ERROR from the server triggers the error event and the disconnect event.
|
|
|
|
# dropping of the connection triggers the disconnect event.
|
|
|
|
|
|
|
|
class IRCError(Exception):
|
|
|
|
"""Represents an IRC exception."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class IRC:
|
|
|
|
"""Class that handles one or several IRC server connections.
|
|
|
|
|
|
|
|
When an IRC object has been instantiated, it can be used to create
|
|
|
|
Connection objects that represent the IRC connections. The
|
|
|
|
responsibility of the IRC object is to provide an event-driven
|
|
|
|
framework for the connections and to keep the connections alive.
|
|
|
|
It runs a select loop to poll each connection's TCP socket and
|
|
|
|
hands over the sockets with incoming data for processing by the
|
|
|
|
corresponding connection.
|
|
|
|
|
|
|
|
The methods of most interest for an IRC client writer are server,
|
|
|
|
add_global_handler, remove_global_handler, execute_at,
|
|
|
|
execute_delayed, process_once and process_forever.
|
|
|
|
|
|
|
|
Here is an example:
|
|
|
|
|
|
|
|
irc = irclib.IRC()
|
|
|
|
server = irc.server()
|
|
|
|
server.connect(\"irc.some.where\", 6667, \"my_nickname\")
|
|
|
|
server.privmsg(\"a_nickname\", \"Hi there!\")
|
|
|
|
irc.process_forever()
|
|
|
|
|
|
|
|
This will connect to the IRC server irc.some.where on port 6667
|
|
|
|
using the nickname my_nickname and send the message \"Hi there!\"
|
|
|
|
to the nickname a_nickname.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, fn_to_add_socket=None,
|
|
|
|
fn_to_remove_socket=None,
|
|
|
|
fn_to_add_timeout=None):
|
|
|
|
"""Constructor for IRC objects.
|
|
|
|
|
|
|
|
Optional arguments are fn_to_add_socket, fn_to_remove_socket
|
|
|
|
and fn_to_add_timeout. The first two specify functions that
|
|
|
|
will be called with a socket object as argument when the IRC
|
|
|
|
object wants to be notified (or stop being notified) of data
|
|
|
|
coming on a new socket. When new data arrives, the method
|
|
|
|
process_data should be called. Similarly, fn_to_add_timeout
|
|
|
|
is called with a number of seconds (a floating point number)
|
|
|
|
as first argument when the IRC object wants to receive a
|
|
|
|
notification (by calling the process_timeout method). So, if
|
|
|
|
e.g. the argument is 42.17, the object wants the
|
|
|
|
process_timeout method to be called after 42 seconds and 170
|
|
|
|
milliseconds.
|
|
|
|
|
|
|
|
The three arguments mainly exist to be able to use an external
|
|
|
|
main loop (for example Tkinter's or PyGTK's main app loop)
|
|
|
|
instead of calling the process_forever method.
|
|
|
|
|
|
|
|
An alternative is to just call ServerConnection.process_once()
|
|
|
|
once in a while.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if fn_to_add_socket and fn_to_remove_socket:
|
|
|
|
self.fn_to_add_socket = fn_to_add_socket
|
|
|
|
self.fn_to_remove_socket = fn_to_remove_socket
|
|
|
|
else:
|
|
|
|
self.fn_to_add_socket = None
|
|
|
|
self.fn_to_remove_socket = None
|
|
|
|
|
|
|
|
self.fn_to_add_timeout = fn_to_add_timeout
|
|
|
|
self.connections = []
|
|
|
|
self.handlers = {}
|
|
|
|
self.delayed_commands = [] # list of tuples in the format (time, function, arguments)
|
|
|
|
|
|
|
|
self.add_global_handler("ping", _ping_ponger, -42)
|
|
|
|
|
|
|
|
def server(self):
|
|
|
|
"""Creates and returns a ServerConnection object."""
|
|
|
|
|
|
|
|
c = ServerConnection(self)
|
|
|
|
self.connections.append(c)
|
|
|
|
return c
|
|
|
|
|
|
|
|
def process_data(self, sockets):
|
|
|
|
"""Called when there is more data to read on connection sockets.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
sockets -- A list of socket objects.
|
|
|
|
|
|
|
|
See documentation for IRC.__init__.
|
|
|
|
"""
|
|
|
|
for s in sockets:
|
|
|
|
for c in self.connections:
|
|
|
|
if s == c._get_socket():
|
|
|
|
c.process_data()
|
|
|
|
|
|
|
|
def process_timeout(self):
|
|
|
|
"""Called when a timeout notification is due.
|
|
|
|
|
|
|
|
See documentation for IRC.__init__.
|
|
|
|
"""
|
|
|
|
t = time.time()
|
|
|
|
while self.delayed_commands:
|
|
|
|
if t >= self.delayed_commands[0][0]:
|
|
|
|
self.delayed_commands[0][1](*self.delayed_commands[0][2])
|
|
|
|
del self.delayed_commands[0]
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
def process_once(self, timeout=0):
|
|
|
|
"""Process data from connections once.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
timeout -- How long the select() call should wait if no
|
|
|
|
data is available.
|
|
|
|
|
|
|
|
This method should be called periodically to check and process
|
|
|
|
incoming data, if there are any. If that seems boring, look
|
|
|
|
at the process_forever method.
|
|
|
|
"""
|
2016-07-28 18:25:38 +00:00
|
|
|
sockets = [x._get_socket() for x in self.connections]
|
|
|
|
sockets = [x for x in sockets if x != None]
|
2016-07-25 18:01:51 +00:00
|
|
|
if sockets:
|
|
|
|
(i, o, e) = select.select(sockets, [], [], timeout)
|
|
|
|
self.process_data(i)
|
|
|
|
else:
|
|
|
|
time.sleep(timeout)
|
|
|
|
self.process_timeout()
|
|
|
|
|
|
|
|
def process_forever(self, timeout=0.2):
|
|
|
|
"""Run an infinite loop, processing data from connections.
|
|
|
|
|
|
|
|
This method repeatedly calls process_once.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
timeout -- Parameter to pass to process_once.
|
|
|
|
"""
|
|
|
|
while 1:
|
|
|
|
self.process_once(timeout)
|
|
|
|
|
|
|
|
def disconnect_all(self, message=""):
|
|
|
|
"""Disconnects all connections."""
|
|
|
|
for c in self.connections:
|
|
|
|
c.disconnect(message)
|
|
|
|
|
|
|
|
def add_global_handler(self, event, handler, priority=0):
|
|
|
|
"""Adds a global handler function for a specific event type.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
event -- Event type (a string). Check the values of the
|
|
|
|
numeric_events dictionary in irclib.py for possible event
|
|
|
|
types.
|
|
|
|
|
|
|
|
handler -- Callback function.
|
|
|
|
|
|
|
|
priority -- A number (the lower number, the higher priority).
|
|
|
|
|
|
|
|
The handler function is called whenever the specified event is
|
|
|
|
triggered in any of the connections. See documentation for
|
|
|
|
the Event class.
|
|
|
|
|
|
|
|
The handler functions are called in priority order (lowest
|
|
|
|
number is highest priority). If a handler function returns
|
|
|
|
\"NO MORE\", no more handlers will be called.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if not event in self.handlers:
|
|
|
|
self.handlers[event] = []
|
|
|
|
bisect.insort(self.handlers[event], ((priority, handler)))
|
|
|
|
|
|
|
|
def remove_global_handler(self, event, handler):
|
|
|
|
"""Removes a global handler function.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
event -- Event type (a string).
|
|
|
|
|
|
|
|
handler -- Callback function.
|
|
|
|
|
|
|
|
Returns 1 on success, otherwise 0.
|
|
|
|
"""
|
|
|
|
if not event in self.handlers:
|
|
|
|
return 0
|
|
|
|
for h in self.handlers[event]:
|
|
|
|
if handler == h[1]:
|
|
|
|
self.handlers[event].remove(h)
|
|
|
|
return 1
|
|
|
|
|
|
|
|
def execute_at(self, at, function, arguments=()):
|
|
|
|
"""Execute a function at a specified time.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
at -- Execute at this time (standard \"time_t\" time).
|
|
|
|
|
|
|
|
function -- Function to call.
|
|
|
|
|
|
|
|
arguments -- Arguments to give the function.
|
|
|
|
"""
|
|
|
|
self.execute_delayed(at-time.time(), function, arguments)
|
|
|
|
|
|
|
|
def execute_delayed(self, delay, function, arguments=()):
|
|
|
|
"""Execute a function after a specified time.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
delay -- How many seconds to wait.
|
|
|
|
|
|
|
|
function -- Function to call.
|
|
|
|
|
|
|
|
arguments -- Arguments to give the function.
|
|
|
|
"""
|
|
|
|
bisect.insort(self.delayed_commands, (delay+time.time(), function, arguments))
|
|
|
|
if self.fn_to_add_timeout:
|
|
|
|
self.fn_to_add_timeout(delay)
|
|
|
|
|
|
|
|
def dcc(self, dcctype="chat"):
|
|
|
|
"""Creates and returns a DCCConnection object.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
dcctype -- "chat" for DCC CHAT connections or "raw" for
|
|
|
|
DCC SEND (or other DCC types). If "chat",
|
|
|
|
incoming data will be split in newline-separated
|
|
|
|
chunks. If "raw", incoming data is not touched.
|
|
|
|
"""
|
|
|
|
c = DCCConnection(self, dcctype)
|
|
|
|
self.connections.append(c)
|
|
|
|
return c
|
|
|
|
|
|
|
|
def _handle_event(self, connection, event):
|
|
|
|
"""[Internal]"""
|
|
|
|
h = self.handlers
|
|
|
|
for handler in h.get("all_events", []) + h.get(event.eventtype(), []):
|
|
|
|
if handler[1](connection, event) == "NO MORE":
|
|
|
|
return
|
|
|
|
|
|
|
|
def _remove_connection(self, connection):
|
|
|
|
"""[Internal]"""
|
|
|
|
self.connections.remove(connection)
|
|
|
|
if self.fn_to_remove_socket:
|
|
|
|
self.fn_to_remove_socket(connection._get_socket())
|
|
|
|
|
|
|
|
_rfc_1459_command_regexp = re.compile("^(:(?P<prefix>[^ ]+) +)?(?P<command>[^ ]+)( *(?P<argument> .+))?")
|
|
|
|
|
|
|
|
|
|
|
|
class Connection:
|
|
|
|
"""Base class for IRC connections.
|
|
|
|
|
|
|
|
Must be overridden.
|
|
|
|
"""
|
|
|
|
def __init__(self, irclibobj):
|
|
|
|
self.irclibobj = irclibobj
|
|
|
|
|
|
|
|
def _get_socket():
|
2016-07-28 18:25:38 +00:00
|
|
|
raise IRCError("Not overridden")
|
2016-07-25 18:01:51 +00:00
|
|
|
|
|
|
|
##############################
|
|
|
|
### Convenience wrappers.
|
|
|
|
|
|
|
|
def execute_at(self, at, function, arguments=()):
|
|
|
|
self.irclibobj.execute_at(at, function, arguments)
|
|
|
|
|
|
|
|
def execute_delayed(self, delay, function, arguments=()):
|
|
|
|
self.irclibobj.execute_delayed(delay, function, arguments)
|
|
|
|
|
|
|
|
|
|
|
|
class ServerConnectionError(IRCError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class ServerNotConnectedError(ServerConnectionError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# Huh!? Crrrrazy EFNet doesn't follow the RFC: their ircd seems to
|
|
|
|
# use \n as message separator! :P
|
|
|
|
_linesep_regexp = re.compile("\r?\n")
|
|
|
|
|
|
|
|
class ServerConnection(Connection):
|
|
|
|
"""This class represents an IRC server connection.
|
|
|
|
|
|
|
|
ServerConnection objects are instantiated by calling the server
|
|
|
|
method on an IRC object.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, irclibobj):
|
|
|
|
Connection.__init__(self, irclibobj)
|
|
|
|
self.connected = 0 # Not connected yet.
|
|
|
|
self.socket = None
|
|
|
|
|
|
|
|
def connect(self, server, port, nickname, password=None, username=None,
|
|
|
|
ircname=None, localaddress="", localport=0):
|
|
|
|
"""Connect/reconnect to a server.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
server -- Server name.
|
|
|
|
|
|
|
|
port -- Port number.
|
|
|
|
|
|
|
|
nickname -- The nickname.
|
|
|
|
|
|
|
|
password -- Password (if any).
|
|
|
|
|
|
|
|
username -- The username.
|
|
|
|
|
|
|
|
ircname -- The IRC name ("realname").
|
|
|
|
|
|
|
|
localaddress -- Bind the connection to a specific local IP address.
|
|
|
|
|
|
|
|
localport -- Bind the connection to a specific local port.
|
|
|
|
|
|
|
|
This function can be called to reconnect a closed connection.
|
|
|
|
|
|
|
|
Returns the ServerConnection object.
|
|
|
|
"""
|
|
|
|
if self.connected:
|
|
|
|
self.disconnect("Changing servers")
|
|
|
|
|
2016-07-28 18:25:38 +00:00
|
|
|
self.previous_buffer = u""
|
2016-07-25 18:01:51 +00:00
|
|
|
self.handlers = {}
|
|
|
|
self.real_server_name = ""
|
|
|
|
self.real_nickname = nickname
|
|
|
|
self.server = server
|
|
|
|
self.port = port
|
|
|
|
self.nickname = nickname
|
|
|
|
self.username = username or nickname
|
|
|
|
self.ircname = ircname or nickname
|
|
|
|
self.password = password
|
|
|
|
self.localaddress = localaddress
|
|
|
|
self.localport = localport
|
|
|
|
self.localhost = socket.gethostname()
|
|
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
try:
|
|
|
|
self.socket.bind((self.localaddress, self.localport))
|
|
|
|
self.socket.connect((self.server, self.port))
|
2016-07-28 18:25:38 +00:00
|
|
|
except socket.error as x:
|
2016-07-25 18:01:51 +00:00
|
|
|
self.socket.close()
|
|
|
|
self.socket = None
|
2016-07-28 18:25:38 +00:00
|
|
|
raise ServerConnectionError("Couldn't connect to socket: %s" % x)
|
2016-07-25 18:01:51 +00:00
|
|
|
self.connected = 1
|
|
|
|
if self.irclibobj.fn_to_add_socket:
|
|
|
|
self.irclibobj.fn_to_add_socket(self.socket)
|
|
|
|
|
|
|
|
# Log on...
|
|
|
|
if self.password:
|
|
|
|
self.pass_(self.password)
|
|
|
|
self.nick(self.nickname)
|
|
|
|
self.user(self.username, self.ircname)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
"""Close the connection.
|
|
|
|
|
|
|
|
This method closes the connection permanently; after it has
|
|
|
|
been called, the object is unusable.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.disconnect("Closing object")
|
|
|
|
self.irclibobj._remove_connection(self)
|
|
|
|
|
|
|
|
def _get_socket(self):
|
|
|
|
"""[Internal]"""
|
|
|
|
return self.socket
|
|
|
|
|
|
|
|
def get_server_name(self):
|
|
|
|
"""Get the (real) server name.
|
|
|
|
|
|
|
|
This method returns the (real) server name, or, more
|
|
|
|
specifically, what the server calls itself.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if self.real_server_name:
|
|
|
|
return self.real_server_name
|
|
|
|
else:
|
|
|
|
return ""
|
|
|
|
|
|
|
|
def get_nickname(self):
|
|
|
|
"""Get the (real) nick name.
|
|
|
|
|
|
|
|
This method returns the (real) nickname. The library keeps
|
|
|
|
track of nick changes, so it might not be the nick name that
|
|
|
|
was passed to the connect() method. """
|
|
|
|
|
|
|
|
return self.real_nickname
|
|
|
|
|
|
|
|
def process_data(self):
|
|
|
|
"""[Internal]"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
new_data = self.socket.recv(2**14)
|
2016-07-28 18:25:38 +00:00
|
|
|
except socket.error as x:
|
2016-07-25 18:01:51 +00:00
|
|
|
# The server hung up.
|
|
|
|
self.disconnect("Connection reset by peer")
|
|
|
|
return
|
|
|
|
if not new_data:
|
|
|
|
# Read nothing: connection must be down.
|
|
|
|
self.disconnect("Connection reset by peer")
|
|
|
|
return
|
|
|
|
|
2016-07-28 18:25:38 +00:00
|
|
|
lines = _linesep_regexp.split(self.previous_buffer + new_data.decode("latin-1", "replace"))
|
2016-07-25 18:01:51 +00:00
|
|
|
|
|
|
|
# Save the last, unfinished line.
|
|
|
|
self.previous_buffer = lines[-1]
|
|
|
|
lines = lines[:-1]
|
|
|
|
|
|
|
|
for line in lines:
|
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("FROM SERVER: %s\n" % line)
|
2016-07-25 18:01:51 +00:00
|
|
|
|
|
|
|
if not line:
|
|
|
|
continue
|
|
|
|
|
|
|
|
prefix = None
|
|
|
|
command = None
|
|
|
|
arguments = None
|
|
|
|
self._handle_event(Event("all_raw_messages",
|
|
|
|
self.get_server_name(),
|
|
|
|
None,
|
|
|
|
[line]))
|
|
|
|
|
|
|
|
m = _rfc_1459_command_regexp.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])
|
|
|
|
|
|
|
|
# Translate numerics into more readable strings.
|
|
|
|
if command in numeric_events:
|
|
|
|
command = numeric_events[command]
|
|
|
|
|
|
|
|
if command == "nick":
|
|
|
|
if nm_to_n(prefix) == self.real_nickname:
|
|
|
|
self.real_nickname = arguments[0]
|
|
|
|
elif command == "welcome":
|
|
|
|
# Record the nickname in case the client changed nick
|
|
|
|
# in a nicknameinuse callback.
|
|
|
|
self.real_nickname = arguments[0]
|
|
|
|
|
|
|
|
if command in ["privmsg", "notice"]:
|
|
|
|
target, message = arguments[0], arguments[1]
|
|
|
|
messages = _ctcp_dequote(message)
|
|
|
|
|
|
|
|
if command == "privmsg":
|
|
|
|
if is_channel(target):
|
|
|
|
command = "pubmsg"
|
|
|
|
else:
|
|
|
|
if is_channel(target):
|
|
|
|
command = "pubnotice"
|
|
|
|
else:
|
|
|
|
command = "privnotice"
|
|
|
|
|
|
|
|
for m in messages:
|
2016-07-28 18:25:38 +00:00
|
|
|
if type(m) is tuple:
|
2016-07-25 18:01:51 +00:00
|
|
|
if command in ["privmsg", "pubmsg"]:
|
|
|
|
command = "ctcp"
|
|
|
|
else:
|
|
|
|
command = "ctcpreply"
|
|
|
|
|
|
|
|
m = list(m)
|
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("command: %s, source: %s, target: %s, arguments: %s" % (
|
|
|
|
command, prefix, target, m))
|
2016-07-25 18:01:51 +00:00
|
|
|
self._handle_event(Event(command, prefix, target, m))
|
|
|
|
if command == "ctcp" and m[0] == "ACTION":
|
|
|
|
self._handle_event(Event("action", prefix, target, m[1:]))
|
|
|
|
else:
|
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("command: %s, source: %s, target: %s, arguments: %s" % (
|
|
|
|
command, prefix, target, [m]))
|
2016-07-25 18:01:51 +00:00
|
|
|
self._handle_event(Event(command, prefix, target, [m]))
|
|
|
|
else:
|
|
|
|
target = None
|
|
|
|
|
|
|
|
if command == "quit":
|
|
|
|
arguments = [arguments[0]]
|
|
|
|
elif command == "ping":
|
|
|
|
target = arguments[0]
|
|
|
|
else:
|
|
|
|
target = arguments[0]
|
|
|
|
arguments = arguments[1:]
|
|
|
|
|
|
|
|
if command == "mode":
|
|
|
|
if not is_channel(target):
|
|
|
|
command = "umode"
|
|
|
|
|
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("command: %s, source: %s, target: %s, arguments: %s" % (
|
|
|
|
command, prefix, target, arguments))
|
2016-07-25 18:01:51 +00:00
|
|
|
self._handle_event(Event(command, prefix, target, arguments))
|
|
|
|
|
|
|
|
def _handle_event(self, event):
|
|
|
|
"""[Internal]"""
|
|
|
|
self.irclibobj._handle_event(self, event)
|
|
|
|
if event.eventtype() in self.handlers:
|
|
|
|
for fn in self.handlers[event.eventtype()]:
|
|
|
|
fn(self, event)
|
|
|
|
|
|
|
|
def is_connected(self):
|
|
|
|
"""Return connection status.
|
|
|
|
|
|
|
|
Returns true if connected, otherwise false.
|
|
|
|
"""
|
|
|
|
return self.connected
|
|
|
|
|
|
|
|
def add_global_handler(self, *args):
|
|
|
|
"""Add global handler.
|
|
|
|
|
|
|
|
See documentation for IRC.add_global_handler.
|
|
|
|
"""
|
|
|
|
self.irclibobj.add_global_handler(*args)
|
|
|
|
|
|
|
|
def remove_global_handler(self, *args):
|
|
|
|
"""Remove global handler.
|
|
|
|
|
|
|
|
See documentation for IRC.remove_global_handler.
|
|
|
|
"""
|
|
|
|
self.irclibobj.remove_global_handler(*args)
|
|
|
|
|
|
|
|
def action(self, target, action):
|
|
|
|
"""Send a CTCP ACTION command."""
|
|
|
|
self.ctcp("ACTION", target, action)
|
|
|
|
|
|
|
|
def admin(self, server=""):
|
|
|
|
"""Send an ADMIN command."""
|
|
|
|
self.send_raw(" ".join(["ADMIN", server]).strip())
|
|
|
|
|
|
|
|
def ctcp(self, ctcptype, target, parameter=""):
|
|
|
|
"""Send a CTCP command."""
|
|
|
|
ctcptype = ctcptype.upper()
|
|
|
|
self.privmsg(target, "\001%s%s\001" % (ctcptype, parameter and (" " + parameter) or ""))
|
|
|
|
|
|
|
|
def ctcp_reply(self, target, parameter):
|
|
|
|
"""Send a CTCP REPLY command."""
|
|
|
|
self.notice(target, "\001%s\001" % parameter)
|
|
|
|
|
|
|
|
def disconnect(self, message=""):
|
|
|
|
"""Hang up the connection.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
message -- Quit message.
|
|
|
|
"""
|
|
|
|
if not self.connected:
|
|
|
|
return
|
|
|
|
|
|
|
|
self.connected = 0
|
|
|
|
|
|
|
|
self.quit(message)
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.socket.close()
|
2016-07-28 18:25:38 +00:00
|
|
|
except socket.error as x:
|
2016-07-25 18:01:51 +00:00
|
|
|
pass
|
|
|
|
self.socket = None
|
|
|
|
self._handle_event(Event("disconnect", self.server, "", [message]))
|
|
|
|
|
|
|
|
def globops(self, text):
|
|
|
|
"""Send a GLOBOPS command."""
|
|
|
|
self.send_raw("GLOBOPS :" + text)
|
|
|
|
|
|
|
|
def info(self, server=""):
|
|
|
|
"""Send an INFO command."""
|
|
|
|
self.send_raw(" ".join(["INFO", server]).strip())
|
|
|
|
|
|
|
|
def invite(self, nick, channel):
|
|
|
|
"""Send an INVITE command."""
|
|
|
|
self.send_raw(" ".join(["INVITE", nick, channel]).strip())
|
|
|
|
|
|
|
|
def ison(self, nicks):
|
|
|
|
"""Send an ISON command.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
nicks -- List of nicks.
|
|
|
|
"""
|
|
|
|
self.send_raw("ISON " + " ".join(nicks))
|
|
|
|
|
|
|
|
def join(self, channel, key=""):
|
|
|
|
"""Send a JOIN command."""
|
|
|
|
self.send_raw("JOIN %s%s" % (channel, (key and (" " + key))))
|
|
|
|
|
|
|
|
def kick(self, channel, nick, comment=""):
|
|
|
|
"""Send a KICK command."""
|
|
|
|
self.send_raw("KICK %s %s%s" % (channel, nick, (comment and (" :" + comment))))
|
|
|
|
|
|
|
|
def links(self, remote_server="", server_mask=""):
|
|
|
|
"""Send a LINKS command."""
|
|
|
|
command = "LINKS"
|
|
|
|
if remote_server:
|
|
|
|
command = command + " " + remote_server
|
|
|
|
if server_mask:
|
|
|
|
command = command + " " + server_mask
|
|
|
|
self.send_raw(command)
|
|
|
|
|
|
|
|
def list(self, channels=None, server=""):
|
|
|
|
"""Send a LIST command."""
|
|
|
|
command = "LIST"
|
|
|
|
if channels:
|
|
|
|
command = command + " " + ",".join(channels)
|
|
|
|
if server:
|
|
|
|
command = command + " " + server
|
|
|
|
self.send_raw(command)
|
|
|
|
|
|
|
|
def lusers(self, server=""):
|
|
|
|
"""Send a LUSERS command."""
|
|
|
|
self.send_raw("LUSERS" + (server and (" " + server)))
|
|
|
|
|
|
|
|
def mode(self, target, command):
|
|
|
|
"""Send a MODE command."""
|
|
|
|
self.send_raw("MODE %s %s" % (target, command))
|
|
|
|
|
|
|
|
def motd(self, server=""):
|
|
|
|
"""Send an MOTD command."""
|
|
|
|
self.send_raw("MOTD" + (server and (" " + server)))
|
|
|
|
|
|
|
|
def names(self, channels=None):
|
|
|
|
"""Send a NAMES command."""
|
|
|
|
self.send_raw("NAMES" + (channels and (" " + ",".join(channels)) or ""))
|
|
|
|
|
|
|
|
def nick(self, newnick):
|
|
|
|
"""Send a NICK command."""
|
|
|
|
self.send_raw("NICK " + newnick)
|
|
|
|
|
|
|
|
def notice(self, target, text):
|
|
|
|
"""Send a NOTICE command."""
|
|
|
|
# Should limit len(text) here!
|
|
|
|
self.send_raw("NOTICE %s :%s" % (target, text))
|
|
|
|
|
|
|
|
def oper(self, nick, password):
|
|
|
|
"""Send an OPER command."""
|
|
|
|
self.send_raw("OPER %s %s" % (nick, password))
|
|
|
|
|
|
|
|
def part(self, channels, message=""):
|
|
|
|
"""Send a PART command."""
|
2016-07-28 18:25:38 +00:00
|
|
|
if type(channels) == bytes:
|
2016-07-25 18:01:51 +00:00
|
|
|
self.send_raw("PART " + channels + (message and (" " + message)))
|
|
|
|
else:
|
|
|
|
self.send_raw("PART " + ",".join(channels) + (message and (" " + message)))
|
|
|
|
|
|
|
|
def pass_(self, password):
|
|
|
|
"""Send a PASS command."""
|
|
|
|
self.send_raw("PASS " + password)
|
|
|
|
|
|
|
|
def ping(self, target, target2=""):
|
|
|
|
"""Send a PING command."""
|
|
|
|
self.send_raw("PING %s%s" % (target, target2 and (" " + target2)))
|
|
|
|
|
|
|
|
def pong(self, target, target2=""):
|
|
|
|
"""Send a PONG command."""
|
|
|
|
self.send_raw("PONG %s%s" % (target, target2 and (" " + target2)))
|
|
|
|
|
|
|
|
def privmsg(self, target, text):
|
|
|
|
"""Send a PRIVMSG command."""
|
|
|
|
# Should limit len(text) here!
|
|
|
|
self.send_raw("PRIVMSG %s :%s" % (target, text))
|
|
|
|
|
|
|
|
def privmsg_many(self, targets, text):
|
|
|
|
"""Send a PRIVMSG command to multiple targets."""
|
|
|
|
# Should limit len(text) here!
|
|
|
|
self.send_raw("PRIVMSG %s :%s" % (",".join(targets), text))
|
|
|
|
|
|
|
|
def quit(self, message=""):
|
|
|
|
"""Send a QUIT command."""
|
|
|
|
# Note that many IRC servers don't use your QUIT message
|
|
|
|
# unless you've been connected for at least 5 minutes!
|
|
|
|
self.send_raw("QUIT" + (message and (" :" + message)))
|
|
|
|
|
|
|
|
def sconnect(self, target, port="", server=""):
|
|
|
|
"""Send an SCONNECT command."""
|
|
|
|
self.send_raw("CONNECT %s%s%s" % (target,
|
|
|
|
port and (" " + port),
|
|
|
|
server and (" " + server)))
|
|
|
|
|
|
|
|
def send_raw(self, string):
|
|
|
|
"""Send raw string to the server.
|
|
|
|
|
|
|
|
The string will be padded with appropriate CR LF.
|
|
|
|
"""
|
|
|
|
if self.socket is None:
|
2016-07-28 18:25:38 +00:00
|
|
|
raise ServerNotConnectedError("Not connected.")
|
2016-07-25 18:01:51 +00:00
|
|
|
try:
|
2016-07-28 18:25:38 +00:00
|
|
|
self.socket.send(string.encode("latin-1", "replace") + b"\r\n")
|
2016-07-25 18:01:51 +00:00
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("TO SERVER: %s\n" % string)
|
|
|
|
except socket.error as x:
|
2016-07-25 18:01:51 +00:00
|
|
|
# Ouch!
|
|
|
|
self.disconnect("Connection reset by peer.")
|
|
|
|
|
|
|
|
def squit(self, server, comment=""):
|
|
|
|
"""Send an SQUIT command."""
|
|
|
|
self.send_raw("SQUIT %s%s" % (server, comment and (" :" + comment)))
|
|
|
|
|
|
|
|
def stats(self, statstype, server=""):
|
|
|
|
"""Send a STATS command."""
|
|
|
|
self.send_raw("STATS %s%s" % (statstype, server and (" " + server)))
|
|
|
|
|
|
|
|
def time(self, server=""):
|
|
|
|
"""Send a TIME command."""
|
|
|
|
self.send_raw("TIME" + (server and (" " + server)))
|
|
|
|
|
|
|
|
def topic(self, channel, new_topic=None):
|
|
|
|
"""Send a TOPIC command."""
|
|
|
|
if new_topic is None:
|
|
|
|
self.send_raw("TOPIC " + channel)
|
|
|
|
else:
|
|
|
|
self.send_raw("TOPIC %s :%s" % (channel, new_topic))
|
|
|
|
|
|
|
|
def trace(self, target=""):
|
|
|
|
"""Send a TRACE command."""
|
|
|
|
self.send_raw("TRACE" + (target and (" " + target)))
|
|
|
|
|
|
|
|
def user(self, username, realname):
|
|
|
|
"""Send a USER command."""
|
|
|
|
self.send_raw("USER %s 0 * :%s" % (username, realname))
|
|
|
|
|
|
|
|
def userhost(self, nicks):
|
|
|
|
"""Send a USERHOST command."""
|
|
|
|
self.send_raw("USERHOST " + ",".join(nicks))
|
|
|
|
|
|
|
|
def users(self, server=""):
|
|
|
|
"""Send a USERS command."""
|
|
|
|
self.send_raw("USERS" + (server and (" " + server)))
|
|
|
|
|
|
|
|
def version(self, server=""):
|
|
|
|
"""Send a VERSION command."""
|
|
|
|
self.send_raw("VERSION" + (server and (" " + server)))
|
|
|
|
|
|
|
|
def wallops(self, text):
|
|
|
|
"""Send a WALLOPS command."""
|
|
|
|
self.send_raw("WALLOPS :" + text)
|
|
|
|
|
|
|
|
def who(self, target="", op=""):
|
|
|
|
"""Send a WHO command."""
|
|
|
|
self.send_raw("WHO%s%s" % (target and (" " + target), op and (" o")))
|
|
|
|
|
|
|
|
def whois(self, targets):
|
|
|
|
"""Send a WHOIS command."""
|
|
|
|
self.send_raw("WHOIS " + ",".join(targets))
|
|
|
|
|
|
|
|
def whowas(self, nick, max="", server=""):
|
|
|
|
"""Send a WHOWAS command."""
|
|
|
|
self.send_raw("WHOWAS %s%s%s" % (nick,
|
|
|
|
max and (" " + max),
|
|
|
|
server and (" " + server)))
|
|
|
|
|
|
|
|
|
|
|
|
class DCCConnectionError(IRCError):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class DCCConnection(Connection):
|
|
|
|
"""This class represents a DCC connection.
|
|
|
|
|
|
|
|
DCCConnection objects are instantiated by calling the dcc
|
|
|
|
method on an IRC object.
|
|
|
|
"""
|
|
|
|
def __init__(self, irclibobj, dcctype):
|
|
|
|
Connection.__init__(self, irclibobj)
|
|
|
|
self.connected = 0
|
|
|
|
self.passive = 0
|
|
|
|
self.dcctype = dcctype
|
|
|
|
self.peeraddress = None
|
|
|
|
self.peerport = None
|
|
|
|
|
|
|
|
def connect(self, address, port):
|
|
|
|
"""Connect/reconnect to a DCC peer.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
address -- Host/IP address of the peer.
|
|
|
|
|
|
|
|
port -- The port number to connect to.
|
|
|
|
|
|
|
|
Returns the DCCConnection object.
|
|
|
|
"""
|
|
|
|
self.peeraddress = socket.gethostbyname(address)
|
|
|
|
self.peerport = port
|
|
|
|
self.socket = None
|
2016-07-28 18:25:38 +00:00
|
|
|
self.previous_buffer = u""
|
2016-07-25 18:01:51 +00:00
|
|
|
self.handlers = {}
|
|
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
self.passive = 0
|
|
|
|
try:
|
|
|
|
self.socket.connect((self.peeraddress, self.peerport))
|
2016-07-28 18:25:38 +00:00
|
|
|
except socket.error as x:
|
|
|
|
raise DCCConnectionError("Couldn't connect to socket: %s" % x)
|
2016-07-25 18:01:51 +00:00
|
|
|
self.connected = 1
|
|
|
|
if self.irclibobj.fn_to_add_socket:
|
|
|
|
self.irclibobj.fn_to_add_socket(self.socket)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def listen(self):
|
|
|
|
"""Wait for a connection/reconnection from a DCC peer.
|
|
|
|
|
|
|
|
Returns the DCCConnection object.
|
|
|
|
|
|
|
|
The local IP address and port are available as
|
|
|
|
self.localaddress and self.localport. After connection from a
|
|
|
|
peer, the peer address and port are available as
|
|
|
|
self.peeraddress and self.peerport.
|
|
|
|
"""
|
2016-07-28 18:25:38 +00:00
|
|
|
self.previous_buffer = u""
|
2016-07-25 18:01:51 +00:00
|
|
|
self.handlers = {}
|
|
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
self.passive = 1
|
|
|
|
try:
|
|
|
|
self.socket.bind((socket.gethostbyname(socket.gethostname()), 0))
|
|
|
|
self.localaddress, self.localport = self.socket.getsockname()
|
|
|
|
self.socket.listen(10)
|
2016-07-28 18:25:38 +00:00
|
|
|
except socket.error as x:
|
|
|
|
raise DCCConnectionError("Couldn't bind socket: %s" % x)
|
2016-07-25 18:01:51 +00:00
|
|
|
return self
|
|
|
|
|
|
|
|
def disconnect(self, message=""):
|
|
|
|
"""Hang up the connection and close the object.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
message -- Quit message.
|
|
|
|
"""
|
|
|
|
if not self.connected:
|
|
|
|
return
|
|
|
|
|
|
|
|
self.connected = 0
|
|
|
|
try:
|
|
|
|
self.socket.close()
|
2016-07-28 18:25:38 +00:00
|
|
|
except socket.error as x:
|
2016-07-25 18:01:51 +00:00
|
|
|
pass
|
|
|
|
self.socket = None
|
|
|
|
self.irclibobj._handle_event(
|
|
|
|
self,
|
|
|
|
Event("dcc_disconnect", self.peeraddress, "", [message]))
|
|
|
|
self.irclibobj._remove_connection(self)
|
|
|
|
|
|
|
|
def process_data(self):
|
|
|
|
"""[Internal]"""
|
|
|
|
|
|
|
|
if self.passive and not self.connected:
|
|
|
|
conn, (self.peeraddress, self.peerport) = self.socket.accept()
|
|
|
|
self.socket.close()
|
|
|
|
self.socket = conn
|
|
|
|
self.connected = 1
|
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("DCC connection from %s:%d" % (
|
|
|
|
self.peeraddress, self.peerport))
|
2016-07-25 18:01:51 +00:00
|
|
|
self.irclibobj._handle_event(
|
|
|
|
self,
|
|
|
|
Event("dcc_connect", self.peeraddress, None, None))
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
new_data = self.socket.recv(2**14)
|
2016-07-28 18:25:38 +00:00
|
|
|
except socket.error as x:
|
2016-07-25 18:01:51 +00:00
|
|
|
# The server hung up.
|
|
|
|
self.disconnect("Connection reset by peer")
|
|
|
|
return
|
|
|
|
if not new_data:
|
|
|
|
# Read nothing: connection must be down.
|
|
|
|
self.disconnect("Connection reset by peer")
|
|
|
|
return
|
|
|
|
|
|
|
|
if self.dcctype == "chat":
|
|
|
|
# The specification says lines are terminated with LF, but
|
|
|
|
# it seems safer to handle CR LF terminations too.
|
|
|
|
chunks = _linesep_regexp.split(self.previous_buffer + new_data)
|
|
|
|
|
|
|
|
# Save the last, unfinished line.
|
|
|
|
self.previous_buffer = chunks[-1]
|
|
|
|
if len(self.previous_buffer) > 2**14:
|
|
|
|
# Bad peer! Naughty peer!
|
|
|
|
self.disconnect()
|
|
|
|
return
|
|
|
|
chunks = chunks[:-1]
|
|
|
|
else:
|
|
|
|
chunks = [new_data]
|
|
|
|
|
|
|
|
command = "dccmsg"
|
|
|
|
prefix = self.peeraddress
|
|
|
|
target = None
|
|
|
|
for chunk in chunks:
|
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("FROM PEER: %s\n" % chunk)
|
2016-07-25 18:01:51 +00:00
|
|
|
arguments = [chunk]
|
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("command: %s, source: %s, target: %s, arguments: %s" % (
|
|
|
|
command, prefix, target, arguments))
|
2016-07-25 18:01:51 +00:00
|
|
|
self.irclibobj._handle_event(
|
|
|
|
self,
|
|
|
|
Event(command, prefix, target, arguments))
|
|
|
|
|
|
|
|
def _get_socket(self):
|
|
|
|
"""[Internal]"""
|
|
|
|
return self.socket
|
|
|
|
|
|
|
|
def privmsg(self, string):
|
|
|
|
"""Send data to DCC peer.
|
|
|
|
|
|
|
|
The string will be padded with appropriate LF if it's a DCC
|
|
|
|
CHAT session.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
self.socket.send(string)
|
|
|
|
if self.dcctype == "chat":
|
|
|
|
self.socket.send("\n")
|
|
|
|
if DEBUG:
|
2016-07-28 18:25:38 +00:00
|
|
|
print("TO PEER: %s\n" % string)
|
|
|
|
except socket.error as x:
|
2016-07-25 18:01:51 +00:00
|
|
|
# Ouch!
|
|
|
|
self.disconnect("Connection reset by peer.")
|
|
|
|
|
|
|
|
class SimpleIRCClient:
|
|
|
|
"""A simple single-server IRC client class.
|
|
|
|
|
|
|
|
This is an example of an object-oriented wrapper of the IRC
|
|
|
|
framework. A real IRC client can be made by subclassing this
|
|
|
|
class and adding appropriate methods.
|
|
|
|
|
|
|
|
The method on_join will be called when a "join" event is created
|
|
|
|
(which is done when the server sends a JOIN messsage/command),
|
|
|
|
on_privmsg will be called for "privmsg" events, and so on. The
|
|
|
|
handler methods get two arguments: the connection object (same as
|
|
|
|
self.connection) and the event object.
|
|
|
|
|
|
|
|
Instance attributes that can be used by sub classes:
|
|
|
|
|
|
|
|
ircobj -- The IRC instance.
|
|
|
|
|
|
|
|
connection -- The ServerConnection instance.
|
|
|
|
|
|
|
|
dcc_connections -- A list of DCCConnection instances.
|
|
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
self.ircobj = IRC()
|
|
|
|
self.connection = self.ircobj.server()
|
|
|
|
self.dcc_connections = []
|
|
|
|
self.ircobj.add_global_handler("all_events", self._dispatcher, -10)
|
|
|
|
self.ircobj.add_global_handler("dcc_disconnect", self._dcc_disconnect, -10)
|
|
|
|
|
|
|
|
def _dispatcher(self, c, e):
|
|
|
|
"""[Internal]"""
|
|
|
|
m = "on_" + e.eventtype()
|
|
|
|
if hasattr(self, m):
|
|
|
|
getattr(self, m)(c, e)
|
|
|
|
|
|
|
|
def _dcc_disconnect(self, c, e):
|
|
|
|
self.dcc_connections.remove(c)
|
|
|
|
|
|
|
|
def connect(self, server, port, nickname, password=None, username=None,
|
|
|
|
ircname=None, localaddress="", localport=0):
|
|
|
|
"""Connect/reconnect to a server.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
server -- Server name.
|
|
|
|
|
|
|
|
port -- Port number.
|
|
|
|
|
|
|
|
nickname -- The nickname.
|
|
|
|
|
|
|
|
password -- Password (if any).
|
|
|
|
|
|
|
|
username -- The username.
|
|
|
|
|
|
|
|
ircname -- The IRC name.
|
|
|
|
|
|
|
|
localaddress -- Bind the connection to a specific local IP address.
|
|
|
|
|
|
|
|
localport -- Bind the connection to a specific local port.
|
|
|
|
|
|
|
|
This function can be called to reconnect a closed connection.
|
|
|
|
"""
|
|
|
|
self.connection.connect(server, port, nickname,
|
|
|
|
password, username, ircname,
|
|
|
|
localaddress, localport)
|
|
|
|
|
|
|
|
def dcc_connect(self, address, port, dcctype="chat"):
|
|
|
|
"""Connect to a DCC peer.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
address -- IP address of the peer.
|
|
|
|
|
|
|
|
port -- Port to connect to.
|
|
|
|
|
|
|
|
Returns a DCCConnection instance.
|
|
|
|
"""
|
|
|
|
dcc = self.ircobj.dcc(dcctype)
|
|
|
|
self.dcc_connections.append(dcc)
|
|
|
|
dcc.connect(address, port)
|
|
|
|
return dcc
|
|
|
|
|
|
|
|
def dcc_listen(self, dcctype="chat"):
|
|
|
|
"""Listen for connections from a DCC peer.
|
|
|
|
|
|
|
|
Returns a DCCConnection instance.
|
|
|
|
"""
|
|
|
|
dcc = self.ircobj.dcc(dcctype)
|
|
|
|
self.dcc_connections.append(dcc)
|
|
|
|
dcc.listen()
|
|
|
|
return dcc
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
"""Start the IRC client."""
|
|
|
|
self.ircobj.process_forever()
|
|
|
|
|
|
|
|
|
|
|
|
class Event:
|
|
|
|
"""Class representing an IRC event."""
|
|
|
|
def __init__(self, eventtype, source, target, arguments=None):
|
|
|
|
"""Constructor of Event objects.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
eventtype -- A string describing the event.
|
|
|
|
|
|
|
|
source -- The originator of the event (a nick mask or a server).
|
|
|
|
|
|
|
|
target -- The target of the event (a nick or a channel).
|
|
|
|
|
|
|
|
arguments -- Any event specific arguments.
|
|
|
|
"""
|
|
|
|
self._eventtype = eventtype
|
|
|
|
self._source = source
|
|
|
|
self._target = target
|
|
|
|
if arguments:
|
|
|
|
self._arguments = arguments
|
|
|
|
else:
|
|
|
|
self._arguments = []
|
|
|
|
|
|
|
|
def eventtype(self):
|
|
|
|
"""Get the event type."""
|
|
|
|
return self._eventtype
|
|
|
|
|
|
|
|
def source(self):
|
|
|
|
"""Get the event source."""
|
|
|
|
return self._source
|
|
|
|
|
|
|
|
def target(self):
|
|
|
|
"""Get the event target."""
|
|
|
|
return self._target
|
|
|
|
|
|
|
|
def arguments(self):
|
|
|
|
"""Get the event arguments."""
|
|
|
|
return self._arguments
|
|
|
|
|
|
|
|
_LOW_LEVEL_QUOTE = "\020"
|
|
|
|
_CTCP_LEVEL_QUOTE = "\134"
|
|
|
|
_CTCP_DELIMITER = "\001"
|
|
|
|
|
|
|
|
_low_level_mapping = {
|
|
|
|
"0": "\000",
|
|
|
|
"n": "\n",
|
|
|
|
"r": "\r",
|
|
|
|
_LOW_LEVEL_QUOTE: _LOW_LEVEL_QUOTE
|
|
|
|
}
|
|
|
|
|
|
|
|
_low_level_regexp = re.compile(_LOW_LEVEL_QUOTE + "(.)")
|
|
|
|
|
|
|
|
def mask_matches(nick, mask):
|
|
|
|
"""Check if a nick matches a mask.
|
|
|
|
|
|
|
|
Returns true if the nick matches, otherwise false.
|
|
|
|
"""
|
|
|
|
nick = irc_lower(nick)
|
|
|
|
mask = irc_lower(mask)
|
|
|
|
mask = mask.replace("\\", "\\\\")
|
|
|
|
for ch in ".$|[](){}+":
|
|
|
|
mask = mask.replace(ch, "\\" + ch)
|
|
|
|
mask = mask.replace("?", ".")
|
|
|
|
mask = mask.replace("*", ".*")
|
|
|
|
r = re.compile(mask, re.IGNORECASE)
|
|
|
|
return r.match(nick)
|
|
|
|
|
|
|
|
_special = "-[]\\`^{}"
|
|
|
|
nick_characters = string.ascii_letters + string.digits + _special
|
2016-07-28 18:25:38 +00:00
|
|
|
if sys.version_info >= (3, 0):
|
|
|
|
_ircstring_translation = bytes.maketrans((string.ascii_uppercase + "[]\\^").encode("latin-1"),
|
|
|
|
(string.ascii_lowercase + "{}|~").encode("latin-1"))
|
|
|
|
else:
|
|
|
|
_ircstring_translation = string.maketrans(string.ascii_uppercase + "[]\\^",
|
|
|
|
string.ascii_lowercase + "{}|~")
|
2016-07-25 18:01:51 +00:00
|
|
|
|
|
|
|
def irc_lower(s):
|
|
|
|
"""Returns a lowercased string.
|
|
|
|
|
|
|
|
The definition of lowercased comes from the IRC specification (RFC
|
|
|
|
1459).
|
|
|
|
"""
|
|
|
|
return s.translate(_ircstring_translation)
|
|
|
|
|
|
|
|
def _ctcp_dequote(message):
|
|
|
|
"""[Internal] Dequote a message according to CTCP specifications.
|
|
|
|
|
|
|
|
The function returns a list where each element can be either a
|
|
|
|
string (normal message) or a tuple of one or two strings (tagged
|
|
|
|
messages). If a tuple has only one element (ie is a singleton),
|
|
|
|
that element is the tag; otherwise the tuple has two elements: the
|
|
|
|
tag and the data.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
|
|
|
|
message -- The message to be decoded.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def _low_level_replace(match_obj):
|
|
|
|
ch = match_obj.group(1)
|
|
|
|
|
|
|
|
# If low_level_mapping doesn't have the character as key, we
|
|
|
|
# should just return the character.
|
|
|
|
return _low_level_mapping.get(ch, ch)
|
|
|
|
|
|
|
|
if _LOW_LEVEL_QUOTE in message:
|
|
|
|
# Yup, there was a quote. Release the dequoter, man!
|
|
|
|
message = _low_level_regexp.sub(_low_level_replace, message)
|
|
|
|
|
|
|
|
if _CTCP_DELIMITER not in message:
|
|
|
|
return [message]
|
|
|
|
else:
|
|
|
|
# Split it into parts. (Does any IRC client actually *use*
|
|
|
|
# CTCP stacking like this?)
|
|
|
|
chunks = message.split(_CTCP_DELIMITER)
|
|
|
|
|
|
|
|
messages = []
|
|
|
|
i = 0
|
|
|
|
while i < len(chunks)-1:
|
|
|
|
# Add message if it's non-empty.
|
|
|
|
if len(chunks[i]) > 0:
|
|
|
|
messages.append(chunks[i])
|
|
|
|
|
|
|
|
if i < len(chunks)-2:
|
|
|
|
# Aye! CTCP tagged data ahead!
|
|
|
|
messages.append(tuple(chunks[i+1].split(" ", 1)))
|
|
|
|
|
|
|
|
i = i + 2
|
|
|
|
|
|
|
|
if len(chunks) % 2 == 0:
|
|
|
|
# Hey, a lonely _CTCP_DELIMITER at the end! This means
|
|
|
|
# that the last chunk, including the delimiter, is a
|
|
|
|
# normal message! (This is according to the CTCP
|
|
|
|
# specification.)
|
|
|
|
messages.append(_CTCP_DELIMITER + chunks[-1])
|
|
|
|
|
|
|
|
return messages
|
|
|
|
|
|
|
|
def is_channel(string):
|
|
|
|
"""Check if a string is a channel name.
|
|
|
|
|
|
|
|
Returns true if the argument is a channel name, otherwise false.
|
|
|
|
"""
|
|
|
|
return string and string[0] in "#&+!"
|
|
|
|
|
|
|
|
def ip_numstr_to_quad(num):
|
|
|
|
"""Convert an IP number as an integer given in ASCII
|
|
|
|
representation (e.g. '3232235521') to an IP address string
|
|
|
|
(e.g. '192.168.0.1')."""
|
2016-07-28 18:25:38 +00:00
|
|
|
n = int(num)
|
|
|
|
p = list(map(str, list(map(int, [n >> 24 & 0xFF, n >> 16 & 0xFF,
|
|
|
|
n >> 8 & 0xFF, n & 0xFF]))))
|
2016-07-25 18:01:51 +00:00
|
|
|
return ".".join(p)
|
|
|
|
|
|
|
|
def ip_quad_to_numstr(quad):
|
|
|
|
"""Convert an IP address string (e.g. '192.168.0.1') to an IP
|
|
|
|
number as an integer given in ASCII representation
|
|
|
|
(e.g. '3232235521')."""
|
2016-07-28 18:25:38 +00:00
|
|
|
p = list(map(int, quad.split(".")))
|
2016-07-25 18:01:51 +00:00
|
|
|
s = str((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3])
|
|
|
|
if s[-1] == "L":
|
|
|
|
s = s[:-1]
|
|
|
|
return s
|
|
|
|
|
|
|
|
def nm_to_n(s):
|
|
|
|
"""Get the nick part of a nickmask.
|
|
|
|
|
|
|
|
(The source of an Event is a nickmask.)
|
|
|
|
"""
|
|
|
|
return s.split("!")[0]
|
|
|
|
|
|
|
|
def nm_to_uh(s):
|
|
|
|
"""Get the userhost part of a nickmask.
|
|
|
|
|
|
|
|
(The source of an Event is a nickmask.)
|
|
|
|
"""
|
|
|
|
return s.split("!")[1]
|
|
|
|
|
|
|
|
def nm_to_h(s):
|
|
|
|
"""Get the host part of a nickmask.
|
|
|
|
|
|
|
|
(The source of an Event is a nickmask.)
|
|
|
|
"""
|
|
|
|
return s.split("@")[1]
|
|
|
|
|
|
|
|
def nm_to_u(s):
|
|
|
|
"""Get the user part of a nickmask.
|
|
|
|
|
|
|
|
(The source of an Event is a nickmask.)
|
|
|
|
"""
|
|
|
|
s = s.split("!")[1]
|
|
|
|
return s.split("@")[0]
|
|
|
|
|
|
|
|
def parse_nick_modes(mode_string):
|
|
|
|
"""Parse a nick mode string.
|
|
|
|
|
|
|
|
The function returns a list of lists with three members: sign,
|
|
|
|
mode and argument. The sign is \"+\" or \"-\". The argument is
|
|
|
|
always None.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
>>> irclib.parse_nick_modes(\"+ab-c\")
|
|
|
|
[['+', 'a', None], ['+', 'b', None], ['-', 'c', None]]
|
|
|
|
"""
|
|
|
|
|
|
|
|
return _parse_modes(mode_string, "")
|
|
|
|
|
|
|
|
def parse_channel_modes(mode_string):
|
|
|
|
"""Parse a channel mode string.
|
|
|
|
|
|
|
|
The function returns a list of lists with three members: sign,
|
|
|
|
mode and argument. The sign is \"+\" or \"-\". The argument is
|
|
|
|
None if mode isn't one of \"b\", \"k\", \"l\", \"v\" or \"o\".
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
>>> irclib.parse_channel_modes(\"+ab-c foo\")
|
|
|
|
[['+', 'a', None], ['+', 'b', 'foo'], ['-', 'c', None]]
|
|
|
|
"""
|
|
|
|
|
|
|
|
return _parse_modes(mode_string, "bklvo")
|
|
|
|
|
|
|
|
def _parse_modes(mode_string, unary_modes=""):
|
|
|
|
"""[Internal]"""
|
|
|
|
modes = []
|
|
|
|
arg_count = 0
|
|
|
|
|
|
|
|
# State variable.
|
|
|
|
sign = ""
|
|
|
|
|
|
|
|
a = mode_string.split()
|
|
|
|
if len(a) == 0:
|
|
|
|
return []
|
|
|
|
else:
|
|
|
|
mode_part, args = a[0], a[1:]
|
|
|
|
|
|
|
|
if mode_part[0] not in "+-":
|
|
|
|
return []
|
|
|
|
for ch in mode_part:
|
|
|
|
if ch in "+-":
|
|
|
|
sign = ch
|
|
|
|
elif ch == " ":
|
|
|
|
collecting_arguments = 1
|
|
|
|
elif ch in unary_modes:
|
|
|
|
if len(args) >= arg_count + 1:
|
|
|
|
modes.append([sign, ch, args[arg_count]])
|
|
|
|
arg_count = arg_count + 1
|
|
|
|
else:
|
|
|
|
modes.append([sign, ch, None])
|
|
|
|
else:
|
|
|
|
modes.append([sign, ch, None])
|
|
|
|
return modes
|
|
|
|
|
|
|
|
def _ping_ponger(connection, event):
|
|
|
|
"""[Internal]"""
|
|
|
|
connection.pong(event.target())
|
|
|
|
|
|
|
|
# Numeric table mostly stolen from the Perl IRC module (Net::IRC).
|
|
|
|
numeric_events = {
|
|
|
|
"001": "welcome",
|
|
|
|
"002": "yourhost",
|
|
|
|
"003": "created",
|
|
|
|
"004": "myinfo",
|
|
|
|
"005": "featurelist", # XXX
|
|
|
|
"200": "tracelink",
|
|
|
|
"201": "traceconnecting",
|
|
|
|
"202": "tracehandshake",
|
|
|
|
"203": "traceunknown",
|
|
|
|
"204": "traceoperator",
|
|
|
|
"205": "traceuser",
|
|
|
|
"206": "traceserver",
|
|
|
|
"207": "traceservice",
|
|
|
|
"208": "tracenewtype",
|
|
|
|
"209": "traceclass",
|
|
|
|
"210": "tracereconnect",
|
|
|
|
"211": "statslinkinfo",
|
|
|
|
"212": "statscommands",
|
|
|
|
"213": "statscline",
|
|
|
|
"214": "statsnline",
|
|
|
|
"215": "statsiline",
|
|
|
|
"216": "statskline",
|
|
|
|
"217": "statsqline",
|
|
|
|
"218": "statsyline",
|
|
|
|
"219": "endofstats",
|
|
|
|
"221": "umodeis",
|
|
|
|
"231": "serviceinfo",
|
|
|
|
"232": "endofservices",
|
|
|
|
"233": "service",
|
|
|
|
"234": "servlist",
|
|
|
|
"235": "servlistend",
|
|
|
|
"241": "statslline",
|
|
|
|
"242": "statsuptime",
|
|
|
|
"243": "statsoline",
|
|
|
|
"244": "statshline",
|
|
|
|
"250": "luserconns",
|
|
|
|
"251": "luserclient",
|
|
|
|
"252": "luserop",
|
|
|
|
"253": "luserunknown",
|
|
|
|
"254": "luserchannels",
|
|
|
|
"255": "luserme",
|
|
|
|
"256": "adminme",
|
|
|
|
"257": "adminloc1",
|
|
|
|
"258": "adminloc2",
|
|
|
|
"259": "adminemail",
|
|
|
|
"261": "tracelog",
|
|
|
|
"262": "endoftrace",
|
|
|
|
"263": "tryagain",
|
|
|
|
"265": "n_local",
|
|
|
|
"266": "n_global",
|
|
|
|
"300": "none",
|
|
|
|
"301": "away",
|
|
|
|
"302": "userhost",
|
|
|
|
"303": "ison",
|
|
|
|
"305": "unaway",
|
|
|
|
"306": "nowaway",
|
|
|
|
"311": "whoisuser",
|
|
|
|
"312": "whoisserver",
|
|
|
|
"313": "whoisoperator",
|
|
|
|
"314": "whowasuser",
|
|
|
|
"315": "endofwho",
|
|
|
|
"316": "whoischanop",
|
|
|
|
"317": "whoisidle",
|
|
|
|
"318": "endofwhois",
|
|
|
|
"319": "whoischannels",
|
|
|
|
"321": "liststart",
|
|
|
|
"322": "list",
|
|
|
|
"323": "listend",
|
|
|
|
"324": "channelmodeis",
|
|
|
|
"329": "channelcreate",
|
|
|
|
"331": "notopic",
|
|
|
|
"332": "currenttopic",
|
|
|
|
"333": "topicinfo",
|
|
|
|
"341": "inviting",
|
|
|
|
"342": "summoning",
|
|
|
|
"346": "invitelist",
|
|
|
|
"347": "endofinvitelist",
|
|
|
|
"348": "exceptlist",
|
|
|
|
"349": "endofexceptlist",
|
|
|
|
"351": "version",
|
|
|
|
"352": "whoreply",
|
|
|
|
"353": "namreply",
|
|
|
|
"361": "killdone",
|
|
|
|
"362": "closing",
|
|
|
|
"363": "closeend",
|
|
|
|
"364": "links",
|
|
|
|
"365": "endoflinks",
|
|
|
|
"366": "endofnames",
|
|
|
|
"367": "banlist",
|
|
|
|
"368": "endofbanlist",
|
|
|
|
"369": "endofwhowas",
|
|
|
|
"371": "info",
|
|
|
|
"372": "motd",
|
|
|
|
"373": "infostart",
|
|
|
|
"374": "endofinfo",
|
|
|
|
"375": "motdstart",
|
|
|
|
"376": "endofmotd",
|
|
|
|
"377": "motd2", # 1997-10-16 -- tkil
|
|
|
|
"381": "youreoper",
|
|
|
|
"382": "rehashing",
|
|
|
|
"384": "myportis",
|
|
|
|
"391": "time",
|
|
|
|
"392": "usersstart",
|
|
|
|
"393": "users",
|
|
|
|
"394": "endofusers",
|
|
|
|
"395": "nousers",
|
|
|
|
"401": "nosuchnick",
|
|
|
|
"402": "nosuchserver",
|
|
|
|
"403": "nosuchchannel",
|
|
|
|
"404": "cannotsendtochan",
|
|
|
|
"405": "toomanychannels",
|
|
|
|
"406": "wasnosuchnick",
|
|
|
|
"407": "toomanytargets",
|
|
|
|
"409": "noorigin",
|
|
|
|
"411": "norecipient",
|
|
|
|
"412": "notexttosend",
|
|
|
|
"413": "notoplevel",
|
|
|
|
"414": "wildtoplevel",
|
|
|
|
"421": "unknowncommand",
|
|
|
|
"422": "nomotd",
|
|
|
|
"423": "noadmininfo",
|
|
|
|
"424": "fileerror",
|
|
|
|
"431": "nonicknamegiven",
|
|
|
|
"432": "erroneusnickname", # Thiss iz how its speld in thee RFC.
|
|
|
|
"433": "nicknameinuse",
|
|
|
|
"436": "nickcollision",
|
|
|
|
"437": "unavailresource", # "Nick temporally unavailable"
|
|
|
|
"441": "usernotinchannel",
|
|
|
|
"442": "notonchannel",
|
|
|
|
"443": "useronchannel",
|
|
|
|
"444": "nologin",
|
|
|
|
"445": "summondisabled",
|
|
|
|
"446": "usersdisabled",
|
|
|
|
"451": "notregistered",
|
|
|
|
"461": "needmoreparams",
|
|
|
|
"462": "alreadyregistered",
|
|
|
|
"463": "nopermforhost",
|
|
|
|
"464": "passwdmismatch",
|
|
|
|
"465": "yourebannedcreep", # I love this one...
|
|
|
|
"466": "youwillbebanned",
|
|
|
|
"467": "keyset",
|
|
|
|
"471": "channelisfull",
|
|
|
|
"472": "unknownmode",
|
|
|
|
"473": "inviteonlychan",
|
|
|
|
"474": "bannedfromchan",
|
|
|
|
"475": "badchannelkey",
|
|
|
|
"476": "badchanmask",
|
|
|
|
"477": "nochanmodes", # "Channel doesn't support modes"
|
|
|
|
"478": "banlistfull",
|
|
|
|
"481": "noprivileges",
|
|
|
|
"482": "chanoprivsneeded",
|
|
|
|
"483": "cantkillserver",
|
|
|
|
"484": "restricted", # Connection is restricted
|
|
|
|
"485": "uniqopprivsneeded",
|
|
|
|
"491": "nooperhost",
|
|
|
|
"492": "noservicehost",
|
|
|
|
"501": "umodeunknownflag",
|
|
|
|
"502": "usersdontmatch",
|
|
|
|
}
|
|
|
|
|
|
|
|
generated_events = [
|
|
|
|
# Generated events
|
|
|
|
"dcc_connect",
|
|
|
|
"dcc_disconnect",
|
|
|
|
"dccmsg",
|
|
|
|
"disconnect",
|
|
|
|
"ctcp",
|
|
|
|
"ctcpreply",
|
|
|
|
]
|
|
|
|
|
|
|
|
protocol_events = [
|
|
|
|
# IRC protocol events
|
|
|
|
"error",
|
|
|
|
"join",
|
|
|
|
"kick",
|
|
|
|
"mode",
|
|
|
|
"part",
|
|
|
|
"ping",
|
|
|
|
"privmsg",
|
|
|
|
"privnotice",
|
|
|
|
"pubmsg",
|
|
|
|
"pubnotice",
|
|
|
|
"quit",
|
|
|
|
"invite",
|
|
|
|
"pong",
|
|
|
|
]
|
|
|
|
|
2016-07-28 18:25:38 +00:00
|
|
|
all_events = generated_events + protocol_events + list(numeric_events.values())
|