1###
2# Copyright (c) 2002-2005, Jeremiah Fincher
3# Copyright (c) 2010, James McCoy
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#
9#   * Redistributions of source code must retain the above copyright notice,
10#     this list of conditions, and the following disclaimer.
11#   * Redistributions in binary form must reproduce the above copyright notice,
12#     this list of conditions, and the following disclaimer in the
13#     documentation and/or other materials provided with the distribution.
14#   * Neither the name of the author of this software nor the name of
15#     contributors to this software may be used to endorse or promote products
16#     derived from this software without specific prior written consent.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28# POSSIBILITY OF SUCH DAMAGE.
29###
30
31"""
32This module provides the basic IrcMsg object used throughout the bot to
33represent the actual messages.  It also provides several helper functions to
34construct such messages in an easier way than the constructor for the IrcMsg
35object (which, as you'll read later, is quite...full-featured :))
36"""
37
38import re
39import time
40import base64
41import datetime
42import warnings
43import functools
44
45from . import conf, ircutils, utils
46from .utils.iter import all
47from .utils import minisix
48
49###
50# IrcMsg class -- used for representing IRC messages acquired from a network.
51###
52
53class MalformedIrcMsg(ValueError):
54    pass
55
56# http://ircv3.net/specs/core/message-tags-3.2.html#escaping-values
57SERVER_TAG_ESCAPE = [
58    ('\\', '\\\\'), # \ -> \\
59    (' ', r'\s'),
60    (';', r'\:'),
61    ('\r', r'\r'),
62    ('\n', r'\n'),
63    ]
64escape_server_tag_value = utils.str.MultipleReplacer(
65        dict(SERVER_TAG_ESCAPE))
66unescape_server_tag_value = utils.str.MultipleReplacer(
67        dict(map(lambda x:(x[1],x[0]), SERVER_TAG_ESCAPE)))
68
69def parse_server_tags(s):
70    server_tags = {}
71    for tag in s.split(';'):
72        if '=' not in tag:
73            server_tags[tag] = None
74        else:
75            (key, value) = tag.split('=', 1)
76            value = unescape_server_tag_value(value)
77            if value == '':
78                # "Implementations MUST interpret empty tag values (e.g. foo=)
79                # as equivalent to missing tag values (e.g. foo)."
80                value = None
81            server_tags[key] = value
82    return server_tags
83def format_server_tags(server_tags):
84    parts = []
85    for (key, value) in server_tags.items():
86        if value is None:
87            parts.append(key)
88        else:
89            parts.append('%s=%s' % (key, escape_server_tag_value(value)))
90    return '@' + ';'.join(parts)
91
92class IrcMsg(object):
93    """Class to represent an IRC message.
94
95    As usual, ignore attributes that begin with an underscore.  They simply
96    don't exist.  Instances of this class are *not* to be modified, since they
97    are hashable.  Public attributes of this class are .prefix, .command,
98    .args, .nick, .user, and .host.
99
100    The constructor for this class is pretty intricate.  It's designed to take
101    any of three major (sets of) arguments.
102
103    Called with no keyword arguments, it takes a single string that is a raw
104    IRC message (such as one taken straight from the network).
105
106    Called with keyword arguments, it *requires* a command parameter.  Args is
107    optional, but with most commands will be necessary.  Prefix is obviously
108    optional, since clients aren't allowed (well, technically, they are, but
109    only in a completely useless way) to send prefixes to the server.
110
111    Since this class isn't to be modified, the constructor also accepts a 'msg'
112    keyword argument representing a message from which to take all the
113    attributes not provided otherwise as keyword arguments.  So, for instance,
114    if a programmer wanted to take a PRIVMSG they'd gotten and simply redirect
115    it to a different source, they could do this:
116
117    IrcMsg(prefix='', args=(newSource, otherMsg.args[1]), msg=otherMsg)
118    """
119    # It's too useful to be able to tag IrcMsg objects with extra, unforeseen
120    # data.  Goodbye, __slots__.
121    # On second thought, let's use methods for tagging.
122    __slots__ = ('args', 'command', 'host', 'nick', 'prefix', 'user',
123                 '_hash', '_str', '_repr', '_len', 'tags', 'reply_env',
124                 'server_tags', 'time')
125    def __init__(self, s='', command='', args=(), prefix='', msg=None,
126            reply_env=None):
127        assert not (msg and s), 'IrcMsg.__init__ cannot accept both s and msg'
128        if not s and not command and not msg:
129            raise MalformedIrcMsg('IRC messages require a command.')
130        self._str = None
131        self._repr = None
132        self._hash = None
133        self._len = None
134        self.reply_env = reply_env
135        self.tags = {}
136        if s:
137            originalString = s
138            try:
139                if not s.endswith('\n'):
140                    s += '\n'
141                self._str = s
142                if s[0] == '@':
143                    (server_tags, s) = s.split(' ', 1)
144                    self.server_tags = parse_server_tags(server_tags[1:])
145                else:
146                    self.server_tags = {}
147                if s[0] == ':':
148                    self.prefix, s = s[1:].split(None, 1)
149                else:
150                    self.prefix = ''
151                if ' :' in s: # Note the space: IPV6 addresses are bad w/o it.
152                    s, last = s.split(' :', 1)
153                    self.args = s.split()
154                    self.args.append(last.rstrip('\r\n'))
155                else:
156                    self.args = s.split()
157                self.command = self.args.pop(0)
158                if 'time' in self.server_tags:
159                    s = self.server_tags['time']
160                    date = datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%fZ')
161                    date = minisix.make_datetime_utc(date)
162                    self.time = minisix.datetime__timestamp(date)
163                else:
164                    self.time = time.time()
165            except (IndexError, ValueError):
166                raise MalformedIrcMsg(repr(originalString))
167        else:
168            if msg is not None:
169                if prefix:
170                    self.prefix = prefix
171                else:
172                    self.prefix = msg.prefix
173                if command:
174                    self.command = command
175                else:
176                    self.command = msg.command
177                if args:
178                    self.args = args
179                else:
180                    self.args = msg.args
181                if reply_env:
182                    self.reply_env = reply_env
183                elif msg.reply_env:
184                    self.reply_env = msg.reply_env.copy()
185                else:
186                    self.reply_env = None
187                self.tags = msg.tags.copy()
188                self.server_tags = msg.server_tags
189                self.time = msg.time
190            else:
191                self.prefix = prefix
192                self.command = command
193                assert all(ircutils.isValidArgument, args), args
194                self.args = args
195                self.time = None
196                self.server_tags = {}
197        self.args = tuple(self.args)
198        if isUserHostmask(self.prefix):
199            (self.nick,self.user,self.host)=ircutils.splitHostmask(self.prefix)
200        else:
201            (self.nick, self.user, self.host) = (self.prefix,)*3
202
203    def __str__(self):
204        if self._str is not None:
205            return self._str
206        if self.prefix:
207            if len(self.args) > 1:
208                self._str = ':%s %s %s :%s\r\n' % \
209                            (self.prefix, self.command,
210                             ' '.join(self.args[:-1]), self.args[-1])
211            else:
212                if self.args:
213                    self._str = ':%s %s :%s\r\n' % \
214                                (self.prefix, self.command, self.args[0])
215                else:
216                    self._str = ':%s %s\r\n' % (self.prefix, self.command)
217        else:
218            if len(self.args) > 1:
219                self._str = '%s %s :%s\r\n' % \
220                            (self.command,
221                             ' '.join(self.args[:-1]), self.args[-1])
222            else:
223                if self.args:
224                    self._str = '%s :%s\r\n' % (self.command, self.args[0])
225                else:
226                    self._str = '%s\r\n' % self.command
227        return self._str
228
229    def __len__(self):
230        return len(str(self))
231
232    def __eq__(self, other):
233        return isinstance(other, self.__class__) and \
234               hash(self) == hash(other) and \
235               self.command == other.command and \
236               self.prefix == other.prefix and \
237               self.args == other.args
238    __req__ = __eq__ # I don't know exactly what this does, but it can't hurt.
239
240    def __ne__(self, other):
241        return not (self == other)
242    __rne__ = __ne__ # Likewise as above.
243
244    def __hash__(self):
245        if self._hash is not None:
246            return self._hash
247        self._hash = hash(self.command) ^ \
248                     hash(self.prefix) ^ \
249                     hash(repr(self.args))
250        return self._hash
251
252    def __repr__(self):
253        if self._repr is not None:
254            return self._repr
255        self._repr = format('IrcMsg(prefix=%q, command=%q, args=%r)',
256                            self.prefix, self.command, self.args)
257        return self._repr
258
259    def __reduce__(self):
260        return (self.__class__, (str(self),))
261
262    def tag(self, tag, value=True):
263        """Affect a key:value pair to this message."""
264        self.tags[tag] = value
265
266    def tagged(self, tag):
267        """Get the value affected to a tag."""
268        return self.tags.get(tag) # Returns None if it's not there.
269
270    def __getattr__(self, attr):
271        if attr.startswith('__'): # Since PEP 487, Python calls __set_name__
272            raise AttributeError("'%s' object has no attribute '%s'" %
273                    (self.__class__.__name__, attr))
274        if attr in self.tags:
275            warnings.warn("msg.<tagname> is deprecated. Use "
276                    "msg.tagged('<tagname>') or msg.tags['<tagname>']"
277                    "instead.", DeprecationWarning)
278            return self.tags[attr]
279        else:
280            # TODO: make this raise AttributeError
281            return None
282
283
284def isCtcp(msg):
285    """Returns whether or not msg is a CTCP message."""
286    return msg.command in ('PRIVMSG', 'NOTICE') and \
287           msg.args[1].startswith('\x01') and \
288           msg.args[1].endswith('\x01') and \
289           len(msg.args[1]) >= 2
290
291def isAction(msg):
292    """A predicate returning true if the PRIVMSG in question is an ACTION"""
293    if isCtcp(msg):
294        s = msg.args[1]
295        payload = s[1:-1] # Chop off \x01.
296        command = payload.split(None, 1)[0]
297        return command == 'ACTION'
298    else:
299        return False
300
301def isSplit(msg):
302    if msg.command == 'QUIT':
303        # It's a quit.
304        quitmsg = msg.args[0]
305        if not quitmsg.startswith('"') and not quitmsg.endswith('"'):
306            # It's not a user-generated quitmsg.
307            servers = quitmsg.split()
308            if len(servers) == 2:
309                # We could check if domains match, or if the hostnames actually
310                # resolve, but we're going to put that off for now.
311                return True
312    return False
313
314_unactionre = re.compile(r'^\x01ACTION\s+(.*)\x01$')
315def unAction(msg):
316    """Returns the payload (i.e., non-ACTION text) of an ACTION msg."""
317    assert isAction(msg)
318    return _unactionre.match(msg.args[1]).group(1)
319
320def _escape(s):
321    s = s.replace('&', '&amp;')
322    s = s.replace('"', '&quot;')
323    s = s.replace('<', '&lt;')
324    s = s.replace('>', '&gt;')
325    return s
326
327def toXml(msg, pretty=True, includeTime=True):
328    assert msg.command == _escape(msg.command)
329    L = []
330    L.append('<msg command="%s" prefix="%s"'%(msg.command,_escape(msg.prefix)))
331    if includeTime:
332        L.append(' time="%s"' % time.time())
333    L.append('>')
334    if pretty:
335        L.append('\n')
336    for arg in msg.args:
337        if pretty:
338            L.append('    ')
339        L.append('<arg>%s</arg>' % _escape(arg))
340        if pretty:
341            L.append('\n')
342    L.append('</msg>\n')
343    return ''.join(L)
344
345def prettyPrint(msg, addRecipients=False, timestampFormat=None, showNick=True):
346    """Provides a client-friendly string form for messages.
347
348    IIRC, I copied BitchX's (or was it XChat's?) format for messages.
349    """
350    def nickorprefix():
351        return msg.nick or msg.prefix
352    def nick():
353        if addRecipients:
354            return '%s/%s' % (msg.nick, msg.args[0])
355        else:
356            return msg.nick
357    if msg.command == 'PRIVMSG':
358        m = _unactionre.match(msg.args[1])
359        if m:
360            s = '* %s %s' % (nick(), m.group(1))
361        else:
362            if not showNick:
363                s = '%s' % msg.args[1]
364            else:
365                s = '<%s> %s' % (nick(), msg.args[1])
366    elif msg.command == 'NOTICE':
367        if not showNick:
368            s = '%s' % msg.args[1]
369        else:
370            s = '-%s- %s' % (nick(), msg.args[1])
371    elif msg.command == 'JOIN':
372        prefix = msg.prefix
373        if msg.nick:
374            prefix = '%s <%s>' % (msg.nick, prefix)
375        s = '*** %s has joined %s' % (prefix, msg.args[0])
376    elif msg.command == 'PART':
377        if len(msg.args) > 1:
378            partmsg = ' (%s)' % msg.args[1]
379        else:
380            partmsg = ''
381        s = '*** %s <%s> has parted %s%s' % (msg.nick, msg.prefix,
382                                             msg.args[0], partmsg)
383    elif msg.command == 'KICK':
384        if len(msg.args) > 2:
385            kickmsg = ' (%s)' % msg.args[1]
386        else:
387            kickmsg = ''
388        s = '*** %s was kicked by %s%s' % (msg.args[1], msg.nick, kickmsg)
389    elif msg.command == 'MODE':
390        s = '*** %s sets mode: %s' % (nickorprefix(), ' '.join(msg.args))
391    elif msg.command == 'QUIT':
392        if msg.args:
393            quitmsg = ' (%s)' % msg.args[0]
394        else:
395            quitmsg = ''
396        s = '*** %s <%s> has quit IRC%s' % (msg.nick, msg.prefix, quitmsg)
397    elif msg.command == 'TOPIC':
398        s = '*** %s changes topic to %s' % (nickorprefix(), msg.args[1])
399    elif msg.command == 'NICK':
400        s = '*** %s is now known as %s' % (msg.nick, msg.args[0])
401    else:
402        s = utils.str.format('--- Unknown command %q', ' '.join(msg.args))
403    at = msg.tagged('receivedAt')
404    if timestampFormat and at:
405        s = '%s %s' % (time.strftime(timestampFormat, time.localtime(at)), s)
406    return s
407
408###
409# Various IrcMsg functions
410###
411
412isNick = ircutils.isNick
413areNicks = ircutils.areNicks
414isChannel = ircutils.isChannel
415areChannels = ircutils.areChannels
416areReceivers = ircutils.areReceivers
417isUserHostmask = ircutils.isUserHostmask
418
419def pong(payload, prefix='', msg=None):
420    """Takes a payload and returns the proper PONG IrcMsg."""
421    if conf.supybot.protocols.irc.strictRfc():
422        assert payload, 'PONG requires a payload'
423    if msg and not prefix:
424        prefix = msg.prefix
425    return IrcMsg(prefix=prefix, command='PONG', args=(payload,), msg=msg)
426
427def ping(payload, prefix='', msg=None):
428    """Takes a payload and returns the proper PING IrcMsg."""
429    if conf.supybot.protocols.irc.strictRfc():
430        assert payload, 'PING requires a payload'
431    if msg and not prefix:
432        prefix = msg.prefix
433    return IrcMsg(prefix=prefix, command='PING', args=(payload,), msg=msg)
434
435def op(channel, nick, prefix='', msg=None):
436    """Returns a MODE to op nick on channel."""
437    if conf.supybot.protocols.irc.strictRfc():
438        assert isChannel(channel), repr(channel)
439        assert isNick(nick), repr(nick)
440    if msg and not prefix:
441        prefix = msg.prefix
442    return IrcMsg(prefix=prefix, command='MODE',
443                  args=(channel, '+o', nick), msg=msg)
444
445def ops(channel, nicks, prefix='', msg=None):
446    """Returns a MODE to op each of nicks on channel."""
447    if conf.supybot.protocols.irc.strictRfc():
448        assert isChannel(channel), repr(channel)
449        assert nicks, 'Nicks must not be empty.'
450        assert all(isNick, nicks), nicks
451    if msg and not prefix:
452        prefix = msg.prefix
453    return IrcMsg(prefix=prefix, command='MODE',
454                  args=(channel, '+' + ('o'*len(nicks))) + tuple(nicks),
455                  msg=msg)
456
457def deop(channel, nick, prefix='', msg=None):
458    """Returns a MODE to deop nick on channel."""
459    if conf.supybot.protocols.irc.strictRfc():
460        assert isChannel(channel), repr(channel)
461        assert isNick(nick), repr(nick)
462    if msg and not prefix:
463        prefix = msg.prefix
464    return IrcMsg(prefix=prefix, command='MODE',
465                  args=(channel, '-o', nick), msg=msg)
466
467def deops(channel, nicks, prefix='', msg=None):
468    """Returns a MODE to deop each of nicks on channel."""
469    if conf.supybot.protocols.irc.strictRfc():
470        assert isChannel(channel), repr(channel)
471        assert nicks, 'Nicks must not be empty.'
472        assert all(isNick, nicks), nicks
473    if msg and not prefix:
474        prefix = msg.prefix
475    return IrcMsg(prefix=prefix, command='MODE', msg=msg,
476                  args=(channel, '-' + ('o'*len(nicks))) + tuple(nicks))
477
478def halfop(channel, nick, prefix='', msg=None):
479    """Returns a MODE to halfop nick on channel."""
480    if conf.supybot.protocols.irc.strictRfc():
481        assert isChannel(channel), repr(channel)
482        assert isNick(nick), repr(nick)
483    if msg and not prefix:
484        prefix = msg.prefix
485    return IrcMsg(prefix=prefix, command='MODE',
486                  args=(channel, '+h', nick), msg=msg)
487
488def halfops(channel, nicks, prefix='', msg=None):
489    """Returns a MODE to halfop each of nicks on channel."""
490    if conf.supybot.protocols.irc.strictRfc():
491        assert isChannel(channel), repr(channel)
492        assert nicks, 'Nicks must not be empty.'
493        assert all(isNick, nicks), nicks
494    if msg and not prefix:
495        prefix = msg.prefix
496    return IrcMsg(prefix=prefix, command='MODE', msg=msg,
497                  args=(channel, '+' + ('h'*len(nicks))) + tuple(nicks))
498
499def dehalfop(channel, nick, prefix='', msg=None):
500    """Returns a MODE to dehalfop nick on channel."""
501    if conf.supybot.protocols.irc.strictRfc():
502        assert isChannel(channel), repr(channel)
503        assert isNick(nick), repr(nick)
504    if msg and not prefix:
505        prefix = msg.prefix
506    return IrcMsg(prefix=prefix, command='MODE',
507                  args=(channel, '-h', nick), msg=msg)
508
509def dehalfops(channel, nicks, prefix='', msg=None):
510    """Returns a MODE to dehalfop each of nicks on channel."""
511    if conf.supybot.protocols.irc.strictRfc():
512        assert isChannel(channel), repr(channel)
513        assert nicks, 'Nicks must not be empty.'
514        assert all(isNick, nicks), nicks
515    if msg and not prefix:
516        prefix = msg.prefix
517    return IrcMsg(prefix=prefix, command='MODE', msg=msg,
518                  args=(channel, '-' + ('h'*len(nicks))) + tuple(nicks))
519
520def voice(channel, nick, prefix='', msg=None):
521    """Returns a MODE to voice nick on channel."""
522    if conf.supybot.protocols.irc.strictRfc():
523        assert isChannel(channel), repr(channel)
524        assert isNick(nick), repr(nick)
525    if msg and not prefix:
526        prefix = msg.prefix
527    return IrcMsg(prefix=prefix, command='MODE',
528                  args=(channel, '+v', nick), msg=msg)
529
530def voices(channel, nicks, prefix='', msg=None):
531    """Returns a MODE to voice each of nicks on channel."""
532    if conf.supybot.protocols.irc.strictRfc():
533        assert isChannel(channel), repr(channel)
534        assert nicks, 'Nicks must not be empty.'
535        assert all(isNick, nicks)
536    if msg and not prefix:
537        prefix = msg.prefix
538    return IrcMsg(prefix=prefix, command='MODE', msg=msg,
539                  args=(channel, '+' + ('v'*len(nicks))) + tuple(nicks))
540
541def devoice(channel, nick, prefix='', msg=None):
542    """Returns a MODE to devoice nick on channel."""
543    if conf.supybot.protocols.irc.strictRfc():
544        assert isChannel(channel), repr(channel)
545        assert isNick(nick), repr(nick)
546    if msg and not prefix:
547        prefix = msg.prefix
548    return IrcMsg(prefix=prefix, command='MODE',
549                  args=(channel, '-v', nick), msg=msg)
550
551def devoices(channel, nicks, prefix='', msg=None):
552    """Returns a MODE to devoice each of nicks on channel."""
553    if conf.supybot.protocols.irc.strictRfc():
554        assert isChannel(channel), repr(channel)
555        assert nicks, 'Nicks must not be empty.'
556        assert all(isNick, nicks), nicks
557    if msg and not prefix:
558        prefix = msg.prefix
559    return IrcMsg(prefix=prefix, command='MODE', msg=msg,
560                  args=(channel, '-' + ('v'*len(nicks))) + tuple(nicks))
561
562def ban(channel, hostmask, exception='', prefix='', msg=None):
563    """Returns a MODE to ban nick on channel."""
564    if conf.supybot.protocols.irc.strictRfc():
565        assert isChannel(channel), repr(channel)
566        assert isUserHostmask(hostmask), repr(hostmask)
567    modes = [('+b', hostmask)]
568    if exception:
569        modes.append(('+e', exception))
570    if msg and not prefix:
571        prefix = msg.prefix
572    return IrcMsg(prefix=prefix, command='MODE',
573                  args=[channel] + ircutils.joinModes(modes), msg=msg)
574
575def bans(channel, hostmasks, exceptions=(), prefix='', msg=None):
576    """Returns a MODE to ban each of nicks on channel."""
577    if conf.supybot.protocols.irc.strictRfc():
578        assert isChannel(channel), repr(channel)
579        assert all(isUserHostmask, hostmasks), hostmasks
580    modes = [('+b', s) for s in hostmasks] + [('+e', s) for s in exceptions]
581    if msg and not prefix:
582        prefix = msg.prefix
583    return IrcMsg(prefix=prefix, command='MODE',
584                  args=[channel] + ircutils.joinModes(modes), msg=msg)
585
586def unban(channel, hostmask, prefix='', msg=None):
587    """Returns a MODE to unban nick on channel."""
588    if conf.supybot.protocols.irc.strictRfc():
589        assert isChannel(channel), repr(channel)
590        assert isUserHostmask(hostmask), repr(hostmask)
591    if msg and not prefix:
592        prefix = msg.prefix
593    return IrcMsg(prefix=prefix, command='MODE',
594                  args=(channel, '-b', hostmask), msg=msg)
595
596def unbans(channel, hostmasks, prefix='', msg=None):
597    """Returns a MODE to unban each of nicks on channel."""
598    if conf.supybot.protocols.irc.strictRfc():
599        assert isChannel(channel), repr(channel)
600        assert all(isUserHostmask, hostmasks), hostmasks
601    modes = [('-b', s) for s in hostmasks]
602    if msg and not prefix:
603        prefix = msg.prefix
604    return IrcMsg(prefix=prefix, command='MODE',
605                  args=[channel] + ircutils.joinModes(modes), msg=msg)
606
607def kick(channel, nick, s='', prefix='', msg=None):
608    """Returns a KICK to kick nick from channel with the message msg."""
609    if conf.supybot.protocols.irc.strictRfc():
610        assert isChannel(channel), repr(channel)
611        assert isNick(nick), repr(nick)
612    if msg and not prefix:
613        prefix = msg.prefix
614    if minisix.PY2 and isinstance(s, unicode):
615        s = s.encode('utf8')
616    assert isinstance(s, str)
617    if s:
618        return IrcMsg(prefix=prefix, command='KICK',
619                      args=(channel, nick, s), msg=msg)
620    else:
621        return IrcMsg(prefix=prefix, command='KICK',
622                      args=(channel, nick), msg=msg)
623
624def kicks(channels, nicks, s='', prefix='', msg=None):
625    """Returns a KICK to kick each of nicks from channel with the message msg.
626    """
627    if isinstance(channels, str): # Backward compatibility
628        channels = [channels]
629    if conf.supybot.protocols.irc.strictRfc():
630        assert areChannels(channels), repr(channels)
631        assert areNicks(nicks), repr(nicks)
632    if msg and not prefix:
633        prefix = msg.prefix
634    if minisix.PY2 and isinstance(s, unicode):
635        s = s.encode('utf8')
636    assert isinstance(s, str)
637    if s:
638        for channel in channels:
639            return IrcMsg(prefix=prefix, command='KICK',
640                          args=(channel, ','.join(nicks), s), msg=msg)
641    else:
642        for channel in channels:
643            return IrcMsg(prefix=prefix, command='KICK',
644                          args=(channel, ','.join(nicks)), msg=msg)
645
646def privmsg(recipient, s, prefix='', msg=None):
647    """Returns a PRIVMSG to recipient with the message msg."""
648    if conf.supybot.protocols.irc.strictRfc():
649        assert (areReceivers(recipient)), repr(recipient)
650        assert s, 's must not be empty.'
651    if minisix.PY2 and isinstance(s, unicode):
652        s = s.encode('utf8')
653    assert isinstance(s, str)
654    if msg and not prefix:
655        prefix = msg.prefix
656    return IrcMsg(prefix=prefix, command='PRIVMSG',
657                  args=(recipient, s), msg=msg)
658
659def dcc(recipient, kind, *args, **kwargs):
660    # Stupid Python won't allow (recipient, kind, *args, prefix=''), so we have
661    # to use the **kwargs form.  Blech.
662    assert isNick(recipient), 'Can\'t DCC a channel.'
663    kind = kind.upper()
664    assert kind in ('SEND', 'CHAT', 'RESUME', 'ACCEPT'), 'Invalid DCC command.'
665    args = (kind,) + args
666    return IrcMsg(prefix=kwargs.get('prefix', ''), command='PRIVMSG',
667                  args=(recipient, ' '.join(args)))
668
669def action(recipient, s, prefix='', msg=None):
670    """Returns a PRIVMSG ACTION to recipient with the message msg."""
671    if conf.supybot.protocols.irc.strictRfc():
672        assert (isChannel(recipient) or isNick(recipient)), repr(recipient)
673    if msg and not prefix:
674        prefix = msg.prefix
675    return IrcMsg(prefix=prefix, command='PRIVMSG',
676                  args=(recipient, '\x01ACTION %s\x01' % s), msg=msg)
677
678def notice(recipient, s, prefix='', msg=None):
679    """Returns a NOTICE to recipient with the message msg."""
680    if conf.supybot.protocols.irc.strictRfc():
681        assert areReceivers(recipient), repr(recipient)
682        assert s, 'msg must not be empty.'
683    if minisix.PY2 and isinstance(s, unicode):
684        s = s.encode('utf8')
685    assert isinstance(s, str)
686    if msg and not prefix:
687        prefix = msg.prefix
688    return IrcMsg(prefix=prefix, command='NOTICE', args=(recipient, s), msg=msg)
689
690def join(channel, key=None, prefix='', msg=None):
691    """Returns a JOIN to a channel"""
692    if conf.supybot.protocols.irc.strictRfc():
693        assert areChannels(channel), repr(channel)
694    if msg and not prefix:
695        prefix = msg.prefix
696    if key is None:
697        return IrcMsg(prefix=prefix, command='JOIN', args=(channel,), msg=msg)
698    else:
699        if conf.supybot.protocols.irc.strictRfc():
700            chars = '\x00\r\n\f\t\v '
701            assert not any([(ord(x) >= 128 or x in chars) for x in key])
702        return IrcMsg(prefix=prefix, command='JOIN',
703                      args=(channel, key), msg=msg)
704
705def joins(channels, keys=None, prefix='', msg=None):
706    """Returns a JOIN to each of channels."""
707    if conf.supybot.protocols.irc.strictRfc():
708        assert all(isChannel, channels), channels
709    if msg and not prefix:
710        prefix = msg.prefix
711    if keys is None:
712        keys = []
713    assert len(keys) <= len(channels), 'Got more keys than channels.'
714    if not keys:
715        return IrcMsg(prefix=prefix,
716                      command='JOIN',
717                      args=(','.join(channels),), msg=msg)
718    else:
719        if conf.supybot.protocols.irc.strictRfc():
720            chars = '\x00\r\n\f\t\v '
721            for key in keys:
722                assert not any([(ord(x) >= 128 or x in chars) for x in key])
723        return IrcMsg(prefix=prefix,
724                      command='JOIN',
725                      args=(','.join(channels), ','.join(keys)), msg=msg)
726
727def part(channel, s='', prefix='', msg=None):
728    """Returns a PART from channel with the message msg."""
729    if conf.supybot.protocols.irc.strictRfc():
730        assert isChannel(channel), repr(channel)
731    if msg and not prefix:
732        prefix = msg.prefix
733    if minisix.PY2 and isinstance(s, unicode):
734        s = s.encode('utf8')
735    assert isinstance(s, str)
736    if s:
737        return IrcMsg(prefix=prefix, command='PART',
738                      args=(channel, s), msg=msg)
739    else:
740        return IrcMsg(prefix=prefix, command='PART',
741                      args=(channel,), msg=msg)
742
743def parts(channels, s='', prefix='', msg=None):
744    """Returns a PART from each of channels with the message msg."""
745    if conf.supybot.protocols.irc.strictRfc():
746        assert all(isChannel, channels), channels
747    if msg and not prefix:
748        prefix = msg.prefix
749    if minisix.PY2 and isinstance(s, unicode):
750        s = s.encode('utf8')
751    assert isinstance(s, str)
752    if s:
753        return IrcMsg(prefix=prefix, command='PART',
754                      args=(','.join(channels), s), msg=msg)
755    else:
756        return IrcMsg(prefix=prefix, command='PART',
757                      args=(','.join(channels),), msg=msg)
758
759def quit(s='', prefix='', msg=None):
760    """Returns a QUIT with the message msg."""
761    if msg and not prefix:
762        prefix = msg.prefix
763    if s:
764        return IrcMsg(prefix=prefix, command='QUIT', args=(s,), msg=msg)
765    else:
766        return IrcMsg(prefix=prefix, command='QUIT', msg=msg)
767
768def topic(channel, topic=None, prefix='', msg=None):
769    """Returns a TOPIC for channel with the topic topic."""
770    if conf.supybot.protocols.irc.strictRfc():
771        assert isChannel(channel), repr(channel)
772    if msg and not prefix:
773        prefix = msg.prefix
774    if topic is None:
775        return IrcMsg(prefix=prefix, command='TOPIC',
776                      args=(channel,), msg=msg)
777    else:
778        if minisix.PY2 and isinstance(topic, unicode):
779            topic = topic.encode('utf8')
780        assert isinstance(topic, str)
781        return IrcMsg(prefix=prefix, command='TOPIC',
782                      args=(channel, topic), msg=msg)
783
784def nick(nick, prefix='', msg=None):
785    """Returns a NICK with nick nick."""
786    if conf.supybot.protocols.irc.strictRfc():
787        assert isNick(nick), repr(nick)
788    if msg and not prefix:
789        prefix = msg.prefix
790    return IrcMsg(prefix=prefix, command='NICK', args=(nick,), msg=msg)
791
792def user(ident, user, prefix='', msg=None):
793    """Returns a USER with ident ident and user user."""
794    if conf.supybot.protocols.irc.strictRfc():
795        assert '\x00' not in ident and \
796               '\r' not in ident and \
797               '\n' not in ident and \
798               ' ' not in ident and \
799               '@' not in ident
800    if msg and not prefix:
801        prefix = msg.prefix
802    return IrcMsg(prefix=prefix, command='USER',
803                  args=(ident, '0', '*', user), msg=msg)
804
805def who(hostmaskOrChannel, prefix='', msg=None, args=()):
806    """Returns a WHO for the hostmask or channel hostmaskOrChannel."""
807    if conf.supybot.protocols.irc.strictRfc():
808        assert isChannel(hostmaskOrChannel) or \
809               isUserHostmask(hostmaskOrChannel), repr(hostmaskOrChannel)
810    if msg and not prefix:
811        prefix = msg.prefix
812    return IrcMsg(prefix=prefix, command='WHO',
813                  args=(hostmaskOrChannel,) + args, msg=msg)
814
815def _whois(COMMAND, nick, mask='', prefix='', msg=None):
816    """Returns a WHOIS for nick."""
817    if conf.supybot.protocols.irc.strictRfc():
818        assert areNicks(nick), repr(nick)
819    if msg and not prefix:
820        prefix = msg.prefix
821    args = (nick,)
822    if mask:
823        args = (nick, mask)
824    return IrcMsg(prefix=prefix, command=COMMAND, args=args, msg=msg)
825whois = functools.partial(_whois, 'WHOIS')
826whowas = functools.partial(_whois, 'WHOWAS')
827
828def names(channel=None, prefix='', msg=None):
829    if conf.supybot.protocols.irc.strictRfc():
830        assert areChannels(channel)
831    if msg and not prefix:
832        prefix = msg.prefix
833    if channel is not None:
834        return IrcMsg(prefix=prefix, command='NAMES', args=(channel,), msg=msg)
835    else:
836        return IrcMsg(prefix=prefix, command='NAMES', msg=msg)
837
838def mode(channel, args=(), prefix='', msg=None):
839    if msg and not prefix:
840        prefix = msg.prefix
841    if isinstance(args, minisix.string_types):
842        args = (args,)
843    else:
844        args = tuple(map(str, args))
845    return IrcMsg(prefix=prefix, command='MODE', args=(channel,)+args, msg=msg)
846
847def modes(channel, args=(), prefix='', msg=None):
848    """Returns a MODE message for the channel for all the (mode, targetOrNone) 2-tuples in 'args'."""
849    if conf.supybot.protocols.irc.strictRfc():
850        assert isChannel(channel), repr(channel)
851    modes = args
852    if msg and not prefix:
853        prefix = msg.prefix
854    return IrcMsg(prefix=prefix, command='MODE',
855                  args=[channel] + ircutils.joinModes(modes), msg=msg)
856
857def limit(channel, limit, prefix='', msg=None):
858    return mode(channel, ['+l', limit], prefix=prefix, msg=msg)
859
860def unlimit(channel, limit, prefix='', msg=None):
861    return mode(channel, ['-l', limit], prefix=prefix, msg=msg)
862
863def invite(nick, channel, prefix='', msg=None):
864    """Returns an INVITE for nick."""
865    if conf.supybot.protocols.irc.strictRfc():
866        assert isNick(nick), repr(nick)
867    if msg and not prefix:
868        prefix = msg.prefix
869    return IrcMsg(prefix=prefix, command='INVITE',
870                  args=(nick, channel), msg=msg)
871
872def password(password, prefix='', msg=None):
873    """Returns a PASS command for accessing a server."""
874    if conf.supybot.protocols.irc.strictRfc():
875        assert password, 'password must not be empty.'
876    if msg and not prefix:
877        prefix = msg.prefix
878    return IrcMsg(prefix=prefix, command='PASS', args=(password,), msg=msg)
879
880def ison(nick, prefix='', msg=None):
881    if conf.supybot.protocols.irc.strictRfc():
882        assert isNick(nick), repr(nick)
883    if msg and not prefix:
884        prefix = msg.prefix
885    return IrcMsg(prefix=prefix, command='ISON', args=(nick,), msg=msg)
886
887def monitor(subcommand, nicks=None, prefix='', msg=None):
888    if conf.supybot.protocols.irc.strictRfc():
889        for nick in nicks:
890            assert isNick(nick), repr(nick)
891        assert subcommand in '+-CLS'
892        if subcommand in 'CLS':
893            assert nicks is None
894    if msg and not prefix:
895        prefix = msg.prefix
896    if not isinstance(nicks, str):
897        nicks = ','.join(nicks)
898    return IrcMsg(prefix=prefix, command='MONITOR', args=(subcommand, nicks),
899            msg=msg)
900
901
902def error(s, msg=None):
903    return IrcMsg(command='ERROR', args=(s,), msg=msg)
904
905# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
906