Source code for hanyuu.ircbot.irclib.connection

# irclib -- Internet Relay Chat (IRC) protocol client library.
# Based on python-irclib by Joel Rosdahl
# Copyright (C) 2011-2013 r/a/dio
#
# 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
#

from __future__ import unicode_literals
from __future__ import print_function
from __future__ import absolute_import
import bisect
import re
import select
import socket
import string
import sys
import time
import types
import codecs
import Queue
import collections
from . import utils, tracker

from . import logger

logger = logger.getChild(__name__)

VERSION = 0, 5, 0
DEBUG = 0


[docs]class IRCError(Exception): """Represents an IRC exception.""" pass
[docs]class Connection(object): """Base class for IRC connections. Must be overridden. """ def __init__(self, irclibobj): self.irclibobj = irclibobj def _get_socket(self): raise IRCError("Not overridden") ############################## ### Convenience wrappers.
[docs] def execute_at(self, at, function, arguments=()): """Executes a function at a specified time. .. seealso:: :meth:`session.Session.execute_at` """ self.irclibobj.execute_at(at, function, arguments)
[docs] def execute_delayed(self, delay, function, arguments=()): """Executes a function after a specified number of seconds. .. seealso:: :meth:`session.Session.execute_delayed` """ self.irclibobj.execute_delayed(delay, function, arguments)
[docs]class ServerConnectionError(IRCError): pass
[docs]class ServerNotConnectedError(ServerConnectionError): pass
[docs]class ServerConnection(Connection): """This class represents an IRC server connection. .. note:: Do not instantiate :class:`ServerConnection` directly; use :meth:`session.Session.server` instead. """ def __init__(self, irclibobj): Connection.__init__(self, irclibobj) self.tracker = tracker.IRCTracker() self.connected = 0 # Not connected yet. self.socket = None self.ssl = None self.message_queue = Queue.Queue() self.sent_bytes = 0 self.send_time = 0 self.last_time = time.time() self._last_ping = time.time() self.encoding = irclibobj.encoding self.featurelist = {} # Contains the featurelist.PREFIX information, maps chars to modes self.prefix = {}
[docs] def connect(self, server, port, nickname, password=None, username=None, ircname=None, localaddress="", localport=0, ssl=False, ipv6=False, encoding='utf-8'): """Connect/reconnect to a server. :param server: Server name. :param port: Port number. :param nickname: The nickname. :param password: IRC Password (if any). .. note:: This is NOT the NickServ password; you have to send that manually. :param username: The IRC username. :param ircname: The IRC name ('realname'). :param localaddress: Bind the connection to a specific local IP address. :param localport: Bind the connection to a specific local port. :param ssl: Enable support for ssl. :param ipv6: Enable support for ipv6. This function can be called to reconnect a closed connection. Returns the ServerConnection object. """ if self.connected: self.disconnect("Changing servers") self.previous_buffer = b"" 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.featurelist = {} self.identities = {} self.motd_sent = False self._ipv6 = ipv6 self._ssl = ssl if ipv6: self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: 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)) if ssl: self.ssl = socket.ssl(self.socket) except socket.error, x: self.socket.close() self.socket = None raise ServerConnectionError("Couldn't connect to socket: {}".format(x)) self.connected = 1 self.irclibobj.register_socket(self.socket, self) # Log on... if self.password: self.pass_(self.password) self.nick(self.nickname) self.user(self.username, self.ircname) return self
[docs] def close(self): """Close the connection. .. warning:: 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
[docs] 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 ""
[docs] 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
[docs] def is_identified(self, nick): """ Checks if a user has identified for their Nickname via NickServ returns boolean """ # Send Message to NickServ `STATUS {nick}` self.privmsg("NickServ", "STATUS {0}".format(nick)) # Wait for NickServ's response # this is handled in the `process_data` function start_time = time.time() while not nick in self.identities: # Timeout after two seconds if (time.time() - start_time) > 2: return False # Not sure if this line should be included. time.sleep(0.1) # Cache the response so we can delete it from the dict output = self.identities[nick] del self.identities[nick] return output
[docs] def process_data(self): """Processes incoming data and dispatches handlers. Only for internal use. """ try: if self.ssl: new_data = self.ssl.read(2**14) else: new_data = self.socket.recv(2**14) except socket.error, x: # 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 self._last_ping = time.time() lines = utils._linesep_regexp.split(self.previous_buffer + new_data) # Save the last, unfinished line. self.previous_buffer = lines.pop() for line in lines: line = line.decode(self.encoding, 'replace') if not line: continue prefix = None command = None arguments = None self._handle_event(Event("all_raw_messages", self.get_server_name(), None, [line])) m = utils._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": old_nick = utils.nm_to_n(prefix) if old_nick == self.real_nickname: # We changed our own nick self.real_nickname = arguments[0] self.tracker.nick(old_nick, 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 = utils._ctcp_dequote(message) if command == "privmsg": if self.is_channel(target): command = "pubmsg" else: if self.is_channel(target): command = "pubnotice" else: command = "privnotice" # Check if the privnotice is NickServ sending us a Nick Status # see `is_identified` function sender = utils.nm_to_n(prefix) if sender.lower() == "nickserv": # Parse the message resp = re.match("STATUS (.*) (\d)", messages[0]) # Stop if it isn't a Nick Status if not resp == None: nick = resp.group(1) # NickServ will return 2 or 3 if it is identified status = (resp.group(2) == '2' or resp.group(2) == '3') # Add the info to the dict self.identities[nick] = status return for m in messages: if type(m) is types.TupleType: if command in ["privmsg", "pubmsg"]: command = "ctcp" else: command = "ctcpreply" m = list(m) if command == "ctcp" and m[0] == "ACTION": self._handle_event(Event("action", prefix, target, m[1:])) else: self._handle_event(Event(command, prefix, target, m)) else: self._handle_event(Event(command, prefix, target, [m])) else: target = None if command == "quit": arguments = [arguments[0]] self.tracker.quit(utils.nm_to_n(prefix)) elif command == "ping": target = arguments[0] else: target = arguments[0] arguments = arguments[1:] if command in ["join", "part"]: getattr(self.tracker, command)(target, utils.nm_to_n(prefix)) elif command == "kick": self.tracker.part(target, arguments[0]) elif command == "topic": self.tracker.topic(target, arguments[0]) elif command == "currenttopic": self.tracker.topic(arguments[0], " ".join(arguments[1:])) elif command == "notopic": self.tracker.topic(target, "") elif command == "featurelist": for feature in arguments: split = feature.split("=") if (len(split) == 2): self.featurelist[split[0]] = split[1] elif (len(split) == 1): self.featurelist[split[0]] = "" elif command == "endofmotd" and not self.motd_sent: # We know now that the motd was only sent once # So don't let us do this again self.motd_sent = True if 'CHANMODES' in self.featurelist: chanmodes = self.featurelist['CHANMODES'] chansplit = chanmodes.split(',') if 'PREFIX' in self.featurelist: match = re.match(r"\((.*?)\)(.*?)$", self.featurelist['PREFIX']) # Map mode chars to modes # keys contains (@, %) etc, vals contains (o, h) etc. self.prefix = dict(zip(match.groups()[1], match.groups()[0])) elif command == "namreply": # Process the name list for a newly joined channel # Argument 0 has something to do with channel type, ignore # Argument 1 is the channel name chan = arguments[1] # Argument 2 is the space delimited name list names = arguments[2].strip().split(' ') for name in names: # We need to find the spot where the nickname starts split = 0 for c in name: # If we found a char that's not a mode char # (like + and @), we know the split point if c not in self.prefix: break split += 1 modes = name[:split] # this contains mode CHARS nick = name[split:] # They've joined the channel... self.tracker.join(chan, nick) for mode in modes: # ...and they have these modes self.tracker.add_mode(chan, nick, self.prefix[mode]) if command == "mode": chan = target if not self.is_channel(target): command = "umode" # Just parse the modes and register them in the tracker modes = self._parse_modes(''.join(arguments)) for (sign, mode, param) in modes: if mode in self.prefix.values(): if sign == '+': self.tracker.add_mode(chan, param, mode) else: self.tracker.rem_mode(chan, param, mode) self._handle_event(Event(command, prefix, target, arguments))
def _handle_event(self, event): """Dispatches low level events to the associated :class:`session.Session` object. """ self.irclibobj._handle_event(self, event)
[docs] def is_connected(self): """Return connection status. Returns True if connected, otherwise False. """ return self.connected
[docs] def action(self, target, action): """Send a CTCP ACTION command.""" self.ctcp("ACTION", target, action)
[docs] def admin(self, server=""): """Send an ADMIN command.""" self.send_raw(" ".join(["ADMIN", server]).strip())
[docs] def ctcp(self, ctcptype, target, parameter=""): """Send a CTCP command.""" ctcptype = ctcptype.upper() self.privmsg(target, "\001{}{}\001".format(ctcptype, parameter and (" " + parameter) or ""))
[docs] def ctcp_reply(self, target, parameter): """Send a CTCP REPLY command.""" self.notice(target, "\001{}\001".format(parameter))
[docs] def disconnect(self, message=""): """Hang up the connection.""" if not self.connected: return self.connected = 0 self.quit(message) try: self.socket.close() except socket.error, x: pass self.socket = None self._handle_event(Event("disconnect", self.server, "", [message]))
[docs] def get_topic(self, channel): """Return the topic of a channel. .. note:: You must be joined to the channel in order to get the topic. """ return self.tracker.topic(channel)
[docs] def globops(self, text): """Send a GLOBOPS command.""" self.send_raw("GLOBOPS :" + text)
[docs] def hasaccess(self, channel, nick): """Check if nick is halfop or higher""" return self.tracker.has_modes(channel, nick, 'oaqh', 'or')
[docs] def hasanymodes(self, channel, nick, modes): """Check if nick has any of the specified modes on a channel.""" return self.tracker.has_modes(channel, nick, modes, 'or')
[docs] def inchannel(self, channel, nick): """Check if nick is in channel""" return self.tracker.in_chan(channel, nick)
[docs] def info(self, server=""): """Send an INFO command.""" self.send_raw(" ".join(["INFO", server]).strip())
[docs] def invite(self, nick, channel): """Send an INVITE command.""" self.send_raw(" ".join(["INVITE", nick, channel]).strip())
[docs] def ishop(self, channel, nick): """Check if nick is half operator on a channel.""" return self.tracker.has_modes(channel, nick, 'h')
[docs] def isnormal(self, channel, nick): """Check if nick is a normal on a channel.""" return not self.tracker.has_modes(channel, nick, 'oaqvh', 'or')
[docs] def ison(self, nicks): """Send an ISON command.""" self.send_raw("ISON " + " ".join(nicks))
[docs] def isop(self, channel, nick): """Check if nick is operator or higher on a channel.""" return self.tracker.has_modes(channel, nick, 'oaq', 'or')
[docs] def isvoice(self, channel, nick): """Check if nick has voice on a channel.""" return self.tracker.has_modes(channel, nick, 'v')
[docs] def join(self, channel, key=""): """Send a JOIN command.""" self.send_raw("JOIN {}{}".format(channel, (key and (" " + key))))
[docs] def kick(self, channel, nick, comment=""): """Send a KICK command.""" self.send_raw("KICK {} {}{}".format(channel, nick, (comment and (" :" + comment))))
[docs] 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)
[docs] def lusers(self, server=""): """Send a LUSERS command.""" self.send_raw(u"LUSERS" + (server and (u" " + server)))
[docs] def mode(self, target, command): """Send a MODE command.""" self.send_raw(u"MODE {} {}".format(target, command))
[docs] def motd(self, server=""): """Send an MOTD command.""" self.send_raw(u"MOTD" + (server and (u" " + server)))
[docs] def names(self, channels=None): """Send a NAMES command.""" self.send_raw(u"NAMES" + (channels and (u" " + u",".join(channels)) or u""))
[docs] def nick(self, newnick): """Send a NICK command.""" self.send_raw(u"NICK " + newnick)
[docs] def notice(self, target, text): """Send a NOTICE command.""" # Should limit len(text) here! self.send_raw(u"NOTICE {} :{}".format(target, text))
[docs] def oper(self, nick, password): """Send an OPER command.""" self.send_raw(u"OPER {} {}".format(nick, password))
[docs] def part(self, channels, message=""): """Send a PART command.""" if type(channels) == types.StringType: self.send_raw(u"PART " + channels + (message and (u" " + message))) else: self.send_raw(u"PART " + u",".join(channels) + (message and (u" " + message)))
[docs] def pass_(self, password): """Send a PASS command.""" self.send_raw(u"PASS " + password)
[docs] def ping(self, target, target2=""): """Send a PING command.""" self.send_raw_instant(u"PING {}{}".format(target, target2 and (u" " + target2)))
[docs] def pong(self, target, target2=""): """Send a PONG command.""" self.send_raw_instant(u"PONG {}{}".format(target, target2 and (u" " + target2)))
[docs] def privmsg(self, target, text): """Send a PRIVMSG command.""" self.send_raw(u"PRIVMSG {} :{}".format(target, text))
[docs] def privmsg_many(self, targets, text): """Send a PRIVMSG command to multiple targets.""" self.send_raw(u"PRIVMSG {} :{}".format(u",".join(targets), text))
[docs] def quit(self, message=""): """Send a QUIT command. .. note:: This is not the same as :meth:`disconnect`. .. note:: Many IRC servers don't use your quit message unless you've been connected for at least 5 minutes. """ self.send_raw_instant(u"QUIT" + (message and (u" :" + message)))
[docs] def reconnect(self, message=""): """Disconnect and connect with same parameters""" self.disconnect(message) self.connect(self.server, self.port, self.nickname, self.password, self.username, self.ircname, self.localaddress, self.localport, self._ssl, self._ipv6)
[docs] def send_raw_instant(self, string): """Send raw string to the server, bypassing the flood protection.""" if self.socket is None: raise ServerNotConnectedError("Not connected.") try: message = string + u'\r\n' if (type(message) == unicode): message = message.encode(self.encoding) if self.ssl: self.ssl.write(message) else: self.socket.sendall(message) if DEBUG: logger.debug("TO SERVER:" + message) except socket.error, x: self.disconnect("Connection reset by peer.")
[docs] 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: raise ServerNotConnectedError("Not connected.") try: self.message_queue.put(string) except (Queue.Full): # Ouch! self.disconnect("Queue is full.")
[docs] def squit(self, server, comment=""): """Send an SQUIT command.""" self.send_raw(u"SQUIT {}{}".format(server, comment and (u" :" + comment)))
[docs] def stats(self, statstype, server=""): """Send a STATS command.""" self.send_raw(u"STATS {}{}".format(statstype, server and (u" " + server)))
[docs] def time(self, server=""): """Send a TIME command.""" self.send_raw(u"TIME" + (server and (u" " + server)))
[docs] def topic(self, channel, new_topic=None): """Send a TOPIC command. .. note:: This method does not return the topic for you; use :meth:`get_topic` for that. It is very rarely necessary to send this command explicitly unless you are setting the topic. """ if new_topic is None: self.send_raw(u"TOPIC " + channel) else: self.send_raw(u"TOPIC {} :{}".format(channel, new_topic))
[docs] def trace(self, target=""): """Send a TRACE command.""" self.send_raw(u"TRACE" + (target and (u" " + target)))
[docs] def user(self, username, realname): """Send a USER command.""" self.send_raw(u"USER {} 0 * :{}".format(username, realname))
[docs] def userhost(self, nicks): """Send a USERHOST command.""" self.send_raw(u"USERHOST " + ",".join(nicks))
[docs] def users(self, server=""): """Send a USERS command.""" self.send_raw(u"USERS" + (server and (u" " + server)))
[docs] def version(self, server=""): """Send a VERSION command.""" self.send_raw(u"VERSION" + (server and (u" " + server)))
[docs] def wallops(self, text): """Send a WALLOPS command.""" self.send_raw(u"WALLOPS :" + text)
[docs] def who(self, target="", op=""): """Send a WHO command.""" self.send_raw(u"WHO{}{}".format(target and (u" " + target), op and (u" o")))
[docs] def whois(self, targets): """Send a WHOIS command.""" self.send_raw(u"WHOIS " + ",".join(targets))
[docs] def whowas(self, nick, max="", server=""): """Send a WHOWAS command.""" self.send_raw(u"WHOWAS {}{}{}".format(nick, max and (u" " + max), server and (u" " + server)))
def _parse_modes(self, mode_string): """ This function parses a mode string based on a set of mode types. It returns a list of tuples like (prefix, mode, parameter), where prefix is either + or -, mode is a character that specifies a mode, and parameter is an optional parameter to the mode. If no parameter was specified, the value is None. The default values are taken from Rizon's ircd. """ if 'CHANMODES' in self.featurelist: chanmodes = self.featurelist['CHANMODES'].split(',') # Groups A and B and the prefix modes need a parameter always_param = chanmodes[0] +\ chanmodes[1] +\ str(self.prefix.values()) # Group C only needs a parameter when set set_param = chanmodes[2] # Group D does not need a parameter no_param = chanmodes[3] else: always_param = 'beIkqaohv' set_param = 'l' no_param='BCMNORScimnpstz' modes = [] sign = '' param_index = 0; split = mode_string.split() if len(split) == 0: return [] else: mode_part, args = split[0], split[1:] if mode_part[0] not in "+-": return [] for ch in mode_part: if ch in "+-": sign = ch elif (ch in always_param) or (ch in set_param and sign == '+'): if param_index < len(args): modes.append((sign, ch, args[param_index])) param_index += 1 else: modes.append((sign, ch, None)) else: # assume that any unknown mode is no_param modes.append((sign, ch, None)) return modes
[docs] def is_channel(self, string): """Check if a string is a channel name. Returns True if the argument is a channel name, otherwise False. """ chan_prefixes = self.featurelist.get('CHANTYPES', None) return string and string[0] in (chan_prefixes or "#&+!")
Event = collections.namedtuple('Event', ('eventtype', 'source', 'target', 'argument'))
[docs]class Event(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. """ arguments = arguments if arguments else [] super(Event, self).__init__(eventtype, source, target, arguments) # Numeric table mostly stolen from the Perl IRC module (Net::IRC).
numeric_events = { "001": "welcome", "002": "yourhost", "003": "created", "004": "myinfo", "005": "featurelist", "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", "307": "whoisidentified", "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", "action" ] protocol_events = [ # IRC protocol events "error", "join", "kick", "mode", "part", "ping", "privmsg", "privnotice", "pubmsg", "pubnotice", "quit", "invite", "pong", "topic", "nick" ] all_events = generated_events + protocol_events + numeric_events.values()

Project Versions

This Page