From 91741757ed6f278c0192ca282405c88653a545a9 Mon Sep 17 00:00:00 2001 From: Page Libre Date: Wed, 4 Mar 2020 14:05:05 +0100 Subject: [PATCH] Some fixes for Python 3 port --- ircbot.py | 195 +++++++++++++++-------------- irclib.py | 366 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 366 insertions(+), 195 deletions(-) diff --git a/ircbot.py b/ircbot.py index 6f29a65..a6ba6dc 100644 --- a/ircbot.py +++ b/ircbot.py @@ -1,4 +1,7 @@ -# Copyright (C) 1999--2002 Joel Rosdahl +#! -*- coding: utf-8 -*- + +# Copyright (C) 1999-2002 Joel Rosdahl +# Portions Copyring © 2011-2012 Jason R. Coombs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,24 +18,20 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Joel Rosdahl -# -# $Id: ircbot.py,v 1.23 2008/09/11 07:38:30 keltus Exp $ -"""ircbot -- Simple IRC bot library. +""" +ircbot -- Simple IRC bot library. This module contains a single-server IRC bot class that can be used to write simpler bots. """ import sys -from UserDict import UserDict -from irclib import SimpleIRCClient -from irclib import nm_to_n, irc_lower, all_events -from irclib import parse_channel_modes, is_channel -from irclib import ServerConnectionError +import irclib +from irclib import nm_to_n -class SingleServerIRCBot(SimpleIRCClient): +class SingleServerIRCBot(irclib.SimpleIRCClient): """A single-server IRC bot class. The bot tries to reconnect if it is disconnected. @@ -62,11 +61,11 @@ class SingleServerIRCBot(SimpleIRCClient): connections. """ - SimpleIRCClient.__init__(self) + super(SingleServerIRCBot, self).__init__() self.channels = IRCDict() self.server_list = server_list if not reconnection_interval or reconnection_interval < 0: - reconnection_interval = 2**31 + reconnection_interval = 2 ** 31 self.reconnection_interval = reconnection_interval self._nickname = nickname @@ -75,7 +74,8 @@ class SingleServerIRCBot(SimpleIRCClient): "namreply", "nick", "part", "quit"]: self.connection.add_global_handler(i, getattr(self, "_on_" + i), - -10) + -20) + def _connected_checker(self): """[Internal]""" if not self.connection.is_connected(): @@ -94,7 +94,7 @@ class SingleServerIRCBot(SimpleIRCClient): self._nickname, password, ircname=self._realname) - except ServerConnectionError: + except irclib.ServerConnectionError: pass def _on_disconnect(self, c, e): @@ -123,9 +123,9 @@ class SingleServerIRCBot(SimpleIRCClient): def _on_mode(self, c, e): """[Internal]""" - modes = parse_channel_modes(" ".join(e.arguments())) + modes = irclib.parse_channel_modes(" ".join(e.arguments())) t = e.target() - if is_channel(t): + if irclib.is_channel(t): ch = self.channels[t] for mode in modes: if mode[0] == "+": @@ -244,71 +244,10 @@ class SingleServerIRCBot(SimpleIRCClient): def start(self): """Start the bot.""" self._connect() - SimpleIRCClient.start(self) + super(SingleServerIRCBot, self).start() -class IRCDict: - """A dictionary suitable for storing IRC-related things. - - Dictionary keys a and b are considered equal if and only if - irc_lower(a) == irc_lower(b) - - Otherwise, it should behave exactly as a normal dictionary. - """ - - def __init__(self, dict=None): - self.data = {} - self.canon_keys = {} # Canonical keys - if dict is not None: - self.update(dict) - def __repr__(self): - return repr(self.data) - def __cmp__(self, dict): - if isinstance(dict, IRCDict): - return cmp(self.data, dict.data) - else: - return cmp(self.data, dict) - def __len__(self): - return len(self.data) - def __getitem__(self, key): - return self.data[self.canon_keys[irc_lower(key)]] - def __setitem__(self, key, item): - if key in self: - del self[key] - self.data[key] = item - self.canon_keys[irc_lower(key)] = key - def __delitem__(self, key): - ck = irc_lower(key) - del self.data[self.canon_keys[ck]] - del self.canon_keys[ck] - def __iter__(self): - return iter(self.data) - def __contains__(self, key): - return self.has_key(key) - def clear(self): - self.data.clear() - self.canon_keys.clear() - def copy(self): - if self.__class__ is UserDict: - return UserDict(self.data) - import copy - return copy.copy(self) - def keys(self): - return self.data.keys() - def items(self): - return self.data.items() - def values(self): - return self.data.values() - def has_key(self, key): - return irc_lower(key) in self.canon_keys - def update(self, dict): - for k, v in dict.items(): - self.data[k] = v - def get(self, key, failobj=None): - return self.data.get(key, failobj) - - -class Channel: +class Channel(object): """A class for keeping information about an IRC channel. This class can be improved a lot. @@ -354,14 +293,15 @@ class Channel: del d[nick] def change_nick(self, before, after): - self.userdict[after] = 1 - del self.userdict[before] + self.userdict[after] = self.userdict.pop(before) if before in self.operdict: - self.operdict[after] = 1 - del self.operdict[before] + self.operdict[after] = self.operdict.pop(before) if before in self.voiceddict: - self.voiceddict[after] = 1 - del self.voiceddict[before] + self.voiceddict[after] = self.voiceddict.pop(before) + + def set_userdetails(self, nick, details): + if nick in self.userdict: + self.userdict[nick] = details def set_mode(self, mode, value=None): """Set mode on the channel. @@ -424,15 +364,88 @@ class Channel: def limit(self): if self.has_limit(): - return self.modes[l] + return self.modes["l"] else: return None def has_key(self): return self.has_mode("k") - def key(self): - if self.has_key(): - return self.modes["k"] - else: - return None +# from jaraco.util.dictlib +class KeyTransformingDict(dict): + """ + A dict subclass that transforms the keys before they're used. + Subclasses may override the default key_transform to customize behavior. + """ + @staticmethod + def key_transform(key): + return key + + def __init__(self, *args, **kargs): + super(KeyTransformingDict, self).__init__() + # build a dictionary using the default constructs + d = dict(*args, **kargs) + # build this dictionary using transformed keys. + for item in d.items(): + self.__setitem__(*item) + + def __setitem__(self, key, val): + key = self.key_transform(key) + super(KeyTransformingDict, self).__setitem__(key, val) + + def __getitem__(self, key): + key = self.key_transform(key) + return super(KeyTransformingDict, self).__getitem__(key) + + def __contains__(self, key): + key = self.key_transform(key) + return super(KeyTransformingDict, self).__contains__(key) + + def __delitem__(self, key): + key = self.key_transform(key) + return super(KeyTransformingDict, self).__delitem__(key) + + def setdefault(self, key, *args, **kwargs): + key = self.key_transform(key) + return super(KeyTransformingDict, self).setdefault(key, *args, **kwargs) + + def pop(self, key, *args, **kwargs): + key = self.key_transform(key) + return super(KeyTransformingDict, self).pop(key, *args, **kwargs) + +class IRCDict(KeyTransformingDict): + """ + A dictionary of names whose keys are case-insensitive according to the + IRC RFC rules. + + >>> d = IRCDict({'[This]': 'that'}, A='foo') + + The dict maintains the original case: + >>> d.keys() + ['A', '[This]'] + + But the keys can be referenced with a different case + >>> d['a'] + 'foo' + + >>> d['{this}'] + 'that' + + >>> d['{THIS}'] + 'that' + + >>> '{thiS]' in d + True + + This should work for operations like delete and pop as well. + >>> d.pop('A') + 'foo' + >>> del d['{This}'] + >>> len(d) + 0 + """ + @staticmethod + def key_transform(key): + if isinstance(key, basestring): + key = irclib.IRCFoldedCase(key) + return key diff --git a/irclib.py b/irclib.py index 581d965..bca43b8 100644 --- a/irclib.py +++ b/irclib.py @@ -1,4 +1,7 @@ -# Copyright (C) 1999--2002 Joel Rosdahl +# -*- coding: utf-8 -*- + +# Copyright (C) 1999-2002 Joel Rosdahl +# Portions Copyright © 2011 Jason R. Coombs # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,10 +18,9 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # keltus -# -# $Id: irclib.py,v 1.47 2008/09/25 22:00:59 keltus Exp $ -"""irclib -- Internet Relay Chat (IRC) protocol client library. +""" +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 @@ -66,12 +68,19 @@ import re import select import socket import string -import sys import time import types +import ssl as ssl_mod +import datetime -VERSION = 0, 4, 8 -DEBUG = 0 +try: + import pkg_resources + _pkg = pkg_resources.require('python-irclib')[0] + VERSION = tuple(int(res) for res in re.findall('\d+', _pkg.version)) +except Exception: + VERSION = () + +DEBUG = False # TODO # ---- @@ -93,7 +102,7 @@ class IRCError(Exception): pass -class IRC: +class IRC(object): """Class that handles one or several IRC server connections. When an IRC object has been instantiated, it can be used to create @@ -112,12 +121,12 @@ class IRC: irc = irclib.IRC() server = irc.server() - server.connect(\"irc.some.where\", 6667, \"my_nickname\") - server.privmsg(\"a_nickname\", \"Hi there!\") + 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!\" + using the nickname my_nickname and send the message "Hi there!" to the nickname a_nickname. """ @@ -157,7 +166,7 @@ class IRC: 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.delayed_commands = [] # list of DelayedCommands self.add_global_handler("ping", _ping_ponger, -42) @@ -187,13 +196,14 @@ class IRC: 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: + command = self.delayed_commands[0] + if not command.due(): break + command.function(*command.arguments) + if isinstance(command, PeriodicCommand): + self._schedule_command(command.next()) + del self.delayed_commands[0] def process_once(self, timeout=0): """Process data from connections once. @@ -252,7 +262,7 @@ class IRC: 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. + "NO MORE", no more handlers will be called. """ if not event in self.handlers: self.handlers[event] = [] @@ -264,7 +274,6 @@ class IRC: Arguments: event -- Event type (a string). - handler -- Callback function. Returns 1 on success, otherwise 0. @@ -281,28 +290,39 @@ class IRC: Arguments: - at -- Execute at this time (standard \"time_t\" time). - + 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) + command = DelayedCommand.at_time(at, function, arguments) + self._schedule_command(command) 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)) + Execute a function after a specified time. + + delay -- How many seconds to wait. + function -- Function to call. + arguments -- Arguments to give the function. + """ + command = DelayedCommand(delay, function, arguments) + self._schedule_command(command) + + def execute_every(self, period, function, arguments=()): + """ + Execute a function every 'period' seconds. + + period -- How often to run (always waits this long for first). + function -- Function to call. + arguments -- Arguments to give the function. + """ + command = PeriodicCommand(period, function, arguments) + self._schedule_command(command) + + def _schedule_command(self, command): + bisect.insort(self.delayed_commands, command) if self.fn_to_add_timeout: - self.fn_to_add_timeout(delay) + self.fn_to_add_timeout(total_seconds(command.delay)) def dcc(self, dcctype="chat"): """Creates and returns a DCCConnection object. @@ -321,7 +341,8 @@ class IRC: def _handle_event(self, connection, event): """[Internal]""" h = self.handlers - for handler in h.get("all_events", []) + h.get(event.eventtype(), []): + th = sorted(h.get("all_events", []) + h.get(event.eventtype(), [])) + for handler in th: if handler[1](connection, event) == "NO MORE": return @@ -331,9 +352,48 @@ class IRC: if self.fn_to_remove_socket: self.fn_to_remove_socket(connection._get_socket()) +class DelayedCommand(datetime.datetime): + """ + A command to be executed after some delay (seconds or timedelta). + """ + def __new__(cls, delay, function, arguments): + if not isinstance(delay, datetime.timedelta): + delay = datetime.timedelta(seconds=delay) + at = datetime.datetime.utcnow() + delay + cmd = datetime.datetime.__new__(DelayedCommand, at.year, + at.month, at.day, at.hour, at.minute, at.second, + at.microsecond, at.tzinfo) + cmd.delay = delay + cmd.function = function + cmd.arguments = arguments + return cmd + + def at_time(cls, at, function, arguments): + """ + Construct a DelayedCommand to come due at `at`, where `at` may be + a datetime or timestamp. + """ + if isinstance(at, int): + at = datetime.datetime.utcfromtimestamp(at) + delay = at - datetime.datetime.utcnow() + return cls(delay, function, arguments) + at_time = classmethod(at_time) + + def due(self): + return datetime.datetime.utcnow() >= self + +class PeriodicCommand(DelayedCommand): + """ + Like a deferred command, but expect this command to run every delay + seconds. + """ + def next(self): + return PeriodicCommand(self.delay, self.function, + self.arguments) + _rfc_1459_command_regexp = re.compile("^(:(?P[^ ]+) +)?(?P[^ ]+)( *(?P .+))?") -class Connection: +class Connection(object): """Base class for IRC connections. Must be overridden. @@ -342,7 +402,7 @@ class Connection: self.irclibobj = irclibobj def _get_socket(): - raise IRCError, "Not overridden" + raise IRCError("Not overridden") ############################## ### Convenience wrappers. @@ -353,6 +413,8 @@ class Connection: def execute_delayed(self, delay, function, arguments=()): self.irclibobj.execute_delayed(delay, function, arguments) + def execute_every(self, period, function, arguments=()): + self.irclibobj.execute_every(period, function, arguments) class ServerConnectionError(IRCError): pass @@ -373,7 +435,7 @@ class ServerConnection(Connection): """ def __init__(self, irclibobj): - Connection.__init__(self, irclibobj) + super(ServerConnection, self).__init__(irclibobj) self.connected = 0 # Not connected yet. self.socket = None self.ssl = None @@ -432,11 +494,11 @@ class ServerConnection(Connection): self.socket.bind((self.localaddress, self.localport)) self.socket.connect((self.server, self.port)) if ssl: - self.ssl = socket.ssl(self.socket) - except socket.error, x: + self.ssl = ssl_mod.wrap_socket(self.socket) + except socket.error as x: self.socket.close() self.socket = None - raise ServerConnectionError, "Couldn't connect to socket: %s" % x + raise ServerConnectionError("Couldn't connect to socket: %s" % x) self.connected = 1 if self.irclibobj.fn_to_add_socket: self.irclibobj.fn_to_add_socket(self.socket) @@ -488,10 +550,10 @@ class ServerConnection(Connection): try: if self.ssl: - new_data = self.ssl.read(2**14) + new_data = self.ssl.read(2 ** 14) else: - new_data = self.socket.recv(2**14) - except socket.error, x: + new_data = self.socket.recv(2 ** 14) + except socket.error: # The server hung up. self.disconnect("Connection reset by peer") return @@ -500,14 +562,14 @@ class ServerConnection(Connection): self.disconnect("Connection reset by peer") return - lines = _linesep_regexp.split(self.previous_buffer + new_data) + lines = _linesep_regexp.split(self.previous_buffer + str(new_data)) # Save the last, unfinished line. self.previous_buffer = lines.pop() for line in lines: if DEBUG: - print "FROM SERVER:", line + print("FROM SERVER:", line) if not line: continue @@ -569,15 +631,15 @@ class ServerConnection(Connection): m = list(m) if DEBUG: - print "command: %s, source: %s, target: %s, arguments: %s" % ( - command, prefix, target, m) + print("command: %s, source: %s, target: %s, arguments: %s" % ( + command, prefix, target, m)) 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: - print "command: %s, source: %s, target: %s, arguments: %s" % ( - command, prefix, target, [m]) + print("command: %s, source: %s, target: %s, arguments: %s" % ( + command, prefix, target, [m])) self._handle_event(Event(command, prefix, target, [m])) else: target = None @@ -595,8 +657,8 @@ class ServerConnection(Connection): command = "umode" if DEBUG: - print "command: %s, source: %s, target: %s, arguments: %s" % ( - command, prefix, target, arguments) + print("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): @@ -660,7 +722,7 @@ class ServerConnection(Connection): try: self.socket.close() - except socket.error, x: + except socket.error: pass self.socket = None self._handle_event(Event("disconnect", self.server, "", [message])) @@ -743,10 +805,13 @@ class ServerConnection(Connection): def part(self, channels, message=""): """Send a PART command.""" - if type(channels) == types.StringType: - self.send_raw("PART " + channels + (message and (" " + message))) - else: - self.send_raw("PART " + ",".join(channels) + (message and (" " + message))) + channels = always_iterable(channels) + cmd_parts = [ + 'PART', + ','.join(channels), + ] + if message: cmd_parts.append(message) + self.send_raw(' '.join(cmd_parts)) def pass_(self, password): """Send a PASS command.""" @@ -780,20 +845,19 @@ class ServerConnection(Connection): """Send raw string to the server. The string will be padded with appropriate CR LF. - From here everything need to be encoded in str from utf-8 """ - string=string.encode('utf-8') - if self.socket is None: - raise ServerNotConnectedError, "Not connected." + raise ServerNotConnectedError("Not connected.") try: if self.ssl: self.ssl.write(string + "\r\n") else: - self.socket.send(string + "\r\n") + #print(type(string + "\r\n")) + obj = string + "\r\n" + self.socket.send(obj.encode()) if DEBUG: - print "TO SERVER:", string - except socket.error, x: + print("TO SERVER:", string) + except socket.error: # Ouch! self.disconnect("Connection reset by peer.") @@ -865,7 +929,7 @@ class DCCConnection(Connection): method on an IRC object. """ def __init__(self, irclibobj, dcctype): - Connection.__init__(self, irclibobj) + super(DCCConnection, self).__init__(irclibobj) self.connected = 0 self.passive = 0 self.dcctype = dcctype @@ -891,8 +955,8 @@ class DCCConnection(Connection): self.passive = 0 try: self.socket.connect((self.peeraddress, self.peerport)) - except socket.error, x: - raise DCCConnectionError, "Couldn't connect to socket: %s" % x + except socket.error as x: + raise DCCConnectionError("Couldn't connect to socket: %s" % x) self.connected = 1 if self.irclibobj.fn_to_add_socket: self.irclibobj.fn_to_add_socket(self.socket) @@ -916,8 +980,8 @@ class DCCConnection(Connection): self.socket.bind((socket.gethostbyname(socket.gethostname()), 0)) self.localaddress, self.localport = self.socket.getsockname() self.socket.listen(10) - except socket.error, x: - raise DCCConnectionError, "Couldn't bind socket: %s" % x + except socket.error as x: + raise DCCConnectionError("Couldn't bind socket: %s" % x) return self def disconnect(self, message=""): @@ -933,7 +997,7 @@ class DCCConnection(Connection): self.connected = 0 try: self.socket.close() - except socket.error, x: + except socket.error: pass self.socket = None self.irclibobj._handle_event( @@ -950,16 +1014,16 @@ class DCCConnection(Connection): self.socket = conn self.connected = 1 if DEBUG: - print "DCC connection from %s:%d" % ( - self.peeraddress, self.peerport) + print("DCC connection from %s:%d" % ( + self.peeraddress, self.peerport)) self.irclibobj._handle_event( self, Event("dcc_connect", self.peeraddress, None, None)) return try: - new_data = self.socket.recv(2**14) - except socket.error, x: + new_data = self.socket.recv(2 ** 14) + except socket.error: # The server hung up. self.disconnect("Connection reset by peer") return @@ -975,7 +1039,7 @@ class DCCConnection(Connection): # Save the last, unfinished line. self.previous_buffer = chunks[-1] - if len(self.previous_buffer) > 2**14: + if len(self.previous_buffer) > 2 ** 14: # Bad peer! Naughty peer! self.disconnect() return @@ -988,11 +1052,11 @@ class DCCConnection(Connection): target = None for chunk in chunks: if DEBUG: - print "FROM PEER:", chunk + print("FROM PEER:", chunk) arguments = [chunk] if DEBUG: - print "command: %s, source: %s, target: %s, arguments: %s" % ( - command, prefix, target, arguments) + print("command: %s, source: %s, target: %s, arguments: %s" % ( + command, prefix, target, arguments)) self.irclibobj._handle_event( self, Event(command, prefix, target, arguments)) @@ -1012,12 +1076,12 @@ class DCCConnection(Connection): if self.dcctype == "chat": self.socket.send("\n") if DEBUG: - print "TO PEER: %s\n" % string - except socket.error, x: + print("TO PEER: %s\n" % string) + except socket.error: # Ouch! self.disconnect("Connection reset by peer.") -class SimpleIRCClient: +class SimpleIRCClient(object): """A simple single-server IRC client class. This is an example of an object-oriented wrapper of the IRC @@ -1047,6 +1111,9 @@ class SimpleIRCClient: def _dispatcher(self, c, e): """[Internal]""" + if DEBUG: + print("irclib.py:_dispatcher:%s" % e.eventtype()) + m = "on_" + e.eventtype() if hasattr(self, m): getattr(self, m)(c, e) @@ -1117,7 +1184,7 @@ class SimpleIRCClient: self.ircobj.process_forever() -class Event: +class Event(object): """Class representing an IRC event.""" def __init__(self, eventtype, source, target, arguments=None): """Constructor of Event objects. @@ -1186,16 +1253,6 @@ def mask_matches(nick, mask): _special = "-[]\\`^{}" nick_characters = string.ascii_letters + string.digits + _special -_ircstring_translation = string.maketrans(string.ascii_uppercase + "[]\\^", - string.ascii_lowercase + "{}|~") - -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. @@ -1231,14 +1288,14 @@ def _ctcp_dequote(message): messages = [] i = 0 - while i < len(chunks)-1: + 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: + if i < len(chunks) - 2: # Aye! CTCP tagged data ahead! - messages.append(tuple(chunks[i+1].split(" ", 1))) + messages.append(tuple(chunks[i + 1].split(" ", 1))) i = i + 2 @@ -1310,12 +1367,12 @@ 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 + mode and argument. The sign is "+" or "-". The argument is always None. Example: - >>> irclib.parse_nick_modes(\"+ab-c\") + >>> parse_nick_modes("+ab-c") [['+', 'a', None], ['+', 'b', None], ['-', 'c', None]] """ @@ -1325,12 +1382,12 @@ 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\". + 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\") + >>> parse_channel_modes("+ab-c foo") [['+', 'a', None], ['+', 'b', 'foo'], ['-', 'c', None]] """ @@ -1356,7 +1413,7 @@ def _parse_modes(mode_string, unary_modes=""): if ch in "+-": sign = ch elif ch == " ": - collecting_arguments = 1 + pass elif ch in unary_modes: if len(args) >= arg_count + 1: modes.append([sign, ch, args[arg_count]]) @@ -1496,7 +1553,7 @@ numeric_events = { "423": "noadmininfo", "424": "fileerror", "431": "nonicknamegiven", - "432": "erroneusnickname", # Thiss iz how its speld in thee RFC. + "432": "erroneusnickname", # Thiss iz how its speld in thee RFC. "433": "nicknameinuse", "436": "nickcollision", "437": "unavailresource", # "Nick temporally unavailable" @@ -1511,7 +1568,7 @@ numeric_events = { "462": "alreadyregistered", "463": "nopermforhost", "464": "passwdmismatch", - "465": "yourebannedcreep", # I love this one... + "465": "yourebannedcreep", # I love this one... "466": "youwillbebanned", "467": "keyset", "471": "channelisfull", @@ -1560,4 +1617,105 @@ protocol_events = [ "pong", ] -all_events = generated_events + protocol_events + numeric_events.values() +all_events = generated_events + protocol_events + list(numeric_events.values()) + +# from jaraco.util.itertools +def always_iterable(item): + """ + Given an object, always return an iterable. If the item is not + already iterable, return a tuple containing only the item. + + >>> always_iterable([1,2,3]) + [1, 2, 3] + >>> always_iterable('foo') + ('foo',) + >>> always_iterable(None) + (None,) + >>> always_iterable(xrange(10)) + xrange(10) + """ + if isinstance(item, basestring) or not hasattr(item, '__iter__'): + item = item, + return item + +# from jaraco.util.string +class FoldedCase(str): + """ + A case insensitive string class; behaves just like str + except compares equal when the only variation is case. + >>> s = FoldedCase('hello world') + + >>> s == 'Hello World' + True + + >>> 'Hello World' == s + True + + >>> s.index('O') + 4 + + >>> s.split('O') + ['hell', ' w', 'rld'] + + >>> names = map(FoldedCase, ['GAMMA', 'alpha', 'Beta']) + >>> names.sort() + >>> names + ['alpha', 'Beta', 'GAMMA'] + """ + def __lt__(self, other): + return self.lower() < other.lower() + + def __gt__(self, other): + return self.lower() > other.lower() + + def __eq__(self, other): + return self.lower() == other.lower() + + def __hash__(self): + return hash(self.lower()) + + # cache lower since it's likely to be called frequently. + def lower(self): + self._lower = super(FoldedCase, self).lower() + self.lower = lambda: self._lower + return self._lower + + def index(self, sub): + return self.lower().index(sub.lower()) + + def split(self, splitter=' ', maxsplit=0): + pattern = re.compile(re.escape(splitter), re.I) + return pattern.split(self, maxsplit) + +class IRCFoldedCase(FoldedCase): + """ + A version of FoldedCase that honors the IRC specification for lowercased + strings (RFC 1459). + + >>> IRCFoldedCase('Foo^').lower() + 'foo~' + >>> IRCFoldedCase('[this]') == IRCFoldedCase('{THIS}') + True + """ + translation = str.maketrans( + string.ascii_uppercase + r"[]\^", + string.ascii_lowercase + r"{}|~", + ) + + def lower(self): + return self.translate(self.translation) + +# for compatibility +def irc_lower(str): + return IRCFoldedCase(str).lower() + +def total_seconds(td): + """ + Python 2.7 adds a total_seconds method to timedelta objects. + See http://docs.python.org/library/datetime.html#datetime.timedelta.total_seconds + """ + try: + result = td.total_seconds() + except AttributeError: + result = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 + return result