1# -*- test-case-name: twisted.words.test.test_irc -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Internet Relay Chat protocol for client and server.
7
8Future Plans
9============
10
11The way the IRCClient class works here encourages people to implement
12IRC clients by subclassing the ephemeral protocol class, and it tends
13to end up with way more state than it should for an object which will
14be destroyed as soon as the TCP transport drops.  Someone oughta do
15something about that, ya know?
16
17The DCC support needs to have more hooks for the client for it to be
18able to ask the user things like "Do you want to accept this session?"
19and "Transfer #2 is 67% done." and otherwise manage the DCC sessions.
20
21Test coverage needs to be better.
22
23@var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC
24    2812 section 2.3.
25
26@var attributes: Singleton instance of L{_CharacterAttributes}, used for
27    constructing formatted text information.
28
29@author: Kevin Turner
30
31@see: RFC 1459: Internet Relay Chat Protocol
32@see: RFC 2812: Internet Relay Chat: Client Protocol
33@see: U{The Client-To-Client-Protocol
34<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
35"""
36
37import errno
38import operator
39import os
40import random
41import re
42import shlex
43import socket
44import stat
45import string
46import struct
47import sys
48import textwrap
49import time
50import traceback
51from functools import reduce
52from os import path
53from typing import Optional
54
55from twisted.internet import protocol, reactor, task
56from twisted.persisted import styles
57from twisted.protocols import basic
58from twisted.python import _textattributes, log, reflect
59
60NUL = chr(0)
61CR = chr(0o15)
62NL = chr(0o12)
63LF = NL
64SPC = chr(0o40)
65
66# This includes the CRLF terminator characters.
67MAX_COMMAND_LENGTH = 512
68
69CHANNEL_PREFIXES = "&#!+"
70
71
72class IRCBadMessage(Exception):
73    pass
74
75
76class IRCPasswordMismatch(Exception):
77    pass
78
79
80class IRCBadModes(ValueError):
81    """
82    A malformed mode was encountered while attempting to parse a mode string.
83    """
84
85
86def parsemsg(s):
87    """
88    Breaks a message from an IRC server into its prefix, command, and
89    arguments.
90
91    @param s: The message to break.
92    @type s: L{bytes}
93
94    @return: A tuple of (prefix, command, args).
95    @rtype: L{tuple}
96    """
97    prefix = ""
98    trailing = []
99    if not s:
100        raise IRCBadMessage("Empty line.")
101    if s[0:1] == ":":
102        prefix, s = s[1:].split(" ", 1)
103    if s.find(" :") != -1:
104        s, trailing = s.split(" :", 1)
105        args = s.split()
106        args.append(trailing)
107    else:
108        args = s.split()
109    command = args.pop(0)
110    return prefix, command, args
111
112
113def split(str, length=80):
114    """
115    Split a string into multiple lines.
116
117    Whitespace near C{str[length]} will be preferred as a breaking point.
118    C{"\\n"} will also be used as a breaking point.
119
120    @param str: The string to split.
121    @type str: C{str}
122
123    @param length: The maximum length which will be allowed for any string in
124        the result.
125    @type length: C{int}
126
127    @return: C{list} of C{str}
128    """
129    return [chunk for line in str.split("\n") for chunk in textwrap.wrap(line, length)]
130
131
132def _intOrDefault(value, default=None):
133    """
134    Convert a value to an integer if possible.
135
136    @rtype: C{int} or type of L{default}
137    @return: An integer when C{value} can be converted to an integer,
138        otherwise return C{default}
139    """
140    if value:
141        try:
142            return int(value)
143        except (TypeError, ValueError):
144            pass
145    return default
146
147
148class UnhandledCommand(RuntimeError):
149    """
150    A command dispatcher could not locate an appropriate command handler.
151    """
152
153
154class _CommandDispatcherMixin:
155    """
156    Dispatch commands to handlers based on their name.
157
158    Command handler names should be of the form C{prefix_commandName},
159    where C{prefix} is the value specified by L{prefix}, and must
160    accept the parameters as given to L{dispatch}.
161
162    Attempting to mix this in more than once for a single class will cause
163    strange behaviour, due to L{prefix} being overwritten.
164
165    @type prefix: C{str}
166    @ivar prefix: Command handler prefix, used to locate handler attributes
167    """
168
169    prefix: Optional[str] = None
170
171    def dispatch(self, commandName, *args):
172        """
173        Perform actual command dispatch.
174        """
175
176        def _getMethodName(command):
177            return f"{self.prefix}_{command}"
178
179        def _getMethod(name):
180            return getattr(self, _getMethodName(name), None)
181
182        method = _getMethod(commandName)
183        if method is not None:
184            return method(*args)
185
186        method = _getMethod("unknown")
187        if method is None:
188            raise UnhandledCommand(
189                f"No handler for {_getMethodName(commandName)!r} could be found"
190            )
191        return method(commandName, *args)
192
193
194def parseModes(modes, params, paramModes=("", "")):
195    """
196    Parse an IRC mode string.
197
198    The mode string is parsed into two lists of mode changes (added and
199    removed), with each mode change represented as C{(mode, param)} where mode
200    is the mode character, and param is the parameter passed for that mode, or
201    L{None} if no parameter is required.
202
203    @type modes: C{str}
204    @param modes: Modes string to parse.
205
206    @type params: C{list}
207    @param params: Parameters specified along with L{modes}.
208
209    @type paramModes: C{(str, str)}
210    @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take
211        parameters when added or removed.
212
213    @returns: Two lists of mode changes, one for modes added and the other for
214        modes removed respectively, mode changes in each list are represented as
215        C{(mode, param)}.
216    """
217    if len(modes) == 0:
218        raise IRCBadModes("Empty mode string")
219
220    if modes[0] not in "+-":
221        raise IRCBadModes(f"Malformed modes string: {modes!r}")
222
223    changes = ([], [])
224
225    direction = None
226    count = -1
227    for ch in modes:
228        if ch in "+-":
229            if count == 0:
230                raise IRCBadModes(f"Empty mode sequence: {modes!r}")
231            direction = "+-".index(ch)
232            count = 0
233        else:
234            param = None
235            if ch in paramModes[direction]:
236                try:
237                    param = params.pop(0)
238                except IndexError:
239                    raise IRCBadModes(f"Not enough parameters: {ch!r}")
240            changes[direction].append((ch, param))
241            count += 1
242
243    if len(params) > 0:
244        raise IRCBadModes(f"Too many parameters: {modes!r} {params!r}")
245
246    if count == 0:
247        raise IRCBadModes(f"Empty mode sequence: {modes!r}")
248
249    return changes
250
251
252class IRC(protocol.Protocol):
253    """
254    Internet Relay Chat server protocol.
255    """
256
257    buffer = ""
258    hostname = None
259
260    encoding: Optional[str] = None
261
262    def connectionMade(self):
263        self.channels = []
264        if self.hostname is None:
265            self.hostname = socket.getfqdn()
266
267    def sendLine(self, line):
268        line = line + CR + LF
269        if isinstance(line, str):
270            useEncoding = self.encoding if self.encoding else "utf-8"
271            line = line.encode(useEncoding)
272        self.transport.write(line)
273
274    def sendMessage(self, command, *parameter_list, **prefix):
275        """
276        Send a line formatted as an IRC message.
277
278        First argument is the command, all subsequent arguments are parameters
279        to that command.  If a prefix is desired, it may be specified with the
280        keyword argument 'prefix'.
281
282        The L{sendCommand} method is generally preferred over this one.
283        Notably, this method does not support sending message tags, while the
284        L{sendCommand} method does.
285        """
286        if not command:
287            raise ValueError("IRC message requires a command.")
288
289        if " " in command or command[0] == ":":
290            # Not the ONLY way to screw up, but provides a little
291            # sanity checking to catch likely dumb mistakes.
292            raise ValueError(
293                "Somebody screwed up, 'cuz this doesn't"
294                " look like a command to me: %s" % command
295            )
296
297        line = " ".join([command] + list(parameter_list))
298        if "prefix" in prefix:
299            line = ":{} {}".format(prefix["prefix"], line)
300        self.sendLine(line)
301
302        if len(parameter_list) > 15:
303            log.msg(
304                "Message has %d parameters (RFC allows 15):\n%s"
305                % (len(parameter_list), line)
306            )
307
308    def sendCommand(self, command, parameters, prefix=None, tags=None):
309        """
310        Send to the remote peer a line formatted as an IRC message.
311
312        @param command: The command or numeric to send.
313        @type command: L{unicode}
314
315        @param parameters: The parameters to send with the command.
316        @type parameters: A L{tuple} or L{list} of L{unicode} parameters
317
318        @param prefix: The prefix to send with the command.  If not
319            given, no prefix is sent.
320        @type prefix: L{unicode}
321
322        @param tags: A dict of message tags.  If not given, no message
323            tags are sent.  The dict key should be the name of the tag
324            to send as a string; the value should be the unescaped value
325            to send with the tag, or either None or "" if no value is to
326            be sent with the tag.
327        @type tags: L{dict} of tags (L{unicode}) => values (L{unicode})
328        @see: U{https://ircv3.net/specs/core/message-tags-3.2.html}
329        """
330        if not command:
331            raise ValueError("IRC message requires a command.")
332
333        if " " in command or command[0] == ":":
334            # Not the ONLY way to screw up, but provides a little
335            # sanity checking to catch likely dumb mistakes.
336            raise ValueError(f'Invalid command: "{command}"')
337
338        if tags is None:
339            tags = {}
340
341        line = " ".join([command] + list(parameters))
342        if prefix:
343            line = f":{prefix} {line}"
344        if tags:
345            tagStr = self._stringTags(tags)
346            line = f"@{tagStr} {line}"
347        self.sendLine(line)
348
349        if len(parameters) > 15:
350            log.msg(
351                "Message has %d parameters (RFC allows 15):\n%s"
352                % (len(parameters), line)
353            )
354
355    def _stringTags(self, tags):
356        """
357        Converts a tag dictionary to a string.
358
359        @param tags: The tag dict passed to sendMsg.
360
361        @rtype: L{unicode}
362        @return: IRCv3-format tag string
363        """
364        self._validateTags(tags)
365        tagStrings = []
366        for tag, value in tags.items():
367            if value:
368                tagStrings.append(f"{tag}={self._escapeTagValue(value)}")
369            else:
370                tagStrings.append(tag)
371        return ";".join(tagStrings)
372
373    def _validateTags(self, tags):
374        """
375        Checks the tag dict for errors and raises L{ValueError} if an
376        error is found.
377
378        @param tags: The tag dict passed to sendMsg.
379        """
380        for tag, value in tags.items():
381            if not tag:
382                raise ValueError("A tag name is required.")
383            for char in tag:
384                if not char.isalnum() and char not in ("-", "/", "."):
385                    raise ValueError("Tag contains invalid characters.")
386
387    def _escapeTagValue(self, value):
388        """
389        Escape the given tag value according to U{escaping rules in IRCv3
390        <https://ircv3.net/specs/core/message-tags-3.2.html>}.
391
392        @param value: The string value to escape.
393        @type value: L{str}
394
395        @return: The escaped string for sending as a message value
396        @rtype: L{str}
397        """
398        return (
399            value.replace("\\", "\\\\")
400            .replace(";", "\\:")
401            .replace(" ", "\\s")
402            .replace("\r", "\\r")
403            .replace("\n", "\\n")
404        )
405
406    def dataReceived(self, data):
407        """
408        This hack is to support mIRC, which sends LF only, even though the RFC
409        says CRLF.  (Also, the flexibility of LineReceiver to turn "line mode"
410        on and off was not required.)
411        """
412        if isinstance(data, bytes):
413            data = data.decode("utf-8")
414        lines = (self.buffer + data).split(LF)
415        # Put the (possibly empty) element after the last LF back in the
416        # buffer
417        self.buffer = lines.pop()
418
419        for line in lines:
420            if len(line) <= 2:
421                # This is a blank line, at best.
422                continue
423            if line[-1] == CR:
424                line = line[:-1]
425            prefix, command, params = parsemsg(line)
426            # mIRC is a big pile of doo-doo
427            command = command.upper()
428            # DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
429
430            self.handleCommand(command, prefix, params)
431
432    def handleCommand(self, command, prefix, params):
433        """
434        Determine the function to call for the given command and call it with
435        the given arguments.
436
437        @param command: The IRC command to determine the function for.
438        @type command: L{bytes}
439
440        @param prefix: The prefix of the IRC message (as returned by
441            L{parsemsg}).
442        @type prefix: L{bytes}
443
444        @param params: A list of parameters to call the function with.
445        @type params: L{list}
446        """
447        method = getattr(self, "irc_%s" % command, None)
448        try:
449            if method is not None:
450                method(prefix, params)
451            else:
452                self.irc_unknown(prefix, command, params)
453        except BaseException:
454            log.deferr()
455
456    def irc_unknown(self, prefix, command, params):
457        """
458        Called by L{handleCommand} on a command that doesn't have a defined
459        handler. Subclasses should override this method.
460        """
461        raise NotImplementedError(command, prefix, params)
462
463    # Helper methods
464    def privmsg(self, sender, recip, message):
465        """
466        Send a message to a channel or user
467
468        @type sender: C{str} or C{unicode}
469        @param sender: Who is sending this message.  Should be of the form
470            username!ident@hostmask (unless you know better!).
471
472        @type recip: C{str} or C{unicode}
473        @param recip: The recipient of this message.  If a channel, it must
474            start with a channel prefix.
475
476        @type message: C{str} or C{unicode}
477        @param message: The message being sent.
478        """
479        self.sendCommand("PRIVMSG", (recip, f":{lowQuote(message)}"), sender)
480
481    def notice(self, sender, recip, message):
482        """
483        Send a "notice" to a channel or user.
484
485        Notices differ from privmsgs in that the RFC claims they are different.
486        Robots are supposed to send notices and not respond to them.  Clients
487        typically display notices differently from privmsgs.
488
489        @type sender: C{str} or C{unicode}
490        @param sender: Who is sending this message.  Should be of the form
491            username!ident@hostmask (unless you know better!).
492
493        @type recip: C{str} or C{unicode}
494        @param recip: The recipient of this message.  If a channel, it must
495            start with a channel prefix.
496
497        @type message: C{str} or C{unicode}
498        @param message: The message being sent.
499        """
500        self.sendCommand("NOTICE", (recip, f":{message}"), sender)
501
502    def action(self, sender, recip, message):
503        """
504        Send an action to a channel or user.
505
506        @type sender: C{str} or C{unicode}
507        @param sender: Who is sending this message.  Should be of the form
508            username!ident@hostmask (unless you know better!).
509
510        @type recip: C{str} or C{unicode}
511        @param recip: The recipient of this message.  If a channel, it must
512            start with a channel prefix.
513
514        @type message: C{str} or C{unicode}
515        @param message: The action being sent.
516        """
517        self.sendLine(f":{sender} ACTION {recip} :{message}")
518
519    def topic(self, user, channel, topic, author=None):
520        """
521        Send the topic to a user.
522
523        @type user: C{str} or C{unicode}
524        @param user: The user receiving the topic.  Only their nickname, not
525            the full hostmask.
526
527        @type channel: C{str} or C{unicode}
528        @param channel: The channel for which this is the topic.
529
530        @type topic: C{str} or C{unicode} or L{None}
531        @param topic: The topic string, unquoted, or None if there is no topic.
532
533        @type author: C{str} or C{unicode}
534        @param author: If the topic is being changed, the full username and
535            hostmask of the person changing it.
536        """
537        if author is None:
538            if topic is None:
539                self.sendLine(
540                    ":%s %s %s %s :%s"
541                    % (self.hostname, RPL_NOTOPIC, user, channel, "No topic is set.")
542                )
543            else:
544                self.sendLine(
545                    ":%s %s %s %s :%s"
546                    % (self.hostname, RPL_TOPIC, user, channel, lowQuote(topic))
547                )
548        else:
549            self.sendLine(f":{author} TOPIC {channel} :{lowQuote(topic)}")
550
551    def topicAuthor(self, user, channel, author, date):
552        """
553        Send the author of and time at which a topic was set for the given
554        channel.
555
556        This sends a 333 reply message, which is not part of the IRC RFC.
557
558        @type user: C{str} or C{unicode}
559        @param user: The user receiving the topic.  Only their nickname, not
560            the full hostmask.
561
562        @type channel: C{str} or C{unicode}
563        @param channel: The channel for which this information is relevant.
564
565        @type author: C{str} or C{unicode}
566        @param author: The nickname (without hostmask) of the user who last set
567            the topic.
568
569        @type date: C{int}
570        @param date: A POSIX timestamp (number of seconds since the epoch) at
571            which the topic was last set.
572        """
573        self.sendLine(
574            ":%s %d %s %s %s %d" % (self.hostname, 333, user, channel, author, date)
575        )
576
577    def names(self, user, channel, names):
578        """
579        Send the names of a channel's participants to a user.
580
581        @type user: C{str} or C{unicode}
582        @param user: The user receiving the name list.  Only their nickname,
583            not the full hostmask.
584
585        @type channel: C{str} or C{unicode}
586        @param channel: The channel for which this is the namelist.
587
588        @type names: C{list} of C{str} or C{unicode}
589        @param names: The names to send.
590        """
591        # XXX If unicode is given, these limits are not quite correct
592        prefixLength = len(channel) + len(user) + 10
593        namesLength = 512 - prefixLength
594
595        L = []
596        count = 0
597        for n in names:
598            if count + len(n) + 1 > namesLength:
599                self.sendLine(
600                    ":%s %s %s = %s :%s"
601                    % (self.hostname, RPL_NAMREPLY, user, channel, " ".join(L))
602                )
603                L = [n]
604                count = len(n)
605            else:
606                L.append(n)
607                count += len(n) + 1
608        if L:
609            self.sendLine(
610                ":%s %s %s = %s :%s"
611                % (self.hostname, RPL_NAMREPLY, user, channel, " ".join(L))
612            )
613        self.sendLine(
614            ":%s %s %s %s :End of /NAMES list"
615            % (self.hostname, RPL_ENDOFNAMES, user, channel)
616        )
617
618    def who(self, user, channel, memberInfo):
619        """
620        Send a list of users participating in a channel.
621
622        @type user: C{str} or C{unicode}
623        @param user: The user receiving this member information.  Only their
624            nickname, not the full hostmask.
625
626        @type channel: C{str} or C{unicode}
627        @param channel: The channel for which this is the member information.
628
629        @type memberInfo: C{list} of C{tuples}
630        @param memberInfo: For each member of the given channel, a 7-tuple
631            containing their username, their hostmask, the server to which they
632            are connected, their nickname, the letter "H" or "G" (standing for
633            "Here" or "Gone"), the hopcount from C{user} to this member, and
634            this member's real name.
635        """
636        for info in memberInfo:
637            (username, hostmask, server, nickname, flag, hops, realName) = info
638            assert flag in ("H", "G")
639            self.sendLine(
640                ":%s %s %s %s %s %s %s %s %s :%d %s"
641                % (
642                    self.hostname,
643                    RPL_WHOREPLY,
644                    user,
645                    channel,
646                    username,
647                    hostmask,
648                    server,
649                    nickname,
650                    flag,
651                    hops,
652                    realName,
653                )
654            )
655
656        self.sendLine(
657            ":%s %s %s %s :End of /WHO list."
658            % (self.hostname, RPL_ENDOFWHO, user, channel)
659        )
660
661    def whois(
662        self,
663        user,
664        nick,
665        username,
666        hostname,
667        realName,
668        server,
669        serverInfo,
670        oper,
671        idle,
672        signOn,
673        channels,
674    ):
675        """
676        Send information about the state of a particular user.
677
678        @type user: C{str} or C{unicode}
679        @param user: The user receiving this information.  Only their nickname,
680            not the full hostmask.
681
682        @type nick: C{str} or C{unicode}
683        @param nick: The nickname of the user this information describes.
684
685        @type username: C{str} or C{unicode}
686        @param username: The user's username (eg, ident response)
687
688        @type hostname: C{str}
689        @param hostname: The user's hostmask
690
691        @type realName: C{str} or C{unicode}
692        @param realName: The user's real name
693
694        @type server: C{str} or C{unicode}
695        @param server: The name of the server to which the user is connected
696
697        @type serverInfo: C{str} or C{unicode}
698        @param serverInfo: A descriptive string about that server
699
700        @type oper: C{bool}
701        @param oper: Indicates whether the user is an IRC operator
702
703        @type idle: C{int}
704        @param idle: The number of seconds since the user last sent a message
705
706        @type signOn: C{int}
707        @param signOn: A POSIX timestamp (number of seconds since the epoch)
708            indicating the time the user signed on
709
710        @type channels: C{list} of C{str} or C{unicode}
711        @param channels: A list of the channels which the user is participating in
712        """
713        self.sendLine(
714            ":%s %s %s %s %s %s * :%s"
715            % (self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName)
716        )
717        self.sendLine(
718            ":%s %s %s %s %s :%s"
719            % (self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo)
720        )
721        if oper:
722            self.sendLine(
723                ":%s %s %s %s :is an IRC operator"
724                % (self.hostname, RPL_WHOISOPERATOR, user, nick)
725            )
726        self.sendLine(
727            ":%s %s %s %s %d %d :seconds idle, signon time"
728            % (self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn)
729        )
730        self.sendLine(
731            ":%s %s %s %s :%s"
732            % (self.hostname, RPL_WHOISCHANNELS, user, nick, " ".join(channels))
733        )
734        self.sendLine(
735            ":%s %s %s %s :End of WHOIS list."
736            % (self.hostname, RPL_ENDOFWHOIS, user, nick)
737        )
738
739    def join(self, who, where):
740        """
741        Send a join message.
742
743        @type who: C{str} or C{unicode}
744        @param who: The name of the user joining.  Should be of the form
745            username!ident@hostmask (unless you know better!).
746
747        @type where: C{str} or C{unicode}
748        @param where: The channel the user is joining.
749        """
750        self.sendLine(f":{who} JOIN {where}")
751
752    def part(self, who, where, reason=None):
753        """
754        Send a part message.
755
756        @type who: C{str} or C{unicode}
757        @param who: The name of the user joining.  Should be of the form
758            username!ident@hostmask (unless you know better!).
759
760        @type where: C{str} or C{unicode}
761        @param where: The channel the user is joining.
762
763        @type reason: C{str} or C{unicode}
764        @param reason: A string describing the misery which caused this poor
765            soul to depart.
766        """
767        if reason:
768            self.sendLine(f":{who} PART {where} :{reason}")
769        else:
770            self.sendLine(f":{who} PART {where}")
771
772    def channelMode(self, user, channel, mode, *args):
773        """
774        Send information about the mode of a channel.
775
776        @type user: C{str} or C{unicode}
777        @param user: The user receiving the name list.  Only their nickname,
778            not the full hostmask.
779
780        @type channel: C{str} or C{unicode}
781        @param channel: The channel for which this is the namelist.
782
783        @type mode: C{str}
784        @param mode: A string describing this channel's modes.
785
786        @param args: Any additional arguments required by the modes.
787        """
788        self.sendLine(
789            ":%s %s %s %s %s %s"
790            % (self.hostname, RPL_CHANNELMODEIS, user, channel, mode, " ".join(args))
791        )
792
793
794class ServerSupportedFeatures(_CommandDispatcherMixin):
795    """
796    Handle ISUPPORT messages.
797
798    Feature names match those in the ISUPPORT RFC draft identically.
799
800    Information regarding the specifics of ISUPPORT was gleaned from
801    <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>.
802    """
803
804    prefix = "isupport"
805
806    def __init__(self):
807        self._features = {
808            "CHANNELLEN": 200,
809            "CHANTYPES": tuple("#&"),
810            "MODES": 3,
811            "NICKLEN": 9,
812            "PREFIX": self._parsePrefixParam("(ovh)@+%"),
813            # The ISUPPORT draft explicitly says that there is no default for
814            # CHANMODES, but we're defaulting it here to handle the case where
815            # the IRC server doesn't send us any ISUPPORT information, since
816            # IRCClient.getChannelModeParams relies on this value.
817            "CHANMODES": self._parseChanModesParam(["b", "", "lk", ""]),
818        }
819
820    @classmethod
821    def _splitParamArgs(cls, params, valueProcessor=None):
822        """
823        Split ISUPPORT parameter arguments.
824
825        Values can optionally be processed by C{valueProcessor}.
826
827        For example::
828
829            >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2'])
830            (('A', '1'), ('B', '2'))
831
832        @type params: C{iterable} of C{str}
833
834        @type valueProcessor: C{callable} taking {str}
835        @param valueProcessor: Callable to process argument values, or L{None}
836            to perform no processing
837
838        @rtype: C{list} of C{(str, object)}
839        @return: Sequence of C{(name, processedValue)}
840        """
841        if valueProcessor is None:
842            valueProcessor = lambda x: x
843
844        def _parse():
845            for param in params:
846                if ":" not in param:
847                    param += ":"
848                a, b = param.split(":", 1)
849                yield a, valueProcessor(b)
850
851        return list(_parse())
852
853    @classmethod
854    def _unescapeParamValue(cls, value):
855        """
856        Unescape an ISUPPORT parameter.
857
858        The only form of supported escape is C{\\xHH}, where HH must be a valid
859        2-digit hexadecimal number.
860
861        @rtype: C{str}
862        """
863
864        def _unescape():
865            parts = value.split("\\x")
866            # The first part can never be preceded by the escape.
867            yield parts.pop(0)
868            for s in parts:
869                octet, rest = s[:2], s[2:]
870                try:
871                    octet = int(octet, 16)
872                except ValueError:
873                    raise ValueError(f"Invalid hex octet: {octet!r}")
874                yield chr(octet) + rest
875
876        if "\\x" not in value:
877            return value
878        return "".join(_unescape())
879
880    @classmethod
881    def _splitParam(cls, param):
882        """
883        Split an ISUPPORT parameter.
884
885        @type param: C{str}
886
887        @rtype: C{(str, list)}
888        @return: C{(key, arguments)}
889        """
890        if "=" not in param:
891            param += "="
892        key, value = param.split("=", 1)
893        return key, [cls._unescapeParamValue(v) for v in value.split(",")]
894
895    @classmethod
896    def _parsePrefixParam(cls, prefix):
897        """
898        Parse the ISUPPORT "PREFIX" parameter.
899
900        The order in which the parameter arguments appear is significant, the
901        earlier a mode appears the more privileges it gives.
902
903        @rtype: C{dict} mapping C{str} to C{(str, int)}
904        @return: A dictionary mapping a mode character to a two-tuple of
905            C({symbol, priority)}, the lower a priority (the lowest being
906            C{0}) the more privileges it gives
907        """
908        if not prefix:
909            return None
910        if prefix[0] != "(" and ")" not in prefix:
911            raise ValueError("Malformed PREFIX parameter")
912        modes, symbols = prefix.split(")", 1)
913        symbols = zip(symbols, range(len(symbols)))
914        modes = modes[1:]
915        return dict(zip(modes, symbols))
916
917    @classmethod
918    def _parseChanModesParam(self, params):
919        """
920        Parse the ISUPPORT "CHANMODES" parameter.
921
922        See L{isupport_CHANMODES} for a detailed explanation of this parameter.
923        """
924        names = ("addressModes", "param", "setParam", "noParam")
925        if len(params) > len(names):
926            raise ValueError(
927                "Expecting a maximum of %d channel mode parameters, got %d"
928                % (len(names), len(params))
929            )
930        items = map(lambda key, value: (key, value or ""), names, params)
931        return dict(items)
932
933    def getFeature(self, feature, default=None):
934        """
935        Get a server supported feature's value.
936
937        A feature with the value L{None} is equivalent to the feature being
938        unsupported.
939
940        @type feature: C{str}
941        @param feature: Feature name
942
943        @type default: C{object}
944        @param default: The value to default to, assuming that C{feature}
945            is not supported
946
947        @return: Feature value
948        """
949        return self._features.get(feature, default)
950
951    def hasFeature(self, feature):
952        """
953        Determine whether a feature is supported or not.
954
955        @rtype: C{bool}
956        """
957        return self.getFeature(feature) is not None
958
959    def parse(self, params):
960        """
961        Parse ISUPPORT parameters.
962
963        If an unknown parameter is encountered, it is simply added to the
964        dictionary, keyed by its name, as a tuple of the parameters provided.
965
966        @type params: C{iterable} of C{str}
967        @param params: Iterable of ISUPPORT parameters to parse
968        """
969        for param in params:
970            key, value = self._splitParam(param)
971            if key.startswith("-"):
972                self._features.pop(key[1:], None)
973            else:
974                self._features[key] = self.dispatch(key, value)
975
976    def isupport_unknown(self, command, params):
977        """
978        Unknown ISUPPORT parameter.
979        """
980        return tuple(params)
981
982    def isupport_CHANLIMIT(self, params):
983        """
984        The maximum number of each channel type a user may join.
985        """
986        return self._splitParamArgs(params, _intOrDefault)
987
988    def isupport_CHANMODES(self, params):
989        """
990        Available channel modes.
991
992        There are 4 categories of channel mode::
993
994            addressModes - Modes that add or remove an address to or from a
995            list, these modes always take a parameter.
996
997            param - Modes that change a setting on a channel, these modes
998            always take a parameter.
999
1000            setParam - Modes that change a setting on a channel, these modes
1001            only take a parameter when being set.
1002
1003            noParam - Modes that change a setting on a channel, these modes
1004            never take a parameter.
1005        """
1006        try:
1007            return self._parseChanModesParam(params)
1008        except ValueError:
1009            return self.getFeature("CHANMODES")
1010
1011    def isupport_CHANNELLEN(self, params):
1012        """
1013        Maximum length of a channel name a client may create.
1014        """
1015        return _intOrDefault(params[0], self.getFeature("CHANNELLEN"))
1016
1017    def isupport_CHANTYPES(self, params):
1018        """
1019        Valid channel prefixes.
1020        """
1021        return tuple(params[0])
1022
1023    def isupport_EXCEPTS(self, params):
1024        """
1025        Mode character for "ban exceptions".
1026
1027        The presence of this parameter indicates that the server supports
1028        this functionality.
1029        """
1030        return params[0] or "e"
1031
1032    def isupport_IDCHAN(self, params):
1033        """
1034        Safe channel identifiers.
1035
1036        The presence of this parameter indicates that the server supports
1037        this functionality.
1038        """
1039        return self._splitParamArgs(params)
1040
1041    def isupport_INVEX(self, params):
1042        """
1043        Mode character for "invite exceptions".
1044
1045        The presence of this parameter indicates that the server supports
1046        this functionality.
1047        """
1048        return params[0] or "I"
1049
1050    def isupport_KICKLEN(self, params):
1051        """
1052        Maximum length of a kick message a client may provide.
1053        """
1054        return _intOrDefault(params[0])
1055
1056    def isupport_MAXLIST(self, params):
1057        """
1058        Maximum number of "list modes" a client may set on a channel at once.
1059
1060        List modes are identified by the "addressModes" key in CHANMODES.
1061        """
1062        return self._splitParamArgs(params, _intOrDefault)
1063
1064    def isupport_MODES(self, params):
1065        """
1066        Maximum number of modes accepting parameters that may be sent, by a
1067        client, in a single MODE command.
1068        """
1069        return _intOrDefault(params[0])
1070
1071    def isupport_NETWORK(self, params):
1072        """
1073        IRC network name.
1074        """
1075        return params[0]
1076
1077    def isupport_NICKLEN(self, params):
1078        """
1079        Maximum length of a nickname the client may use.
1080        """
1081        return _intOrDefault(params[0], self.getFeature("NICKLEN"))
1082
1083    def isupport_PREFIX(self, params):
1084        """
1085        Mapping of channel modes that clients may have to status flags.
1086        """
1087        try:
1088            return self._parsePrefixParam(params[0])
1089        except ValueError:
1090            return self.getFeature("PREFIX")
1091
1092    def isupport_SAFELIST(self, params):
1093        """
1094        Flag indicating that a client may request a LIST without being
1095        disconnected due to the large amount of data generated.
1096        """
1097        return True
1098
1099    def isupport_STATUSMSG(self, params):
1100        """
1101        The server supports sending messages to only to clients on a channel
1102        with a specific status.
1103        """
1104        return params[0]
1105
1106    def isupport_TARGMAX(self, params):
1107        """
1108        Maximum number of targets allowable for commands that accept multiple
1109        targets.
1110        """
1111        return dict(self._splitParamArgs(params, _intOrDefault))
1112
1113    def isupport_TOPICLEN(self, params):
1114        """
1115        Maximum length of a topic that may be set.
1116        """
1117        return _intOrDefault(params[0])
1118
1119
1120class IRCClient(basic.LineReceiver):
1121    """
1122    Internet Relay Chat client protocol, with sprinkles.
1123
1124    In addition to providing an interface for an IRC client protocol,
1125    this class also contains reasonable implementations of many common
1126    CTCP methods.
1127
1128    TODO
1129    ====
1130     - Limit the length of messages sent (because the IRC server probably
1131       does).
1132     - Add flood protection/rate limiting for my CTCP replies.
1133     - NickServ cooperation.  (a mix-in?)
1134
1135    @ivar nickname: Nickname the client will use.
1136    @ivar password: Password used to log on to the server.  May be L{None}.
1137    @ivar realname: Supplied to the server during login as the "Real name"
1138        or "ircname".  May be L{None}.
1139    @ivar username: Supplied to the server during login as the "User name".
1140        May be L{None}
1141
1142    @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query.  If L{None}, no
1143        USERINFO reply will be sent.
1144        "This is used to transmit a string which is settable by
1145        the user (and never should be set by the client)."
1146    @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query.  If L{None}, no
1147        FINGER reply will be sent.
1148    @type fingerReply: Callable or String
1149
1150    @ivar versionName: CTCP VERSION reply, client name.  If L{None}, no VERSION
1151        reply will be sent.
1152    @type versionName: C{str}, or None.
1153    @ivar versionNum: CTCP VERSION reply, client version.
1154    @type versionNum: C{str}, or None.
1155    @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
1156    @type versionEnv: C{str}, or None.
1157
1158    @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
1159        client may be found.  If L{None}, no SOURCE reply will be sent.
1160
1161    @ivar lineRate: Minimum delay between lines sent to the server.  If
1162        L{None}, no delay will be imposed.
1163    @type lineRate: Number of Seconds.
1164
1165    @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and
1166        I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content
1167        of an I{RPL_MOTD} message.
1168
1169    @ivar erroneousNickFallback: Default nickname assigned when an unregistered
1170        client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register
1171        with an illegal nickname.
1172    @type erroneousNickFallback: C{str}
1173
1174    @ivar _registered: Whether or not the user is registered. It becomes True
1175        once a welcome has been received from the server.
1176    @type _registered: C{bool}
1177
1178    @ivar _attemptedNick: The nickname that will try to get registered. It may
1179        change if it is illegal or already taken. L{nickname} becomes the
1180        L{_attemptedNick} that is successfully registered.
1181    @type _attemptedNick:  C{str}
1182
1183    @type supported: L{ServerSupportedFeatures}
1184    @ivar supported: Available ISUPPORT features on the server
1185
1186    @type hostname: C{str}
1187    @ivar hostname: Host name of the IRC server the client is connected to.
1188        Initially the host name is L{None} and later is set to the host name
1189        from which the I{RPL_WELCOME} message is received.
1190
1191    @type _heartbeat: L{task.LoopingCall}
1192    @ivar _heartbeat: Looping call to perform the keepalive by calling
1193        L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or
1194        L{None} if there is no heartbeat.
1195
1196    @type heartbeatInterval: C{float}
1197    @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to
1198        the server as a form of keepalive, defaults to 120 seconds. Use L{None}
1199        to disable the heartbeat.
1200    """
1201
1202    hostname = None
1203    motd = None
1204    nickname = "irc"
1205    password = None
1206    realname = None
1207    username = None
1208    ### Responses to various CTCP queries.
1209
1210    userinfo = None
1211    # fingerReply is a callable returning a string, or a str()able object.
1212    fingerReply = None
1213    versionName = None
1214    versionNum = None
1215    versionEnv = None
1216
1217    sourceURL = "http://twistedmatrix.com/downloads/"
1218
1219    dcc_destdir = "."
1220    dcc_sessions = None
1221
1222    # If this is false, no attempt will be made to identify
1223    # ourself to the server.
1224    performLogin = 1
1225
1226    lineRate = None
1227    _queue = None
1228    _queueEmptying = None
1229
1230    delimiter = b"\n"  # b'\r\n' will also work (see dataReceived)
1231
1232    __pychecker__ = "unusednames=params,prefix,channel"
1233
1234    _registered = False
1235    _attemptedNick = ""
1236    erroneousNickFallback = "defaultnick"
1237
1238    _heartbeat = None
1239    heartbeatInterval = 120
1240
1241    def _reallySendLine(self, line):
1242        quoteLine = lowQuote(line)
1243        if isinstance(quoteLine, str):
1244            quoteLine = quoteLine.encode("utf-8")
1245        quoteLine += b"\r"
1246        return basic.LineReceiver.sendLine(self, quoteLine)
1247
1248    def sendLine(self, line):
1249        if self.lineRate is None:
1250            self._reallySendLine(line)
1251        else:
1252            self._queue.append(line)
1253            if not self._queueEmptying:
1254                self._sendLine()
1255
1256    def _sendLine(self):
1257        if self._queue:
1258            self._reallySendLine(self._queue.pop(0))
1259            self._queueEmptying = reactor.callLater(self.lineRate, self._sendLine)
1260        else:
1261            self._queueEmptying = None
1262
1263    def connectionLost(self, reason):
1264        basic.LineReceiver.connectionLost(self, reason)
1265        self.stopHeartbeat()
1266
1267    def _createHeartbeat(self):
1268        """
1269        Create the heartbeat L{LoopingCall}.
1270        """
1271        return task.LoopingCall(self._sendHeartbeat)
1272
1273    def _sendHeartbeat(self):
1274        """
1275        Send a I{PING} message to the IRC server as a form of keepalive.
1276        """
1277        self.sendLine("PING " + self.hostname)
1278
1279    def stopHeartbeat(self):
1280        """
1281        Stop sending I{PING} messages to keep the connection to the server
1282        alive.
1283
1284        @since: 11.1
1285        """
1286        if self._heartbeat is not None:
1287            self._heartbeat.stop()
1288            self._heartbeat = None
1289
1290    def startHeartbeat(self):
1291        """
1292        Start sending I{PING} messages every L{IRCClient.heartbeatInterval}
1293        seconds to keep the connection to the server alive during periods of no
1294        activity.
1295
1296        @since: 11.1
1297        """
1298        self.stopHeartbeat()
1299        if self.heartbeatInterval is None:
1300            return
1301        self._heartbeat = self._createHeartbeat()
1302        self._heartbeat.start(self.heartbeatInterval, now=False)
1303
1304    ### Interface level client->user output methods
1305    ###
1306    ### You'll want to override these.
1307
1308    ### Methods relating to the server itself
1309
1310    def created(self, when):
1311        """
1312        Called with creation date information about the server, usually at logon.
1313
1314        @type when: C{str}
1315        @param when: A string describing when the server was created, probably.
1316        """
1317
1318    def yourHost(self, info):
1319        """
1320        Called with daemon information about the server, usually at logon.
1321
1322        @type info: C{str}
1323        @param info: A string describing what software the server is running, probably.
1324        """
1325
1326    def myInfo(self, servername, version, umodes, cmodes):
1327        """
1328        Called with information about the server, usually at logon.
1329
1330        @type servername: C{str}
1331        @param servername: The hostname of this server.
1332
1333        @type version: C{str}
1334        @param version: A description of what software this server runs.
1335
1336        @type umodes: C{str}
1337        @param umodes: All the available user modes.
1338
1339        @type cmodes: C{str}
1340        @param cmodes: All the available channel modes.
1341        """
1342
1343    def luserClient(self, info):
1344        """
1345        Called with information about the number of connections, usually at logon.
1346
1347        @type info: C{str}
1348        @param info: A description of the number of clients and servers
1349        connected to the network, probably.
1350        """
1351
1352    def bounce(self, info):
1353        """
1354        Called with information about where the client should reconnect.
1355
1356        @type info: C{str}
1357        @param info: A plaintext description of the address that should be
1358        connected to.
1359        """
1360
1361    def isupport(self, options):
1362        """
1363        Called with various information about what the server supports.
1364
1365        @type options: C{list} of C{str}
1366        @param options: Descriptions of features or limits of the server, possibly
1367        in the form "NAME=VALUE".
1368        """
1369
1370    def luserChannels(self, channels):
1371        """
1372        Called with the number of channels existent on the server.
1373
1374        @type channels: C{int}
1375        """
1376
1377    def luserOp(self, ops):
1378        """
1379        Called with the number of ops logged on to the server.
1380
1381        @type ops: C{int}
1382        """
1383
1384    def luserMe(self, info):
1385        """
1386        Called with information about the server connected to.
1387
1388        @type info: C{str}
1389        @param info: A plaintext string describing the number of users and servers
1390        connected to this server.
1391        """
1392
1393    ### Methods involving me directly
1394
1395    def privmsg(self, user, channel, message):
1396        """
1397        Called when I have a message from a user to me or a channel.
1398        """
1399        pass
1400
1401    def joined(self, channel):
1402        """
1403        Called when I finish joining a channel.
1404
1405        channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
1406        intact.
1407        """
1408
1409    def left(self, channel):
1410        """
1411        Called when I have left a channel.
1412
1413        channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
1414        intact.
1415        """
1416
1417    def noticed(self, user, channel, message):
1418        """
1419        Called when I have a notice from a user to me or a channel.
1420
1421        If the client makes any automated replies, it must not do so in
1422        response to a NOTICE message, per the RFC::
1423
1424            The difference between NOTICE and PRIVMSG is that
1425            automatic replies MUST NEVER be sent in response to a
1426            NOTICE message. [...] The object of this rule is to avoid
1427            loops between clients automatically sending something in
1428            response to something it received.
1429        """
1430
1431    def modeChanged(self, user, channel, set, modes, args):
1432        """
1433        Called when users or channel's modes are changed.
1434
1435        @type user: C{str}
1436        @param user: The user and hostmask which instigated this change.
1437
1438        @type channel: C{str}
1439        @param channel: The channel where the modes are changed. If args is
1440        empty the channel for which the modes are changing. If the changes are
1441        at server level it could be equal to C{user}.
1442
1443        @type set: C{bool} or C{int}
1444        @param set: True if the mode(s) is being added, False if it is being
1445        removed. If some modes are added and others removed at the same time
1446        this function will be called twice, the first time with all the added
1447        modes, the second with the removed ones. (To change this behaviour
1448        override the irc_MODE method)
1449
1450        @type modes: C{str}
1451        @param modes: The mode or modes which are being changed.
1452
1453        @type args: C{tuple}
1454        @param args: Any additional information required for the mode
1455        change.
1456        """
1457
1458    def pong(self, user, secs):
1459        """
1460        Called with the results of a CTCP PING query.
1461        """
1462        pass
1463
1464    def signedOn(self):
1465        """
1466        Called after successfully signing on to the server.
1467        """
1468        pass
1469
1470    def kickedFrom(self, channel, kicker, message):
1471        """
1472        Called when I am kicked from a channel.
1473        """
1474        pass
1475
1476    def nickChanged(self, nick):
1477        """
1478        Called when my nick has been changed.
1479        """
1480        self.nickname = nick
1481
1482    ### Things I observe other people doing in a channel.
1483
1484    def userJoined(self, user, channel):
1485        """
1486        Called when I see another user joining a channel.
1487        """
1488        pass
1489
1490    def userLeft(self, user, channel):
1491        """
1492        Called when I see another user leaving a channel.
1493        """
1494        pass
1495
1496    def userQuit(self, user, quitMessage):
1497        """
1498        Called when I see another user disconnect from the network.
1499        """
1500        pass
1501
1502    def userKicked(self, kickee, channel, kicker, message):
1503        """
1504        Called when I observe someone else being kicked from a channel.
1505        """
1506        pass
1507
1508    def action(self, user, channel, data):
1509        """
1510        Called when I see a user perform an ACTION on a channel.
1511        """
1512        pass
1513
1514    def topicUpdated(self, user, channel, newTopic):
1515        """
1516        In channel, user changed the topic to newTopic.
1517
1518        Also called when first joining a channel.
1519        """
1520        pass
1521
1522    def userRenamed(self, oldname, newname):
1523        """
1524        A user changed their name from oldname to newname.
1525        """
1526        pass
1527
1528    ### Information from the server.
1529
1530    def receivedMOTD(self, motd):
1531        """
1532        I received a message-of-the-day banner from the server.
1533
1534        motd is a list of strings, where each string was sent as a separate
1535        message from the server. To display, you might want to use::
1536
1537            '\\n'.join(motd)
1538
1539        to get a nicely formatted string.
1540        """
1541        pass
1542
1543    ### user input commands, client->server
1544    ### Your client will want to invoke these.
1545
1546    def join(self, channel, key=None):
1547        """
1548        Join a channel.
1549
1550        @type channel: C{str}
1551        @param channel: The name of the channel to join. If it has no prefix,
1552            C{'#'} will be prepended to it.
1553        @type key: C{str}
1554        @param key: If specified, the key used to join the channel.
1555        """
1556        if channel[0] not in CHANNEL_PREFIXES:
1557            channel = "#" + channel
1558        if key:
1559            self.sendLine(f"JOIN {channel} {key}")
1560        else:
1561            self.sendLine(f"JOIN {channel}")
1562
1563    def leave(self, channel, reason=None):
1564        """
1565        Leave a channel.
1566
1567        @type channel: C{str}
1568        @param channel: The name of the channel to leave. If it has no prefix,
1569            C{'#'} will be prepended to it.
1570        @type reason: C{str}
1571        @param reason: If given, the reason for leaving.
1572        """
1573        if channel[0] not in CHANNEL_PREFIXES:
1574            channel = "#" + channel
1575        if reason:
1576            self.sendLine(f"PART {channel} :{reason}")
1577        else:
1578            self.sendLine(f"PART {channel}")
1579
1580    def kick(self, channel, user, reason=None):
1581        """
1582        Attempt to kick a user from a channel.
1583
1584        @type channel: C{str}
1585        @param channel: The name of the channel to kick the user from. If it has
1586            no prefix, C{'#'} will be prepended to it.
1587        @type user: C{str}
1588        @param user: The nick of the user to kick.
1589        @type reason: C{str}
1590        @param reason: If given, the reason for kicking the user.
1591        """
1592        if channel[0] not in CHANNEL_PREFIXES:
1593            channel = "#" + channel
1594        if reason:
1595            self.sendLine(f"KICK {channel} {user} :{reason}")
1596        else:
1597            self.sendLine(f"KICK {channel} {user}")
1598
1599    part = leave
1600
1601    def invite(self, user, channel):
1602        """
1603        Attempt to invite user to channel
1604
1605        @type user: C{str}
1606        @param user: The user to invite
1607        @type channel: C{str}
1608        @param channel: The channel to invite the user too
1609
1610        @since: 11.0
1611        """
1612        if channel[0] not in CHANNEL_PREFIXES:
1613            channel = "#" + channel
1614        self.sendLine(f"INVITE {user} {channel}")
1615
1616    def topic(self, channel, topic=None):
1617        """
1618        Attempt to set the topic of the given channel, or ask what it is.
1619
1620        If topic is None, then I sent a topic query instead of trying to set the
1621        topic. The server should respond with a TOPIC message containing the
1622        current topic of the given channel.
1623
1624        @type channel: C{str}
1625        @param channel: The name of the channel to change the topic on. If it
1626            has no prefix, C{'#'} will be prepended to it.
1627        @type topic: C{str}
1628        @param topic: If specified, what to set the topic to.
1629        """
1630        # << TOPIC #xtestx :fff
1631        if channel[0] not in CHANNEL_PREFIXES:
1632            channel = "#" + channel
1633        if topic != None:
1634            self.sendLine(f"TOPIC {channel} :{topic}")
1635        else:
1636            self.sendLine(f"TOPIC {channel}")
1637
1638    def mode(self, chan, set, modes, limit=None, user=None, mask=None):
1639        """
1640        Change the modes on a user or channel.
1641
1642        The C{limit}, C{user}, and C{mask} parameters are mutually exclusive.
1643
1644        @type chan: C{str}
1645        @param chan: The name of the channel to operate on.
1646        @type set: C{bool}
1647        @param set: True to give the user or channel permissions and False to
1648            remove them.
1649        @type modes: C{str}
1650        @param modes: The mode flags to set on the user or channel.
1651        @type limit: C{int}
1652        @param limit: In conjunction with the C{'l'} mode flag, limits the
1653             number of users on the channel.
1654        @type user: C{str}
1655        @param user: The user to change the mode on.
1656        @type mask: C{str}
1657        @param mask: In conjunction with the C{'b'} mode flag, sets a mask of
1658            users to be banned from the channel.
1659        """
1660        if set:
1661            line = f"MODE {chan} +{modes}"
1662        else:
1663            line = f"MODE {chan} -{modes}"
1664        if limit is not None:
1665            line = "%s %d" % (line, limit)
1666        elif user is not None:
1667            line = f"{line} {user}"
1668        elif mask is not None:
1669            line = f"{line} {mask}"
1670        self.sendLine(line)
1671
1672    def say(self, channel, message, length=None):
1673        """
1674        Send a message to a channel
1675
1676        @type channel: C{str}
1677        @param channel: The channel to say the message on. If it has no prefix,
1678            C{'#'} will be prepended to it.
1679        @type message: C{str}
1680        @param message: The message to say.
1681        @type length: C{int}
1682        @param length: The maximum number of octets to send at a time.  This has
1683            the effect of turning a single call to C{msg()} into multiple
1684            commands to the server.  This is useful when long messages may be
1685            sent that would otherwise cause the server to kick us off or
1686            silently truncate the text we are sending.  If None is passed, the
1687            entire message is always send in one command.
1688        """
1689        if channel[0] not in CHANNEL_PREFIXES:
1690            channel = "#" + channel
1691        self.msg(channel, message, length)
1692
1693    def _safeMaximumLineLength(self, command):
1694        """
1695        Estimate a safe maximum line length for the given command.
1696
1697        This is done by assuming the maximum values for nickname length,
1698        realname and hostname combined with the command that needs to be sent
1699        and some guessing. A theoretical maximum value is used because it is
1700        possible that our nickname, username or hostname changes (on the server
1701        side) while the length is still being calculated.
1702        """
1703        # :nickname!realname@hostname COMMAND ...
1704        theoretical = ":{}!{}@{} {}".format(
1705            "a" * self.supported.getFeature("NICKLEN"),
1706            # This value is based on observation.
1707            "b" * 10,
1708            # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>.
1709            "c" * 63,
1710            command,
1711        )
1712        # Fingers crossed.
1713        fudge = 10
1714        return MAX_COMMAND_LENGTH - len(theoretical) - fudge
1715
1716    def msg(self, user, message, length=None):
1717        """
1718        Send a message to a user or channel.
1719
1720        The message will be split into multiple commands to the server if:
1721         - The message contains any newline characters
1722         - Any span between newline characters is longer than the given
1723           line-length.
1724
1725        @param user: Username or channel name to which to direct the
1726            message.
1727        @type user: C{str}
1728
1729        @param message: Text to send.
1730        @type message: C{str}
1731
1732        @param length: Maximum number of octets to send in a single
1733            command, including the IRC protocol framing. If L{None} is given
1734            then L{IRCClient._safeMaximumLineLength} is used to determine a
1735            value.
1736        @type length: C{int}
1737        """
1738        fmt = f"PRIVMSG {user} :"
1739
1740        if length is None:
1741            length = self._safeMaximumLineLength(fmt)
1742
1743        # Account for the line terminator.
1744        minimumLength = len(fmt) + 2
1745        if length <= minimumLength:
1746            raise ValueError(
1747                "Maximum length must exceed %d for message "
1748                "to %s" % (minimumLength, user)
1749            )
1750        for line in split(message, length - minimumLength):
1751            self.sendLine(fmt + line)
1752
1753    def notice(self, user, message):
1754        """
1755        Send a notice to a user.
1756
1757        Notices are like normal message, but should never get automated
1758        replies.
1759
1760        @type user: C{str}
1761        @param user: The user to send a notice to.
1762        @type message: C{str}
1763        @param message: The contents of the notice to send.
1764        """
1765        self.sendLine(f"NOTICE {user} :{message}")
1766
1767    def away(self, message=""):
1768        """
1769        Mark this client as away.
1770
1771        @type message: C{str}
1772        @param message: If specified, the away message.
1773        """
1774        self.sendLine("AWAY :%s" % message)
1775
1776    def back(self):
1777        """
1778        Clear the away status.
1779        """
1780        # An empty away marks us as back
1781        self.away()
1782
1783    def whois(self, nickname, server=None):
1784        """
1785        Retrieve user information about the given nickname.
1786
1787        @type nickname: C{str}
1788        @param nickname: The nickname about which to retrieve information.
1789
1790        @since: 8.2
1791        """
1792        if server is None:
1793            self.sendLine("WHOIS " + nickname)
1794        else:
1795            self.sendLine(f"WHOIS {server} {nickname}")
1796
1797    def register(self, nickname, hostname="foo", servername="bar"):
1798        """
1799        Login to the server.
1800
1801        @type nickname: C{str}
1802        @param nickname: The nickname to register.
1803        @type hostname: C{str}
1804        @param hostname: If specified, the hostname to logon as.
1805        @type servername: C{str}
1806        @param servername: If specified, the servername to logon as.
1807        """
1808        if self.password is not None:
1809            self.sendLine("PASS %s" % self.password)
1810        self.setNick(nickname)
1811        if self.username is None:
1812            self.username = nickname
1813        self.sendLine(
1814            "USER {} {} {} :{}".format(
1815                self.username, hostname, servername, self.realname
1816            )
1817        )
1818
1819    def setNick(self, nickname):
1820        """
1821        Set this client's nickname.
1822
1823        @type nickname: C{str}
1824        @param nickname: The nickname to change to.
1825        """
1826        self._attemptedNick = nickname
1827        self.sendLine("NICK %s" % nickname)
1828
1829    def quit(self, message=""):
1830        """
1831        Disconnect from the server
1832
1833        @type message: C{str}
1834
1835        @param message: If specified, the message to give when quitting the
1836            server.
1837        """
1838        self.sendLine("QUIT :%s" % message)
1839
1840    ### user input commands, client->client
1841
1842    def describe(self, channel, action):
1843        """
1844        Strike a pose.
1845
1846        @type channel: C{str}
1847        @param channel: The name of the channel to have an action on. If it
1848            has no prefix, it is sent to the user of that name.
1849        @type action: C{str}
1850        @param action: The action to preform.
1851        @since: 9.0
1852        """
1853        self.ctcpMakeQuery(channel, [("ACTION", action)])
1854
1855    _pings = None
1856    _MAX_PINGRING = 12
1857
1858    def ping(self, user, text=None):
1859        """
1860        Measure round-trip delay to another IRC client.
1861        """
1862        if self._pings is None:
1863            self._pings = {}
1864
1865        if text is None:
1866            chars = string.ascii_letters + string.digits + string.punctuation
1867            key = "".join([random.choice(chars) for i in range(12)])
1868        else:
1869            key = str(text)
1870        self._pings[(user, key)] = time.time()
1871        self.ctcpMakeQuery(user, [("PING", key)])
1872
1873        if len(self._pings) > self._MAX_PINGRING:
1874            # Remove some of the oldest entries.
1875            byValue = [(v, k) for (k, v) in self._pings.items()]
1876            byValue.sort()
1877            excess = len(self._pings) - self._MAX_PINGRING
1878            for i in range(excess):
1879                del self._pings[byValue[i][1]]
1880
1881    def dccSend(self, user, file):
1882        """
1883        This is supposed to send a user a file directly.  This generally
1884        doesn't work on any client, and this method is included only for
1885        backwards compatibility and completeness.
1886
1887        @param user: C{str} representing the user
1888        @param file: an open file (unknown, since this is not implemented)
1889        """
1890        raise NotImplementedError(
1891            "XXX!!! Help!  I need to bind a socket, have it listen, and tell me its address.  "
1892            "(and stop accepting once we've made a single connection.)"
1893        )
1894
1895    def dccResume(self, user, fileName, port, resumePos):
1896        """
1897        Send a DCC RESUME request to another user.
1898        """
1899        self.ctcpMakeQuery(user, [("DCC", ["RESUME", fileName, port, resumePos])])
1900
1901    def dccAcceptResume(self, user, fileName, port, resumePos):
1902        """
1903        Send a DCC ACCEPT response to clients who have requested a resume.
1904        """
1905        self.ctcpMakeQuery(user, [("DCC", ["ACCEPT", fileName, port, resumePos])])
1906
1907    ### server->client messages
1908    ### You might want to fiddle with these,
1909    ### but it is safe to leave them alone.
1910
1911    def irc_ERR_NICKNAMEINUSE(self, prefix, params):
1912        """
1913        Called when we try to register or change to a nickname that is already
1914        taken.
1915        """
1916        self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
1917        self.setNick(self._attemptedNick)
1918
1919    def alterCollidedNick(self, nickname):
1920        """
1921        Generate an altered version of a nickname that caused a collision in an
1922        effort to create an unused related name for subsequent registration.
1923
1924        @param nickname: The nickname a user is attempting to register.
1925        @type nickname: C{str}
1926
1927        @returns: A string that is in some way different from the nickname.
1928        @rtype: C{str}
1929        """
1930        return nickname + "_"
1931
1932    def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
1933        """
1934        Called when we try to register or change to an illegal nickname.
1935
1936        The server should send this reply when the nickname contains any
1937        disallowed characters.  The bot will stall, waiting for RPL_WELCOME, if
1938        we don't handle this during sign-on.
1939
1940        @note: The method uses the spelling I{erroneus}, as it appears in
1941            the RFC, section 6.1.
1942        """
1943        if not self._registered:
1944            self.setNick(self.erroneousNickFallback)
1945
1946    def irc_ERR_PASSWDMISMATCH(self, prefix, params):
1947        """
1948        Called when the login was incorrect.
1949        """
1950        raise IRCPasswordMismatch("Password Incorrect.")
1951
1952    def irc_RPL_WELCOME(self, prefix, params):
1953        """
1954        Called when we have received the welcome from the server.
1955        """
1956        self.hostname = prefix
1957        self._registered = True
1958        self.nickname = self._attemptedNick
1959        self.signedOn()
1960        self.startHeartbeat()
1961
1962    def irc_JOIN(self, prefix, params):
1963        """
1964        Called when a user joins a channel.
1965        """
1966        nick = prefix.split("!")[0]
1967        channel = params[-1]
1968        if nick == self.nickname:
1969            self.joined(channel)
1970        else:
1971            self.userJoined(nick, channel)
1972
1973    def irc_PART(self, prefix, params):
1974        """
1975        Called when a user leaves a channel.
1976        """
1977        nick = prefix.split("!")[0]
1978        channel = params[0]
1979        if nick == self.nickname:
1980            self.left(channel)
1981        else:
1982            self.userLeft(nick, channel)
1983
1984    def irc_QUIT(self, prefix, params):
1985        """
1986        Called when a user has quit.
1987        """
1988        nick = prefix.split("!")[0]
1989        self.userQuit(nick, params[0])
1990
1991    def irc_MODE(self, user, params):
1992        """
1993        Parse a server mode change message.
1994        """
1995        channel, modes, args = params[0], params[1], params[2:]
1996
1997        if modes[0] not in "-+":
1998            modes = "+" + modes
1999
2000        if channel == self.nickname:
2001            # This is a mode change to our individual user, not a channel mode
2002            # that involves us.
2003            paramModes = self.getUserModeParams()
2004        else:
2005            paramModes = self.getChannelModeParams()
2006
2007        try:
2008            added, removed = parseModes(modes, args, paramModes)
2009        except IRCBadModes:
2010            log.err(
2011                None,
2012                "An error occurred while parsing the following "
2013                "MODE message: MODE %s" % (" ".join(params),),
2014            )
2015        else:
2016            if added:
2017                modes, params = zip(*added)
2018                self.modeChanged(user, channel, True, "".join(modes), params)
2019
2020            if removed:
2021                modes, params = zip(*removed)
2022                self.modeChanged(user, channel, False, "".join(modes), params)
2023
2024    def irc_PING(self, prefix, params):
2025        """
2026        Called when some has pinged us.
2027        """
2028        self.sendLine("PONG %s" % params[-1])
2029
2030    def irc_PRIVMSG(self, prefix, params):
2031        """
2032        Called when we get a message.
2033        """
2034        user = prefix
2035        channel = params[0]
2036        message = params[-1]
2037
2038        if not message:
2039            # Don't raise an exception if we get blank message.
2040            return
2041
2042        if message[0] == X_DELIM:
2043            m = ctcpExtract(message)
2044            if m["extended"]:
2045                self.ctcpQuery(user, channel, m["extended"])
2046
2047            if not m["normal"]:
2048                return
2049
2050            message = " ".join(m["normal"])
2051
2052        self.privmsg(user, channel, message)
2053
2054    def irc_NOTICE(self, prefix, params):
2055        """
2056        Called when a user gets a notice.
2057        """
2058        user = prefix
2059        channel = params[0]
2060        message = params[-1]
2061
2062        if message[0] == X_DELIM:
2063            m = ctcpExtract(message)
2064            if m["extended"]:
2065                self.ctcpReply(user, channel, m["extended"])
2066
2067            if not m["normal"]:
2068                return
2069
2070            message = " ".join(m["normal"])
2071
2072        self.noticed(user, channel, message)
2073
2074    def irc_NICK(self, prefix, params):
2075        """
2076        Called when a user changes their nickname.
2077        """
2078        nick = prefix.split("!", 1)[0]
2079        if nick == self.nickname:
2080            self.nickChanged(params[0])
2081        else:
2082            self.userRenamed(nick, params[0])
2083
2084    def irc_KICK(self, prefix, params):
2085        """
2086        Called when a user is kicked from a channel.
2087        """
2088        kicker = prefix.split("!")[0]
2089        channel = params[0]
2090        kicked = params[1]
2091        message = params[-1]
2092        if kicked.lower() == self.nickname.lower():
2093            # Yikes!
2094            self.kickedFrom(channel, kicker, message)
2095        else:
2096            self.userKicked(kicked, channel, kicker, message)
2097
2098    def irc_TOPIC(self, prefix, params):
2099        """
2100        Someone in the channel set the topic.
2101        """
2102        user = prefix.split("!")[0]
2103        channel = params[0]
2104        newtopic = params[1]
2105        self.topicUpdated(user, channel, newtopic)
2106
2107    def irc_RPL_TOPIC(self, prefix, params):
2108        """
2109        Called when the topic for a channel is initially reported or when it
2110        subsequently changes.
2111        """
2112        user = prefix.split("!")[0]
2113        channel = params[1]
2114        newtopic = params[2]
2115        self.topicUpdated(user, channel, newtopic)
2116
2117    def irc_RPL_NOTOPIC(self, prefix, params):
2118        user = prefix.split("!")[0]
2119        channel = params[1]
2120        newtopic = ""
2121        self.topicUpdated(user, channel, newtopic)
2122
2123    def irc_RPL_MOTDSTART(self, prefix, params):
2124        if params[-1].startswith("- "):
2125            params[-1] = params[-1][2:]
2126        self.motd = [params[-1]]
2127
2128    def irc_RPL_MOTD(self, prefix, params):
2129        if params[-1].startswith("- "):
2130            params[-1] = params[-1][2:]
2131        if self.motd is None:
2132            self.motd = []
2133        self.motd.append(params[-1])
2134
2135    def irc_RPL_ENDOFMOTD(self, prefix, params):
2136        """
2137        I{RPL_ENDOFMOTD} indicates the end of the message of the day
2138        messages.  Deliver the accumulated lines to C{receivedMOTD}.
2139        """
2140        motd = self.motd
2141        self.motd = None
2142        self.receivedMOTD(motd)
2143
2144    def irc_RPL_CREATED(self, prefix, params):
2145        self.created(params[1])
2146
2147    def irc_RPL_YOURHOST(self, prefix, params):
2148        self.yourHost(params[1])
2149
2150    def irc_RPL_MYINFO(self, prefix, params):
2151        info = params[1].split(None, 3)
2152        while len(info) < 4:
2153            info.append(None)
2154        self.myInfo(*info)
2155
2156    def irc_RPL_BOUNCE(self, prefix, params):
2157        self.bounce(params[1])
2158
2159    def irc_RPL_ISUPPORT(self, prefix, params):
2160        args = params[1:-1]
2161        # Several ISUPPORT messages, in no particular order, may be sent
2162        # to the client at any given point in time (usually only on connect,
2163        # though.) For this reason, ServerSupportedFeatures.parse is intended
2164        # to mutate the supported feature list.
2165        self.supported.parse(args)
2166        self.isupport(args)
2167
2168    def irc_RPL_LUSERCLIENT(self, prefix, params):
2169        self.luserClient(params[1])
2170
2171    def irc_RPL_LUSEROP(self, prefix, params):
2172        try:
2173            self.luserOp(int(params[1]))
2174        except ValueError:
2175            pass
2176
2177    def irc_RPL_LUSERCHANNELS(self, prefix, params):
2178        try:
2179            self.luserChannels(int(params[1]))
2180        except ValueError:
2181            pass
2182
2183    def irc_RPL_LUSERME(self, prefix, params):
2184        self.luserMe(params[1])
2185
2186    def irc_unknown(self, prefix, command, params):
2187        pass
2188
2189    ### Receiving a CTCP query from another party
2190    ### It is safe to leave these alone.
2191
2192    def ctcpQuery(self, user, channel, messages):
2193        """
2194        Dispatch method for any CTCP queries received.
2195
2196        Duplicated CTCP queries are ignored and no dispatch is
2197        made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}.
2198        """
2199        seen = set()
2200        for tag, data in messages:
2201            method = getattr(self, "ctcpQuery_%s" % tag, None)
2202            if tag not in seen:
2203                if method is not None:
2204                    method(user, channel, data)
2205                else:
2206                    self.ctcpUnknownQuery(user, channel, tag, data)
2207            seen.add(tag)
2208
2209    def ctcpUnknownQuery(self, user, channel, tag, data):
2210        """
2211        Fallback handler for unrecognized CTCP queries.
2212
2213        No CTCP I{ERRMSG} reply is made to remove a potential denial of service
2214        avenue.
2215        """
2216        log.msg(f"Unknown CTCP query from {user!r}: {tag!r} {data!r}")
2217
2218    def ctcpQuery_ACTION(self, user, channel, data):
2219        self.action(user, channel, data)
2220
2221    def ctcpQuery_PING(self, user, channel, data):
2222        nick = user.split("!")[0]
2223        self.ctcpMakeReply(nick, [("PING", data)])
2224
2225    def ctcpQuery_FINGER(self, user, channel, data):
2226        if data is not None:
2227            self.quirkyMessage(f"Why did {user} send '{data}' with a FINGER query?")
2228        if not self.fingerReply:
2229            return
2230
2231        if callable(self.fingerReply):
2232            reply = self.fingerReply()
2233        else:
2234            reply = str(self.fingerReply)
2235
2236        nick = user.split("!")[0]
2237        self.ctcpMakeReply(nick, [("FINGER", reply)])
2238
2239    def ctcpQuery_VERSION(self, user, channel, data):
2240        if data is not None:
2241            self.quirkyMessage(f"Why did {user} send '{data}' with a VERSION query?")
2242
2243        if self.versionName:
2244            nick = user.split("!")[0]
2245            self.ctcpMakeReply(
2246                nick,
2247                [
2248                    (
2249                        "VERSION",
2250                        "%s:%s:%s"
2251                        % (
2252                            self.versionName,
2253                            self.versionNum or "",
2254                            self.versionEnv or "",
2255                        ),
2256                    )
2257                ],
2258            )
2259
2260    def ctcpQuery_SOURCE(self, user, channel, data):
2261        if data is not None:
2262            self.quirkyMessage(f"Why did {user} send '{data}' with a SOURCE query?")
2263        if self.sourceURL:
2264            nick = user.split("!")[0]
2265            # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
2266            # replies should be responded to with the location of an anonymous
2267            # FTP server in host:directory:file format.  I'm taking the liberty
2268            # of bringing it into the 21st century by sending a URL instead.
2269            self.ctcpMakeReply(nick, [("SOURCE", self.sourceURL), ("SOURCE", None)])
2270
2271    def ctcpQuery_USERINFO(self, user, channel, data):
2272        if data is not None:
2273            self.quirkyMessage(f"Why did {user} send '{data}' with a USERINFO query?")
2274        if self.userinfo:
2275            nick = user.split("!")[0]
2276            self.ctcpMakeReply(nick, [("USERINFO", self.userinfo)])
2277
2278    def ctcpQuery_CLIENTINFO(self, user, channel, data):
2279        """
2280        A master index of what CTCP tags this client knows.
2281
2282        If no arguments are provided, respond with a list of known tags, sorted
2283        in alphabetical order.
2284        If an argument is provided, provide human-readable help on
2285        the usage of that tag.
2286        """
2287        nick = user.split("!")[0]
2288        if not data:
2289            # XXX: prefixedMethodNames gets methods from my *class*,
2290            # but it's entirely possible that this *instance* has more
2291            # methods.
2292            names = sorted(reflect.prefixedMethodNames(self.__class__, "ctcpQuery_"))
2293
2294            self.ctcpMakeReply(nick, [("CLIENTINFO", " ".join(names))])
2295        else:
2296            args = data.split()
2297            method = getattr(self, f"ctcpQuery_{args[0]}", None)
2298            if not method:
2299                self.ctcpMakeReply(
2300                    nick,
2301                    [
2302                        (
2303                            "ERRMSG",
2304                            "CLIENTINFO %s :" "Unknown query '%s'" % (data, args[0]),
2305                        )
2306                    ],
2307                )
2308                return
2309            doc = getattr(method, "__doc__", "")
2310            self.ctcpMakeReply(nick, [("CLIENTINFO", doc)])
2311
2312    def ctcpQuery_ERRMSG(self, user, channel, data):
2313        # Yeah, this seems strange, but that's what the spec says to do
2314        # when faced with an ERRMSG query (not a reply).
2315        nick = user.split("!")[0]
2316        self.ctcpMakeReply(nick, [("ERRMSG", "%s :No error has occurred." % data)])
2317
2318    def ctcpQuery_TIME(self, user, channel, data):
2319        if data is not None:
2320            self.quirkyMessage(f"Why did {user} send '{data}' with a TIME query?")
2321        nick = user.split("!")[0]
2322        self.ctcpMakeReply(
2323            nick, [("TIME", ":%s" % time.asctime(time.localtime(time.time())))]
2324        )
2325
2326    def ctcpQuery_DCC(self, user, channel, data):
2327        """
2328        Initiate a Direct Client Connection
2329
2330        @param user: The hostmask of the user/client.
2331        @type user: L{bytes}
2332
2333        @param channel: The name of the IRC channel.
2334        @type channel: L{bytes}
2335
2336        @param data: The DCC request message.
2337        @type data: L{bytes}
2338        """
2339
2340        if not data:
2341            return
2342        dcctype = data.split(None, 1)[0].upper()
2343        handler = getattr(self, "dcc_" + dcctype, None)
2344        if handler:
2345            if self.dcc_sessions is None:
2346                self.dcc_sessions = []
2347            data = data[len(dcctype) + 1 :]
2348            handler(user, channel, data)
2349        else:
2350            nick = user.split("!")[0]
2351            self.ctcpMakeReply(
2352                nick,
2353                [("ERRMSG", f"DCC {data} :Unknown DCC type '{dcctype}'")],
2354            )
2355            self.quirkyMessage(f"{user} offered unknown DCC type {dcctype}")
2356
2357    def dcc_SEND(self, user, channel, data):
2358        # Use shlex.split for those who send files with spaces in the names.
2359        data = shlex.split(data)
2360        if len(data) < 3:
2361            raise IRCBadMessage(f"malformed DCC SEND request: {data!r}")
2362
2363        (filename, address, port) = data[:3]
2364
2365        address = dccParseAddress(address)
2366        try:
2367            port = int(port)
2368        except ValueError:
2369            raise IRCBadMessage(f"Indecipherable port {port!r}")
2370
2371        size = -1
2372        if len(data) >= 4:
2373            try:
2374                size = int(data[3])
2375            except ValueError:
2376                pass
2377
2378        # XXX Should we bother passing this data?
2379        self.dccDoSend(user, address, port, filename, size, data)
2380
2381    def dcc_ACCEPT(self, user, channel, data):
2382        data = shlex.split(data)
2383        if len(data) < 3:
2384            raise IRCBadMessage(f"malformed DCC SEND ACCEPT request: {data!r}")
2385        (filename, port, resumePos) = data[:3]
2386        try:
2387            port = int(port)
2388            resumePos = int(resumePos)
2389        except ValueError:
2390            return
2391
2392        self.dccDoAcceptResume(user, filename, port, resumePos)
2393
2394    def dcc_RESUME(self, user, channel, data):
2395        data = shlex.split(data)
2396        if len(data) < 3:
2397            raise IRCBadMessage(f"malformed DCC SEND RESUME request: {data!r}")
2398        (filename, port, resumePos) = data[:3]
2399        try:
2400            port = int(port)
2401            resumePos = int(resumePos)
2402        except ValueError:
2403            return
2404
2405        self.dccDoResume(user, filename, port, resumePos)
2406
2407    def dcc_CHAT(self, user, channel, data):
2408        data = shlex.split(data)
2409        if len(data) < 3:
2410            raise IRCBadMessage(f"malformed DCC CHAT request: {data!r}")
2411
2412        (filename, address, port) = data[:3]
2413
2414        address = dccParseAddress(address)
2415        try:
2416            port = int(port)
2417        except ValueError:
2418            raise IRCBadMessage(f"Indecipherable port {port!r}")
2419
2420        self.dccDoChat(user, channel, address, port, data)
2421
2422    ### The dccDo methods are the slightly higher-level siblings of
2423    ### common dcc_ methods; the arguments have been parsed for them.
2424
2425    def dccDoSend(self, user, address, port, fileName, size, data):
2426        """
2427        Called when I receive a DCC SEND offer from a client.
2428
2429        By default, I do nothing here.
2430
2431        @param user: The hostmask of the requesting user.
2432        @type user: L{bytes}
2433
2434        @param address: The IP address of the requesting user.
2435        @type address: L{bytes}
2436
2437        @param port: An integer representing the port of the requesting user.
2438        @type port: L{int}
2439
2440        @param fileName: The name of the file to be transferred.
2441        @type fileName: L{bytes}
2442
2443        @param size: The size of the file to be transferred, which may be C{-1}
2444            if the size of the file was not specified in the DCC SEND request.
2445        @type size: L{int}
2446
2447        @param data: A 3-list of [fileName, address, port].
2448        @type data: L{list}
2449        """
2450
2451    def dccDoResume(self, user, file, port, resumePos):
2452        """
2453        Called when a client is trying to resume an offered file via DCC send.
2454        It should be either replied to with a DCC ACCEPT or ignored (default).
2455
2456        @param user: The hostmask of the user who wants to resume the transfer
2457            of a file previously offered via DCC send.
2458        @type user: L{bytes}
2459
2460        @param file: The name of the file to resume the transfer of.
2461        @type file: L{bytes}
2462
2463        @param port: An integer representing the port of the requesting user.
2464        @type port: L{int}
2465
2466        @param resumePos: The position in the file from where the transfer
2467            should resume.
2468        @type resumePos: L{int}
2469        """
2470        pass
2471
2472    def dccDoAcceptResume(self, user, file, port, resumePos):
2473        """
2474        Called when a client has verified and accepted a DCC resume request
2475        made by us.  By default it will do nothing.
2476
2477        @param user: The hostmask of the user who has accepted the DCC resume
2478            request.
2479        @type user: L{bytes}
2480
2481        @param file: The name of the file to resume the transfer of.
2482        @type file: L{bytes}
2483
2484        @param port: An integer representing the port of the accepting user.
2485        @type port: L{int}
2486
2487        @param resumePos: The position in the file from where the transfer
2488            should resume.
2489        @type resumePos: L{int}
2490        """
2491        pass
2492
2493    def dccDoChat(self, user, channel, address, port, data):
2494        pass
2495        # factory = DccChatFactory(self, queryData=(user, channel, data))
2496        # reactor.connectTCP(address, port, factory)
2497        # self.dcc_sessions.append(factory)
2498
2499    # def ctcpQuery_SED(self, user, data):
2500    #    """Simple Encryption Doodoo
2501    #
2502    #    Feel free to implement this, but no specification is available.
2503    #    """
2504    #    raise NotImplementedError
2505
2506    def ctcpMakeReply(self, user, messages):
2507        """
2508        Send one or more C{extended messages} as a CTCP reply.
2509
2510        @type messages: a list of extended messages.  An extended
2511        message is a (tag, data) tuple, where 'data' may be L{None}.
2512        """
2513        self.notice(user, ctcpStringify(messages))
2514
2515    ### client CTCP query commands
2516
2517    def ctcpMakeQuery(self, user, messages):
2518        """
2519        Send one or more C{extended messages} as a CTCP query.
2520
2521        @type messages: a list of extended messages.  An extended
2522        message is a (tag, data) tuple, where 'data' may be L{None}.
2523        """
2524        self.msg(user, ctcpStringify(messages))
2525
2526    ### Receiving a response to a CTCP query (presumably to one we made)
2527    ### You may want to add methods here, or override UnknownReply.
2528
2529    def ctcpReply(self, user, channel, messages):
2530        """
2531        Dispatch method for any CTCP replies received.
2532        """
2533        for m in messages:
2534            method = getattr(self, "ctcpReply_%s" % m[0], None)
2535            if method:
2536                method(user, channel, m[1])
2537            else:
2538                self.ctcpUnknownReply(user, channel, m[0], m[1])
2539
2540    def ctcpReply_PING(self, user, channel, data):
2541        nick = user.split("!", 1)[0]
2542        if (not self._pings) or ((nick, data) not in self._pings):
2543            raise IRCBadMessage(f"Bogus PING response from {user}: {data}")
2544
2545        t0 = self._pings[(nick, data)]
2546        self.pong(user, time.time() - t0)
2547
2548    def ctcpUnknownReply(self, user, channel, tag, data):
2549        """
2550        Called when a fitting ctcpReply_ method is not found.
2551
2552        @param user: The hostmask of the user.
2553        @type user: L{bytes}
2554
2555        @param channel: The name of the IRC channel.
2556        @type channel: L{bytes}
2557
2558        @param tag: The CTCP request tag for which no fitting method is found.
2559        @type tag: L{bytes}
2560
2561        @param data: The CTCP message.
2562        @type data: L{bytes}
2563        """
2564        # FIXME:7560:
2565        # Add code for handling arbitrary queries and not treat them as
2566        # anomalies.
2567
2568        log.msg(f"Unknown CTCP reply from {user}: {tag} {data}\n")
2569
2570    ### Error handlers
2571    ### You may override these with something more appropriate to your UI.
2572
2573    def badMessage(self, line, excType, excValue, tb):
2574        """
2575        When I get a message that's so broken I can't use it.
2576
2577        @param line: The indecipherable message.
2578        @type line: L{bytes}
2579
2580        @param excType: The exception type of the exception raised by the
2581            message.
2582        @type excType: L{type}
2583
2584        @param excValue: The exception parameter of excType or its associated
2585            value(the second argument to C{raise}).
2586        @type excValue: L{BaseException}
2587
2588        @param tb: The Traceback as a traceback object.
2589        @type tb: L{traceback}
2590        """
2591        log.msg(line)
2592        log.msg("".join(traceback.format_exception(excType, excValue, tb)))
2593
2594    def quirkyMessage(self, s):
2595        """
2596        This is called when I receive a message which is peculiar, but not
2597        wholly indecipherable.
2598
2599        @param s: The peculiar message.
2600        @type s: L{bytes}
2601        """
2602        log.msg(s + "\n")
2603
2604    ### Protocol methods
2605
2606    def connectionMade(self):
2607        self.supported = ServerSupportedFeatures()
2608        self._queue = []
2609        if self.performLogin:
2610            self.register(self.nickname)
2611
2612    def dataReceived(self, data):
2613        if isinstance(data, str):
2614            data = data.encode("utf-8")
2615        data = data.replace(b"\r", b"")
2616        basic.LineReceiver.dataReceived(self, data)
2617
2618    def lineReceived(self, line):
2619        if bytes != str and isinstance(line, bytes):
2620            # decode bytes from transport to unicode
2621            line = line.decode("utf-8")
2622
2623        line = lowDequote(line)
2624        try:
2625            prefix, command, params = parsemsg(line)
2626            if command in numeric_to_symbolic:
2627                command = numeric_to_symbolic[command]
2628            self.handleCommand(command, prefix, params)
2629        except IRCBadMessage:
2630            self.badMessage(line, *sys.exc_info())
2631
2632    def getUserModeParams(self):
2633        """
2634        Get user modes that require parameters for correct parsing.
2635
2636        @rtype: C{[str, str]}
2637        @return: C{[add, remove]}
2638        """
2639        return ["", ""]
2640
2641    def getChannelModeParams(self):
2642        """
2643        Get channel modes that require parameters for correct parsing.
2644
2645        @rtype: C{[str, str]}
2646        @return: C{[add, remove]}
2647        """
2648        # PREFIX modes are treated as "type B" CHANMODES, they always take
2649        # parameter.
2650        params = ["", ""]
2651        prefixes = self.supported.getFeature("PREFIX", {})
2652        params[0] = params[1] = "".join(prefixes.keys())
2653
2654        chanmodes = self.supported.getFeature("CHANMODES")
2655        if chanmodes is not None:
2656            params[0] += chanmodes.get("addressModes", "")
2657            params[0] += chanmodes.get("param", "")
2658            params[1] = params[0]
2659            params[0] += chanmodes.get("setParam", "")
2660        return params
2661
2662    def handleCommand(self, command, prefix, params):
2663        """
2664        Determine the function to call for the given command and call it with
2665        the given arguments.
2666
2667        @param command: The IRC command to determine the function for.
2668        @type command: L{bytes}
2669
2670        @param prefix: The prefix of the IRC message (as returned by
2671            L{parsemsg}).
2672        @type prefix: L{bytes}
2673
2674        @param params: A list of parameters to call the function with.
2675        @type params: L{list}
2676        """
2677        method = getattr(self, "irc_%s" % command, None)
2678        try:
2679            if method is not None:
2680                method(prefix, params)
2681            else:
2682                self.irc_unknown(prefix, command, params)
2683        except BaseException:
2684            log.deferr()
2685
2686    def __getstate__(self):
2687        dct = self.__dict__.copy()
2688        dct["dcc_sessions"] = None
2689        dct["_pings"] = None
2690        return dct
2691
2692
2693def dccParseAddress(address):
2694    if "." in address:
2695        pass
2696    else:
2697        try:
2698            address = int(address)
2699        except ValueError:
2700            raise IRCBadMessage(f"Indecipherable address {address!r}")
2701        else:
2702            address = (
2703                (address >> 24) & 0xFF,
2704                (address >> 16) & 0xFF,
2705                (address >> 8) & 0xFF,
2706                address & 0xFF,
2707            )
2708            address = ".".join(map(str, address))
2709    return address
2710
2711
2712class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
2713    """
2714    Bare protocol to receive a Direct Client Connection SEND stream.
2715
2716    This does enough to keep the other guy talking, but you'll want to extend
2717    my dataReceived method to *do* something with the data I get.
2718
2719    @ivar bytesReceived: An integer representing the number of bytes of data
2720        received.
2721    @type bytesReceived: L{int}
2722    """
2723
2724    bytesReceived = 0
2725
2726    def __init__(self, resumeOffset=0):
2727        """
2728        @param resumeOffset: An integer representing the amount of bytes from
2729            where the transfer of data should be resumed.
2730        @type resumeOffset: L{int}
2731        """
2732        self.bytesReceived = resumeOffset
2733        self.resume = resumeOffset != 0
2734
2735    def dataReceived(self, data):
2736        """
2737        See: L{protocol.Protocol.dataReceived}
2738
2739        Warning: This just acknowledges to the remote host that the data has
2740        been received; it doesn't I{do} anything with the data, so you'll want
2741        to override this.
2742        """
2743        self.bytesReceived = self.bytesReceived + len(data)
2744        self.transport.write(struct.pack("!i", self.bytesReceived))
2745
2746
2747class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
2748    """
2749    Protocol for an outgoing Direct Client Connection SEND.
2750
2751    @ivar blocksize: An integer representing the size of an individual block of
2752        data.
2753    @type blocksize: L{int}
2754
2755    @ivar file: The file to be sent.  This can be either a file object or
2756        simply the name of the file.
2757    @type file: L{file} or L{bytes}
2758
2759    @ivar bytesSent: An integer representing the number of bytes sent.
2760    @type bytesSent: L{int}
2761
2762    @ivar completed: An integer representing whether the transfer has been
2763        completed or not.
2764    @type completed: L{int}
2765
2766    @ivar connected: An integer representing whether the connection has been
2767        established or not.
2768    @type connected: L{int}
2769    """
2770
2771    blocksize = 1024
2772    file = None
2773    bytesSent = 0
2774    completed = 0
2775    connected = 0
2776
2777    def __init__(self, file):
2778        if type(file) is str:
2779            self.file = open(file)
2780
2781    def connectionMade(self):
2782        self.connected = 1
2783        self.sendBlock()
2784
2785    def dataReceived(self, data):
2786        # XXX: Do we need to check to see if len(data) != fmtsize?
2787
2788        bytesShesGot = struct.unpack("!I", data)
2789        if bytesShesGot < self.bytesSent:
2790            # Wait for her.
2791            # XXX? Add some checks to see if we've stalled out?
2792            return
2793        elif bytesShesGot > self.bytesSent:
2794            # self.transport.log("DCC SEND %s: She says she has %d bytes "
2795            #                    "but I've only sent %d.  I'm stopping "
2796            #                    "this screwy transfer."
2797            #                    % (self.file,
2798            #                       bytesShesGot, self.bytesSent))
2799            self.transport.loseConnection()
2800            return
2801
2802        self.sendBlock()
2803
2804    def sendBlock(self):
2805        block = self.file.read(self.blocksize)
2806        if block:
2807            self.transport.write(block)
2808            self.bytesSent = self.bytesSent + len(block)
2809        else:
2810            # Nothing more to send, transfer complete.
2811            self.transport.loseConnection()
2812            self.completed = 1
2813
2814    def connectionLost(self, reason):
2815        self.connected = 0
2816        if hasattr(self.file, "close"):
2817            self.file.close()
2818
2819
2820class DccSendFactory(protocol.Factory):
2821    protocol = DccSendProtocol  # type: ignore[assignment]
2822
2823    def __init__(self, file):
2824        self.file = file
2825
2826    def buildProtocol(self, connection):
2827        p = self.protocol(self.file)
2828        p.factory = self
2829        return p
2830
2831
2832def fileSize(file):
2833    """
2834    I'll try my damndest to determine the size of this file object.
2835
2836    @param file: The file object to determine the size of.
2837    @type file: L{io.IOBase}
2838
2839    @rtype: L{int} or L{None}
2840    @return: The size of the file object as an integer if it can be determined,
2841        otherwise return L{None}.
2842    """
2843    size = None
2844    if hasattr(file, "fileno"):
2845        fileno = file.fileno()
2846        try:
2847            stat_ = os.fstat(fileno)
2848            size = stat_[stat.ST_SIZE]
2849        except BaseException:
2850            pass
2851        else:
2852            return size
2853
2854    if hasattr(file, "name") and path.exists(file.name):
2855        try:
2856            size = path.getsize(file.name)
2857        except BaseException:
2858            pass
2859        else:
2860            return size
2861
2862    if hasattr(file, "seek") and hasattr(file, "tell"):
2863        try:
2864            try:
2865                file.seek(0, 2)
2866                size = file.tell()
2867            finally:
2868                file.seek(0, 0)
2869        except BaseException:
2870            pass
2871        else:
2872            return size
2873
2874    return size
2875
2876
2877class DccChat(basic.LineReceiver, styles.Ephemeral):
2878    """
2879    Direct Client Connection protocol type CHAT.
2880
2881    DCC CHAT is really just your run o' the mill basic.LineReceiver
2882    protocol.  This class only varies from that slightly, accepting
2883    either LF or CR LF for a line delimeter for incoming messages
2884    while always using CR LF for outgoing.
2885
2886    The lineReceived method implemented here uses the DCC connection's
2887    'client' attribute (provided upon construction) to deliver incoming
2888    lines from the DCC chat via IRCClient's normal privmsg interface.
2889    That's something of a spoof, which you may well want to override.
2890    """
2891
2892    queryData = None
2893    delimiter = CR.encode("ascii") + NL.encode("ascii")
2894    client = None
2895    remoteParty = None
2896    buffer = b""
2897
2898    def __init__(self, client, queryData=None):
2899        """
2900        Initialize a new DCC CHAT session.
2901
2902        queryData is a 3-tuple of
2903        (fromUser, targetUserOrChannel, data)
2904        as received by the CTCP query.
2905
2906        (To be honest, fromUser is the only thing that's currently
2907        used here. targetUserOrChannel is potentially useful, while
2908        the 'data' argument is solely for informational purposes.)
2909        """
2910        self.client = client
2911        if queryData:
2912            self.queryData = queryData
2913            self.remoteParty = self.queryData[0]
2914
2915    def dataReceived(self, data):
2916        self.buffer = self.buffer + data
2917        lines = self.buffer.split(LF)
2918        # Put the (possibly empty) element after the last LF back in the
2919        # buffer
2920        self.buffer = lines.pop()
2921
2922        for line in lines:
2923            if line[-1] == CR:
2924                line = line[:-1]
2925            self.lineReceived(line)
2926
2927    def lineReceived(self, line):
2928        log.msg(f"DCC CHAT<{self.remoteParty}> {line}")
2929        self.client.privmsg(self.remoteParty, self.client.nickname, line)
2930
2931
2932class DccChatFactory(protocol.ClientFactory):
2933    protocol = DccChat  # type: ignore[assignment]
2934    noisy = False
2935
2936    def __init__(self, client, queryData):
2937        self.client = client
2938        self.queryData = queryData
2939
2940    def buildProtocol(self, addr):
2941        p = self.protocol(client=self.client, queryData=self.queryData)
2942        p.factory = self
2943        return p
2944
2945    def clientConnectionFailed(self, unused_connector, unused_reason):
2946        self.client.dcc_sessions.remove(self)
2947
2948    def clientConnectionLost(self, unused_connector, unused_reason):
2949        self.client.dcc_sessions.remove(self)
2950
2951
2952def dccDescribe(data):
2953    """
2954    Given the data chunk from a DCC query, return a descriptive string.
2955
2956    @param data: The data from a DCC query.
2957    @type data: L{bytes}
2958
2959    @rtype: L{bytes}
2960    @return: A descriptive string.
2961    """
2962
2963    orig_data = data
2964    data = data.split()
2965    if len(data) < 4:
2966        return orig_data
2967
2968    (dcctype, arg, address, port) = data[:4]
2969
2970    if "." in address:
2971        pass
2972    else:
2973        try:
2974            address = int(address)
2975        except ValueError:
2976            pass
2977        else:
2978            address = (
2979                (address >> 24) & 0xFF,
2980                (address >> 16) & 0xFF,
2981                (address >> 8) & 0xFF,
2982                address & 0xFF,
2983            )
2984            address = ".".join(map(str, address))
2985
2986    if dcctype == "SEND":
2987        filename = arg
2988
2989        size_txt = ""
2990        if len(data) >= 5:
2991            try:
2992                size = int(data[4])
2993                size_txt = " of size %d bytes" % (size,)
2994            except ValueError:
2995                pass
2996
2997        dcc_text = "SEND for file '{}'{} at host {}, port {}".format(
2998            filename,
2999            size_txt,
3000            address,
3001            port,
3002        )
3003    elif dcctype == "CHAT":
3004        dcc_text = f"CHAT for host {address}, port {port}"
3005    else:
3006        dcc_text = orig_data
3007
3008    return dcc_text
3009
3010
3011class DccFileReceive(DccFileReceiveBasic):
3012    """
3013    Higher-level coverage for getting a file from DCC SEND.
3014
3015    I allow you to change the file's name and destination directory.  I won't
3016    overwrite an existing file unless I've been told it's okay to do so.  If
3017    passed the resumeOffset keyword argument I will attempt to resume the file
3018    from that amount of bytes.
3019
3020    XXX: I need to let the client know when I am finished.
3021    XXX: I need to decide how to keep a progress indicator updated.
3022    XXX: Client needs a way to tell me "Do not finish until I say so."
3023    XXX: I need to make sure the client understands if the file cannot be written.
3024
3025    @ivar filename: The name of the file to get.
3026    @type filename: L{bytes}
3027
3028    @ivar fileSize: The size of the file to get, which has a default value of
3029        C{-1} if the size of the file was not specified in the DCC SEND
3030        request.
3031    @type fileSize: L{int}
3032
3033    @ivar destDir: The destination directory for the file to be received.
3034    @type destDir: L{bytes}
3035
3036    @ivar overwrite: An integer representing whether an existing file should be
3037        overwritten or not.  This initially is an L{int} but can be modified to
3038        be a L{bool} using the L{set_overwrite} method.
3039    @type overwrite: L{int} or L{bool}
3040
3041    @ivar queryData: queryData is a 3-tuple of (user, channel, data).
3042    @type queryData: L{tuple}
3043
3044    @ivar fromUser: This is the hostmask of the requesting user and is found at
3045        index 0 of L{queryData}.
3046    @type fromUser: L{bytes}
3047    """
3048
3049    filename = "dcc"
3050    fileSize = -1
3051    destDir = "."
3052    overwrite = 0
3053    fromUser: Optional[bytes] = None
3054    queryData = None
3055
3056    def __init__(
3057        self, filename, fileSize=-1, queryData=None, destDir=".", resumeOffset=0
3058    ):
3059        DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
3060        self.filename = filename
3061        self.destDir = destDir
3062        self.fileSize = fileSize
3063        self._resumeOffset = resumeOffset
3064
3065        if queryData:
3066            self.queryData = queryData
3067            self.fromUser = self.queryData[0]
3068
3069    def set_directory(self, directory):
3070        """
3071        Set the directory where the downloaded file will be placed.
3072
3073        May raise OSError if the supplied directory path is not suitable.
3074
3075        @param directory: The directory where the file to be received will be
3076            placed.
3077        @type directory: L{bytes}
3078        """
3079        if not path.exists(directory):
3080            raise OSError(errno.ENOENT, "You see no directory there.", directory)
3081        if not path.isdir(directory):
3082            raise OSError(
3083                errno.ENOTDIR,
3084                "You cannot put a file into " "something which is not a directory.",
3085                directory,
3086            )
3087        if not os.access(directory, os.X_OK | os.W_OK):
3088            raise OSError(
3089                errno.EACCES, "This directory is too hard to write in to.", directory
3090            )
3091        self.destDir = directory
3092
3093    def set_filename(self, filename):
3094        """
3095        Change the name of the file being transferred.
3096
3097        This replaces the file name provided by the sender.
3098
3099        @param filename: The new name for the file.
3100        @type filename: L{bytes}
3101        """
3102        self.filename = filename
3103
3104    def set_overwrite(self, boolean):
3105        """
3106        May I overwrite existing files?
3107
3108        @param boolean: A boolean value representing whether existing files
3109            should be overwritten or not.
3110        @type boolean: L{bool}
3111        """
3112        self.overwrite = boolean
3113
3114    # Protocol-level methods.
3115
3116    def connectionMade(self):
3117        dst = path.abspath(path.join(self.destDir, self.filename))
3118        exists = path.exists(dst)
3119        if self.resume and exists:
3120            # I have been told I want to resume, and a file already
3121            # exists - Here we go
3122            self.file = open(dst, "rb+")
3123            self.file.seek(self._resumeOffset)
3124            self.file.truncate()
3125            log.msg(
3126                "Attempting to resume %s - starting from %d bytes"
3127                % (self.file, self.file.tell())
3128            )
3129        elif self.resume and not exists:
3130            raise OSError(
3131                errno.ENOENT,
3132                "You cannot resume writing to a file " "that does not exist!",
3133                dst,
3134            )
3135        elif self.overwrite or not exists:
3136            self.file = open(dst, "wb")
3137        else:
3138            raise OSError(
3139                errno.EEXIST,
3140                "There's a file in the way.  " "Perhaps that's why you cannot open it.",
3141                dst,
3142            )
3143
3144    def dataReceived(self, data):
3145        self.file.write(data)
3146        DccFileReceiveBasic.dataReceived(self, data)
3147
3148        # XXX: update a progress indicator here?
3149
3150    def connectionLost(self, reason):
3151        """
3152        When the connection is lost, I close the file.
3153
3154        @param reason: The reason why the connection was lost.
3155        @type reason: L{Failure}
3156        """
3157        self.connected = 0
3158        logmsg = f"{self} closed."
3159        if self.fileSize > 0:
3160            logmsg = "%s  %d/%d bytes received" % (
3161                logmsg,
3162                self.bytesReceived,
3163                self.fileSize,
3164            )
3165            if self.bytesReceived == self.fileSize:
3166                pass  # Hooray!
3167            elif self.bytesReceived < self.fileSize:
3168                logmsg = "%s (Warning: %d bytes short)" % (
3169                    logmsg,
3170                    self.fileSize - self.bytesReceived,
3171                )
3172            else:
3173                logmsg = f"{logmsg} (file larger than expected)"
3174        else:
3175            logmsg = "%s  %d bytes received" % (logmsg, self.bytesReceived)
3176
3177        if hasattr(self, "file"):
3178            logmsg = f"{logmsg} and written to {self.file.name}.\n"
3179            if hasattr(self.file, "close"):
3180                self.file.close()
3181
3182        # self.transport.log(logmsg)
3183
3184    def __str__(self) -> str:
3185        if not self.connected:
3186            return f"<Unconnected DccFileReceive object at {id(self):x}>"
3187        transport = self.transport
3188        assert transport is not None
3189        from_ = str(transport.getPeer())
3190        if self.fromUser is not None:
3191            from_ = f"{self.fromUser!r} ({from_})"
3192
3193        s = f"DCC transfer of '{self.filename}' from {from_}"
3194        return s
3195
3196    def __repr__(self) -> str:
3197        s = f"<{self.__class__} at {id(self):x}: GET {self.filename}>"
3198        return s
3199
3200
3201_OFF = "\x0f"
3202_BOLD = "\x02"
3203_COLOR = "\x03"
3204_REVERSE_VIDEO = "\x16"
3205_UNDERLINE = "\x1f"
3206
3207# Mapping of IRC color names to their color values.
3208_IRC_COLORS = dict(
3209    zip(
3210        [
3211            "white",
3212            "black",
3213            "blue",
3214            "green",
3215            "lightRed",
3216            "red",
3217            "magenta",
3218            "orange",
3219            "yellow",
3220            "lightGreen",
3221            "cyan",
3222            "lightCyan",
3223            "lightBlue",
3224            "lightMagenta",
3225            "gray",
3226            "lightGray",
3227        ],
3228        range(16),
3229    )
3230)
3231
3232# Mapping of IRC color values to their color names.
3233_IRC_COLOR_NAMES = {code: name for name, code in _IRC_COLORS.items()}
3234
3235
3236class _CharacterAttributes(_textattributes.CharacterAttributesMixin):
3237    """
3238    Factory for character attributes, including foreground and background color
3239    and non-color attributes such as bold, reverse video and underline.
3240
3241    Character attributes are applied to actual text by using object
3242    indexing-syntax (C{obj['abc']}) after accessing a factory attribute, for
3243    example::
3244
3245        attributes.bold['Some text']
3246
3247    These can be nested to mix attributes::
3248
3249        attributes.bold[attributes.underline['Some text']]
3250
3251    And multiple values can be passed::
3252
3253        attributes.normal[attributes.bold['Some'], ' text']
3254
3255    Non-color attributes can be accessed by attribute name, available
3256    attributes are:
3257
3258        - bold
3259        - reverseVideo
3260        - underline
3261
3262    Available colors are:
3263
3264        0. white
3265        1. black
3266        2. blue
3267        3. green
3268        4. light red
3269        5. red
3270        6. magenta
3271        7. orange
3272        8. yellow
3273        9. light green
3274        10. cyan
3275        11. light cyan
3276        12. light blue
3277        13. light magenta
3278        14. gray
3279        15. light gray
3280
3281    @ivar fg: Foreground colors accessed by attribute name, see above
3282        for possible names.
3283
3284    @ivar bg: Background colors accessed by attribute name, see above
3285        for possible names.
3286
3287    @since: 13.1
3288    """
3289
3290    fg = _textattributes._ColorAttribute(
3291        _textattributes._ForegroundColorAttr, _IRC_COLORS
3292    )
3293    bg = _textattributes._ColorAttribute(
3294        _textattributes._BackgroundColorAttr, _IRC_COLORS
3295    )
3296
3297    attrs = {"bold": _BOLD, "reverseVideo": _REVERSE_VIDEO, "underline": _UNDERLINE}
3298
3299
3300attributes = _CharacterAttributes()
3301
3302
3303class _FormattingState(_textattributes._FormattingStateMixin):
3304    """
3305    Formatting state/attributes of a single character.
3306
3307    Attributes include:
3308        - Formatting nullifier
3309        - Bold
3310        - Underline
3311        - Reverse video
3312        - Foreground color
3313        - Background color
3314
3315    @since: 13.1
3316    """
3317
3318    compareAttributes = (
3319        "off",
3320        "bold",
3321        "underline",
3322        "reverseVideo",
3323        "foreground",
3324        "background",
3325    )
3326
3327    def __init__(
3328        self,
3329        off=False,
3330        bold=False,
3331        underline=False,
3332        reverseVideo=False,
3333        foreground=None,
3334        background=None,
3335    ):
3336        self.off = off
3337        self.bold = bold
3338        self.underline = underline
3339        self.reverseVideo = reverseVideo
3340        self.foreground = foreground
3341        self.background = background
3342
3343    def toMIRCControlCodes(self):
3344        """
3345        Emit a mIRC control sequence that will set up all the attributes this
3346        formatting state has set.
3347
3348        @return: A string containing mIRC control sequences that mimic this
3349            formatting state.
3350        """
3351        attrs = []
3352        if self.bold:
3353            attrs.append(_BOLD)
3354        if self.underline:
3355            attrs.append(_UNDERLINE)
3356        if self.reverseVideo:
3357            attrs.append(_REVERSE_VIDEO)
3358        if self.foreground is not None or self.background is not None:
3359            c = ""
3360            if self.foreground is not None:
3361                c += "%02d" % (self.foreground,)
3362            if self.background is not None:
3363                c += ",%02d" % (self.background,)
3364            attrs.append(_COLOR + c)
3365        return _OFF + "".join(map(str, attrs))
3366
3367
3368def _foldr(f, z, xs):
3369    """
3370    Apply a function of two arguments cumulatively to the items of
3371    a sequence, from right to left, so as to reduce the sequence to
3372    a single value.
3373
3374    @type f: C{callable} taking 2 arguments
3375
3376    @param z: Initial value.
3377
3378    @param xs: Sequence to reduce.
3379
3380    @return: Single value resulting from reducing C{xs}.
3381    """
3382    return reduce(lambda x, y: f(y, x), reversed(xs), z)
3383
3384
3385class _FormattingParser(_CommandDispatcherMixin):
3386    """
3387    A finite-state machine that parses formatted IRC text.
3388
3389    Currently handled formatting includes: bold, reverse, underline,
3390    mIRC color codes and the ability to remove all current formatting.
3391
3392    @see: U{http://www.mirc.co.uk/help/color.txt}
3393
3394    @type _formatCodes: C{dict} mapping C{str} to C{str}
3395    @cvar _formatCodes: Mapping of format code values to names.
3396
3397    @type state: C{str}
3398    @ivar state: Current state of the finite-state machine.
3399
3400    @type _buffer: C{str}
3401    @ivar _buffer: Buffer, containing the text content, of the formatting
3402        sequence currently being parsed, the buffer is used as the content for
3403        L{_attrs} before being added to L{_result} and emptied upon calling
3404        L{emit}.
3405
3406    @type _attrs: C{set}
3407    @ivar _attrs: Set of the applicable formatting states (bold, underline,
3408        etc.) for the current L{_buffer}, these are applied to L{_buffer} when
3409        calling L{emit}.
3410
3411    @type foreground: L{_ForegroundColorAttr}
3412    @ivar foreground: Current foreground color attribute, or L{None}.
3413
3414    @type background: L{_BackgroundColorAttr}
3415    @ivar background: Current background color attribute, or L{None}.
3416
3417    @ivar _result: Current parse result.
3418    """
3419
3420    prefix = "state"
3421
3422    _formatCodes = {
3423        _OFF: "off",
3424        _BOLD: "bold",
3425        _COLOR: "color",
3426        _REVERSE_VIDEO: "reverseVideo",
3427        _UNDERLINE: "underline",
3428    }
3429
3430    def __init__(self):
3431        self.state = "TEXT"
3432        self._buffer = ""
3433        self._attrs = set()
3434        self._result = None
3435        self.foreground = None
3436        self.background = None
3437
3438    def process(self, ch):
3439        """
3440        Handle input.
3441
3442        @type ch: C{str}
3443        @param ch: A single character of input to process
3444        """
3445        self.dispatch(self.state, ch)
3446
3447    def complete(self):
3448        """
3449        Flush the current buffer and return the final parsed result.
3450
3451        @return: Structured text and attributes.
3452        """
3453        self.emit()
3454        if self._result is None:
3455            self._result = attributes.normal
3456        return self._result
3457
3458    def emit(self):
3459        """
3460        Add the currently parsed input to the result.
3461        """
3462        if self._buffer:
3463            attrs = [getattr(attributes, name) for name in self._attrs]
3464            attrs.extend(filter(None, [self.foreground, self.background]))
3465            if not attrs:
3466                attrs.append(attributes.normal)
3467            attrs.append(self._buffer)
3468
3469            attr = _foldr(operator.getitem, attrs.pop(), attrs)
3470            if self._result is None:
3471                self._result = attr
3472            else:
3473                self._result[attr]
3474            self._buffer = ""
3475
3476    def state_TEXT(self, ch):
3477        """
3478        Handle the "text" state.
3479
3480        Along with regular text, single token formatting codes are handled
3481        in this state too.
3482
3483        @param ch: The character being processed.
3484        """
3485        formatName = self._formatCodes.get(ch)
3486        if formatName == "color":
3487            self.emit()
3488            self.state = "COLOR_FOREGROUND"
3489        else:
3490            if formatName is None:
3491                self._buffer += ch
3492            else:
3493                self.emit()
3494                if formatName == "off":
3495                    self._attrs = set()
3496                    self.foreground = self.background = None
3497                else:
3498                    self._attrs.symmetric_difference_update([formatName])
3499
3500    def state_COLOR_FOREGROUND(self, ch):
3501        """
3502        Handle the foreground color state.
3503
3504        Foreground colors can consist of up to two digits and may optionally
3505        end in a I{,}. Any non-digit or non-comma characters are treated as
3506        invalid input and result in the state being reset to "text".
3507
3508        @param ch: The character being processed.
3509        """
3510        # Color codes may only be a maximum of two characters.
3511        if ch.isdigit() and len(self._buffer) < 2:
3512            self._buffer += ch
3513        else:
3514            if self._buffer:
3515                # Wrap around for color numbers higher than we support, like
3516                # most other IRC clients.
3517                col = int(self._buffer) % len(_IRC_COLORS)
3518                self.foreground = getattr(attributes.fg, _IRC_COLOR_NAMES[col])
3519            else:
3520                # If there were no digits, then this has been an empty color
3521                # code and we can reset the color state.
3522                self.foreground = self.background = None
3523
3524            if ch == "," and self._buffer:
3525                # If there's a comma and it's not the first thing, move on to
3526                # the background state.
3527                self._buffer = ""
3528                self.state = "COLOR_BACKGROUND"
3529            else:
3530                # Otherwise, this is a bogus color code, fall back to text.
3531                self._buffer = ""
3532                self.state = "TEXT"
3533                self.emit()
3534                self.process(ch)
3535
3536    def state_COLOR_BACKGROUND(self, ch):
3537        """
3538        Handle the background color state.
3539
3540        Background colors can consist of up to two digits and must occur after
3541        a foreground color and must be preceded by a I{,}. Any non-digit
3542        character is treated as invalid input and results in the state being
3543        set to "text".
3544
3545        @param ch: The character being processed.
3546        """
3547        # Color codes may only be a maximum of two characters.
3548        if ch.isdigit() and len(self._buffer) < 2:
3549            self._buffer += ch
3550        else:
3551            if self._buffer:
3552                # Wrap around for color numbers higher than we support, like
3553                # most other IRC clients.
3554                col = int(self._buffer) % len(_IRC_COLORS)
3555                self.background = getattr(attributes.bg, _IRC_COLOR_NAMES[col])
3556                self._buffer = ""
3557
3558            self.emit()
3559            self.state = "TEXT"
3560            self.process(ch)
3561
3562
3563def parseFormattedText(text):
3564    """
3565    Parse text containing IRC formatting codes into structured information.
3566
3567    Color codes are mapped from 0 to 15 and wrap around if greater than 15.
3568
3569    @type text: C{str}
3570    @param text: Formatted text to parse.
3571
3572    @return: Structured text and attributes.
3573
3574    @since: 13.1
3575    """
3576    state = _FormattingParser()
3577    for ch in text:
3578        state.process(ch)
3579    return state.complete()
3580
3581
3582def assembleFormattedText(formatted):
3583    """
3584    Assemble formatted text from structured information.
3585
3586    Currently handled formatting includes: bold, reverse, underline,
3587    mIRC color codes and the ability to remove all current formatting.
3588
3589    It is worth noting that assembled text will always begin with the control
3590    code to disable other attributes for the sake of correctness.
3591
3592    For example::
3593
3594        from twisted.words.protocols.irc import attributes as A
3595        assembleFormattedText(
3596            A.normal[A.bold['Time: '], A.fg.lightRed['Now!']])
3597
3598    Would produce "Time: " in bold formatting, followed by "Now!" with a
3599    foreground color of light red and without any additional formatting.
3600
3601    Available attributes are:
3602        - bold
3603        - reverseVideo
3604        - underline
3605
3606    Available colors are:
3607        0. white
3608        1. black
3609        2. blue
3610        3. green
3611        4. light red
3612        5. red
3613        6. magenta
3614        7. orange
3615        8. yellow
3616        9. light green
3617        10. cyan
3618        11. light cyan
3619        12. light blue
3620        13. light magenta
3621        14. gray
3622        15. light gray
3623
3624    @see: U{http://www.mirc.co.uk/help/color.txt}
3625
3626    @param formatted: Structured text and attributes.
3627
3628    @rtype: C{str}
3629    @return: String containing mIRC control sequences that mimic those
3630        specified by I{formatted}.
3631
3632    @since: 13.1
3633    """
3634    return _textattributes.flatten(formatted, _FormattingState(), "toMIRCControlCodes")
3635
3636
3637def stripFormatting(text):
3638    """
3639    Remove all formatting codes from C{text}, leaving only the text.
3640
3641    @type text: C{str}
3642    @param text: Formatted text to parse.
3643
3644    @rtype: C{str}
3645    @return: Plain text without any control sequences.
3646
3647    @since: 13.1
3648    """
3649    formatted = parseFormattedText(text)
3650    return _textattributes.flatten(formatted, _textattributes.DefaultFormattingState())
3651
3652
3653# CTCP constants and helper functions
3654
3655X_DELIM = chr(0o01)
3656
3657
3658def ctcpExtract(message):
3659    """
3660    Extract CTCP data from a string.
3661
3662    @return: A C{dict} containing two keys:
3663       - C{'extended'}: A list of CTCP (tag, data) tuples.
3664       - C{'normal'}: A list of strings which were not inside a CTCP delimiter.
3665    """
3666    extended_messages = []
3667    normal_messages = []
3668    retval = {"extended": extended_messages, "normal": normal_messages}
3669
3670    messages = message.split(X_DELIM)
3671    odd = 0
3672
3673    # X1 extended data X2 nomal data X3 extended data X4 normal...
3674    while messages:
3675        if odd:
3676            extended_messages.append(messages.pop(0))
3677        else:
3678            normal_messages.append(messages.pop(0))
3679        odd = not odd
3680
3681    extended_messages[:] = list(filter(None, extended_messages))
3682    normal_messages[:] = list(filter(None, normal_messages))
3683
3684    extended_messages[:] = list(map(ctcpDequote, extended_messages))
3685    for i in range(len(extended_messages)):
3686        m = extended_messages[i].split(SPC, 1)
3687        tag = m[0]
3688        if len(m) > 1:
3689            data = m[1]
3690        else:
3691            data = None
3692
3693        extended_messages[i] = (tag, data)
3694
3695    return retval
3696
3697
3698# CTCP escaping
3699
3700M_QUOTE = chr(0o20)
3701
3702mQuoteTable = {
3703    NUL: M_QUOTE + "0",
3704    NL: M_QUOTE + "n",
3705    CR: M_QUOTE + "r",
3706    M_QUOTE: M_QUOTE + M_QUOTE,
3707}
3708
3709mDequoteTable = {}
3710for k, v in mQuoteTable.items():
3711    mDequoteTable[v[-1]] = k
3712del k, v
3713
3714mEscape_re = re.compile(f"{re.escape(M_QUOTE)}.", re.DOTALL)
3715
3716
3717def lowQuote(s):
3718    for c in (M_QUOTE, NUL, NL, CR):
3719        s = s.replace(c, mQuoteTable[c])
3720    return s
3721
3722
3723def lowDequote(s):
3724    def sub(matchobj, mDequoteTable=mDequoteTable):
3725        s = matchobj.group()[1]
3726        try:
3727            s = mDequoteTable[s]
3728        except KeyError:
3729            s = s
3730        return s
3731
3732    return mEscape_re.sub(sub, s)
3733
3734
3735X_QUOTE = "\\"
3736
3737xQuoteTable = {X_DELIM: X_QUOTE + "a", X_QUOTE: X_QUOTE + X_QUOTE}
3738
3739xDequoteTable = {}
3740
3741for k, v in xQuoteTable.items():
3742    xDequoteTable[v[-1]] = k
3743
3744xEscape_re = re.compile(f"{re.escape(X_QUOTE)}.", re.DOTALL)
3745
3746
3747def ctcpQuote(s):
3748    for c in (X_QUOTE, X_DELIM):
3749        s = s.replace(c, xQuoteTable[c])
3750    return s
3751
3752
3753def ctcpDequote(s):
3754    def sub(matchobj, xDequoteTable=xDequoteTable):
3755        s = matchobj.group()[1]
3756        try:
3757            s = xDequoteTable[s]
3758        except KeyError:
3759            s = s
3760        return s
3761
3762    return xEscape_re.sub(sub, s)
3763
3764
3765def ctcpStringify(messages):
3766    """
3767    @type messages: a list of extended messages.  An extended
3768    message is a (tag, data) tuple, where 'data' may be L{None}, a
3769    string, or a list of strings to be joined with whitespace.
3770
3771    @returns: String
3772    """
3773    coded_messages = []
3774    for (tag, data) in messages:
3775        if data:
3776            if not isinstance(data, str):
3777                try:
3778                    # data as list-of-strings
3779                    data = " ".join(map(str, data))
3780                except TypeError:
3781                    # No?  Then use it's %s representation.
3782                    pass
3783            m = f"{tag} {data}"
3784        else:
3785            m = str(tag)
3786        m = ctcpQuote(m)
3787        m = f"{X_DELIM}{m}{X_DELIM}"
3788        coded_messages.append(m)
3789
3790    line = "".join(coded_messages)
3791    return line
3792
3793
3794# Constants (from RFC 2812)
3795RPL_WELCOME = "001"
3796RPL_YOURHOST = "002"
3797RPL_CREATED = "003"
3798RPL_MYINFO = "004"
3799RPL_ISUPPORT = "005"
3800RPL_BOUNCE = "010"
3801RPL_USERHOST = "302"
3802RPL_ISON = "303"
3803RPL_AWAY = "301"
3804RPL_UNAWAY = "305"
3805RPL_NOWAWAY = "306"
3806RPL_WHOISUSER = "311"
3807RPL_WHOISSERVER = "312"
3808RPL_WHOISOPERATOR = "313"
3809RPL_WHOISIDLE = "317"
3810RPL_ENDOFWHOIS = "318"
3811RPL_WHOISCHANNELS = "319"
3812RPL_WHOWASUSER = "314"
3813RPL_ENDOFWHOWAS = "369"
3814RPL_LISTSTART = "321"
3815RPL_LIST = "322"
3816RPL_LISTEND = "323"
3817RPL_UNIQOPIS = "325"
3818RPL_CHANNELMODEIS = "324"
3819RPL_NOTOPIC = "331"
3820RPL_TOPIC = "332"
3821RPL_INVITING = "341"
3822RPL_SUMMONING = "342"
3823RPL_INVITELIST = "346"
3824RPL_ENDOFINVITELIST = "347"
3825RPL_EXCEPTLIST = "348"
3826RPL_ENDOFEXCEPTLIST = "349"
3827RPL_VERSION = "351"
3828RPL_WHOREPLY = "352"
3829RPL_ENDOFWHO = "315"
3830RPL_NAMREPLY = "353"
3831RPL_ENDOFNAMES = "366"
3832RPL_LINKS = "364"
3833RPL_ENDOFLINKS = "365"
3834RPL_BANLIST = "367"
3835RPL_ENDOFBANLIST = "368"
3836RPL_INFO = "371"
3837RPL_ENDOFINFO = "374"
3838RPL_MOTDSTART = "375"
3839RPL_MOTD = "372"
3840RPL_ENDOFMOTD = "376"
3841RPL_YOUREOPER = "381"
3842RPL_REHASHING = "382"
3843RPL_YOURESERVICE = "383"
3844RPL_TIME = "391"
3845RPL_USERSSTART = "392"
3846RPL_USERS = "393"
3847RPL_ENDOFUSERS = "394"
3848RPL_NOUSERS = "395"
3849RPL_TRACELINK = "200"
3850RPL_TRACECONNECTING = "201"
3851RPL_TRACEHANDSHAKE = "202"
3852RPL_TRACEUNKNOWN = "203"
3853RPL_TRACEOPERATOR = "204"
3854RPL_TRACEUSER = "205"
3855RPL_TRACESERVER = "206"
3856RPL_TRACESERVICE = "207"
3857RPL_TRACENEWTYPE = "208"
3858RPL_TRACECLASS = "209"
3859RPL_TRACERECONNECT = "210"
3860RPL_TRACELOG = "261"
3861RPL_TRACEEND = "262"
3862RPL_STATSLINKINFO = "211"
3863RPL_STATSCOMMANDS = "212"
3864RPL_ENDOFSTATS = "219"
3865RPL_STATSUPTIME = "242"
3866RPL_STATSOLINE = "243"
3867RPL_UMODEIS = "221"
3868RPL_SERVLIST = "234"
3869RPL_SERVLISTEND = "235"
3870RPL_LUSERCLIENT = "251"
3871RPL_LUSEROP = "252"
3872RPL_LUSERUNKNOWN = "253"
3873RPL_LUSERCHANNELS = "254"
3874RPL_LUSERME = "255"
3875RPL_ADMINME = "256"
3876RPL_ADMINLOC1 = "257"
3877RPL_ADMINLOC2 = "258"
3878RPL_ADMINEMAIL = "259"
3879RPL_TRYAGAIN = "263"
3880ERR_NOSUCHNICK = "401"
3881ERR_NOSUCHSERVER = "402"
3882ERR_NOSUCHCHANNEL = "403"
3883ERR_CANNOTSENDTOCHAN = "404"
3884ERR_TOOMANYCHANNELS = "405"
3885ERR_WASNOSUCHNICK = "406"
3886ERR_TOOMANYTARGETS = "407"
3887ERR_NOSUCHSERVICE = "408"
3888ERR_NOORIGIN = "409"
3889ERR_NORECIPIENT = "411"
3890ERR_NOTEXTTOSEND = "412"
3891ERR_NOTOPLEVEL = "413"
3892ERR_WILDTOPLEVEL = "414"
3893ERR_BADMASK = "415"
3894# Defined in errata.
3895# https://www.rfc-editor.org/errata_search.php?rfc=2812&eid=2822
3896ERR_TOOMANYMATCHES = "416"
3897ERR_UNKNOWNCOMMAND = "421"
3898ERR_NOMOTD = "422"
3899ERR_NOADMININFO = "423"
3900ERR_FILEERROR = "424"
3901ERR_NONICKNAMEGIVEN = "431"
3902ERR_ERRONEUSNICKNAME = "432"
3903ERR_NICKNAMEINUSE = "433"
3904ERR_NICKCOLLISION = "436"
3905ERR_UNAVAILRESOURCE = "437"
3906ERR_USERNOTINCHANNEL = "441"
3907ERR_NOTONCHANNEL = "442"
3908ERR_USERONCHANNEL = "443"
3909ERR_NOLOGIN = "444"
3910ERR_SUMMONDISABLED = "445"
3911ERR_USERSDISABLED = "446"
3912ERR_NOTREGISTERED = "451"
3913ERR_NEEDMOREPARAMS = "461"
3914ERR_ALREADYREGISTRED = "462"
3915ERR_NOPERMFORHOST = "463"
3916ERR_PASSWDMISMATCH = "464"
3917ERR_YOUREBANNEDCREEP = "465"
3918ERR_YOUWILLBEBANNED = "466"
3919ERR_KEYSET = "467"
3920ERR_CHANNELISFULL = "471"
3921ERR_UNKNOWNMODE = "472"
3922ERR_INVITEONLYCHAN = "473"
3923ERR_BANNEDFROMCHAN = "474"
3924ERR_BADCHANNELKEY = "475"
3925ERR_BADCHANMASK = "476"
3926ERR_NOCHANMODES = "477"
3927ERR_BANLISTFULL = "478"
3928ERR_NOPRIVILEGES = "481"
3929ERR_CHANOPRIVSNEEDED = "482"
3930ERR_CANTKILLSERVER = "483"
3931ERR_RESTRICTED = "484"
3932ERR_UNIQOPPRIVSNEEDED = "485"
3933ERR_NOOPERHOST = "491"
3934ERR_NOSERVICEHOST = "492"
3935ERR_UMODEUNKNOWNFLAG = "501"
3936ERR_USERSDONTMATCH = "502"
3937
3938# And hey, as long as the strings are already intern'd...
3939symbolic_to_numeric = {
3940    "RPL_WELCOME": "001",
3941    "RPL_YOURHOST": "002",
3942    "RPL_CREATED": "003",
3943    "RPL_MYINFO": "004",
3944    "RPL_ISUPPORT": "005",
3945    "RPL_BOUNCE": "010",
3946    "RPL_USERHOST": "302",
3947    "RPL_ISON": "303",
3948    "RPL_AWAY": "301",
3949    "RPL_UNAWAY": "305",
3950    "RPL_NOWAWAY": "306",
3951    "RPL_WHOISUSER": "311",
3952    "RPL_WHOISSERVER": "312",
3953    "RPL_WHOISOPERATOR": "313",
3954    "RPL_WHOISIDLE": "317",
3955    "RPL_ENDOFWHOIS": "318",
3956    "RPL_WHOISCHANNELS": "319",
3957    "RPL_WHOWASUSER": "314",
3958    "RPL_ENDOFWHOWAS": "369",
3959    "RPL_LISTSTART": "321",
3960    "RPL_LIST": "322",
3961    "RPL_LISTEND": "323",
3962    "RPL_UNIQOPIS": "325",
3963    "RPL_CHANNELMODEIS": "324",
3964    "RPL_NOTOPIC": "331",
3965    "RPL_TOPIC": "332",
3966    "RPL_INVITING": "341",
3967    "RPL_SUMMONING": "342",
3968    "RPL_INVITELIST": "346",
3969    "RPL_ENDOFINVITELIST": "347",
3970    "RPL_EXCEPTLIST": "348",
3971    "RPL_ENDOFEXCEPTLIST": "349",
3972    "RPL_VERSION": "351",
3973    "RPL_WHOREPLY": "352",
3974    "RPL_ENDOFWHO": "315",
3975    "RPL_NAMREPLY": "353",
3976    "RPL_ENDOFNAMES": "366",
3977    "RPL_LINKS": "364",
3978    "RPL_ENDOFLINKS": "365",
3979    "RPL_BANLIST": "367",
3980    "RPL_ENDOFBANLIST": "368",
3981    "RPL_INFO": "371",
3982    "RPL_ENDOFINFO": "374",
3983    "RPL_MOTDSTART": "375",
3984    "RPL_MOTD": "372",
3985    "RPL_ENDOFMOTD": "376",
3986    "RPL_YOUREOPER": "381",
3987    "RPL_REHASHING": "382",
3988    "RPL_YOURESERVICE": "383",
3989    "RPL_TIME": "391",
3990    "RPL_USERSSTART": "392",
3991    "RPL_USERS": "393",
3992    "RPL_ENDOFUSERS": "394",
3993    "RPL_NOUSERS": "395",
3994    "RPL_TRACELINK": "200",
3995    "RPL_TRACECONNECTING": "201",
3996    "RPL_TRACEHANDSHAKE": "202",
3997    "RPL_TRACEUNKNOWN": "203",
3998    "RPL_TRACEOPERATOR": "204",
3999    "RPL_TRACEUSER": "205",
4000    "RPL_TRACESERVER": "206",
4001    "RPL_TRACESERVICE": "207",
4002    "RPL_TRACENEWTYPE": "208",
4003    "RPL_TRACECLASS": "209",
4004    "RPL_TRACERECONNECT": "210",
4005    "RPL_TRACELOG": "261",
4006    "RPL_TRACEEND": "262",
4007    "RPL_STATSLINKINFO": "211",
4008    "RPL_STATSCOMMANDS": "212",
4009    "RPL_ENDOFSTATS": "219",
4010    "RPL_STATSUPTIME": "242",
4011    "RPL_STATSOLINE": "243",
4012    "RPL_UMODEIS": "221",
4013    "RPL_SERVLIST": "234",
4014    "RPL_SERVLISTEND": "235",
4015    "RPL_LUSERCLIENT": "251",
4016    "RPL_LUSEROP": "252",
4017    "RPL_LUSERUNKNOWN": "253",
4018    "RPL_LUSERCHANNELS": "254",
4019    "RPL_LUSERME": "255",
4020    "RPL_ADMINME": "256",
4021    "RPL_ADMINLOC1": "257",
4022    "RPL_ADMINLOC2": "258",
4023    "RPL_ADMINEMAIL": "259",
4024    "RPL_TRYAGAIN": "263",
4025    "ERR_NOSUCHNICK": "401",
4026    "ERR_NOSUCHSERVER": "402",
4027    "ERR_NOSUCHCHANNEL": "403",
4028    "ERR_CANNOTSENDTOCHAN": "404",
4029    "ERR_TOOMANYCHANNELS": "405",
4030    "ERR_WASNOSUCHNICK": "406",
4031    "ERR_TOOMANYTARGETS": "407",
4032    "ERR_NOSUCHSERVICE": "408",
4033    "ERR_NOORIGIN": "409",
4034    "ERR_NORECIPIENT": "411",
4035    "ERR_NOTEXTTOSEND": "412",
4036    "ERR_NOTOPLEVEL": "413",
4037    "ERR_WILDTOPLEVEL": "414",
4038    "ERR_BADMASK": "415",
4039    "ERR_TOOMANYMATCHES": "416",
4040    "ERR_UNKNOWNCOMMAND": "421",
4041    "ERR_NOMOTD": "422",
4042    "ERR_NOADMININFO": "423",
4043    "ERR_FILEERROR": "424",
4044    "ERR_NONICKNAMEGIVEN": "431",
4045    "ERR_ERRONEUSNICKNAME": "432",
4046    "ERR_NICKNAMEINUSE": "433",
4047    "ERR_NICKCOLLISION": "436",
4048    "ERR_UNAVAILRESOURCE": "437",
4049    "ERR_USERNOTINCHANNEL": "441",
4050    "ERR_NOTONCHANNEL": "442",
4051    "ERR_USERONCHANNEL": "443",
4052    "ERR_NOLOGIN": "444",
4053    "ERR_SUMMONDISABLED": "445",
4054    "ERR_USERSDISABLED": "446",
4055    "ERR_NOTREGISTERED": "451",
4056    "ERR_NEEDMOREPARAMS": "461",
4057    "ERR_ALREADYREGISTRED": "462",
4058    "ERR_NOPERMFORHOST": "463",
4059    "ERR_PASSWDMISMATCH": "464",
4060    "ERR_YOUREBANNEDCREEP": "465",
4061    "ERR_YOUWILLBEBANNED": "466",
4062    "ERR_KEYSET": "467",
4063    "ERR_CHANNELISFULL": "471",
4064    "ERR_UNKNOWNMODE": "472",
4065    "ERR_INVITEONLYCHAN": "473",
4066    "ERR_BANNEDFROMCHAN": "474",
4067    "ERR_BADCHANNELKEY": "475",
4068    "ERR_BADCHANMASK": "476",
4069    "ERR_NOCHANMODES": "477",
4070    "ERR_BANLISTFULL": "478",
4071    "ERR_NOPRIVILEGES": "481",
4072    "ERR_CHANOPRIVSNEEDED": "482",
4073    "ERR_CANTKILLSERVER": "483",
4074    "ERR_RESTRICTED": "484",
4075    "ERR_UNIQOPPRIVSNEEDED": "485",
4076    "ERR_NOOPERHOST": "491",
4077    "ERR_NOSERVICEHOST": "492",
4078    "ERR_UMODEUNKNOWNFLAG": "501",
4079    "ERR_USERSDONTMATCH": "502",
4080}
4081
4082numeric_to_symbolic = {}
4083for k, v in symbolic_to_numeric.items():
4084    numeric_to_symbolic[v] = k
4085