Limnoria/src/ircutils.py

420 lines
12 KiB
Python
Raw Normal View History

2003-03-12 07:26:59 +01:00
#!/usr/bin/env python
###
# Copyright (c) 2002, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
2003-08-02 18:48:43 +02:00
"""
Provides a great number of useful utility functions IRC. Things to muck around
with hostmasks, set bold or color on strings, IRC-case-insensitive dicts, a
nick class to handle nicks (so comparisons and hashing and whatnot work in an
IRC-case-insensitive fashion), and numerous other things.
"""
2003-11-25 09:38:19 +01:00
__revision__ = "$Id$"
import fix
2003-03-12 07:26:59 +01:00
import re
import copy
2003-08-12 21:12:12 +02:00
import sets
import time
import random
2004-01-02 21:50:43 +01:00
import socket
2003-03-12 07:26:59 +01:00
import string
import fnmatch
import operator
from itertools import imap
from cStringIO import StringIO as sio
2003-03-12 07:26:59 +01:00
2004-01-16 18:33:51 +01:00
import utils
2003-03-12 07:26:59 +01:00
def isUserHostmask(s):
2003-04-20 11:32:52 +02:00
"""Returns whether or not the string s is a valid User hostmask."""
2003-03-12 07:26:59 +01:00
p1 = s.find('!')
p2 = s.find('@')
if p1 < p2-1 and p1 >= 1 and p2 >= 3 and len(s) > p2+1:
return True
else:
return False
def isServerHostmask(s):
2003-04-20 11:32:52 +02:00
"""Returns True if s is a valid server hostmask."""
return not isUserHostmask(s)
2003-03-12 07:26:59 +01:00
def nickFromHostmask(hostmask):
2003-04-20 11:32:52 +02:00
"""Returns the nick from a user hostmask."""
2003-04-03 10:17:21 +02:00
assert isUserHostmask(hostmask)
2003-08-12 21:12:12 +02:00
return hostmask.split('!', 1)[0]
2003-03-12 07:26:59 +01:00
def userFromHostmask(hostmask):
2003-04-20 11:32:52 +02:00
"""Returns the user from a user hostmask."""
2003-04-03 10:17:21 +02:00
assert isUserHostmask(hostmask)
2003-03-12 07:26:59 +01:00
return hostmask.split('!', 1)[1].split('@', 1)[0]
def hostFromHostmask(hostmask):
2003-04-20 11:32:52 +02:00
"""Returns the host from a user hostmask."""
2003-04-03 10:17:21 +02:00
assert isUserHostmask(hostmask)
2003-03-12 07:26:59 +01:00
return hostmask.split('@', 1)[1]
def splitHostmask(hostmask):
2003-04-20 11:32:52 +02:00
"""Returns the nick, user, host of a user hostmask."""
2003-04-03 10:17:21 +02:00
assert isUserHostmask(hostmask)
2003-08-12 21:12:12 +02:00
nick, rest = hostmask.split('!', 1)
2003-03-12 07:26:59 +01:00
user, host = rest.split('@', 1)
2003-08-12 21:12:12 +02:00
return (nick, user, host)
2003-03-12 07:26:59 +01:00
def joinHostmask(nick, ident, host):
2003-04-20 11:32:52 +02:00
"""Joins the nick, ident, host into a user hostmask."""
2003-04-03 10:17:21 +02:00
assert nick and ident and host
2003-03-12 07:26:59 +01:00
return '%s!%s@%s' % (nick, ident, host)
_lowertrans = string.maketrans(string.ascii_uppercase + r'\[]~',
string.ascii_lowercase + r'|{}^')
2003-04-20 11:32:52 +02:00
def toLower(s):
"""Returns the string s lowered according to IRC case rules."""
return intern(s.translate(_lowertrans))
2003-03-12 07:26:59 +01:00
def nickEqual(nick1, nick2):
2003-04-20 11:32:52 +02:00
"""Returns True if nick1 == nick2 according to IRC case rules."""
return toLower(nick1) == toLower(nick2)
2003-03-12 07:26:59 +01:00
2004-02-11 06:57:34 +01:00
2003-06-02 07:31:13 +02:00
_nickchars = r'_[]\`^{}|-'
2004-02-11 06:57:34 +01:00
nickRe = re.compile(r'^[A-Za-z%s][0-9A-Za-z%s]*$'
% (re.escape(_nickchars), re.escape(_nickchars)))
2003-12-10 19:49:45 +01:00
2003-03-12 07:26:59 +01:00
def isNick(s):
2003-04-20 11:32:52 +02:00
"""Returns True if s is a valid IRC nick."""
2004-02-24 12:21:12 +01:00
return bool(nickRe.match(s))
2003-03-12 07:26:59 +01:00
def isChannel(s):
2003-04-20 11:32:52 +02:00
"""Returns True if s is a valid IRC channel name."""
return (s and s[0] in '#&+!' and len(s) <= 50 and \
'\x07' not in s and ',' not in s and ' ' not in s)
2003-03-12 07:26:59 +01:00
2004-02-11 06:57:34 +01:00
def isCtcp(msg):
"""Returns whether or not msg is a CTCP message."""
return msg.command == 'PRIVMSG' and \
msg.args[1].startswith('\x01') and \
msg.args[1].endswith('\x01')
_patternCache = {}
def _hostmaskPatternEqual(pattern, hostmask):
2003-04-20 11:32:52 +02:00
"""Returns True if hostmask matches the hostmask pattern pattern."""
try:
return _patternCache[pattern](hostmask) is not None
except KeyError:
# We make our own regexps, rather than use fnmatch, because fnmatch's
# case-insensitivity is not IRC's case-insensitity.
fd = sio()
for c in pattern:
if c == '*':
fd.write('.*')
elif c == '?':
fd.write('.')
elif c in '[{':
fd.write('[[{]')
elif c in '}]':
fd.write(r'[}\]]')
elif c in '|\\':
fd.write(r'[|\\]')
elif c in '^~':
fd.write('[~^]')
else:
fd.write(re.escape(c))
fd.write('$')
f = re.compile(fd.getvalue(), re.I).match
_patternCache[pattern] = f
return f(hostmask) is not None
_hostmaskPatternEqualCache = {}
def hostmaskPatternEqual(pattern, hostmask):
try:
return _hostmaskPatternEqualCache[(pattern, hostmask)]
except KeyError:
b = _hostmaskPatternEqual(pattern, hostmask)
_hostmaskPatternEqualCache[(pattern, hostmask)] = b
return b
2003-03-12 07:26:59 +01:00
def banmask(hostmask):
"""Returns a properly generic banning hostmask for a hostmask.
>>> banmask('nick!user@host.domain.tld')
'*!*@*.domain.tld'
>>> banmask('nick!user@10.0.0.1')
'*!*@10.0.0.*'
"""
2003-10-02 06:47:33 +02:00
assert isUserHostmask(hostmask)
2003-03-12 07:26:59 +01:00
host = hostFromHostmask(hostmask)
2004-01-16 18:33:51 +01:00
if utils.isIP(host):
2004-01-02 21:50:43 +01:00
L = host.split('.')
L[-1] = '*'
return '*!*@' + '.'.join(L)
2004-01-16 18:33:51 +01:00
elif utils.isIPV6(host):
2004-01-02 21:50:43 +01:00
L = host.split(':')
L[-1] = '*'
return '*!*@' + ':'.join(L)
2003-03-12 07:26:59 +01:00
else:
if '.' in host:
return '*!*@*%s' % host[host.find('.'):]
else:
return '*!*@' + host
2003-03-12 07:26:59 +01:00
2004-02-11 07:57:35 +01:00
_plusRequireArguments = 'ovhblkqe'
_minusRequireArguments = 'ovhbkqe'
2004-02-11 07:57:35 +01:00
def separateModes(args):
2003-08-02 18:48:43 +02:00
"""Separates modelines into single mode change tuples. Basically, you
should give it the .args of a MODE IrcMsg.
2003-03-12 07:26:59 +01:00
Examples:
>>> separateModes(['+ooo', 'jemfinch', 'StoneTable', 'philmes'])
[('+o', 'jemfinch'), ('+o', 'StoneTable'), ('+o', 'philmes')]
>>> separateModes(['+o-o', 'jemfinch', 'PeterB'])
[('+o', 'jemfinch'), ('-o', 'PeterB')]
>>> separateModes(['+s-o', 'test'])
[('+s', None), ('-o', 'test')]
>>> separateModes(['+sntl', '100'])
[('+s', None), ('+n', None), ('+t', None), ('+l', 100)]
2003-03-12 07:26:59 +01:00
"""
2004-02-11 07:57:35 +01:00
if not args:
return []
2003-03-12 07:26:59 +01:00
modes = args[0]
assert modes[0] in '+-', 'Invalid args: %r' % args
2003-03-12 07:26:59 +01:00
args = list(args[1:])
ret = []
length = len(modes)
for c in modes:
if c in '+-':
last = c
2003-03-12 07:26:59 +01:00
else:
2004-02-11 07:57:35 +01:00
if last == '+':
requireArguments = _plusRequireArguments
else:
requireArguments = _minusRequireArguments
if c in requireArguments:
arg = args.pop(0)
try:
arg = int(arg)
except ValueError:
pass
ret.append((last + c, arg))
2003-03-12 07:26:59 +01:00
else:
ret.append((last + c, None))
2004-02-11 07:57:35 +01:00
return ret
2003-03-12 07:26:59 +01:00
def joinModes(modes):
"""Joins modes of the same form as returned by separateModes."""
args = []
modeChars = []
currentMode = '\x00'
for (mode, arg) in modes:
if arg is not None:
args.append(arg)
if not mode.startswith(currentMode):
currentMode = mode[0]
modeChars.append(mode[0])
modeChars.append(mode[1])
args.insert(0, ''.join(modeChars))
return args
2003-03-12 07:26:59 +01:00
def bold(s):
"""Returns the string s, bolded."""
return '\x02%s\x02' % s
def reverse(s):
"""Returns the string s, reverse-videoed."""
return '\x16%s\x16' % s
def underline(s):
"""Returns the string s, underlined."""
return '\x1F%s\x1F' % s
2003-03-12 07:26:59 +01:00
mircColors = {
None: '',
'white': 0,
'black': 1,
'blue': 2,
'green': 3,
'red': 4,
'brown': 5,
'purple': 6,
'orange': 7,
'yellow': 8,
'light green': 9,
'teal': 10,
'light blue': 11,
'dark blue': 12,
'pink': 13,
'dark grey': 14,
'light grey': 15,
}
2003-08-02 18:48:43 +02:00
# Offer a reverse mapping from integers to their associated colors.
for (k, v) in mircColors.items():
if k is not None: # Ignore empty string for None.
mircColors[v] = k
def mircColor(s, fg=None, bg=None):
2003-08-02 18:48:43 +02:00
"""Returns s with the appropriate mIRC color codes applied."""
if fg is None and bg is None:
return s
2003-07-30 13:04:59 +02:00
if fg is None or isinstance(fg, str):
fg = mircColors[fg]
if bg is None:
return '\x03%s%s\x03' % (fg, s)
else:
if isinstance(bg, str):
bg = mircColors[bg]
return '\x03%s,%s%s\x03' % (fg, bg, s)
2003-07-30 13:04:59 +02:00
def canonicalColor(s, bg=False, shift=0):
"""Assigns an (fg, bg) canonical color pair to a string based on its hash
value. This means it might change between Python versions. This pair can
be used as a *parameter to mircColor. The shift parameter is how much to
right-shift the hash value initially.
"""
h = hash(s) >> shift
2003-08-02 18:48:43 +02:00
fg = h % 14 + 2 # The + 2 is to rule out black and white.
if bg:
bg = (h >> 4) & 3 # The 5th, 6th, and 7th least significant bits.
if fg < 8:
bg += 8
else:
bg += 2
return (fg, bg)
2003-08-02 18:48:43 +02:00
else:
return (fg, None)
_unColorRe = re.compile(r'\x03(?:\d{1,2},\d{1,2}|\d{1,2}|,\d{1,2}|)')
def unColor(s):
"""Removes the color from a string."""
return _unColorRe.sub('', s)
def isValidArgument(s):
"""Returns whether s is strictly a valid argument for an IRC message."""
2003-03-12 07:26:59 +01:00
return '\r' not in s and '\n' not in s and '\x00' not in s
def safeArgument(s):
2003-04-20 11:32:52 +02:00
"""If s is unsafe for IRC, returns a safe version."""
if isinstance(s, unicode):
s = s.encode('utf-8')
if isValidArgument(s):
return s
else:
return repr(s)
2003-03-12 07:26:59 +01:00
def replyTo(msg):
2003-04-20 11:32:52 +02:00
"""Returns the appropriate target to send responses to msg."""
2003-03-12 07:26:59 +01:00
if isChannel(msg.args[0]):
return msg.args[0]
else:
return msg.nick
2003-10-09 18:12:53 +02:00
def dccIP(ip):
"""Returns in IP in the proper for DCC."""
2004-01-16 18:33:51 +01:00
assert utils.isIP(ip), \
'argument must be a string ip in xxx.yyy.zzz.www format.'
2003-10-09 18:12:53 +02:00
i = 0
x = 256**3
for quad in ip.split('.'):
i += int(quad)*x
x /= 256
return i
def unDccIP(i):
"""Takes an integer DCC IP and return a normal string IP."""
assert isinstance(i, (int, long)), '%r is not an number.' % i
L = []
while len(L) < 4:
L.append(i % 256)
2003-10-09 18:12:53 +02:00
i /= 256
L.reverse()
return '.'.join(imap(str, L))
class IrcString(str):
"""This class does case-insensitive comparison and hashing of nicks."""
def __init__(self, s):
2003-12-16 19:14:48 +01:00
assert isinstance(s, basestring), \
'Cannot make an IrcString from %s' % type(s)
2003-12-06 15:16:18 +01:00
str.__init__(self, intern(s)) # This does nothing, I fear.
self.lowered = toLower(s)
def __eq__(self, s):
try:
return toLower(s) == self.lowered
except:
return False
2003-03-12 07:26:59 +01:00
def __ne__(self, s):
2004-01-23 14:08:42 +01:00
return not (self == s)
def __hash__(self):
return hash(self.lowered)
2003-03-12 07:26:59 +01:00
class IrcDict(utils.InsensitivePreservingDict):
2003-04-20 11:32:52 +02:00
"""Subclass of dict to make key comparison IRC-case insensitive."""
key = staticmethod(toLower)
2004-01-19 23:29:55 +01:00
2003-08-12 21:12:12 +02:00
class IrcSet(sets.Set):
"""A sets.Set using IrcStrings instead of regular strings."""
def __init__(self, seq=()):
2003-11-04 00:25:04 +01:00
self.__parent = super(IrcSet, self)
self.__parent.__init__()
for elt in seq:
self.add(elt)
2003-11-04 00:25:04 +01:00
2003-08-12 21:12:12 +02:00
def add(self, s):
2003-11-04 00:25:04 +01:00
return self.__parent.add(IrcString(s))
2003-08-12 21:12:12 +02:00
def remove(self, s):
2003-11-04 00:25:04 +01:00
return self.__parent.remove(IrcString(s))
2003-08-12 21:12:12 +02:00
def discard(self, s):
2003-11-04 00:25:04 +01:00
return self.__parent.discard(IrcString(s))
2003-08-12 21:12:12 +02:00
def __contains__(self, s):
2003-11-04 00:25:04 +01:00
return self.__parent.__contains__(IrcString(s))
2003-08-12 21:12:12 +02:00
has_key = __contains__
def __reduce__(self):
return (self.__class__, (list(self),))
2003-03-12 07:26:59 +01:00
if __name__ == '__main__':
import sys, doctest
doctest.testmod(sys.modules['__main__'])
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: