1
# -*- test-case-name: twisted.words.test.test_irc -*-
2
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
Internet Relay Chat Protocol for client and server.
11
The way the IRCClient class works here encourages people to implement
12
IRC clients by subclassing the ephemeral protocol class, and it tends
13
to end up with way more state than it should for an object which will
14
be destroyed as soon as the TCP transport drops. Someone oughta do
15
something about that, ya know?
17
The DCC support needs to have more hooks for the client for it to be
18
able to ask the user things like "Do you want to accept this session?"
19
and "Transfer #2 is 67% done." and otherwise manage the DCC sessions.
21
Test coverage needs to be better.
25
@see: RFC 1459: Internet Relay Chat Protocol
26
@see: RFC 2812: Internet Relay Chat: Client Protocol
27
@see: U{The Client-To-Client-Protocol
28
<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
31
import errno, os, random, re, stat, struct, sys, time, types, traceback
36
from twisted.internet import reactor, protocol
37
from twisted.persisted import styles
38
from twisted.protocols import basic
39
from twisted.python import log, reflect, text
47
CHANNEL_PREFIXES = '&#!+'
49
class IRCBadMessage(Exception):
52
class IRCPasswordMismatch(Exception):
57
class IRCBadModes(ValueError):
59
A malformed mode was encountered while attempting to parse a mode string.
65
"""Breaks a message from an IRC server into its prefix, command, and arguments.
70
raise IRCBadMessage("Empty line.")
72
prefix, s = s[1:].split(' ', 1)
73
if s.find(' :') != -1:
74
s, trailing = s.split(' :', 1)
80
return prefix, command, args
83
def split(str, length = 80):
84
"""I break a message into multiple lines.
86
I prefer to break at whitespace near str[length]. I also break at \\n.
88
@returns: list of strings
91
raise ValueError("Length must be a number greater than zero")
93
while len(str) > length:
94
w, n = str[:length].rfind(' '), str[:length].find('\n')
95
if w == -1 and n == -1:
96
line, str = str[:length], str[length:]
102
if i == 0: # just skip the space or newline. don't append any output.
105
line, str = str[:i], str[i+1:]
108
r.extend(str.split('\n'))
113
def _intOrDefault(value, default=None):
115
Convert a value to an integer if possible.
117
@rtype: C{int} or type of L{default}
118
@return: An integer when C{value} can be converted to an integer,
119
otherwise return C{default}
124
except (TypeError, ValueError):
130
class UnhandledCommand(RuntimeError):
132
A command dispatcher could not locate an appropriate command handler.
137
class _CommandDispatcherMixin(object):
139
Dispatch commands to handlers based on their name.
141
Command handler names should be of the form C{prefix_commandName},
142
where C{prefix} is the value specified by L{prefix}, and must
143
accept the parameters as given to L{dispatch}.
145
Attempting to mix this in more than once for a single class will cause
146
strange behaviour, due to L{prefix} being overwritten.
149
@ivar prefix: Command handler prefix, used to locate handler attributes
153
def dispatch(self, commandName, *args):
155
Perform actual command dispatch.
157
def _getMethodName(command):
158
return '%s_%s' % (self.prefix, command)
160
def _getMethod(name):
161
return getattr(self, _getMethodName(name), None)
163
method = _getMethod(commandName)
164
if method is not None:
167
method = _getMethod('unknown')
169
raise UnhandledCommand("No handler for %r could be found" % (_getMethodName(commandName),))
170
return method(commandName, *args)
176
def parseModes(modes, params, paramModes=('', '')):
178
Parse an IRC mode string.
180
The mode string is parsed into two lists of mode changes (added and
181
removed), with each mode change represented as C{(mode, param)} where mode
182
is the mode character, and param is the parameter passed for that mode, or
183
C{None} if no parameter is required.
186
@param modes: Modes string to parse.
188
@type params: C{list}
189
@param params: Parameters specified along with L{modes}.
191
@type paramModes: C{(str, str)}
192
@param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take
193
parameters when added or removed.
195
@returns: Two lists of mode changes, one for modes added and the other for
196
modes removed respectively, mode changes in each list are represented as
200
raise IRCBadModes('Empty mode string')
202
if modes[0] not in '+-':
203
raise IRCBadModes('Malformed modes string: %r' % (modes,))
212
raise IRCBadModes('Empty mode sequence: %r' % (modes,))
213
direction = '+-'.index(ch)
217
if ch in paramModes[direction]:
219
param = params.pop(0)
221
raise IRCBadModes('Not enough parameters: %r' % (ch,))
222
changes[direction].append((ch, param))
226
raise IRCBadModes('Too many parameters: %r %r' % (modes, params))
229
raise IRCBadModes('Empty mode sequence: %r' % (modes,))
235
class IRC(protocol.Protocol):
237
Internet Relay Chat server protocol.
245
def connectionMade(self):
247
if self.hostname is None:
248
self.hostname = socket.getfqdn()
251
def sendLine(self, line):
252
if self.encoding is not None:
253
if isinstance(line, unicode):
254
line = line.encode(self.encoding)
255
self.transport.write("%s%s%s" % (line, CR, LF))
258
def sendMessage(self, command, *parameter_list, **prefix):
260
Send a line formatted as an IRC message.
262
First argument is the command, all subsequent arguments are parameters
263
to that command. If a prefix is desired, it may be specified with the
264
keyword argument 'prefix'.
268
raise ValueError, "IRC message requires a command."
270
if ' ' in command or command[0] == ':':
271
# Not the ONLY way to screw up, but provides a little
272
# sanity checking to catch likely dumb mistakes.
273
raise ValueError, "Somebody screwed up, 'cuz this doesn't" \
274
" look like a command to me: %s" % command
276
line = string.join([command] + list(parameter_list))
277
if prefix.has_key('prefix'):
278
line = ":%s %s" % (prefix['prefix'], line)
281
if len(parameter_list) > 15:
282
log.msg("Message has %d parameters (RFC allows 15):\n%s" %
283
(len(parameter_list), line))
286
def dataReceived(self, data):
288
This hack is to support mIRC, which sends LF only, even though the RFC
289
says CRLF. (Also, the flexibility of LineReceiver to turn "line mode"
290
on and off was not required.)
292
lines = (self.buffer + data).split(LF)
293
# Put the (possibly empty) element after the last LF back in the
295
self.buffer = lines.pop()
299
# This is a blank line, at best.
303
prefix, command, params = parsemsg(line)
304
# mIRC is a big pile of doo-doo
305
command = command.upper()
306
# DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
308
self.handleCommand(command, prefix, params)
311
def handleCommand(self, command, prefix, params):
313
Determine the function to call for the given command and call it with
316
method = getattr(self, "irc_%s" % command, None)
318
if method is not None:
319
method(prefix, params)
321
self.irc_unknown(prefix, command, params)
326
def irc_unknown(self, prefix, command, params):
328
Called by L{handleCommand} on a command that doesn't have a defined
329
handler. Subclasses should override this method.
331
raise NotImplementedError(command, prefix, params)
335
def privmsg(self, sender, recip, message):
337
Send a message to a channel or user
339
@type sender: C{str} or C{unicode}
340
@param sender: Who is sending this message. Should be of the form
341
username!ident@hostmask (unless you know better!).
343
@type recip: C{str} or C{unicode}
344
@param recip: The recipient of this message. If a channel, it must
345
start with a channel prefix.
347
@type message: C{str} or C{unicode}
348
@param message: The message being sent.
350
self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message)))
353
def notice(self, sender, recip, message):
355
Send a "notice" to a channel or user.
357
Notices differ from privmsgs in that the RFC claims they are different.
358
Robots are supposed to send notices and not respond to them. Clients
359
typically display notices differently from privmsgs.
361
@type sender: C{str} or C{unicode}
362
@param sender: Who is sending this message. Should be of the form
363
username!ident@hostmask (unless you know better!).
365
@type recip: C{str} or C{unicode}
366
@param recip: The recipient of this message. If a channel, it must
367
start with a channel prefix.
369
@type message: C{str} or C{unicode}
370
@param message: The message being sent.
372
self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message))
375
def action(self, sender, recip, message):
377
Send an action to a channel or user.
379
@type sender: C{str} or C{unicode}
380
@param sender: Who is sending this message. Should be of the form
381
username!ident@hostmask (unless you know better!).
383
@type recip: C{str} or C{unicode}
384
@param recip: The recipient of this message. If a channel, it must
385
start with a channel prefix.
387
@type message: C{str} or C{unicode}
388
@param message: The action being sent.
390
self.sendLine(":%s ACTION %s :%s" % (sender, recip, message))
393
def topic(self, user, channel, topic, author=None):
395
Send the topic to a user.
397
@type user: C{str} or C{unicode}
398
@param user: The user receiving the topic. Only their nick name, not
401
@type channel: C{str} or C{unicode}
402
@param channel: The channel for which this is the topic.
404
@type topic: C{str} or C{unicode} or C{None}
405
@param topic: The topic string, unquoted, or None if there is no topic.
407
@type author: C{str} or C{unicode}
408
@param author: If the topic is being changed, the full username and
409
hostmask of the person changing it.
413
self.sendLine(':%s %s %s %s :%s' % (
414
self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.'))
416
self.sendLine(":%s %s %s %s :%s" % (
417
self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)))
419
self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)))
422
def topicAuthor(self, user, channel, author, date):
424
Send the author of and time at which a topic was set for the given
427
This sends a 333 reply message, which is not part of the IRC RFC.
429
@type user: C{str} or C{unicode}
430
@param user: The user receiving the topic. Only their nick name, not
433
@type channel: C{str} or C{unicode}
434
@param channel: The channel for which this information is relevant.
436
@type author: C{str} or C{unicode}
437
@param author: The nickname (without hostmask) of the user who last set
441
@param date: A POSIX timestamp (number of seconds since the epoch) at
442
which the topic was last set.
444
self.sendLine(':%s %d %s %s %s %d' % (
445
self.hostname, 333, user, channel, author, date))
448
def names(self, user, channel, names):
450
Send the names of a channel's participants to a user.
452
@type user: C{str} or C{unicode}
453
@param user: The user receiving the name list. Only their nick name,
454
not the full hostmask.
456
@type channel: C{str} or C{unicode}
457
@param channel: The channel for which this is the namelist.
459
@type names: C{list} of C{str} or C{unicode}
460
@param names: The names to send.
462
# XXX If unicode is given, these limits are not quite correct
463
prefixLength = len(channel) + len(user) + 10
464
namesLength = 512 - prefixLength
469
if count + len(n) + 1 > namesLength:
470
self.sendLine(":%s %s %s = %s :%s" % (
471
self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
478
self.sendLine(":%s %s %s = %s :%s" % (
479
self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
480
self.sendLine(":%s %s %s %s :End of /NAMES list" % (
481
self.hostname, RPL_ENDOFNAMES, user, channel))
484
def who(self, user, channel, memberInfo):
486
Send a list of users participating in a channel.
488
@type user: C{str} or C{unicode}
489
@param user: The user receiving this member information. Only their
490
nick name, not the full hostmask.
492
@type channel: C{str} or C{unicode}
493
@param channel: The channel for which this is the member information.
495
@type memberInfo: C{list} of C{tuples}
496
@param memberInfo: For each member of the given channel, a 7-tuple
497
containing their username, their hostmask, the server to which they
498
are connected, their nickname, the letter "H" or "G" (standing for
499
"Here" or "Gone"), the hopcount from C{user} to this member, and
500
this member's real name.
502
for info in memberInfo:
503
(username, hostmask, server, nickname, flag, hops, realName) = info
504
assert flag in ("H", "G")
505
self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % (
506
self.hostname, RPL_WHOREPLY, user, channel,
507
username, hostmask, server, nickname, flag, hops, realName))
509
self.sendLine(":%s %s %s %s :End of /WHO list." % (
510
self.hostname, RPL_ENDOFWHO, user, channel))
513
def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels):
515
Send information about the state of a particular user.
517
@type user: C{str} or C{unicode}
518
@param user: The user receiving this information. Only their nick name,
519
not the full hostmask.
521
@type nick: C{str} or C{unicode}
522
@param nick: The nickname of the user this information describes.
524
@type username: C{str} or C{unicode}
525
@param username: The user's username (eg, ident response)
527
@type hostname: C{str}
528
@param hostname: The user's hostmask
530
@type realName: C{str} or C{unicode}
531
@param realName: The user's real name
533
@type server: C{str} or C{unicode}
534
@param server: The name of the server to which the user is connected
536
@type serverInfo: C{str} or C{unicode}
537
@param serverInfo: A descriptive string about that server
540
@param oper: Indicates whether the user is an IRC operator
543
@param idle: The number of seconds since the user last sent a message
546
@param signOn: A POSIX timestamp (number of seconds since the epoch)
547
indicating the time the user signed on
549
@type channels: C{list} of C{str} or C{unicode}
550
@param channels: A list of the channels which the user is participating in
552
self.sendLine(":%s %s %s %s %s %s * :%s" % (
553
self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName))
554
self.sendLine(":%s %s %s %s %s :%s" % (
555
self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo))
557
self.sendLine(":%s %s %s %s :is an IRC operator" % (
558
self.hostname, RPL_WHOISOPERATOR, user, nick))
559
self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % (
560
self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn))
561
self.sendLine(":%s %s %s %s :%s" % (
562
self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels)))
563
self.sendLine(":%s %s %s %s :End of WHOIS list." % (
564
self.hostname, RPL_ENDOFWHOIS, user, nick))
567
def join(self, who, where):
571
@type who: C{str} or C{unicode}
572
@param who: The name of the user joining. Should be of the form
573
username!ident@hostmask (unless you know better!).
575
@type where: C{str} or C{unicode}
576
@param where: The channel the user is joining.
578
self.sendLine(":%s JOIN %s" % (who, where))
581
def part(self, who, where, reason=None):
585
@type who: C{str} or C{unicode}
586
@param who: The name of the user joining. Should be of the form
587
username!ident@hostmask (unless you know better!).
589
@type where: C{str} or C{unicode}
590
@param where: The channel the user is joining.
592
@type reason: C{str} or C{unicode}
593
@param reason: A string describing the misery which caused this poor
597
self.sendLine(":%s PART %s :%s" % (who, where, reason))
599
self.sendLine(":%s PART %s" % (who, where))
602
def channelMode(self, user, channel, mode, *args):
604
Send information about the mode of a channel.
606
@type user: C{str} or C{unicode}
607
@param user: The user receiving the name list. Only their nick name,
608
not the full hostmask.
610
@type channel: C{str} or C{unicode}
611
@param channel: The channel for which this is the namelist.
614
@param mode: A string describing this channel's modes.
616
@param args: Any additional arguments required by the modes.
618
self.sendLine(":%s %s %s %s %s %s" % (
619
self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args)))
623
class ServerSupportedFeatures(_CommandDispatcherMixin):
625
Handle ISUPPORT messages.
627
Feature names match those in the ISUPPORT RFC draft identically.
629
Information regarding the specifics of ISUPPORT was gleaned from
630
<http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>.
637
'CHANTYPES': tuple('#&'),
640
'PREFIX': self._parsePrefixParam('(ovh)@+%'),
641
# The ISUPPORT draft explicitly says that there is no default for
642
# CHANMODES, but we're defaulting it here to handle the case where
643
# the IRC server doesn't send us any ISUPPORT information, since
644
# IRCClient.getChannelModeParams relies on this value.
645
'CHANMODES': self._parseChanModesParam(['b', '', 'lk'])}
648
def _splitParamArgs(cls, params, valueProcessor=None):
650
Split ISUPPORT parameter arguments.
652
Values can optionally be processed by C{valueProcessor}.
656
>>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2'])
657
(('A', '1'), ('B', '2'))
659
@type params: C{iterable} of C{str}
661
@type valueProcessor: C{callable} taking {str}
662
@param valueProcessor: Callable to process argument values, or C{None}
663
to perform no processing
665
@rtype: C{list} of C{(str, object)}
666
@return: Sequence of C{(name, processedValue)}
668
if valueProcessor is None:
669
valueProcessor = lambda x: x
675
a, b = param.split(':', 1)
676
yield a, valueProcessor(b)
677
return list(_parse())
678
_splitParamArgs = classmethod(_splitParamArgs)
681
def _unescapeParamValue(cls, value):
683
Unescape an ISUPPORT parameter.
685
The only form of supported escape is C{\\xHH}, where HH must be a valid
686
2-digit hexadecimal number.
691
parts = value.split('\\x')
692
# The first part can never be preceeded by the escape.
695
octet, rest = s[:2], s[2:]
697
octet = int(octet, 16)
699
raise ValueError('Invalid hex octet: %r' % (octet,))
700
yield chr(octet) + rest
702
if '\\x' not in value:
704
return ''.join(_unescape())
705
_unescapeParamValue = classmethod(_unescapeParamValue)
708
def _splitParam(cls, param):
710
Split an ISUPPORT parameter.
714
@rtype: C{(str, list)}
715
@return C{(key, arguments)}
719
key, value = param.split('=', 1)
720
return key, map(cls._unescapeParamValue, value.split(','))
721
_splitParam = classmethod(_splitParam)
724
def _parsePrefixParam(cls, prefix):
726
Parse the ISUPPORT "PREFIX" parameter.
728
The order in which the parameter arguments appear is significant, the
729
earlier a mode appears the more privileges it gives.
731
@rtype: C{dict} mapping C{str} to C{(str, int)}
732
@return: A dictionary mapping a mode character to a two-tuple of
733
C({symbol, priority)}, the lower a priority (the lowest being
734
C{0}) the more privileges it gives
738
if prefix[0] != '(' and ')' not in prefix:
739
raise ValueError('Malformed PREFIX parameter')
740
modes, symbols = prefix.split(')', 1)
741
symbols = zip(symbols, xrange(len(symbols)))
743
return dict(zip(modes, symbols))
744
_parsePrefixParam = classmethod(_parsePrefixParam)
747
def _parseChanModesParam(self, params):
749
Parse the ISUPPORT "CHANMODES" parameter.
751
See L{isupport_CHANMODES} for a detailed explanation of this parameter.
753
names = ('addressModes', 'param', 'setParam', 'noParam')
754
if len(params) > len(names):
756
'Expecting a maximum of %d channel mode parameters, got %d' % (
757
len(names), len(params)))
758
items = map(lambda key, value: (key, value or ''), names, params)
760
_parseChanModesParam = classmethod(_parseChanModesParam)
763
def getFeature(self, feature, default=None):
765
Get a server supported feature's value.
767
A feature with the value C{None} is equivalent to the feature being
770
@type feature: C{str}
771
@param feature: Feature name
773
@type default: C{object}
774
@param default: The value to default to, assuming that C{feature}
777
@return: Feature value
779
return self._features.get(feature, default)
782
def hasFeature(self, feature):
784
Determine whether a feature is supported or not.
788
return self.getFeature(feature) is not None
791
def parse(self, params):
793
Parse ISUPPORT parameters.
795
If an unknown parameter is encountered, it is simply added to the
796
dictionary, keyed by its name, as a tuple of the parameters provided.
798
@type params: C{iterable} of C{str}
799
@param params: Iterable of ISUPPORT parameters to parse
802
key, value = self._splitParam(param)
803
if key.startswith('-'):
804
self._features.pop(key[1:], None)
806
self._features[key] = self.dispatch(key, value)
809
def isupport_unknown(self, command, params):
811
Unknown ISUPPORT parameter.
816
def isupport_CHANLIMIT(self, params):
818
The maximum number of each channel type a user may join.
820
return self._splitParamArgs(params, _intOrDefault)
823
def isupport_CHANMODES(self, params):
825
Available channel modes.
827
There are 4 categories of channel mode::
829
addressModes - Modes that add or remove an address to or from a
830
list, these modes always take a parameter.
832
param - Modes that change a setting on a channel, these modes
833
always take a parameter.
835
setParam - Modes that change a setting on a channel, these modes
836
only take a parameter when being set.
838
noParam - Modes that change a setting on a channel, these modes
839
never take a parameter.
842
return self._parseChanModesParam(params)
844
return self.getFeature('CHANMODES')
847
def isupport_CHANNELLEN(self, params):
849
Maximum length of a channel name a client may create.
851
return _intOrDefault(params[0], self.getFeature('CHANNELLEN'))
854
def isupport_CHANTYPES(self, params):
856
Valid channel prefixes.
858
return tuple(params[0])
861
def isupport_EXCEPTS(self, params):
863
Mode character for "ban exceptions".
865
The presence of this parameter indicates that the server supports
868
return params[0] or 'e'
871
def isupport_IDCHAN(self, params):
873
Safe channel identifiers.
875
The presence of this parameter indicates that the server supports
878
return self._splitParamArgs(params)
881
def isupport_INVEX(self, params):
883
Mode character for "invite exceptions".
885
The presence of this parameter indicates that the server supports
888
return params[0] or 'I'
891
def isupport_KICKLEN(self, params):
893
Maximum length of a kick message a client may provide.
895
return _intOrDefault(params[0])
898
def isupport_MAXLIST(self, params):
900
Maximum number of "list modes" a client may set on a channel at once.
902
List modes are identified by the "addressModes" key in CHANMODES.
904
return self._splitParamArgs(params, _intOrDefault)
907
def isupport_MODES(self, params):
909
Maximum number of modes accepting parameters that may be sent, by a
910
client, in a single MODE command.
912
return _intOrDefault(params[0])
915
def isupport_NETWORK(self, params):
922
def isupport_NICKLEN(self, params):
924
Maximum length of a nickname the client may use.
926
return _intOrDefault(params[0], self.getFeature('NICKLEN'))
929
def isupport_PREFIX(self, params):
931
Mapping of channel modes that clients may have to status flags.
934
return self._parsePrefixParam(params[0])
936
return self.getFeature('PREFIX')
939
def isupport_SAFELIST(self, params):
941
Flag indicating that a client may request a LIST without being
942
disconnected due to the large amount of data generated.
947
def isupport_STATUSMSG(self, params):
949
The server supports sending messages to only to clients on a channel
950
with a specific status.
955
def isupport_TARGMAX(self, params):
957
Maximum number of targets allowable for commands that accept multiple
960
return dict(self._splitParamArgs(params, _intOrDefault))
963
def isupport_TOPICLEN(self, params):
965
Maximum length of a topic that may be set.
967
return _intOrDefault(params[0])
971
class IRCClient(basic.LineReceiver):
972
"""Internet Relay Chat client protocol, with sprinkles.
974
In addition to providing an interface for an IRC client protocol,
975
this class also contains reasonable implementations of many common
980
- Limit the length of messages sent (because the IRC server probably
982
- Add flood protection/rate limiting for my CTCP replies.
983
- NickServ cooperation. (a mix-in?)
984
- Heartbeat. The transport may die in such a way that it does not realize
985
it is dead until it is written to. Sending something (like "PING
986
this.irc-host.net") during idle peroids would alleviate that. If
987
you're concerned with the stability of the host as well as that of the
988
transport, you might care to watch for the corresponding PONG.
990
@ivar nickname: Nickname the client will use.
991
@ivar password: Password used to log on to the server. May be C{None}.
992
@ivar realname: Supplied to the server during login as the "Real name"
993
or "ircname". May be C{None}.
994
@ivar username: Supplied to the server during login as the "User name".
997
@ivar userinfo: Sent in reply to a C{USERINFO} CTCP query. If C{None}, no
998
USERINFO reply will be sent.
999
"This is used to transmit a string which is settable by
1000
the user (and never should be set by the client)."
1001
@ivar fingerReply: Sent in reply to a C{FINGER} CTCP query. If C{None}, no
1002
FINGER reply will be sent.
1003
@type fingerReply: Callable or String
1005
@ivar versionName: CTCP VERSION reply, client name. If C{None}, no VERSION
1007
@type versionName: C{str}, or None.
1008
@ivar versionNum: CTCP VERSION reply, client version.
1009
@type versionNum: C{str}, or None.
1010
@ivar versionEnv: CTCP VERSION reply, environment the client is running in.
1011
@type versionEnv: C{str}, or None.
1013
@ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
1014
client may be found. If C{None}, no SOURCE reply will be sent.
1016
@ivar lineRate: Minimum delay between lines sent to the server. If
1017
C{None}, no delay will be imposed.
1018
@type lineRate: Number of Seconds.
1020
@ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and
1021
I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content
1022
of an I{RPL_MOTD} message.
1024
@ivar erroneousNickFallback: Default nickname assigned when an unregistered
1025
client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register
1026
with an illegal nickname.
1027
@type erroneousNickFallback: C{str}
1029
@ivar _registered: Whether or not the user is registered. It becomes True
1030
once a welcome has been received from the server.
1031
@type _registered: C{bool}
1033
@ivar _attemptedNick: The nickname that will try to get registered. It may
1034
change if it is illegal or already taken. L{nickname} becomes the
1035
L{_attemptedNick} that is successfully registered.
1036
@type _attemptedNick: C{str}
1038
@type supported: L{ServerSupportedFeatures}
1039
@ivar supported: Available ISUPPORT features on the server
1046
### Responses to various CTCP queries.
1049
# fingerReply is a callable returning a string, or a str()able object.
1055
sourceURL = "http://twistedmatrix.com/downloads/"
1060
# If this is false, no attempt will be made to identify
1061
# ourself to the server.
1066
_queueEmptying = None
1068
delimiter = '\n' # '\r\n' will also work (see dataReceived)
1070
__pychecker__ = 'unusednames=params,prefix,channel'
1074
erroneousNickFallback = 'defaultnick'
1076
def _reallySendLine(self, line):
1077
return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
1079
def sendLine(self, line):
1080
if self.lineRate is None:
1081
self._reallySendLine(line)
1083
self._queue.append(line)
1084
if not self._queueEmptying:
1087
def _sendLine(self):
1089
self._reallySendLine(self._queue.pop(0))
1090
self._queueEmptying = reactor.callLater(self.lineRate,
1093
self._queueEmptying = None
1096
### Interface level client->user output methods
1098
### You'll want to override these.
1100
### Methods relating to the server itself
1102
def created(self, when):
1103
"""Called with creation date information about the server, usually at logon.
1106
@param when: A string describing when the server was created, probably.
1109
def yourHost(self, info):
1110
"""Called with daemon information about the server, usually at logon.
1113
@param when: A string describing what software the server is running, probably.
1116
def myInfo(self, servername, version, umodes, cmodes):
1117
"""Called with information about the server, usually at logon.
1119
@type servername: C{str}
1120
@param servername: The hostname of this server.
1122
@type version: C{str}
1123
@param version: A description of what software this server runs.
1125
@type umodes: C{str}
1126
@param umodes: All the available user modes.
1128
@type cmodes: C{str}
1129
@param cmodes: All the available channel modes.
1132
def luserClient(self, info):
1133
"""Called with information about the number of connections, usually at logon.
1136
@param info: A description of the number of clients and servers
1137
connected to the network, probably.
1140
def bounce(self, info):
1141
"""Called with information about where the client should reconnect.
1144
@param info: A plaintext description of the address that should be
1148
def isupport(self, options):
1149
"""Called with various information about what the server supports.
1151
@type options: C{list} of C{str}
1152
@param options: Descriptions of features or limits of the server, possibly
1153
in the form "NAME=VALUE".
1156
def luserChannels(self, channels):
1157
"""Called with the number of channels existant on the server.
1159
@type channels: C{int}
1162
def luserOp(self, ops):
1163
"""Called with the number of ops logged on to the server.
1168
def luserMe(self, info):
1169
"""Called with information about the server connected to.
1172
@param info: A plaintext string describing the number of users and servers
1173
connected to this server.
1176
### Methods involving me directly
1178
def privmsg(self, user, channel, message):
1179
"""Called when I have a message from a user to me or a channel.
1183
def joined(self, channel):
1185
Called when I finish joining a channel.
1187
channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
1191
def left(self, channel):
1193
Called when I have left a channel.
1195
channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
1199
def noticed(self, user, channel, message):
1200
"""Called when I have a notice from a user to me or a channel.
1202
By default, this is equivalent to IRCClient.privmsg, but if your
1203
client makes any automated replies, you must override this!
1206
The difference between NOTICE and PRIVMSG is that
1207
automatic replies MUST NEVER be sent in response to a
1208
NOTICE message. [...] The object of this rule is to avoid
1209
loops between clients automatically sending something in
1210
response to something it received.
1212
self.privmsg(user, channel, message)
1214
def modeChanged(self, user, channel, set, modes, args):
1215
"""Called when users or channel's modes are changed.
1218
@param user: The user and hostmask which instigated this change.
1220
@type channel: C{str}
1221
@param channel: The channel where the modes are changed. If args is
1222
empty the channel for which the modes are changing. If the changes are
1223
at server level it could be equal to C{user}.
1225
@type set: C{bool} or C{int}
1226
@param set: True if the mode(s) is being added, False if it is being
1227
removed. If some modes are added and others removed at the same time
1228
this function will be called twice, the first time with all the added
1229
modes, the second with the removed ones. (To change this behaviour
1230
override the irc_MODE method)
1233
@param modes: The mode or modes which are being changed.
1235
@type args: C{tuple}
1236
@param args: Any additional information required for the mode
1240
def pong(self, user, secs):
1241
"""Called with the results of a CTCP PING query.
1246
"""Called after sucessfully signing on to the server.
1250
def kickedFrom(self, channel, kicker, message):
1251
"""Called when I am kicked from a channel.
1255
def nickChanged(self, nick):
1256
"""Called when my nick has been changed.
1258
self.nickname = nick
1261
### Things I observe other people doing in a channel.
1263
def userJoined(self, user, channel):
1264
"""Called when I see another user joining a channel.
1268
def userLeft(self, user, channel):
1269
"""Called when I see another user leaving a channel.
1273
def userQuit(self, user, quitMessage):
1274
"""Called when I see another user disconnect from the network.
1278
def userKicked(self, kickee, channel, kicker, message):
1279
"""Called when I observe someone else being kicked from a channel.
1283
def action(self, user, channel, data):
1284
"""Called when I see a user perform an ACTION on a channel.
1288
def topicUpdated(self, user, channel, newTopic):
1289
"""In channel, user changed the topic to newTopic.
1291
Also called when first joining a channel.
1295
def userRenamed(self, oldname, newname):
1296
"""A user changed their name from oldname to newname.
1300
### Information from the server.
1302
def receivedMOTD(self, motd):
1303
"""I received a message-of-the-day banner from the server.
1305
motd is a list of strings, where each string was sent as a seperate
1306
message from the server. To display, you might want to use::
1310
to get a nicely formatted string.
1314
### user input commands, client->server
1315
### Your client will want to invoke these.
1317
def join(self, channel, key=None):
1321
@type channel: C{str}
1322
@param channel: The name of the channel to join. If it has no prefix,
1323
C{'#'} will be prepended to it.
1325
@param key: If specified, the key used to join the channel.
1327
if channel[0] not in CHANNEL_PREFIXES:
1328
channel = '#' + channel
1330
self.sendLine("JOIN %s %s" % (channel, key))
1332
self.sendLine("JOIN %s" % (channel,))
1334
def leave(self, channel, reason=None):
1338
@type channel: C{str}
1339
@param channel: The name of the channel to leave. If it has no prefix,
1340
C{'#'} will be prepended to it.
1341
@type reason: C{str}
1342
@param reason: If given, the reason for leaving.
1344
if channel[0] not in CHANNEL_PREFIXES:
1345
channel = '#' + channel
1347
self.sendLine("PART %s :%s" % (channel, reason))
1349
self.sendLine("PART %s" % (channel,))
1351
def kick(self, channel, user, reason=None):
1353
Attempt to kick a user from a channel.
1355
@type channel: C{str}
1356
@param channel: The name of the channel to kick the user from. If it has
1357
no prefix, C{'#'} will be prepended to it.
1359
@param user: The nick of the user to kick.
1360
@type reason: C{str}
1361
@param reason: If given, the reason for kicking the user.
1363
if channel[0] not in CHANNEL_PREFIXES:
1364
channel = '#' + channel
1366
self.sendLine("KICK %s %s :%s" % (channel, user, reason))
1368
self.sendLine("KICK %s %s" % (channel, user))
1372
def topic(self, channel, topic=None):
1374
Attempt to set the topic of the given channel, or ask what it is.
1376
If topic is None, then I sent a topic query instead of trying to set the
1377
topic. The server should respond with a TOPIC message containing the
1378
current topic of the given channel.
1380
@type channel: C{str}
1381
@param channel: The name of the channel to change the topic on. If it
1382
has no prefix, C{'#'} will be prepended to it.
1384
@param topic: If specified, what to set the topic to.
1386
# << TOPIC #xtestx :fff
1387
if channel[0] not in CHANNEL_PREFIXES:
1388
channel = '#' + channel
1390
self.sendLine("TOPIC %s :%s" % (channel, topic))
1392
self.sendLine("TOPIC %s" % (channel,))
1394
def mode(self, chan, set, modes, limit = None, user = None, mask = None):
1396
Change the modes on a user or channel.
1398
The C{limit}, C{user}, and C{mask} parameters are mutually exclusive.
1401
@param chan: The name of the channel to operate on.
1403
@param set: True to give the user or channel permissions and False to
1406
@param modes: The mode flags to set on the user or channel.
1408
@param limit: In conjuction with the C{'l'} mode flag, limits the
1409
number of users on the channel.
1411
@param user: The user to change the mode on.
1413
@param mask: In conjuction with the C{'b'} mode flag, sets a mask of
1414
users to be banned from the channel.
1417
line = 'MODE %s +%s' % (chan, modes)
1419
line = 'MODE %s -%s' % (chan, modes)
1420
if limit is not None:
1421
line = '%s %d' % (line, limit)
1422
elif user is not None:
1423
line = '%s %s' % (line, user)
1424
elif mask is not None:
1425
line = '%s %s' % (line, mask)
1429
def say(self, channel, message, length = None):
1431
Send a message to a channel
1433
@type channel: C{str}
1434
@param channel: The channel to say the message on. If it has no prefix,
1435
C{'#'} will be prepended to it.
1436
@type message: C{str}
1437
@param message: The message to say.
1438
@type length: C{int}
1439
@param length: The maximum number of octets to send at a time. This has
1440
the effect of turning a single call to C{msg()} into multiple
1441
commands to the server. This is useful when long messages may be
1442
sent that would otherwise cause the server to kick us off or
1443
silently truncate the text we are sending. If None is passed, the
1444
entire message is always send in one command.
1446
if channel[0] not in CHANNEL_PREFIXES:
1447
channel = '#' + channel
1448
self.msg(channel, message, length)
1451
def msg(self, user, message, length = None):
1452
"""Send a message to a user or channel.
1455
@param user: The username or channel name to which to direct the
1458
@type message: C{str}
1459
@param message: The text to send
1461
@type length: C{int}
1462
@param length: The maximum number of octets to send at a time. This
1463
has the effect of turning a single call to msg() into multiple
1464
commands to the server. This is useful when long messages may be
1465
sent that would otherwise cause the server to kick us off or silently
1466
truncate the text we are sending. If None is passed, the entire
1467
message is always send in one command.
1470
fmt = "PRIVMSG %s :%%s" % (user,)
1473
self.sendLine(fmt % (message,))
1475
# NOTE: minimumLength really equals len(fmt) - 2 (for '%s') + n
1476
# where n is how many bytes sendLine sends to end the line.
1477
# n was magic numbered to 2, I think incorrectly
1478
minimumLength = len(fmt)
1479
if length <= minimumLength:
1480
raise ValueError("Maximum length must exceed %d for message "
1481
"to %s" % (minimumLength, user))
1482
lines = split(message, length - minimumLength)
1483
map(lambda line, self=self, fmt=fmt: self.sendLine(fmt % line),
1486
def notice(self, user, message):
1488
Send a notice to a user.
1490
Notices are like normal message, but should never get automated
1494
@param user: The user to send a notice to.
1495
@type message: C{str}
1496
@param message: The contents of the notice to send.
1498
self.sendLine("NOTICE %s :%s" % (user, message))
1500
def away(self, message=''):
1502
Mark this client as away.
1504
@type message: C{str}
1505
@param message: If specified, the away message.
1507
self.sendLine("AWAY :%s" % message)
1513
Clear the away status.
1515
# An empty away marks us as back
1519
def whois(self, nickname, server=None):
1521
Retrieve user information about the given nick name.
1523
@type nickname: C{str}
1524
@param nickname: The nick name about which to retrieve information.
1529
self.sendLine('WHOIS ' + nickname)
1531
self.sendLine('WHOIS %s %s' % (server, nickname))
1534
def register(self, nickname, hostname='foo', servername='bar'):
1536
Login to the server.
1538
@type nickname: C{str}
1539
@param nickname: The nickname to register.
1540
@type hostname: C{str}
1541
@param hostname: If specified, the hostname to logon as.
1542
@type servername: C{str}
1543
@param servername: If specified, the servername to logon as.
1545
if self.password is not None:
1546
self.sendLine("PASS %s" % self.password)
1547
self.setNick(nickname)
1548
if self.username is None:
1549
self.username = nickname
1550
self.sendLine("USER %s %s %s :%s" % (self.username, hostname, servername, self.realname))
1552
def setNick(self, nickname):
1554
Set this client's nickname.
1556
@type nickname: C{str}
1557
@param nickname: The nickname to change to.
1559
self._attemptedNick = nickname
1560
self.sendLine("NICK %s" % nickname)
1562
def quit(self, message = ''):
1564
Disconnect from the server
1566
@type message: C{str}
1568
@param message: If specified, the message to give when quitting the
1571
self.sendLine("QUIT :%s" % message)
1573
### user input commands, client->client
1575
def describe(self, channel, action):
1579
@type channel: C{str}
1580
@param channel: The name of the channel to have an action on. If it
1581
has no prefix, it is sent to the user of that name.
1582
@type action: C{str}
1583
@param action: The action to preform.
1586
self.ctcpMakeQuery(channel, [('ACTION', action)])
1589
def me(self, channel, action):
1593
This function is deprecated since Twisted 9.0. Use describe().
1595
@type channel: C{str}
1596
@param channel: The name of the channel to have an action on. If it
1597
has no prefix, C{'#'} will be prepended to it.
1598
@type action: C{str}
1599
@param action: The action to preform.
1601
warnings.warn("me() is deprecated since Twisted 9.0. Use IRCClient.describe().",
1602
DeprecationWarning, stacklevel=2)
1604
if channel[0] not in CHANNEL_PREFIXES:
1605
channel = '#' + channel
1606
self.describe(channel, action)
1612
def ping(self, user, text = None):
1614
Measure round-trip delay to another IRC client.
1616
if self._pings is None:
1620
chars = string.letters + string.digits + string.punctuation
1621
key = ''.join([random.choice(chars) for i in range(12)])
1624
self._pings[(user, key)] = time.time()
1625
self.ctcpMakeQuery(user, [('PING', key)])
1627
if len(self._pings) > self._MAX_PINGRING:
1628
# Remove some of the oldest entries.
1629
byValue = [(v, k) for (k, v) in self._pings.items()]
1631
excess = self._MAX_PINGRING - len(self._pings)
1632
for i in xrange(excess):
1633
del self._pings[byValue[i][1]]
1635
def dccSend(self, user, file):
1636
if type(file) == types.StringType:
1637
file = open(file, 'r')
1639
size = fileSize(file)
1641
name = getattr(file, "name", "file@%s" % (id(file),))
1643
factory = DccSendFactory(file)
1644
port = reactor.listenTCP(0, factory, 1)
1646
raise NotImplementedError,(
1647
"XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. "
1648
"(and stop accepting once we've made a single connection.)")
1650
my_address = struct.pack("!I", my_address)
1652
args = ['SEND', name, my_address, str(port)]
1654
if not (size is None):
1657
args = string.join(args, ' ')
1659
self.ctcpMakeQuery(user, [('DCC', args)])
1661
def dccResume(self, user, fileName, port, resumePos):
1662
"""Send a DCC RESUME request to another user."""
1663
self.ctcpMakeQuery(user, [
1664
('DCC', ['RESUME', fileName, port, resumePos])])
1666
def dccAcceptResume(self, user, fileName, port, resumePos):
1667
"""Send a DCC ACCEPT response to clients who have requested a resume.
1669
self.ctcpMakeQuery(user, [
1670
('DCC', ['ACCEPT', fileName, port, resumePos])])
1672
### server->client messages
1673
### You might want to fiddle with these,
1674
### but it is safe to leave them alone.
1676
def irc_ERR_NICKNAMEINUSE(self, prefix, params):
1678
Called when we try to register or change to a nickname that is already
1681
self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
1682
self.setNick(self._attemptedNick)
1685
def alterCollidedNick(self, nickname):
1687
Generate an altered version of a nickname that caused a collision in an
1688
effort to create an unused related name for subsequent registration.
1690
@param nickname: The nickname a user is attempting to register.
1691
@type nickname: C{str}
1693
@returns: A string that is in some way different from the nickname.
1696
return nickname + '_'
1699
def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
1701
Called when we try to register or change to an illegal nickname.
1703
The server should send this reply when the nickname contains any
1704
disallowed characters. The bot will stall, waiting for RPL_WELCOME, if
1705
we don't handle this during sign-on.
1707
@note: The method uses the spelling I{erroneus}, as it appears in
1708
the RFC, section 6.1.
1710
if not self._registered:
1711
self.setNick(self.erroneousNickFallback)
1714
def irc_ERR_PASSWDMISMATCH(self, prefix, params):
1716
Called when the login was incorrect.
1718
raise IRCPasswordMismatch("Password Incorrect.")
1720
def irc_RPL_WELCOME(self, prefix, params):
1722
Called when we have received the welcome from the server.
1724
self._registered = True
1725
self.nickname = self._attemptedNick
1728
def irc_JOIN(self, prefix, params):
1730
Called when a user joins a channel.
1732
nick = string.split(prefix,'!')[0]
1733
channel = params[-1]
1734
if nick == self.nickname:
1735
self.joined(channel)
1737
self.userJoined(nick, channel)
1739
def irc_PART(self, prefix, params):
1741
Called when a user leaves a channel.
1743
nick = string.split(prefix,'!')[0]
1745
if nick == self.nickname:
1748
self.userLeft(nick, channel)
1750
def irc_QUIT(self, prefix, params):
1752
Called when a user has quit.
1754
nick = string.split(prefix,'!')[0]
1755
self.userQuit(nick, params[0])
1758
def irc_MODE(self, user, params):
1760
Parse a server mode change message.
1762
channel, modes, args = params[0], params[1], params[2:]
1764
if modes[0] not in '-+':
1767
if channel == self.nickname:
1768
# This is a mode change to our individual user, not a channel mode
1770
paramModes = self.getUserModeParams()
1772
paramModes = self.getChannelModeParams()
1775
added, removed = parseModes(modes, args, paramModes)
1777
log.err(None, 'An error occured while parsing the following '
1778
'MODE message: MODE %s' % (' '.join(params),))
1781
modes, params = zip(*added)
1782
self.modeChanged(user, channel, True, ''.join(modes), params)
1785
modes, params = zip(*removed)
1786
self.modeChanged(user, channel, False, ''.join(modes), params)
1789
def irc_PING(self, prefix, params):
1791
Called when some has pinged us.
1793
self.sendLine("PONG %s" % params[-1])
1795
def irc_PRIVMSG(self, prefix, params):
1797
Called when we get a message.
1801
message = params[-1]
1803
if not message: return # don't raise an exception if some idiot sends us a blank message
1805
if message[0]==X_DELIM:
1806
m = ctcpExtract(message)
1808
self.ctcpQuery(user, channel, m['extended'])
1813
message = string.join(m['normal'], ' ')
1815
self.privmsg(user, channel, message)
1817
def irc_NOTICE(self, prefix, params):
1819
Called when a user gets a notice.
1823
message = params[-1]
1825
if message[0]==X_DELIM:
1826
m = ctcpExtract(message)
1828
self.ctcpReply(user, channel, m['extended'])
1833
message = string.join(m['normal'], ' ')
1835
self.noticed(user, channel, message)
1837
def irc_NICK(self, prefix, params):
1839
Called when a user changes their nickname.
1841
nick = string.split(prefix,'!', 1)[0]
1842
if nick == self.nickname:
1843
self.nickChanged(params[0])
1845
self.userRenamed(nick, params[0])
1847
def irc_KICK(self, prefix, params):
1849
Called when a user is kicked from a channel.
1851
kicker = string.split(prefix,'!')[0]
1854
message = params[-1]
1855
if string.lower(kicked) == string.lower(self.nickname):
1857
self.kickedFrom(channel, kicker, message)
1859
self.userKicked(kicked, channel, kicker, message)
1861
def irc_TOPIC(self, prefix, params):
1863
Someone in the channel set the topic.
1865
user = string.split(prefix, '!')[0]
1867
newtopic = params[1]
1868
self.topicUpdated(user, channel, newtopic)
1870
def irc_RPL_TOPIC(self, prefix, params):
1872
Called when the topic for a channel is initially reported or when it
1873
subsequently changes.
1875
user = string.split(prefix, '!')[0]
1877
newtopic = params[2]
1878
self.topicUpdated(user, channel, newtopic)
1880
def irc_RPL_NOTOPIC(self, prefix, params):
1881
user = string.split(prefix, '!')[0]
1884
self.topicUpdated(user, channel, newtopic)
1886
def irc_RPL_MOTDSTART(self, prefix, params):
1887
if params[-1].startswith("- "):
1888
params[-1] = params[-1][2:]
1889
self.motd = [params[-1]]
1891
def irc_RPL_MOTD(self, prefix, params):
1892
if params[-1].startswith("- "):
1893
params[-1] = params[-1][2:]
1894
if self.motd is None:
1896
self.motd.append(params[-1])
1899
def irc_RPL_ENDOFMOTD(self, prefix, params):
1901
I{RPL_ENDOFMOTD} indicates the end of the message of the day
1902
messages. Deliver the accumulated lines to C{receivedMOTD}.
1906
self.receivedMOTD(motd)
1909
def irc_RPL_CREATED(self, prefix, params):
1910
self.created(params[1])
1912
def irc_RPL_YOURHOST(self, prefix, params):
1913
self.yourHost(params[1])
1915
def irc_RPL_MYINFO(self, prefix, params):
1916
info = params[1].split(None, 3)
1917
while len(info) < 4:
1921
def irc_RPL_BOUNCE(self, prefix, params):
1922
self.bounce(params[1])
1924
def irc_RPL_ISUPPORT(self, prefix, params):
1926
# Several ISUPPORT messages, in no particular order, may be sent
1927
# to the client at any given point in time (usually only on connect,
1928
# though.) For this reason, ServerSupportedFeatures.parse is intended
1929
# to mutate the supported feature list.
1930
self.supported.parse(args)
1933
def irc_RPL_LUSERCLIENT(self, prefix, params):
1934
self.luserClient(params[1])
1936
def irc_RPL_LUSEROP(self, prefix, params):
1938
self.luserOp(int(params[1]))
1942
def irc_RPL_LUSERCHANNELS(self, prefix, params):
1944
self.luserChannels(int(params[1]))
1948
def irc_RPL_LUSERME(self, prefix, params):
1949
self.luserMe(params[1])
1951
def irc_unknown(self, prefix, command, params):
1954
### Receiving a CTCP query from another party
1955
### It is safe to leave these alone.
1957
def ctcpQuery(self, user, channel, messages):
1958
"""Dispatch method for any CTCP queries received.
1961
method = getattr(self, "ctcpQuery_%s" % m[0], None)
1963
method(user, channel, m[1])
1965
self.ctcpUnknownQuery(user, channel, m[0], m[1])
1967
def ctcpQuery_ACTION(self, user, channel, data):
1968
self.action(user, channel, data)
1970
def ctcpQuery_PING(self, user, channel, data):
1971
nick = string.split(user,"!")[0]
1972
self.ctcpMakeReply(nick, [("PING", data)])
1974
def ctcpQuery_FINGER(self, user, channel, data):
1975
if data is not None:
1976
self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
1978
if not self.fingerReply:
1981
if callable(self.fingerReply):
1982
reply = self.fingerReply()
1984
reply = str(self.fingerReply)
1986
nick = string.split(user,"!")[0]
1987
self.ctcpMakeReply(nick, [('FINGER', reply)])
1989
def ctcpQuery_VERSION(self, user, channel, data):
1990
if data is not None:
1991
self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
1994
if self.versionName:
1995
nick = string.split(user,"!")[0]
1996
self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
1998
self.versionNum or '',
1999
self.versionEnv or ''))])
2001
def ctcpQuery_SOURCE(self, user, channel, data):
2002
if data is not None:
2003
self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
2006
nick = string.split(user,"!")[0]
2007
# The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
2008
# replies should be responded to with the location of an anonymous
2009
# FTP server in host:directory:file format. I'm taking the liberty
2010
# of bringing it into the 21st century by sending a URL instead.
2011
self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
2014
def ctcpQuery_USERINFO(self, user, channel, data):
2015
if data is not None:
2016
self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
2019
nick = string.split(user,"!")[0]
2020
self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
2022
def ctcpQuery_CLIENTINFO(self, user, channel, data):
2023
"""A master index of what CTCP tags this client knows.
2025
If no arguments are provided, respond with a list of known tags.
2026
If an argument is provided, provide human-readable help on
2027
the usage of that tag.
2030
nick = string.split(user,"!")[0]
2032
# XXX: prefixedMethodNames gets methods from my *class*,
2033
# but it's entirely possible that this *instance* has more
2035
names = reflect.prefixedMethodNames(self.__class__,
2038
self.ctcpMakeReply(nick, [('CLIENTINFO',
2039
string.join(names, ' '))])
2041
args = string.split(data)
2042
method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
2044
self.ctcpMakeReply(nick, [('ERRMSG',
2046
"Unknown query '%s'"
2047
% (data, args[0]))])
2049
doc = getattr(method, '__doc__', '')
2050
self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
2053
def ctcpQuery_ERRMSG(self, user, channel, data):
2054
# Yeah, this seems strange, but that's what the spec says to do
2055
# when faced with an ERRMSG query (not a reply).
2056
nick = string.split(user,"!")[0]
2057
self.ctcpMakeReply(nick, [('ERRMSG',
2058
"%s :No error has occoured." % data)])
2060
def ctcpQuery_TIME(self, user, channel, data):
2061
if data is not None:
2062
self.quirkyMessage("Why did %s send '%s' with a TIME query?"
2064
nick = string.split(user,"!")[0]
2065
self.ctcpMakeReply(nick,
2067
time.asctime(time.localtime(time.time())))])
2069
def ctcpQuery_DCC(self, user, channel, data):
2070
"""Initiate a Direct Client Connection
2074
dcctype = data.split(None, 1)[0].upper()
2075
handler = getattr(self, "dcc_" + dcctype, None)
2077
if self.dcc_sessions is None:
2078
self.dcc_sessions = []
2079
data = data[len(dcctype)+1:]
2080
handler(user, channel, data)
2082
nick = string.split(user,"!")[0]
2083
self.ctcpMakeReply(nick, [('ERRMSG',
2084
"DCC %s :Unknown DCC type '%s'"
2085
% (data, dcctype))])
2086
self.quirkyMessage("%s offered unknown DCC type %s"
2089
def dcc_SEND(self, user, channel, data):
2090
# Use splitQuoted for those who send files with spaces in the names.
2091
data = text.splitQuoted(data)
2093
raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
2095
(filename, address, port) = data[:3]
2097
address = dccParseAddress(address)
2101
raise IRCBadMessage, "Indecipherable port %r" % (port,)
2110
# XXX Should we bother passing this data?
2111
self.dccDoSend(user, address, port, filename, size, data)
2113
def dcc_ACCEPT(self, user, channel, data):
2114
data = text.splitQuoted(data)
2116
raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
2117
(filename, port, resumePos) = data[:3]
2120
resumePos = int(resumePos)
2124
self.dccDoAcceptResume(user, filename, port, resumePos)
2126
def dcc_RESUME(self, user, channel, data):
2127
data = text.splitQuoted(data)
2129
raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
2130
(filename, port, resumePos) = data[:3]
2133
resumePos = int(resumePos)
2136
self.dccDoResume(user, filename, port, resumePos)
2138
def dcc_CHAT(self, user, channel, data):
2139
data = text.splitQuoted(data)
2141
raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
2143
(filename, address, port) = data[:3]
2145
address = dccParseAddress(address)
2149
raise IRCBadMessage, "Indecipherable port %r" % (port,)
2151
self.dccDoChat(user, channel, address, port, data)
2153
### The dccDo methods are the slightly higher-level siblings of
2154
### common dcc_ methods; the arguments have been parsed for them.
2156
def dccDoSend(self, user, address, port, fileName, size, data):
2157
"""Called when I receive a DCC SEND offer from a client.
2159
By default, I do nothing here."""
2160
## filename = path.basename(arg)
2161
## protocol = DccFileReceive(filename, size,
2162
## (user,channel,data),self.dcc_destdir)
2163
## reactor.clientTCP(address, port, protocol)
2164
## self.dcc_sessions.append(protocol)
2167
def dccDoResume(self, user, file, port, resumePos):
2168
"""Called when a client is trying to resume an offered file
2169
via DCC send. It should be either replied to with a DCC
2170
ACCEPT or ignored (default)."""
2173
def dccDoAcceptResume(self, user, file, port, resumePos):
2174
"""Called when a client has verified and accepted a DCC resume
2175
request made by us. By default it will do nothing."""
2178
def dccDoChat(self, user, channel, address, port, data):
2180
#factory = DccChatFactory(self, queryData=(user, channel, data))
2181
#reactor.connectTCP(address, port, factory)
2182
#self.dcc_sessions.append(factory)
2184
#def ctcpQuery_SED(self, user, data):
2185
# """Simple Encryption Doodoo
2187
# Feel free to implement this, but no specification is available.
2189
# raise NotImplementedError
2191
def ctcpUnknownQuery(self, user, channel, tag, data):
2192
nick = string.split(user,"!")[0]
2193
self.ctcpMakeReply(nick, [('ERRMSG',
2194
"%s %s: Unknown query '%s'"
2195
% (tag, data, tag))])
2197
log.msg("Unknown CTCP query from %s: %s %s\n"
2198
% (user, tag, data))
2200
def ctcpMakeReply(self, user, messages):
2202
Send one or more C{extended messages} as a CTCP reply.
2204
@type messages: a list of extended messages. An extended
2205
message is a (tag, data) tuple, where 'data' may be C{None}.
2207
self.notice(user, ctcpStringify(messages))
2209
### client CTCP query commands
2211
def ctcpMakeQuery(self, user, messages):
2213
Send one or more C{extended messages} as a CTCP query.
2215
@type messages: a list of extended messages. An extended
2216
message is a (tag, data) tuple, where 'data' may be C{None}.
2218
self.msg(user, ctcpStringify(messages))
2220
### Receiving a response to a CTCP query (presumably to one we made)
2221
### You may want to add methods here, or override UnknownReply.
2223
def ctcpReply(self, user, channel, messages):
2225
Dispatch method for any CTCP replies received.
2228
method = getattr(self, "ctcpReply_%s" % m[0], None)
2230
method(user, channel, m[1])
2232
self.ctcpUnknownReply(user, channel, m[0], m[1])
2234
def ctcpReply_PING(self, user, channel, data):
2235
nick = user.split('!', 1)[0]
2236
if (not self._pings) or (not self._pings.has_key((nick, data))):
2237
raise IRCBadMessage,\
2238
"Bogus PING response from %s: %s" % (user, data)
2240
t0 = self._pings[(nick, data)]
2241
self.pong(user, time.time() - t0)
2243
def ctcpUnknownReply(self, user, channel, tag, data):
2244
"""Called when a fitting ctcpReply_ method is not found.
2246
XXX: If the client makes arbitrary CTCP queries,
2247
this method should probably show the responses to
2248
them instead of treating them as anomolies.
2250
log.msg("Unknown CTCP reply from %s: %s %s\n"
2251
% (user, tag, data))
2254
### You may override these with something more appropriate to your UI.
2256
def badMessage(self, line, excType, excValue, tb):
2257
"""When I get a message that's so broken I can't use it.
2260
log.msg(string.join(traceback.format_exception(excType,
2264
def quirkyMessage(self, s):
2265
"""This is called when I receive a message which is peculiar,
2266
but not wholly indecipherable.
2270
### Protocool methods
2272
def connectionMade(self):
2273
self.supported = ServerSupportedFeatures()
2275
if self.performLogin:
2276
self.register(self.nickname)
2278
def dataReceived(self, data):
2279
basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
2281
def lineReceived(self, line):
2282
line = lowDequote(line)
2284
prefix, command, params = parsemsg(line)
2285
if numeric_to_symbolic.has_key(command):
2286
command = numeric_to_symbolic[command]
2287
self.handleCommand(command, prefix, params)
2288
except IRCBadMessage:
2289
self.badMessage(line, *sys.exc_info())
2292
def getUserModeParams(self):
2294
Get user modes that require parameters for correct parsing.
2296
@rtype: C{[str, str]}
2297
@return C{[add, remove]}
2302
def getChannelModeParams(self):
2304
Get channel modes that require parameters for correct parsing.
2306
@rtype: C{[str, str]}
2307
@return C{[add, remove]}
2309
# PREFIX modes are treated as "type B" CHANMODES, they always take
2312
prefixes = self.supported.getFeature('PREFIX', {})
2313
params[0] = params[1] = ''.join(prefixes.iterkeys())
2315
chanmodes = self.supported.getFeature('CHANMODES')
2316
if chanmodes is not None:
2317
params[0] += chanmodes.get('addressModes', '')
2318
params[0] += chanmodes.get('param', '')
2319
params[1] = params[0]
2320
params[0] += chanmodes.get('setParam', '')
2324
def handleCommand(self, command, prefix, params):
2325
"""Determine the function to call for the given command and call
2326
it with the given arguments.
2328
method = getattr(self, "irc_%s" % command, None)
2330
if method is not None:
2331
method(prefix, params)
2333
self.irc_unknown(prefix, command, params)
2338
def __getstate__(self):
2339
dct = self.__dict__.copy()
2340
dct['dcc_sessions'] = None
2341
dct['_pings'] = None
2345
def dccParseAddress(address):
2350
address = long(address)
2352
raise IRCBadMessage,\
2353
"Indecipherable address %r" % (address,)
2356
(address >> 24) & 0xFF,
2357
(address >> 16) & 0xFF,
2358
(address >> 8) & 0xFF,
2361
address = '.'.join(map(str,address))
2365
class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
2366
"""Bare protocol to receive a Direct Client Connection SEND stream.
2368
This does enough to keep the other guy talking, but you'll want to
2369
extend my dataReceived method to *do* something with the data I get.
2374
def __init__(self, resumeOffset=0):
2375
self.bytesReceived = resumeOffset
2376
self.resume = (resumeOffset != 0)
2378
def dataReceived(self, data):
2379
"""Called when data is received.
2381
Warning: This just acknowledges to the remote host that the
2382
data has been received; it doesn't *do* anything with the
2383
data, so you'll want to override this.
2385
self.bytesReceived = self.bytesReceived + len(data)
2386
self.transport.write(struct.pack('!i', self.bytesReceived))
2389
class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
2390
"""Protocol for an outgoing Direct Client Connection SEND.
2399
def __init__(self, file):
2400
if type(file) is types.StringType:
2401
self.file = open(file, 'r')
2403
def connectionMade(self):
2407
def dataReceived(self, data):
2408
# XXX: Do we need to check to see if len(data) != fmtsize?
2410
bytesShesGot = struct.unpack("!I", data)
2411
if bytesShesGot < self.bytesSent:
2413
# XXX? Add some checks to see if we've stalled out?
2415
elif bytesShesGot > self.bytesSent:
2416
# self.transport.log("DCC SEND %s: She says she has %d bytes "
2417
# "but I've only sent %d. I'm stopping "
2418
# "this screwy transfer."
2420
# bytesShesGot, self.bytesSent))
2421
self.transport.loseConnection()
2426
def sendBlock(self):
2427
block = self.file.read(self.blocksize)
2429
self.transport.write(block)
2430
self.bytesSent = self.bytesSent + len(block)
2432
# Nothing more to send, transfer complete.
2433
self.transport.loseConnection()
2436
def connectionLost(self, reason):
2438
if hasattr(self.file, "close"):
2442
class DccSendFactory(protocol.Factory):
2443
protocol = DccSendProtocol
2444
def __init__(self, file):
2447
def buildProtocol(self, connection):
2448
p = self.protocol(self.file)
2454
"""I'll try my damndest to determine the size of this file object.
2457
if hasattr(file, "fileno"):
2458
fileno = file.fileno()
2460
stat_ = os.fstat(fileno)
2461
size = stat_[stat.ST_SIZE]
2467
if hasattr(file, "name") and path.exists(file.name):
2469
size = path.getsize(file.name)
2475
if hasattr(file, "seek") and hasattr(file, "tell"):
2489
class DccChat(basic.LineReceiver, styles.Ephemeral):
2490
"""Direct Client Connection protocol type CHAT.
2492
DCC CHAT is really just your run o' the mill basic.LineReceiver
2493
protocol. This class only varies from that slightly, accepting
2494
either LF or CR LF for a line delimeter for incoming messages
2495
while always using CR LF for outgoing.
2497
The lineReceived method implemented here uses the DCC connection's
2498
'client' attribute (provided upon construction) to deliver incoming
2499
lines from the DCC chat via IRCClient's normal privmsg interface.
2500
That's something of a spoof, which you may well want to override.
2509
def __init__(self, client, queryData=None):
2510
"""Initialize a new DCC CHAT session.
2512
queryData is a 3-tuple of
2513
(fromUser, targetUserOrChannel, data)
2514
as received by the CTCP query.
2516
(To be honest, fromUser is the only thing that's currently
2517
used here. targetUserOrChannel is potentially useful, while
2518
the 'data' argument is soley for informational purposes.)
2520
self.client = client
2522
self.queryData = queryData
2523
self.remoteParty = self.queryData[0]
2525
def dataReceived(self, data):
2526
self.buffer = self.buffer + data
2527
lines = string.split(self.buffer, LF)
2528
# Put the (possibly empty) element after the last LF back in the
2530
self.buffer = lines.pop()
2535
self.lineReceived(line)
2537
def lineReceived(self, line):
2538
log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
2539
self.client.privmsg(self.remoteParty,
2540
self.client.nickname, line)
2543
class DccChatFactory(protocol.ClientFactory):
2546
def __init__(self, client, queryData):
2547
self.client = client
2548
self.queryData = queryData
2550
def buildProtocol(self, addr):
2551
p = self.protocol(client=self.client, queryData=self.queryData)
2554
def clientConnectionFailed(self, unused_connector, unused_reason):
2555
self.client.dcc_sessions.remove(self)
2557
def clientConnectionLost(self, unused_connector, unused_reason):
2558
self.client.dcc_sessions.remove(self)
2561
def dccDescribe(data):
2562
"""Given the data chunk from a DCC query, return a descriptive string.
2566
data = string.split(data)
2570
(dcctype, arg, address, port) = data[:4]
2576
address = long(address)
2581
(address >> 24) & 0xFF,
2582
(address >> 16) & 0xFF,
2583
(address >> 8) & 0xFF,
2586
# The mapping to 'int' is to get rid of those accursed
2587
# "L"s which python 1.5.2 puts on the end of longs.
2588
address = string.join(map(str,map(int,address)), ".")
2590
if dcctype == 'SEND':
2597
size_txt = ' of size %d bytes' % (size,)
2601
dcc_text = ("SEND for file '%s'%s at host %s, port %s"
2602
% (filename, size_txt, address, port))
2603
elif dcctype == 'CHAT':
2604
dcc_text = ("CHAT for host %s, port %s"
2607
dcc_text = orig_data
2612
class DccFileReceive(DccFileReceiveBasic):
2613
"""Higher-level coverage for getting a file from DCC SEND.
2615
I allow you to change the file's name and destination directory.
2616
I won't overwrite an existing file unless I've been told it's okay
2617
to do so. If passed the resumeOffset keyword argument I will attempt to
2618
resume the file from that amount of bytes.
2620
XXX: I need to let the client know when I am finished.
2621
XXX: I need to decide how to keep a progress indicator updated.
2622
XXX: Client needs a way to tell me "Do not finish until I say so."
2623
XXX: I need to make sure the client understands if the file cannot be written.
2633
def __init__(self, filename, fileSize=-1, queryData=None,
2634
destDir='.', resumeOffset=0):
2635
DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
2636
self.filename = filename
2637
self.destDir = destDir
2638
self.fileSize = fileSize
2641
self.queryData = queryData
2642
self.fromUser = self.queryData[0]
2644
def set_directory(self, directory):
2645
"""Set the directory where the downloaded file will be placed.
2647
May raise OSError if the supplied directory path is not suitable.
2649
if not path.exists(directory):
2650
raise OSError(errno.ENOENT, "You see no directory there.",
2652
if not path.isdir(directory):
2653
raise OSError(errno.ENOTDIR, "You cannot put a file into "
2654
"something which is not a directory.",
2656
if not os.access(directory, os.X_OK | os.W_OK):
2657
raise OSError(errno.EACCES,
2658
"This directory is too hard to write in to.",
2660
self.destDir = directory
2662
def set_filename(self, filename):
2663
"""Change the name of the file being transferred.
2665
This replaces the file name provided by the sender.
2667
self.filename = filename
2669
def set_overwrite(self, boolean):
2670
"""May I overwrite existing files?
2672
self.overwrite = boolean
2675
# Protocol-level methods.
2677
def connectionMade(self):
2678
dst = path.abspath(path.join(self.destDir,self.filename))
2679
exists = path.exists(dst)
2680
if self.resume and exists:
2681
# I have been told I want to resume, and a file already
2682
# exists - Here we go
2683
self.file = open(dst, 'ab')
2684
log.msg("Attempting to resume %s - starting from %d bytes" %
2685
(self.file, self.file.tell()))
2686
elif self.overwrite or not exists:
2687
self.file = open(dst, 'wb')
2689
raise OSError(errno.EEXIST,
2690
"There's a file in the way. "
2691
"Perhaps that's why you cannot open it.",
2694
def dataReceived(self, data):
2695
self.file.write(data)
2696
DccFileReceiveBasic.dataReceived(self, data)
2698
# XXX: update a progress indicator here?
2700
def connectionLost(self, reason):
2701
"""When the connection is lost, I close the file.
2704
logmsg = ("%s closed." % (self,))
2705
if self.fileSize > 0:
2706
logmsg = ("%s %d/%d bytes received"
2707
% (logmsg, self.bytesReceived, self.fileSize))
2708
if self.bytesReceived == self.fileSize:
2710
elif self.bytesReceived < self.fileSize:
2711
logmsg = ("%s (Warning: %d bytes short)"
2712
% (logmsg, self.fileSize - self.bytesReceived))
2714
logmsg = ("%s (file larger than expected)"
2717
logmsg = ("%s %d bytes received"
2718
% (logmsg, self.bytesReceived))
2720
if hasattr(self, 'file'):
2721
logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
2722
if hasattr(self.file, 'close'): self.file.close()
2724
# self.transport.log(logmsg)
2727
if not self.connected:
2728
return "<Unconnected DccFileReceive object at %x>" % (id(self),)
2729
from_ = self.transport.getPeer()
2731
from_ = "%s (%s)" % (self.fromUser, from_)
2733
s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
2737
s = ("<%s at %x: GET %s>"
2738
% (self.__class__, id(self), self.filename))
2742
# CTCP constants and helper functions
2746
def ctcpExtract(message):
2747
"""Extract CTCP data from a string.
2749
Returns a dictionary with two items:
2751
- C{'extended'}: a list of CTCP (tag, data) tuples
2752
- C{'normal'}: a list of strings which were not inside a CTCP delimeter
2755
extended_messages = []
2756
normal_messages = []
2757
retval = {'extended': extended_messages,
2758
'normal': normal_messages }
2760
messages = string.split(message, X_DELIM)
2763
# X1 extended data X2 nomal data X3 extended data X4 normal...
2766
extended_messages.append(messages.pop(0))
2768
normal_messages.append(messages.pop(0))
2771
extended_messages[:] = filter(None, extended_messages)
2772
normal_messages[:] = filter(None, normal_messages)
2774
extended_messages[:] = map(ctcpDequote, extended_messages)
2775
for i in xrange(len(extended_messages)):
2776
m = string.split(extended_messages[i], SPC, 1)
2783
extended_messages[i] = (tag, data)
2795
M_QUOTE: M_QUOTE + M_QUOTE
2799
for k, v in mQuoteTable.items():
2800
mDequoteTable[v[-1]] = k
2803
mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
2806
for c in (M_QUOTE, NUL, NL, CR):
2807
s = string.replace(s, c, mQuoteTable[c])
2811
def sub(matchobj, mDequoteTable=mDequoteTable):
2812
s = matchobj.group()[1]
2814
s = mDequoteTable[s]
2819
return mEscape_re.sub(sub, s)
2824
X_DELIM: X_QUOTE + 'a',
2825
X_QUOTE: X_QUOTE + X_QUOTE
2830
for k, v in xQuoteTable.items():
2831
xDequoteTable[v[-1]] = k
2833
xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
2836
for c in (X_QUOTE, X_DELIM):
2837
s = string.replace(s, c, xQuoteTable[c])
2841
def sub(matchobj, xDequoteTable=xDequoteTable):
2842
s = matchobj.group()[1]
2844
s = xDequoteTable[s]
2849
return xEscape_re.sub(sub, s)
2851
def ctcpStringify(messages):
2853
@type messages: a list of extended messages. An extended
2854
message is a (tag, data) tuple, where 'data' may be C{None}, a
2855
string, or a list of strings to be joined with whitespace.
2860
for (tag, data) in messages:
2862
if not isinstance(data, types.StringType):
2864
# data as list-of-strings
2865
data = " ".join(map(str, data))
2867
# No? Then use it's %s representation.
2869
m = "%s %s" % (tag, data)
2873
m = "%s%s%s" % (X_DELIM, m, X_DELIM)
2874
coded_messages.append(m)
2876
line = string.join(coded_messages, '')
2880
# Constants (from RFC 2812)
2882
RPL_YOURHOST = '002'
2885
RPL_ISUPPORT = '005'
2887
RPL_USERHOST = '302'
2892
RPL_WHOISUSER = '311'
2893
RPL_WHOISSERVER = '312'
2894
RPL_WHOISOPERATOR = '313'
2895
RPL_WHOISIDLE = '317'
2896
RPL_ENDOFWHOIS = '318'
2897
RPL_WHOISCHANNELS = '319'
2898
RPL_WHOWASUSER = '314'
2899
RPL_ENDOFWHOWAS = '369'
2900
RPL_LISTSTART = '321'
2903
RPL_UNIQOPIS = '325'
2904
RPL_CHANNELMODEIS = '324'
2907
RPL_INVITING = '341'
2908
RPL_SUMMONING = '342'
2909
RPL_INVITELIST = '346'
2910
RPL_ENDOFINVITELIST = '347'
2911
RPL_EXCEPTLIST = '348'
2912
RPL_ENDOFEXCEPTLIST = '349'
2914
RPL_WHOREPLY = '352'
2915
RPL_ENDOFWHO = '315'
2916
RPL_NAMREPLY = '353'
2917
RPL_ENDOFNAMES = '366'
2919
RPL_ENDOFLINKS = '365'
2921
RPL_ENDOFBANLIST = '368'
2923
RPL_ENDOFINFO = '374'
2924
RPL_MOTDSTART = '375'
2926
RPL_ENDOFMOTD = '376'
2927
RPL_YOUREOPER = '381'
2928
RPL_REHASHING = '382'
2929
RPL_YOURESERVICE = '383'
2931
RPL_USERSSTART = '392'
2933
RPL_ENDOFUSERS = '394'
2935
RPL_TRACELINK = '200'
2936
RPL_TRACECONNECTING = '201'
2937
RPL_TRACEHANDSHAKE = '202'
2938
RPL_TRACEUNKNOWN = '203'
2939
RPL_TRACEOPERATOR = '204'
2940
RPL_TRACEUSER = '205'
2941
RPL_TRACESERVER = '206'
2942
RPL_TRACESERVICE = '207'
2943
RPL_TRACENEWTYPE = '208'
2944
RPL_TRACECLASS = '209'
2945
RPL_TRACERECONNECT = '210'
2946
RPL_TRACELOG = '261'
2947
RPL_TRACEEND = '262'
2948
RPL_STATSLINKINFO = '211'
2949
RPL_STATSCOMMANDS = '212'
2950
RPL_ENDOFSTATS = '219'
2951
RPL_STATSUPTIME = '242'
2952
RPL_STATSOLINE = '243'
2954
RPL_SERVLIST = '234'
2955
RPL_SERVLISTEND = '235'
2956
RPL_LUSERCLIENT = '251'
2958
RPL_LUSERUNKNOWN = '253'
2959
RPL_LUSERCHANNELS = '254'
2962
RPL_ADMINLOC = '257'
2963
RPL_ADMINLOC = '258'
2964
RPL_ADMINEMAIL = '259'
2965
RPL_TRYAGAIN = '263'
2966
ERR_NOSUCHNICK = '401'
2967
ERR_NOSUCHSERVER = '402'
2968
ERR_NOSUCHCHANNEL = '403'
2969
ERR_CANNOTSENDTOCHAN = '404'
2970
ERR_TOOMANYCHANNELS = '405'
2971
ERR_WASNOSUCHNICK = '406'
2972
ERR_TOOMANYTARGETS = '407'
2973
ERR_NOSUCHSERVICE = '408'
2974
ERR_NOORIGIN = '409'
2975
ERR_NORECIPIENT = '411'
2976
ERR_NOTEXTTOSEND = '412'
2977
ERR_NOTOPLEVEL = '413'
2978
ERR_WILDTOPLEVEL = '414'
2980
ERR_UNKNOWNCOMMAND = '421'
2982
ERR_NOADMININFO = '423'
2983
ERR_FILEERROR = '424'
2984
ERR_NONICKNAMEGIVEN = '431'
2985
ERR_ERRONEUSNICKNAME = '432'
2986
ERR_NICKNAMEINUSE = '433'
2987
ERR_NICKCOLLISION = '436'
2988
ERR_UNAVAILRESOURCE = '437'
2989
ERR_USERNOTINCHANNEL = '441'
2990
ERR_NOTONCHANNEL = '442'
2991
ERR_USERONCHANNEL = '443'
2993
ERR_SUMMONDISABLED = '445'
2994
ERR_USERSDISABLED = '446'
2995
ERR_NOTREGISTERED = '451'
2996
ERR_NEEDMOREPARAMS = '461'
2997
ERR_ALREADYREGISTRED = '462'
2998
ERR_NOPERMFORHOST = '463'
2999
ERR_PASSWDMISMATCH = '464'
3000
ERR_YOUREBANNEDCREEP = '465'
3001
ERR_YOUWILLBEBANNED = '466'
3003
ERR_CHANNELISFULL = '471'
3004
ERR_UNKNOWNMODE = '472'
3005
ERR_INVITEONLYCHAN = '473'
3006
ERR_BANNEDFROMCHAN = '474'
3007
ERR_BADCHANNELKEY = '475'
3008
ERR_BADCHANMASK = '476'
3009
ERR_NOCHANMODES = '477'
3010
ERR_BANLISTFULL = '478'
3011
ERR_NOPRIVILEGES = '481'
3012
ERR_CHANOPRIVSNEEDED = '482'
3013
ERR_CANTKILLSERVER = '483'
3014
ERR_RESTRICTED = '484'
3015
ERR_UNIQOPPRIVSNEEDED = '485'
3016
ERR_NOOPERHOST = '491'
3017
ERR_NOSERVICEHOST = '492'
3018
ERR_UMODEUNKNOWNFLAG = '501'
3019
ERR_USERSDONTMATCH = '502'
3021
# And hey, as long as the strings are already intern'd...
3022
symbolic_to_numeric = {
3023
"RPL_WELCOME": '001',
3024
"RPL_YOURHOST": '002',
3025
"RPL_CREATED": '003',
3026
"RPL_MYINFO": '004',
3027
"RPL_ISUPPORT": '005',
3028
"RPL_BOUNCE": '010',
3029
"RPL_USERHOST": '302',
3032
"RPL_UNAWAY": '305',
3033
"RPL_NOWAWAY": '306',
3034
"RPL_WHOISUSER": '311',
3035
"RPL_WHOISSERVER": '312',
3036
"RPL_WHOISOPERATOR": '313',
3037
"RPL_WHOISIDLE": '317',
3038
"RPL_ENDOFWHOIS": '318',
3039
"RPL_WHOISCHANNELS": '319',
3040
"RPL_WHOWASUSER": '314',
3041
"RPL_ENDOFWHOWAS": '369',
3042
"RPL_LISTSTART": '321',
3044
"RPL_LISTEND": '323',
3045
"RPL_UNIQOPIS": '325',
3046
"RPL_CHANNELMODEIS": '324',
3047
"RPL_NOTOPIC": '331',
3049
"RPL_INVITING": '341',
3050
"RPL_SUMMONING": '342',
3051
"RPL_INVITELIST": '346',
3052
"RPL_ENDOFINVITELIST": '347',
3053
"RPL_EXCEPTLIST": '348',
3054
"RPL_ENDOFEXCEPTLIST": '349',
3055
"RPL_VERSION": '351',
3056
"RPL_WHOREPLY": '352',
3057
"RPL_ENDOFWHO": '315',
3058
"RPL_NAMREPLY": '353',
3059
"RPL_ENDOFNAMES": '366',
3061
"RPL_ENDOFLINKS": '365',
3062
"RPL_BANLIST": '367',
3063
"RPL_ENDOFBANLIST": '368',
3065
"RPL_ENDOFINFO": '374',
3066
"RPL_MOTDSTART": '375',
3068
"RPL_ENDOFMOTD": '376',
3069
"RPL_YOUREOPER": '381',
3070
"RPL_REHASHING": '382',
3071
"RPL_YOURESERVICE": '383',
3073
"RPL_USERSSTART": '392',
3075
"RPL_ENDOFUSERS": '394',
3076
"RPL_NOUSERS": '395',
3077
"RPL_TRACELINK": '200',
3078
"RPL_TRACECONNECTING": '201',
3079
"RPL_TRACEHANDSHAKE": '202',
3080
"RPL_TRACEUNKNOWN": '203',
3081
"RPL_TRACEOPERATOR": '204',
3082
"RPL_TRACEUSER": '205',
3083
"RPL_TRACESERVER": '206',
3084
"RPL_TRACESERVICE": '207',
3085
"RPL_TRACENEWTYPE": '208',
3086
"RPL_TRACECLASS": '209',
3087
"RPL_TRACERECONNECT": '210',
3088
"RPL_TRACELOG": '261',
3089
"RPL_TRACEEND": '262',
3090
"RPL_STATSLINKINFO": '211',
3091
"RPL_STATSCOMMANDS": '212',
3092
"RPL_ENDOFSTATS": '219',
3093
"RPL_STATSUPTIME": '242',
3094
"RPL_STATSOLINE": '243',
3095
"RPL_UMODEIS": '221',
3096
"RPL_SERVLIST": '234',
3097
"RPL_SERVLISTEND": '235',
3098
"RPL_LUSERCLIENT": '251',
3099
"RPL_LUSEROP": '252',
3100
"RPL_LUSERUNKNOWN": '253',
3101
"RPL_LUSERCHANNELS": '254',
3102
"RPL_LUSERME": '255',
3103
"RPL_ADMINME": '256',
3104
"RPL_ADMINLOC": '257',
3105
"RPL_ADMINLOC": '258',
3106
"RPL_ADMINEMAIL": '259',
3107
"RPL_TRYAGAIN": '263',
3108
"ERR_NOSUCHNICK": '401',
3109
"ERR_NOSUCHSERVER": '402',
3110
"ERR_NOSUCHCHANNEL": '403',
3111
"ERR_CANNOTSENDTOCHAN": '404',
3112
"ERR_TOOMANYCHANNELS": '405',
3113
"ERR_WASNOSUCHNICK": '406',
3114
"ERR_TOOMANYTARGETS": '407',
3115
"ERR_NOSUCHSERVICE": '408',
3116
"ERR_NOORIGIN": '409',
3117
"ERR_NORECIPIENT": '411',
3118
"ERR_NOTEXTTOSEND": '412',
3119
"ERR_NOTOPLEVEL": '413',
3120
"ERR_WILDTOPLEVEL": '414',
3121
"ERR_BADMASK": '415',
3122
"ERR_UNKNOWNCOMMAND": '421',
3123
"ERR_NOMOTD": '422',
3124
"ERR_NOADMININFO": '423',
3125
"ERR_FILEERROR": '424',
3126
"ERR_NONICKNAMEGIVEN": '431',
3127
"ERR_ERRONEUSNICKNAME": '432',
3128
"ERR_NICKNAMEINUSE": '433',
3129
"ERR_NICKCOLLISION": '436',
3130
"ERR_UNAVAILRESOURCE": '437',
3131
"ERR_USERNOTINCHANNEL": '441',
3132
"ERR_NOTONCHANNEL": '442',
3133
"ERR_USERONCHANNEL": '443',
3134
"ERR_NOLOGIN": '444',
3135
"ERR_SUMMONDISABLED": '445',
3136
"ERR_USERSDISABLED": '446',
3137
"ERR_NOTREGISTERED": '451',
3138
"ERR_NEEDMOREPARAMS": '461',
3139
"ERR_ALREADYREGISTRED": '462',
3140
"ERR_NOPERMFORHOST": '463',
3141
"ERR_PASSWDMISMATCH": '464',
3142
"ERR_YOUREBANNEDCREEP": '465',
3143
"ERR_YOUWILLBEBANNED": '466',
3144
"ERR_KEYSET": '467',
3145
"ERR_CHANNELISFULL": '471',
3146
"ERR_UNKNOWNMODE": '472',
3147
"ERR_INVITEONLYCHAN": '473',
3148
"ERR_BANNEDFROMCHAN": '474',
3149
"ERR_BADCHANNELKEY": '475',
3150
"ERR_BADCHANMASK": '476',
3151
"ERR_NOCHANMODES": '477',
3152
"ERR_BANLISTFULL": '478',
3153
"ERR_NOPRIVILEGES": '481',
3154
"ERR_CHANOPRIVSNEEDED": '482',
3155
"ERR_CANTKILLSERVER": '483',
3156
"ERR_RESTRICTED": '484',
3157
"ERR_UNIQOPPRIVSNEEDED": '485',
3158
"ERR_NOOPERHOST": '491',
3159
"ERR_NOSERVICEHOST": '492',
3160
"ERR_UMODEUNKNOWNFLAG": '501',
3161
"ERR_USERSDONTMATCH": '502',
3164
numeric_to_symbolic = {}
3165
for k, v in symbolic_to_numeric.items():
3166
numeric_to_symbolic[v] = k