1
# -*- test-case-name: twisted.words.test.test_irc -*-
2
# Copyright (c) 2001-2005 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
"""Internet Relay Chat Protocol for client and server.
8
Stability: semi-stable.
13
The way the IRCClient class works here encourages people to implement
14
IRC clients by subclassing the ephemeral protocol class, and it tends
15
to end up with way more state than it should for an object which will
16
be destroyed as soon as the TCP transport drops. Someone oughta do
17
something about that, ya know?
19
The DCC support needs to have more hooks for the client for it to be
20
able to ask the user things like \"Do you want to accept this session?\"
21
and \"Transfer #2 is 67% done.\" and otherwise manage the DCC sessions.
23
Test coverage needs to be better.
25
@author: U{Kevin Turner<mailto:acapnotic@twistedmatrix.com>}
27
@see: RFC 1459: Internet Relay Chat Protocol
28
@see: RFC 2812: Internet Relay Chat: Client Protocol
29
@see: U{The Client-To-Client-Protocol
30
<http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
33
__version__ = '$Revision: 1.94 $'[11:-2]
35
from twisted.internet import reactor, protocol
36
from twisted.persisted import styles
37
from twisted.protocols import basic
38
from twisted.python import log, reflect, text
63
CHANNEL_PREFIXES = '&#!+'
65
class IRCBadMessage(Exception):
68
class IRCPasswordMismatch(Exception):
72
"""Breaks a message from an IRC server into its prefix, command, and arguments.
77
raise IRCBadMessage("Empty line.")
79
prefix, s = s[1:].split(' ', 1)
80
if s.find(' :') != -1:
81
s, trailing = s.split(' :', 1)
87
return prefix, command, args
90
def split(str, length = 80):
91
"""I break a message into multiple lines.
93
I prefer to break at whitespace near str[length]. I also break at \\n.
95
@returns: list of strings
98
raise ValueError("Length must be a number greater than zero")
100
while len(str) > length:
101
w, n = str[:length].rfind(' '), str[:length].find('\n')
102
if w == -1 and n == -1:
103
line, str = str[:length], str[length:]
105
i = n == -1 and w or n
106
line, str = str[:i], str[i+1:]
109
r.extend(str.split('\n'))
112
class IRC(protocol.Protocol):
113
"""Internet Relay Chat server protocol.
121
def connectionMade(self):
123
if self.hostname is None:
124
self.hostname = socket.getfqdn()
127
def sendLine(self, line):
128
if self.encoding is not None:
129
if isinstance(line, unicode):
130
line = line.encode(self.encoding)
131
self.transport.write("%s%s%s" % (line, CR, LF))
134
def sendMessage(self, command, *parameter_list, **prefix):
135
"""Send a line formatted as an IRC message.
137
First argument is the command, all subsequent arguments
138
are parameters to that command. If a prefix is desired,
139
it may be specified with the keyword argument 'prefix'.
143
raise ValueError, "IRC message requires a command."
145
if ' ' in command or command[0] == ':':
146
# Not the ONLY way to screw up, but provides a little
147
# sanity checking to catch likely dumb mistakes.
148
raise ValueError, "Somebody screwed up, 'cuz this doesn't" \
149
" look like a command to me: %s" % command
151
line = string.join([command] + list(parameter_list))
152
if prefix.has_key('prefix'):
153
line = ":%s %s" % (prefix['prefix'], line)
156
if len(parameter_list) > 15:
157
log.msg("Message has %d parameters (RFC allows 15):\n%s" %
158
(len(parameter_list), line))
161
def dataReceived(self, data):
162
"""This hack is to support mIRC, which sends LF only,
163
even though the RFC says CRLF. (Also, the flexibility
164
of LineReceiver to turn "line mode" on and off was not
167
lines = (self.buffer + data).split(LF)
168
# Put the (possibly empty) element after the last LF back in the
170
self.buffer = lines.pop()
174
# This is a blank line, at best.
178
prefix, command, params = parsemsg(line)
179
# mIRC is a big pile of doo-doo
180
command = command.upper()
181
# DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
183
self.handleCommand(command, prefix, params)
186
def handleCommand(self, command, prefix, params):
187
"""Determine the function to call for the given command and call
188
it with the given arguments.
190
method = getattr(self, "irc_%s" % command, None)
192
if method is not None:
193
method(prefix, params)
195
self.irc_unknown(prefix, command, params)
200
def irc_unknown(self, prefix, command, params):
202
raise NotImplementedError(command, prefix, params)
206
def privmsg(self, sender, recip, message):
207
"""Send a message to a channel or user
209
@type sender: C{str} or C{unicode}
210
@param sender: Who is sending this message. Should be of the form
211
username!ident@hostmask (unless you know better!).
213
@type recip: C{str} or C{unicode}
214
@param recip: The recipient of this message. If a channel, it
215
must start with a channel prefix.
217
@type message: C{str} or C{unicode}
218
@param message: The message being sent.
220
self.sendLine(":%s PRIVMSG %s :%s" % (sender, recip, lowQuote(message)))
223
def notice(self, sender, recip, message):
224
"""Send a \"notice\" to a channel or user.
226
Notices differ from privmsgs in that the RFC claims they are different.
227
Robots are supposed to send notices and not respond to them. Clients
228
typically display notices differently from privmsgs.
230
@type sender: C{str} or C{unicode}
231
@param sender: Who is sending this message. Should be of the form
232
username!ident@hostmask (unless you know better!).
234
@type recip: C{str} or C{unicode}
235
@param recip: The recipient of this message. If a channel, it
236
must start with a channel prefix.
238
@type message: C{str} or C{unicode}
239
@param message: The message being sent.
241
self.sendLine(":%s NOTICE %s :%s" % (sender, recip, message))
244
def action(self, sender, recip, message):
245
"""Send an action to a channel or user.
247
@type sender: C{str} or C{unicode}
248
@param sender: Who is sending this message. Should be of the form
249
username!ident@hostmask (unless you know better!).
251
@type recip: C{str} or C{unicode}
252
@param recip: The recipient of this message. If a channel, it
253
must start with a channel prefix.
255
@type message: C{str} or C{unicode}
256
@param message: The action being sent.
258
self.sendLine(":%s ACTION %s :%s" % (sender, recip, message))
261
def topic(self, user, channel, topic, author=None):
262
"""Send the topic to a user.
264
@type user: C{str} or C{unicode}
265
@param user: The user receiving the topic. Only their nick name, not
268
@type channel: C{str} or C{unicode}
269
@param channel: The channel for which this is the topic.
271
@type topic: C{str} or C{unicode} or C{None}
272
@param topic: The topic string, unquoted, or None if there is
275
@type author: C{str} or C{unicode}
276
@param author: If the topic is being changed, the full username and hostmask
277
of the person changing it.
281
self.sendLine(':%s %s %s %s :%s' % (
282
self.hostname, RPL_NOTOPIC, user, channel, 'No topic is set.'))
284
self.sendLine(":%s %s %s %s :%s" % (
285
self.hostname, RPL_TOPIC, user, channel, lowQuote(topic)))
287
self.sendLine(":%s TOPIC %s :%s" % (author, channel, lowQuote(topic)))
290
def topicAuthor(self, user, channel, author, date):
292
Send the author of and time at which a topic was set for the given
295
This sends a 333 reply message, which is not part of the IRC RFC.
297
@type user: C{str} or C{unicode}
298
@param user: The user receiving the topic. Only their nick name, not
301
@type channel: C{str} or C{unicode}
302
@param channel: The channel for which this information is relevant.
304
@type author: C{str} or C{unicode}
305
@param author: The nickname (without hostmask) of the user who last
309
@param date: A POSIX timestamp (number of seconds since the epoch)
310
at which the topic was last set.
312
self.sendLine(':%s %d %s %s %s %d' % (
313
self.hostname, 333, user, channel, author, date))
316
def names(self, user, channel, names):
317
"""Send the names of a channel's participants to a user.
319
@type user: C{str} or C{unicode}
320
@param user: The user receiving the name list. Only their nick
321
name, not the full hostmask.
323
@type channel: C{str} or C{unicode}
324
@param channel: The channel for which this is the namelist.
326
@type names: C{list} of C{str} or C{unicode}
327
@param names: The names to send.
329
# XXX If unicode is given, these limits are not quite correct
330
prefixLength = len(channel) + len(user) + 10
331
namesLength = 512 - prefixLength
336
if count + len(n) + 1 > namesLength:
337
self.sendLine(":%s %s %s = %s :%s" % (
338
self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
345
self.sendLine(":%s %s %s = %s :%s" % (
346
self.hostname, RPL_NAMREPLY, user, channel, ' '.join(L)))
347
self.sendLine(":%s %s %s %s :End of /NAMES list" % (
348
self.hostname, RPL_ENDOFNAMES, user, channel))
351
def who(self, user, channel, memberInfo):
353
Send a list of users participating in a channel.
355
@type user: C{str} or C{unicode}
356
@param user: The user receiving this member information. Only their
357
nick name, not the full hostmask.
359
@type channel: C{str} or C{unicode}
360
@param channel: The channel for which this is the member
363
@type memberInfo: C{list} of C{tuples}
364
@param memberInfo: For each member of the given channel, a 7-tuple
365
containing their username, their hostmask, the server to which they
366
are connected, their nickname, the letter "H" or "G" (wtf do these
367
mean?), the hopcount from C{user} to this member, and this member's
370
for info in memberInfo:
371
(username, hostmask, server, nickname, flag, hops, realName) = info
372
assert flag in ("H", "G")
373
self.sendLine(":%s %s %s %s %s %s %s %s %s :%d %s" % (
374
self.hostname, RPL_WHOREPLY, user, channel,
375
username, hostmask, server, nickname, flag, hops, realName))
377
self.sendLine(":%s %s %s %s :End of /WHO list." % (
378
self.hostname, RPL_ENDOFWHO, user, channel))
381
def whois(self, user, nick, username, hostname, realName, server, serverInfo, oper, idle, signOn, channels):
383
Send information about the state of a particular user.
385
@type user: C{str} or C{unicode}
386
@param user: The user receiving this information. Only their nick
387
name, not the full hostmask.
389
@type nick: C{str} or C{unicode}
390
@param nick: The nickname of the user this information describes.
392
@type username: C{str} or C{unicode}
393
@param username: The user's username (eg, ident response)
395
@type hostname: C{str}
396
@param hostname: The user's hostmask
398
@type realName: C{str} or C{unicode}
399
@param realName: The user's real name
401
@type server: C{str} or C{unicode}
402
@param server: The name of the server to which the user is connected
404
@type serverInfo: C{str} or C{unicode}
405
@param serverInfo: A descriptive string about that server
408
@param oper: Indicates whether the user is an IRC operator
411
@param idle: The number of seconds since the user last sent a message
414
@param signOn: A POSIX timestamp (number of seconds since the epoch)
415
indicating the time the user signed on
417
@type channels: C{list} of C{str} or C{unicode}
418
@param channels: A list of the channels which the user is participating in
420
self.sendLine(":%s %s %s %s %s %s * :%s" % (
421
self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName))
422
self.sendLine(":%s %s %s %s %s :%s" % (
423
self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo))
425
self.sendLine(":%s %s %s %s :is an IRC operator" % (
426
self.hostname, RPL_WHOISOPERATOR, user, nick))
427
self.sendLine(":%s %s %s %s %d %d :seconds idle, signon time" % (
428
self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn))
429
self.sendLine(":%s %s %s %s :%s" % (
430
self.hostname, RPL_WHOISCHANNELS, user, nick, ' '.join(channels)))
431
self.sendLine(":%s %s %s %s :End of WHOIS list." % (
432
self.hostname, RPL_ENDOFWHOIS, user, nick))
435
def join(self, who, where):
436
"""Send a join message.
438
@type who: C{str} or C{unicode}
439
@param who: The name of the user joining. Should be of the form
440
username!ident@hostmask (unless you know better!).
442
@type where: C{str} or C{unicode}
443
@param where: The channel the user is joining.
445
self.sendLine(":%s JOIN %s" % (who, where))
448
def part(self, who, where, reason=None):
449
"""Send a part message.
451
@type who: C{str} or C{unicode}
452
@param who: The name of the user joining. Should be of the form
453
username!ident@hostmask (unless you know better!).
455
@type where: C{str} or C{unicode}
456
@param where: The channel the user is joining.
458
@type reason: C{str} or C{unicode}
459
@param reason: A string describing the misery which caused
460
this poor soul to depart.
463
self.sendLine(":%s PART %s :%s" % (who, where, reason))
465
self.sendLine(":%s PART %s" % (who, where))
468
def channelMode(self, user, channel, mode, *args):
470
Send information about the mode of a channel.
472
@type user: C{str} or C{unicode}
473
@param user: The user receiving the name list. Only their nick
474
name, not the full hostmask.
476
@type channel: C{str} or C{unicode}
477
@param channel: The channel for which this is the namelist.
480
@param mode: A string describing this channel's modes.
482
@param args: Any additional arguments required by the modes.
484
self.sendLine(":%s %s %s %s %s %s" % (
485
self.hostname, RPL_CHANNELMODEIS, user, channel, mode, ' '.join(args)))
488
class IRCClient(basic.LineReceiver):
489
"""Internet Relay Chat client protocol, with sprinkles.
491
In addition to providing an interface for an IRC client protocol,
492
this class also contains reasonable implementations of many common
497
- Limit the length of messages sent (because the IRC server probably
499
- Add flood protection/rate limiting for my CTCP replies.
500
- NickServ cooperation. (a mix-in?)
501
- Heartbeat. The transport may die in such a way that it does not realize
502
it is dead until it is written to. Sending something (like \"PING
503
this.irc-host.net\") during idle peroids would alleviate that. If
504
you're concerned with the stability of the host as well as that of the
505
transport, you might care to watch for the corresponding PONG.
507
@ivar nickname: Nickname the client will use.
508
@ivar password: Password used to log on to the server. May be C{None}.
509
@ivar realname: Supplied to the server during login as the \"Real name\"
510
or \"ircname\". May be C{None}.
511
@ivar username: Supplied to the server during login as the \"User name\".
514
@ivar userinfo: Sent in reply to a X{USERINFO} CTCP query. If C{None}, no
515
USERINFO reply will be sent.
516
\"This is used to transmit a string which is settable by
517
the user (and never should be set by the client).\"
518
@ivar fingerReply: Sent in reply to a X{FINGER} CTCP query. If C{None}, no
519
FINGER reply will be sent.
520
@type fingerReply: Callable or String
522
@ivar versionName: CTCP VERSION reply, client name. If C{None}, no VERSION
524
@ivar versionNum: CTCP VERSION reply, client version,
525
@ivar versionEnv: CTCP VERSION reply, environment the client is running in.
527
@ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
528
client may be found. If C{None}, no SOURCE reply will be sent.
530
@ivar lineRate: Minimum delay between lines sent to the server. If
531
C{None}, no delay will be imposed.
532
@type lineRate: Number of Seconds.
540
### Responses to various CTCP queries.
543
# fingerReply is a callable returning a string, or a str()able object.
549
sourceURL = "http://twistedmatrix.com/downloads/"
554
# If this is false, no attempt will be made to identify
555
# ourself to the server.
560
_queueEmptying = None
562
delimiter = '\n' # '\r\n' will also work (see dataReceived)
564
__pychecker__ = 'unusednames=params,prefix,channel'
567
def _reallySendLine(self, line):
568
return basic.LineReceiver.sendLine(self, lowQuote(line) + '\r')
570
def sendLine(self, line):
571
if self.lineRate is None:
572
self._reallySendLine(line)
574
self._queue.append(line)
575
if not self._queueEmptying:
580
self._reallySendLine(self._queue.pop(0))
581
self._queueEmptying = reactor.callLater(self.lineRate,
584
self._queueEmptying = None
587
### Interface level client->user output methods
589
### You'll want to override these.
591
### Methods relating to the server itself
593
def created(self, when):
594
"""Called with creation date information about the server, usually at logon.
597
@param when: A string describing when the server was created, probably.
600
def yourHost(self, info):
601
"""Called with daemon information about the server, usually at logon.
604
@param when: A string describing what software the server is running, probably.
607
def myInfo(self, servername, version, umodes, cmodes):
608
"""Called with information about the server, usually at logon.
610
@type servername: C{str}
611
@param servername: The hostname of this server.
613
@type version: C{str}
614
@param version: A description of what software this server runs.
617
@param umodes: All the available user modes.
620
@param cmodes: All the available channel modes.
623
def luserClient(self, info):
624
"""Called with information about the number of connections, usually at logon.
627
@param info: A description of the number of clients and servers
628
connected to the network, probably.
631
def bounce(self, info):
632
"""Called with information about where the client should reconnect.
635
@param info: A plaintext description of the address that should be
639
def isupport(self, options):
640
"""Called with various information about what the server supports.
642
@type options: C{list} of C{str}
643
@param options: Descriptions of features or limits of the server, possibly
644
in the form "NAME=VALUE".
647
def luserChannels(self, channels):
648
"""Called with the number of channels existant on the server.
650
@type channels: C{int}
653
def luserOp(self, ops):
654
"""Called with the number of ops logged on to the server.
659
def luserMe(self, info):
660
"""Called with information about the server connected to.
663
@param info: A plaintext string describing the number of users and servers
664
connected to this server.
667
### Methods involving me directly
669
def privmsg(self, user, channel, message):
670
"""Called when I have a message from a user to me or a channel.
674
def joined(self, channel):
675
"""Called when I finish joining a channel.
677
channel has the starting character (# or &) intact.
681
def left(self, channel):
682
"""Called when I have left a channel.
684
channel has the starting character (# or &) intact.
688
def noticed(self, user, channel, message):
689
"""Called when I have a notice from a user to me or a channel.
691
By default, this is equivalent to IRCClient.privmsg, but if your
692
client makes any automated replies, you must override this!
695
The difference between NOTICE and PRIVMSG is that
696
automatic replies MUST NEVER be sent in response to a
697
NOTICE message. [...] The object of this rule is to avoid
698
loops between clients automatically sending something in
699
response to something it received.
701
self.privmsg(user, channel, message)
703
def modeChanged(self, user, channel, set, modes, args):
704
"""Called when a channel's modes are changed
707
@param user: The user and hostmask which instigated this change.
709
@type channel: C{str}
710
@param channel: The channel for which the modes are changing.
712
@type set: C{bool} or C{int}
713
@param set: true if the mode is being added, false if it is being
717
@param modes: The mode or modes which are being changed.
720
@param args: Any additional information required for the mode
724
def pong(self, user, secs):
725
"""Called with the results of a CTCP PING query.
730
"""Called after sucessfully signing on to the server.
734
def kickedFrom(self, channel, kicker, message):
735
"""Called when I am kicked from a channel.
739
def nickChanged(self, nick):
740
"""Called when my nick has been changed.
745
### Things I observe other people doing in a channel.
747
def userJoined(self, user, channel):
748
"""Called when I see another user joining a channel.
752
def userLeft(self, user, channel):
753
"""Called when I see another user leaving a channel.
757
def userQuit(self, user, quitMessage):
758
"""Called when I see another user disconnect from the network.
762
def userKicked(self, kickee, channel, kicker, message):
763
"""Called when I observe someone else being kicked from a channel.
767
def action(self, user, channel, data):
768
"""Called when I see a user perform an ACTION on a channel.
772
def topicUpdated(self, user, channel, newTopic):
773
"""In channel, user changed the topic to newTopic.
775
Also called when first joining a channel.
779
def userRenamed(self, oldname, newname):
780
"""A user changed their name from oldname to newname.
784
### Information from the server.
786
def receivedMOTD(self, motd):
787
"""I received a message-of-the-day banner from the server.
789
motd is a list of strings, where each string was sent as a seperate
790
message from the server. To display, you might want to use::
792
string.join(motd, '\\n')
794
to get a nicely formatted string.
798
### user input commands, client->server
799
### Your client will want to invoke these.
801
def join(self, channel, key=None):
802
if channel[0] not in '&#!+': channel = '#' + channel
804
self.sendLine("JOIN %s %s" % (channel, key))
806
self.sendLine("JOIN %s" % (channel,))
808
def leave(self, channel, reason=None):
809
if channel[0] not in '&#!+': channel = '#' + channel
811
self.sendLine("PART %s :%s" % (channel, reason))
813
self.sendLine("PART %s" % (channel,))
815
def kick(self, channel, user, reason=None):
816
if channel[0] not in '&#!+': channel = '#' + channel
818
self.sendLine("KICK %s %s :%s" % (channel, user, reason))
820
self.sendLine("KICK %s %s" % (channel, user))
824
def topic(self, channel, topic=None):
825
"""Attempt to set the topic of the given channel, or ask what it is.
827
If topic is None, then I sent a topic query instead of trying to set
828
the topic. The server should respond with a TOPIC message containing
829
the current topic of the given channel.
831
# << TOPIC #xtestx :fff
832
if channel[0] not in '&#!+': channel = '#' + channel
834
self.sendLine("TOPIC %s :%s" % (channel, topic))
836
self.sendLine("TOPIC %s" % (channel,))
838
def mode(self, chan, set, modes, limit = None, user = None, mask = None):
839
"""Change the modes on a user or channel."""
841
line = 'MODE %s +%s' % (chan, modes)
843
line = 'MODE %s -%s' % (chan, modes)
844
if limit is not None:
845
line = '%s %d' % (line, limit)
846
elif user is not None:
847
line = '%s %s' % (line, user)
848
elif mask is not None:
849
line = '%s %s' % (line, mask)
853
def say(self, channel, message, length = None):
854
if channel[0] not in '&#!+': channel = '#' + channel
855
self.msg(channel, message, length)
857
def msg(self, user, message, length = None):
858
"""Send a message to a user or channel.
861
@param user: The username or channel name to which to direct the
864
@type message: C{str}
865
@param message: The text to send
868
@param length: The maximum number of octets to send at a time. This
869
has the effect of turning a single call to msg() into multiple
870
commands to the server. This is useful when long messages may be
871
sent that would otherwise cause the server to kick us off or silently
872
truncate the text we are sending. If None is passed, the entire
873
message is always send in one command.
876
fmt = "PRIVMSG %s :%%s" % (user,)
879
self.sendLine(fmt % (message,))
881
# NOTE: minimumLength really equals len(fmt) - 2 (for '%s') + n
882
# where n is how many bytes sendLine sends to end the line.
883
# n was magic numbered to 2, I think incorrectly
884
minimumLength = len(fmt)
885
if length <= minimumLength:
886
raise ValueError("Maximum length must exceed %d for message "
887
"to %s" % (minimumLength, user))
888
lines = split(message, length - minimumLength)
889
map(lambda line, self=self, fmt=fmt: self.sendLine(fmt % line),
892
def notice(self, user, message):
893
self.sendLine("NOTICE %s :%s" % (user, message))
895
def away(self, message=''):
896
self.sendLine("AWAY :%s" % message)
898
def register(self, nickname, hostname='foo', servername='bar'):
899
if self.password is not None:
900
self.sendLine("PASS %s" % self.password)
901
self.setNick(nickname)
902
if self.username is None:
903
self.username = nickname
904
self.sendLine("USER %s foo bar :%s" % (self.username, self.realname))
906
def setNick(self, nickname):
907
self.nickname = nickname
908
self.sendLine("NICK %s" % nickname)
910
def quit(self, message = ''):
911
self.sendLine("QUIT :%s" % message)
913
### user input commands, client->client
915
def me(self, channel, action):
918
if channel[0] not in '&#!+': channel = '#' + channel
919
self.ctcpMakeQuery(channel, [('ACTION', action)])
924
def ping(self, user, text = None):
925
"""Measure round-trip delay to another IRC client.
927
if self._pings is None:
931
chars = string.letters + string.digits + string.punctuation
932
key = ''.join([random.choice(chars) for i in range(12)])
935
self._pings[(user, key)] = time.time()
936
self.ctcpMakeQuery(user, [('PING', key)])
938
if len(self._pings) > self._MAX_PINGRING:
939
# Remove some of the oldest entries.
940
byValue = [(v, k) for (k, v) in self._pings.items()]
942
excess = self._MAX_PINGRING - len(self._pings)
943
for i in xrange(excess):
944
del self._pings[byValue[i][1]]
946
def dccSend(self, user, file):
947
if type(file) == types.StringType:
948
file = open(file, 'r')
950
size = fileSize(file)
952
name = getattr(file, "name", "file@%s" % (id(file),))
954
factory = DccSendFactory(file)
955
port = reactor.listenTCP(0, factory, 1)
957
raise NotImplementedError,(
958
"XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. "
959
"(and stop accepting once we've made a single connection.)")
961
my_address = struct.pack("!I", my_address)
963
args = ['SEND', name, my_address, str(port)]
965
if not (size is None):
968
args = string.join(args, ' ')
970
self.ctcpMakeQuery(user, [('DCC', args)])
972
def dccResume(self, user, fileName, port, resumePos):
973
"""Send a DCC RESUME request to another user."""
974
self.ctcpMakeQuery(user, [
975
('DCC', ['RESUME', fileName, port, resumePos])])
977
def dccAcceptResume(self, user, fileName, port, resumePos):
978
"""Send a DCC ACCEPT response to clients who have requested a resume.
980
self.ctcpMakeQuery(user, [
981
('DCC', ['ACCEPT', fileName, port, resumePos])])
983
### server->client messages
984
### You might want to fiddle with these,
985
### but it is safe to leave them alone.
987
def irc_ERR_NICKNAMEINUSE(self, prefix, params):
988
self.register(self.nickname+'_')
990
def irc_ERR_PASSWDMISMATCH(self, prefix, params):
991
raise IRCPasswordMismatch("Password Incorrect.")
993
def irc_RPL_WELCOME(self, prefix, params):
996
def irc_JOIN(self, prefix, params):
997
nick = string.split(prefix,'!')[0]
999
if nick == self.nickname:
1000
self.joined(channel)
1002
self.userJoined(nick, channel)
1004
def irc_PART(self, prefix, params):
1005
nick = string.split(prefix,'!')[0]
1007
if nick == self.nickname:
1010
self.userLeft(nick, channel)
1012
def irc_QUIT(self, prefix, params):
1013
nick = string.split(prefix,'!')[0]
1014
self.userQuit(nick, params[0])
1016
def irc_MODE(self, prefix, params):
1017
channel, rest = params[0], params[1:]
1018
set = rest[0][0] == '+'
1021
self.modeChanged(prefix, channel, set, modes, tuple(args))
1023
def irc_PING(self, prefix, params):
1024
self.sendLine("PONG %s" % params[-1])
1026
def irc_PRIVMSG(self, prefix, params):
1029
message = params[-1]
1031
if not message: return # don't raise an exception if some idiot sends us a blank message
1033
if message[0]==X_DELIM:
1034
m = ctcpExtract(message)
1036
self.ctcpQuery(user, channel, m['extended'])
1041
message = string.join(m['normal'], ' ')
1043
self.privmsg(user, channel, message)
1045
def irc_NOTICE(self, prefix, params):
1048
message = params[-1]
1050
if message[0]==X_DELIM:
1051
m = ctcpExtract(message)
1053
self.ctcpReply(user, channel, m['extended'])
1058
message = string.join(m['normal'], ' ')
1060
self.noticed(user, channel, message)
1062
def irc_NICK(self, prefix, params):
1063
nick = string.split(prefix,'!', 1)[0]
1064
if nick == self.nickname:
1065
self.nickChanged(params[0])
1067
self.userRenamed(nick, params[0])
1069
def irc_KICK(self, prefix, params):
1070
"""Kicked? Who? Not me, I hope.
1072
kicker = string.split(prefix,'!')[0]
1075
message = params[-1]
1076
if string.lower(kicked) == string.lower(self.nickname):
1078
self.kickedFrom(channel, kicker, message)
1080
self.userKicked(kicked, channel, kicker, message)
1082
def irc_TOPIC(self, prefix, params):
1083
"""Someone in the channel set the topic.
1085
user = string.split(prefix, '!')[0]
1087
newtopic = params[1]
1088
self.topicUpdated(user, channel, newtopic)
1090
def irc_RPL_TOPIC(self, prefix, params):
1091
"""I just joined the channel, and the server is telling me the current topic.
1093
user = string.split(prefix, '!')[0]
1095
newtopic = params[2]
1096
self.topicUpdated(user, channel, newtopic)
1098
def irc_RPL_NOTOPIC(self, prefix, params):
1099
user = string.split(prefix, '!')[0]
1102
self.topicUpdated(user, channel, newtopic)
1104
def irc_RPL_MOTDSTART(self, prefix, params):
1105
if params[-1].startswith("- "):
1106
params[-1] = params[-1][2:]
1107
self.motd = [params[-1]]
1109
def irc_RPL_MOTD(self, prefix, params):
1110
if params[-1].startswith("- "):
1111
params[-1] = params[-1][2:]
1112
self.motd.append(params[-1])
1114
def irc_RPL_ENDOFMOTD(self, prefix, params):
1115
self.receivedMOTD(self.motd)
1117
def irc_RPL_CREATED(self, prefix, params):
1118
self.created(params[1])
1120
def irc_RPL_YOURHOST(self, prefix, params):
1121
self.yourHost(params[1])
1123
def irc_RPL_MYINFO(self, prefix, params):
1124
info = params[1].split(None, 3)
1125
while len(info) < 4:
1129
def irc_RPL_BOUNCE(self, prefix, params):
1130
# 005 is doubly assigned. Piece of crap dirty trash protocol.
1131
if params[-1] == "are available on this server":
1132
self.isupport(params[1:-1])
1134
self.bounce(params[1])
1136
def irc_RPL_LUSERCLIENT(self, prefix, params):
1137
self.luserClient(params[1])
1139
def irc_RPL_LUSEROP(self, prefix, params):
1141
self.luserOp(int(params[1]))
1145
def irc_RPL_LUSERCHANNELS(self, prefix, params):
1147
self.luserChannels(int(params[1]))
1151
def irc_RPL_LUSERME(self, prefix, params):
1152
self.luserMe(params[1])
1154
def irc_unknown(self, prefix, command, params):
1157
### Receiving a CTCP query from another party
1158
### It is safe to leave these alone.
1160
def ctcpQuery(self, user, channel, messages):
1161
"""Dispatch method for any CTCP queries received.
1164
method = getattr(self, "ctcpQuery_%s" % m[0], None)
1166
method(user, channel, m[1])
1168
self.ctcpUnknownQuery(user, channel, m[0], m[1])
1170
def ctcpQuery_ACTION(self, user, channel, data):
1171
self.action(user, channel, data)
1173
def ctcpQuery_PING(self, user, channel, data):
1174
nick = string.split(user,"!")[0]
1175
self.ctcpMakeReply(nick, [("PING", data)])
1177
def ctcpQuery_FINGER(self, user, channel, data):
1178
if data is not None:
1179
self.quirkyMessage("Why did %s send '%s' with a FINGER query?"
1181
if not self.fingerReply:
1184
if callable(self.fingerReply):
1185
reply = self.fingerReply()
1187
reply = str(self.fingerReply)
1189
nick = string.split(user,"!")[0]
1190
self.ctcpMakeReply(nick, [('FINGER', reply)])
1192
def ctcpQuery_VERSION(self, user, channel, data):
1193
if data is not None:
1194
self.quirkyMessage("Why did %s send '%s' with a VERSION query?"
1197
if self.versionName:
1198
nick = string.split(user,"!")[0]
1199
self.ctcpMakeReply(nick, [('VERSION', '%s:%s:%s' %
1204
def ctcpQuery_SOURCE(self, user, channel, data):
1205
if data is not None:
1206
self.quirkyMessage("Why did %s send '%s' with a SOURCE query?"
1209
nick = string.split(user,"!")[0]
1210
# The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
1211
# replies should be responded to with the location of an anonymous
1212
# FTP server in host:directory:file format. I'm taking the liberty
1213
# of bringing it into the 21st century by sending a URL instead.
1214
self.ctcpMakeReply(nick, [('SOURCE', self.sourceURL),
1217
def ctcpQuery_USERINFO(self, user, channel, data):
1218
if data is not None:
1219
self.quirkyMessage("Why did %s send '%s' with a USERINFO query?"
1222
nick = string.split(user,"!")[0]
1223
self.ctcpMakeReply(nick, [('USERINFO', self.userinfo)])
1225
def ctcpQuery_CLIENTINFO(self, user, channel, data):
1226
"""A master index of what CTCP tags this client knows.
1228
If no arguments are provided, respond with a list of known tags.
1229
If an argument is provided, provide human-readable help on
1230
the usage of that tag.
1233
nick = string.split(user,"!")[0]
1235
# XXX: prefixedMethodNames gets methods from my *class*,
1236
# but it's entirely possible that this *instance* has more
1238
names = reflect.prefixedMethodNames(self.__class__,
1241
self.ctcpMakeReply(nick, [('CLIENTINFO',
1242
string.join(names, ' '))])
1244
args = string.split(data)
1245
method = getattr(self, 'ctcpQuery_%s' % (args[0],), None)
1247
self.ctcpMakeReply(nick, [('ERRMSG',
1249
"Unknown query '%s'"
1250
% (data, args[0]))])
1252
doc = getattr(method, '__doc__', '')
1253
self.ctcpMakeReply(nick, [('CLIENTINFO', doc)])
1256
def ctcpQuery_ERRMSG(self, user, channel, data):
1257
# Yeah, this seems strange, but that's what the spec says to do
1258
# when faced with an ERRMSG query (not a reply).
1259
nick = string.split(user,"!")[0]
1260
self.ctcpMakeReply(nick, [('ERRMSG',
1261
"%s :No error has occoured." % data)])
1263
def ctcpQuery_TIME(self, user, channel, data):
1264
if data is not None:
1265
self.quirkyMessage("Why did %s send '%s' with a TIME query?"
1267
nick = string.split(user,"!")[0]
1268
self.ctcpMakeReply(nick,
1270
time.asctime(time.localtime(time.time())))])
1272
def ctcpQuery_DCC(self, user, channel, data):
1273
"""Initiate a Direct Client Connection
1277
dcctype = data.split(None, 1)[0].upper()
1278
handler = getattr(self, "dcc_" + dcctype, None)
1280
if self.dcc_sessions is None:
1281
self.dcc_sessions = []
1282
data = data[len(dcctype)+1:]
1283
handler(user, channel, data)
1285
nick = string.split(user,"!")[0]
1286
self.ctcpMakeReply(nick, [('ERRMSG',
1287
"DCC %s :Unknown DCC type '%s'"
1288
% (data, dcctype))])
1289
self.quirkyMessage("%s offered unknown DCC type %s"
1292
def dcc_SEND(self, user, channel, data):
1293
# Use splitQuoted for those who send files with spaces in the names.
1294
data = text.splitQuoted(data)
1296
raise IRCBadMessage, "malformed DCC SEND request: %r" % (data,)
1298
(filename, address, port) = data[:3]
1300
address = dccParseAddress(address)
1304
raise IRCBadMessage, "Indecipherable port %r" % (port,)
1313
# XXX Should we bother passing this data?
1314
self.dccDoSend(user, address, port, filename, size, data)
1316
def dcc_ACCEPT(self, user, channel, data):
1317
data = text.splitQuoted(data)
1319
raise IRCBadMessage, "malformed DCC SEND ACCEPT request: %r" % (data,)
1320
(filename, port, resumePos) = data[:3]
1323
resumePos = int(resumePos)
1327
self.dccDoAcceptResume(user, filename, port, resumePos)
1329
def dcc_RESUME(self, user, channel, data):
1330
data = text.splitQuoted(data)
1332
raise IRCBadMessage, "malformed DCC SEND RESUME request: %r" % (data,)
1333
(filename, port, resumePos) = data[:3]
1336
resumePos = int(resumePos)
1339
self.dccDoResume(user, filename, port, resumePos)
1341
def dcc_CHAT(self, user, channel, data):
1342
data = text.splitQuoted(data)
1344
raise IRCBadMessage, "malformed DCC CHAT request: %r" % (data,)
1346
(filename, address, port) = data[:3]
1348
address = dccParseAddress(address)
1352
raise IRCBadMessage, "Indecipherable port %r" % (port,)
1354
self.dccDoChat(user, channel, address, port, data)
1356
### The dccDo methods are the slightly higher-level siblings of
1357
### common dcc_ methods; the arguments have been parsed for them.
1359
def dccDoSend(self, user, address, port, fileName, size, data):
1360
"""Called when I receive a DCC SEND offer from a client.
1362
By default, I do nothing here."""
1363
## filename = path.basename(arg)
1364
## protocol = DccFileReceive(filename, size,
1365
## (user,channel,data),self.dcc_destdir)
1366
## reactor.clientTCP(address, port, protocol)
1367
## self.dcc_sessions.append(protocol)
1370
def dccDoResume(self, user, file, port, resumePos):
1371
"""Called when a client is trying to resume an offered file
1372
via DCC send. It should be either replied to with a DCC
1373
ACCEPT or ignored (default)."""
1376
def dccDoAcceptResume(self, user, file, port, resumePos):
1377
"""Called when a client has verified and accepted a DCC resume
1378
request made by us. By default it will do nothing."""
1381
def dccDoChat(self, user, channel, address, port, data):
1383
#factory = DccChatFactory(self, queryData=(user, channel, data))
1384
#reactor.connectTCP(address, port, factory)
1385
#self.dcc_sessions.append(factory)
1387
#def ctcpQuery_SED(self, user, data):
1388
# """Simple Encryption Doodoo
1390
# Feel free to implement this, but no specification is available.
1392
# raise NotImplementedError
1394
def ctcpUnknownQuery(self, user, channel, tag, data):
1395
nick = string.split(user,"!")[0]
1396
self.ctcpMakeReply(nick, [('ERRMSG',
1397
"%s %s: Unknown query '%s'"
1398
% (tag, data, tag))])
1400
log.msg("Unknown CTCP query from %s: %s %s\n"
1401
% (user, tag, data))
1403
def ctcpMakeReply(self, user, messages):
1404
"""Send one or more X{extended messages} as a CTCP reply.
1406
@type messages: a list of extended messages. An extended
1407
message is a (tag, data) tuple, where 'data' may be C{None}.
1409
self.notice(user, ctcpStringify(messages))
1411
### client CTCP query commands
1413
def ctcpMakeQuery(self, user, messages):
1414
"""Send one or more X{extended messages} as a CTCP query.
1416
@type messages: a list of extended messages. An extended
1417
message is a (tag, data) tuple, where 'data' may be C{None}.
1419
self.msg(user, ctcpStringify(messages))
1421
### Receiving a response to a CTCP query (presumably to one we made)
1422
### You may want to add methods here, or override UnknownReply.
1424
def ctcpReply(self, user, channel, messages):
1425
"""Dispatch method for any CTCP replies received.
1428
method = getattr(self, "ctcpReply_%s" % m[0], None)
1430
method(user, channel, m[1])
1432
self.ctcpUnknownReply(user, channel, m[0], m[1])
1434
def ctcpReply_PING(self, user, channel, data):
1435
nick = user.split('!', 1)[0]
1436
if (not self._pings) or (not self._pings.has_key((nick, data))):
1437
raise IRCBadMessage,\
1438
"Bogus PING response from %s: %s" % (user, data)
1440
t0 = self._pings[(nick, data)]
1441
self.pong(user, time.time() - t0)
1443
def ctcpUnknownReply(self, user, channel, tag, data):
1444
"""Called when a fitting ctcpReply_ method is not found.
1446
XXX: If the client makes arbitrary CTCP queries,
1447
this method should probably show the responses to
1448
them instead of treating them as anomolies.
1450
log.msg("Unknown CTCP reply from %s: %s %s\n"
1451
% (user, tag, data))
1454
### You may override these with something more appropriate to your UI.
1456
def badMessage(self, line, excType, excValue, tb):
1457
"""When I get a message that's so broken I can't use it.
1460
log.msg(string.join(traceback.format_exception(excType,
1464
def quirkyMessage(self, s):
1465
"""This is called when I receive a message which is peculiar,
1466
but not wholly indecipherable.
1470
### Protocool methods
1472
def connectionMade(self):
1474
if self.performLogin:
1475
self.register(self.nickname)
1477
def dataReceived(self, data):
1478
basic.LineReceiver.dataReceived(self, data.replace('\r', ''))
1480
def lineReceived(self, line):
1481
line = lowDequote(line)
1483
prefix, command, params = parsemsg(line)
1484
if numeric_to_symbolic.has_key(command):
1485
command = numeric_to_symbolic[command]
1486
self.handleCommand(command, prefix, params)
1487
except IRCBadMessage:
1488
self.badMessage(line, *sys.exc_info())
1491
def handleCommand(self, command, prefix, params):
1492
"""Determine the function to call for the given command and call
1493
it with the given arguments.
1495
method = getattr(self, "irc_%s" % command, None)
1497
if method is not None:
1498
method(prefix, params)
1500
self.irc_unknown(prefix, command, params)
1505
def __getstate__(self):
1506
dct = self.__dict__.copy()
1507
dct['dcc_sessions'] = None
1508
dct['_pings'] = None
1512
def dccParseAddress(address):
1517
address = long(address)
1519
raise IRCBadMessage,\
1520
"Indecipherable address %r" % (address,)
1523
(address >> 24) & 0xFF,
1524
(address >> 16) & 0xFF,
1525
(address >> 8) & 0xFF,
1528
address = '.'.join(map(str,address))
1532
class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
1533
"""Bare protocol to receive a Direct Client Connection SEND stream.
1535
This does enough to keep the other guy talking, but you'll want to
1536
extend my dataReceived method to *do* something with the data I get.
1541
def __init__(self, resumeOffset=0):
1542
self.bytesReceived = resumeOffset
1543
self.resume = (resumeOffset != 0)
1545
def dataReceived(self, data):
1546
"""Called when data is received.
1548
Warning: This just acknowledges to the remote host that the
1549
data has been received; it doesn't *do* anything with the
1550
data, so you'll want to override this.
1552
self.bytesReceived = self.bytesReceived + len(data)
1553
self.transport.write(struct.pack('!i', self.bytesReceived))
1556
class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
1557
"""Protocol for an outgoing Direct Client Connection SEND.
1566
def __init__(self, file):
1567
if type(file) is types.StringType:
1568
self.file = open(file, 'r')
1570
def connectionMade(self):
1574
def dataReceived(self, data):
1575
# XXX: Do we need to check to see if len(data) != fmtsize?
1577
bytesShesGot = struct.unpack("!I", data)
1578
if bytesShesGot < self.bytesSent:
1580
# XXX? Add some checks to see if we've stalled out?
1582
elif bytesShesGot > self.bytesSent:
1583
# self.transport.log("DCC SEND %s: She says she has %d bytes "
1584
# "but I've only sent %d. I'm stopping "
1585
# "this screwy transfer."
1587
# bytesShesGot, self.bytesSent))
1588
self.transport.loseConnection()
1593
def sendBlock(self):
1594
block = self.file.read(self.blocksize)
1596
self.transport.write(block)
1597
self.bytesSent = self.bytesSent + len(block)
1599
# Nothing more to send, transfer complete.
1600
self.transport.loseConnection()
1603
def connectionLost(self, reason):
1605
if hasattr(self.file, "close"):
1609
class DccSendFactory(protocol.Factory):
1610
protocol = DccSendProtocol
1611
def __init__(self, file):
1614
def buildProtocol(self, connection):
1615
p = self.protocol(self.file)
1621
"""I'll try my damndest to determine the size of this file object.
1624
if hasattr(file, "fileno"):
1625
fileno = file.fileno()
1627
stat_ = os.fstat(fileno)
1628
size = stat_[stat.ST_SIZE]
1634
if hasattr(file, "name") and path.exists(file.name):
1636
size = path.getsize(file.name)
1642
if hasattr(file, "seek") and hasattr(file, "tell"):
1656
class DccChat(basic.LineReceiver, styles.Ephemeral):
1657
"""Direct Client Connection protocol type CHAT.
1659
DCC CHAT is really just your run o' the mill basic.LineReceiver
1660
protocol. This class only varies from that slightly, accepting
1661
either LF or CR LF for a line delimeter for incoming messages
1662
while always using CR LF for outgoing.
1664
The lineReceived method implemented here uses the DCC connection's
1665
'client' attribute (provided upon construction) to deliver incoming
1666
lines from the DCC chat via IRCClient's normal privmsg interface.
1667
That's something of a spoof, which you may well want to override.
1676
def __init__(self, client, queryData=None):
1677
"""Initialize a new DCC CHAT session.
1679
queryData is a 3-tuple of
1680
(fromUser, targetUserOrChannel, data)
1681
as received by the CTCP query.
1683
(To be honest, fromUser is the only thing that's currently
1684
used here. targetUserOrChannel is potentially useful, while
1685
the 'data' argument is soley for informational purposes.)
1687
self.client = client
1689
self.queryData = queryData
1690
self.remoteParty = self.queryData[0]
1692
def dataReceived(self, data):
1693
self.buffer = self.buffer + data
1694
lines = string.split(self.buffer, LF)
1695
# Put the (possibly empty) element after the last LF back in the
1697
self.buffer = lines.pop()
1702
self.lineReceived(line)
1704
def lineReceived(self, line):
1705
log.msg("DCC CHAT<%s> %s" % (self.remoteParty, line))
1706
self.client.privmsg(self.remoteParty,
1707
self.client.nickname, line)
1710
class DccChatFactory(protocol.ClientFactory):
1713
def __init__(self, client, queryData):
1714
self.client = client
1715
self.queryData = queryData
1717
def buildProtocol(self, addr):
1718
p = self.protocol(client=self.client, queryData=self.queryData)
1721
def clientConnectionFailed(self, unused_connector, unused_reason):
1722
self.client.dcc_sessions.remove(self)
1724
def clientConnectionLost(self, unused_connector, unused_reason):
1725
self.client.dcc_sessions.remove(self)
1728
def dccDescribe(data):
1729
"""Given the data chunk from a DCC query, return a descriptive string.
1733
data = string.split(data)
1737
(dcctype, arg, address, port) = data[:4]
1743
address = long(address)
1748
(address >> 24) & 0xFF,
1749
(address >> 16) & 0xFF,
1750
(address >> 8) & 0xFF,
1753
# The mapping to 'int' is to get rid of those accursed
1754
# "L"s which python 1.5.2 puts on the end of longs.
1755
address = string.join(map(str,map(int,address)), ".")
1757
if dcctype == 'SEND':
1764
size_txt = ' of size %d bytes' % (size,)
1768
dcc_text = ("SEND for file '%s'%s at host %s, port %s"
1769
% (filename, size_txt, address, port))
1770
elif dcctype == 'CHAT':
1771
dcc_text = ("CHAT for host %s, port %s"
1774
dcc_text = orig_data
1779
class DccFileReceive(DccFileReceiveBasic):
1780
"""Higher-level coverage for getting a file from DCC SEND.
1782
I allow you to change the file's name and destination directory.
1783
I won't overwrite an existing file unless I've been told it's okay
1784
to do so. If passed the resumeOffset keyword argument I will attempt to
1785
resume the file from that amount of bytes.
1787
XXX: I need to let the client know when I am finished.
1788
XXX: I need to decide how to keep a progress indicator updated.
1789
XXX: Client needs a way to tell me \"Do not finish until I say so.\"
1790
XXX: I need to make sure the client understands if the file cannot be written.
1800
def __init__(self, filename, fileSize=-1, queryData=None,
1801
destDir='.', resumeOffset=0):
1802
DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
1803
self.filename = filename
1804
self.destDir = destDir
1805
self.fileSize = fileSize
1808
self.queryData = queryData
1809
self.fromUser = self.queryData[0]
1811
def set_directory(self, directory):
1812
"""Set the directory where the downloaded file will be placed.
1814
May raise OSError if the supplied directory path is not suitable.
1816
if not path.exists(directory):
1817
raise OSError(errno.ENOENT, "You see no directory there.",
1819
if not path.isdir(directory):
1820
raise OSError(errno.ENOTDIR, "You cannot put a file into "
1821
"something which is not a directory.",
1823
if not os.access(directory, os.X_OK | os.W_OK):
1824
raise OSError(errno.EACCES,
1825
"This directory is too hard to write in to.",
1827
self.destDir = directory
1829
def set_filename(self, filename):
1830
"""Change the name of the file being transferred.
1832
This replaces the file name provided by the sender.
1834
self.filename = filename
1836
def set_overwrite(self, boolean):
1837
"""May I overwrite existing files?
1839
self.overwrite = boolean
1842
# Protocol-level methods.
1844
def connectionMade(self):
1845
dst = path.abspath(path.join(self.destDir,self.filename))
1846
exists = path.exists(dst)
1847
if self.resume and exists:
1848
# I have been told I want to resume, and a file already
1849
# exists - Here we go
1850
self.file = open(dst, 'ab')
1851
log.msg("Attempting to resume %s - starting from %d bytes" %
1852
(self.file, self.file.tell()))
1853
elif self.overwrite or not exists:
1854
self.file = open(dst, 'wb')
1856
raise OSError(errno.EEXIST,
1857
"There's a file in the way. "
1858
"Perhaps that's why you cannot open it.",
1861
def dataReceived(self, data):
1862
self.file.write(data)
1863
DccFileReceiveBasic.dataReceived(self, data)
1865
# XXX: update a progress indicator here?
1867
def connectionLost(self, reason):
1868
"""When the connection is lost, I close the file.
1871
logmsg = ("%s closed." % (self,))
1872
if self.fileSize > 0:
1873
logmsg = ("%s %d/%d bytes received"
1874
% (logmsg, self.bytesReceived, self.fileSize))
1875
if self.bytesReceived == self.fileSize:
1877
elif self.bytesReceived < self.fileSize:
1878
logmsg = ("%s (Warning: %d bytes short)"
1879
% (logmsg, self.fileSize - self.bytesReceived))
1881
logmsg = ("%s (file larger than expected)"
1884
logmsg = ("%s %d bytes received"
1885
% (logmsg, self.bytesReceived))
1887
if hasattr(self, 'file'):
1888
logmsg = "%s and written to %s.\n" % (logmsg, self.file.name)
1889
if hasattr(self.file, 'close'): self.file.close()
1891
# self.transport.log(logmsg)
1894
if not self.connected:
1895
return "<Unconnected DccFileReceive object at %x>" % (id(self),)
1896
from_ = self.transport.getPeer()
1898
from_ = "%s (%s)" % (self.fromUser, from_)
1900
s = ("DCC transfer of '%s' from %s" % (self.filename, from_))
1904
s = ("<%s at %x: GET %s>"
1905
% (self.__class__, id(self), self.filename))
1909
# CTCP constants and helper functions
1913
def ctcpExtract(message):
1914
"""Extract CTCP data from a string.
1916
Returns a dictionary with two items:
1918
- C{'extended'}: a list of CTCP (tag, data) tuples
1919
- C{'normal'}: a list of strings which were not inside a CTCP delimeter
1922
extended_messages = []
1923
normal_messages = []
1924
retval = {'extended': extended_messages,
1925
'normal': normal_messages }
1927
messages = string.split(message, X_DELIM)
1930
# X1 extended data X2 nomal data X3 extended data X4 normal...
1933
extended_messages.append(messages.pop(0))
1935
normal_messages.append(messages.pop(0))
1938
extended_messages[:] = filter(None, extended_messages)
1939
normal_messages[:] = filter(None, normal_messages)
1941
extended_messages[:] = map(ctcpDequote, extended_messages)
1942
for i in xrange(len(extended_messages)):
1943
m = string.split(extended_messages[i], SPC, 1)
1950
extended_messages[i] = (tag, data)
1962
M_QUOTE: M_QUOTE + M_QUOTE
1966
for k, v in mQuoteTable.items():
1967
mDequoteTable[v[-1]] = k
1970
mEscape_re = re.compile('%s.' % (re.escape(M_QUOTE),), re.DOTALL)
1973
for c in (M_QUOTE, NUL, NL, CR):
1974
s = string.replace(s, c, mQuoteTable[c])
1978
def sub(matchobj, mDequoteTable=mDequoteTable):
1979
s = matchobj.group()[1]
1981
s = mDequoteTable[s]
1986
return mEscape_re.sub(sub, s)
1991
X_DELIM: X_QUOTE + 'a',
1992
X_QUOTE: X_QUOTE + X_QUOTE
1997
for k, v in xQuoteTable.items():
1998
xDequoteTable[v[-1]] = k
2000
xEscape_re = re.compile('%s.' % (re.escape(X_QUOTE),), re.DOTALL)
2003
for c in (X_QUOTE, X_DELIM):
2004
s = string.replace(s, c, xQuoteTable[c])
2008
def sub(matchobj, xDequoteTable=xDequoteTable):
2009
s = matchobj.group()[1]
2011
s = xDequoteTable[s]
2016
return xEscape_re.sub(sub, s)
2018
def ctcpStringify(messages):
2020
@type messages: a list of extended messages. An extended
2021
message is a (tag, data) tuple, where 'data' may be C{None}, a
2022
string, or a list of strings to be joined with whitespace.
2027
for (tag, data) in messages:
2029
if not isinstance(data, types.StringType):
2031
# data as list-of-strings
2032
data = " ".join(map(str, data))
2034
# No? Then use it's %s representation.
2036
m = "%s %s" % (tag, data)
2040
m = "%s%s%s" % (X_DELIM, m, X_DELIM)
2041
coded_messages.append(m)
2043
line = string.join(coded_messages, '')
2047
# Constants (from RFC 2812)
2049
RPL_YOURHOST = '002'
2053
RPL_USERHOST = '302'
2058
RPL_WHOISUSER = '311'
2059
RPL_WHOISSERVER = '312'
2060
RPL_WHOISOPERATOR = '313'
2061
RPL_WHOISIDLE = '317'
2062
RPL_ENDOFWHOIS = '318'
2063
RPL_WHOISCHANNELS = '319'
2064
RPL_WHOWASUSER = '314'
2065
RPL_ENDOFWHOWAS = '369'
2066
RPL_LISTSTART = '321'
2069
RPL_UNIQOPIS = '325'
2070
RPL_CHANNELMODEIS = '324'
2073
RPL_INVITING = '341'
2074
RPL_SUMMONING = '342'
2075
RPL_INVITELIST = '346'
2076
RPL_ENDOFINVITELIST = '347'
2077
RPL_EXCEPTLIST = '348'
2078
RPL_ENDOFEXCEPTLIST = '349'
2080
RPL_WHOREPLY = '352'
2081
RPL_ENDOFWHO = '315'
2082
RPL_NAMREPLY = '353'
2083
RPL_ENDOFNAMES = '366'
2085
RPL_ENDOFLINKS = '365'
2087
RPL_ENDOFBANLIST = '368'
2089
RPL_ENDOFINFO = '374'
2090
RPL_MOTDSTART = '375'
2092
RPL_ENDOFMOTD = '376'
2093
RPL_YOUREOPER = '381'
2094
RPL_REHASHING = '382'
2095
RPL_YOURESERVICE = '383'
2097
RPL_USERSSTART = '392'
2099
RPL_ENDOFUSERS = '394'
2101
RPL_TRACELINK = '200'
2102
RPL_TRACECONNECTING = '201'
2103
RPL_TRACEHANDSHAKE = '202'
2104
RPL_TRACEUNKNOWN = '203'
2105
RPL_TRACEOPERATOR = '204'
2106
RPL_TRACEUSER = '205'
2107
RPL_TRACESERVER = '206'
2108
RPL_TRACESERVICE = '207'
2109
RPL_TRACENEWTYPE = '208'
2110
RPL_TRACECLASS = '209'
2111
RPL_TRACERECONNECT = '210'
2112
RPL_TRACELOG = '261'
2113
RPL_TRACEEND = '262'
2114
RPL_STATSLINKINFO = '211'
2115
RPL_STATSCOMMANDS = '212'
2116
RPL_ENDOFSTATS = '219'
2117
RPL_STATSUPTIME = '242'
2118
RPL_STATSOLINE = '243'
2120
RPL_SERVLIST = '234'
2121
RPL_SERVLISTEND = '235'
2122
RPL_LUSERCLIENT = '251'
2124
RPL_LUSERUNKNOWN = '253'
2125
RPL_LUSERCHANNELS = '254'
2128
RPL_ADMINLOC = '257'
2129
RPL_ADMINLOC = '258'
2130
RPL_ADMINEMAIL = '259'
2131
RPL_TRYAGAIN = '263'
2132
ERR_NOSUCHNICK = '401'
2133
ERR_NOSUCHSERVER = '402'
2134
ERR_NOSUCHCHANNEL = '403'
2135
ERR_CANNOTSENDTOCHAN = '404'
2136
ERR_TOOMANYCHANNELS = '405'
2137
ERR_WASNOSUCHNICK = '406'
2138
ERR_TOOMANYTARGETS = '407'
2139
ERR_NOSUCHSERVICE = '408'
2140
ERR_NOORIGIN = '409'
2141
ERR_NORECIPIENT = '411'
2142
ERR_NOTEXTTOSEND = '412'
2143
ERR_NOTOPLEVEL = '413'
2144
ERR_WILDTOPLEVEL = '414'
2146
ERR_UNKNOWNCOMMAND = '421'
2148
ERR_NOADMININFO = '423'
2149
ERR_FILEERROR = '424'
2150
ERR_NONICKNAMEGIVEN = '431'
2151
ERR_ERRONEUSNICKNAME = '432'
2152
ERR_NICKNAMEINUSE = '433'
2153
ERR_NICKCOLLISION = '436'
2154
ERR_UNAVAILRESOURCE = '437'
2155
ERR_USERNOTINCHANNEL = '441'
2156
ERR_NOTONCHANNEL = '442'
2157
ERR_USERONCHANNEL = '443'
2159
ERR_SUMMONDISABLED = '445'
2160
ERR_USERSDISABLED = '446'
2161
ERR_NOTREGISTERED = '451'
2162
ERR_NEEDMOREPARAMS = '461'
2163
ERR_ALREADYREGISTRED = '462'
2164
ERR_NOPERMFORHOST = '463'
2165
ERR_PASSWDMISMATCH = '464'
2166
ERR_YOUREBANNEDCREEP = '465'
2167
ERR_YOUWILLBEBANNED = '466'
2169
ERR_CHANNELISFULL = '471'
2170
ERR_UNKNOWNMODE = '472'
2171
ERR_INVITEONLYCHAN = '473'
2172
ERR_BANNEDFROMCHAN = '474'
2173
ERR_BADCHANNELKEY = '475'
2174
ERR_BADCHANMASK = '476'
2175
ERR_NOCHANMODES = '477'
2176
ERR_BANLISTFULL = '478'
2177
ERR_NOPRIVILEGES = '481'
2178
ERR_CHANOPRIVSNEEDED = '482'
2179
ERR_CANTKILLSERVER = '483'
2180
ERR_RESTRICTED = '484'
2181
ERR_UNIQOPPRIVSNEEDED = '485'
2182
ERR_NOOPERHOST = '491'
2183
ERR_NOSERVICEHOST = '492'
2184
ERR_UMODEUNKNOWNFLAG = '501'
2185
ERR_USERSDONTMATCH = '502'
2187
# And hey, as long as the strings are already intern'd...
2188
symbolic_to_numeric = {
2189
"RPL_WELCOME": '001',
2190
"RPL_YOURHOST": '002',
2191
"RPL_CREATED": '003',
2192
"RPL_MYINFO": '004',
2193
"RPL_BOUNCE": '005',
2194
"RPL_USERHOST": '302',
2197
"RPL_UNAWAY": '305',
2198
"RPL_NOWAWAY": '306',
2199
"RPL_WHOISUSER": '311',
2200
"RPL_WHOISSERVER": '312',
2201
"RPL_WHOISOPERATOR": '313',
2202
"RPL_WHOISIDLE": '317',
2203
"RPL_ENDOFWHOIS": '318',
2204
"RPL_WHOISCHANNELS": '319',
2205
"RPL_WHOWASUSER": '314',
2206
"RPL_ENDOFWHOWAS": '369',
2207
"RPL_LISTSTART": '321',
2209
"RPL_LISTEND": '323',
2210
"RPL_UNIQOPIS": '325',
2211
"RPL_CHANNELMODEIS": '324',
2212
"RPL_NOTOPIC": '331',
2214
"RPL_INVITING": '341',
2215
"RPL_SUMMONING": '342',
2216
"RPL_INVITELIST": '346',
2217
"RPL_ENDOFINVITELIST": '347',
2218
"RPL_EXCEPTLIST": '348',
2219
"RPL_ENDOFEXCEPTLIST": '349',
2220
"RPL_VERSION": '351',
2221
"RPL_WHOREPLY": '352',
2222
"RPL_ENDOFWHO": '315',
2223
"RPL_NAMREPLY": '353',
2224
"RPL_ENDOFNAMES": '366',
2226
"RPL_ENDOFLINKS": '365',
2227
"RPL_BANLIST": '367',
2228
"RPL_ENDOFBANLIST": '368',
2230
"RPL_ENDOFINFO": '374',
2231
"RPL_MOTDSTART": '375',
2233
"RPL_ENDOFMOTD": '376',
2234
"RPL_YOUREOPER": '381',
2235
"RPL_REHASHING": '382',
2236
"RPL_YOURESERVICE": '383',
2238
"RPL_USERSSTART": '392',
2240
"RPL_ENDOFUSERS": '394',
2241
"RPL_NOUSERS": '395',
2242
"RPL_TRACELINK": '200',
2243
"RPL_TRACECONNECTING": '201',
2244
"RPL_TRACEHANDSHAKE": '202',
2245
"RPL_TRACEUNKNOWN": '203',
2246
"RPL_TRACEOPERATOR": '204',
2247
"RPL_TRACEUSER": '205',
2248
"RPL_TRACESERVER": '206',
2249
"RPL_TRACESERVICE": '207',
2250
"RPL_TRACENEWTYPE": '208',
2251
"RPL_TRACECLASS": '209',
2252
"RPL_TRACERECONNECT": '210',
2253
"RPL_TRACELOG": '261',
2254
"RPL_TRACEEND": '262',
2255
"RPL_STATSLINKINFO": '211',
2256
"RPL_STATSCOMMANDS": '212',
2257
"RPL_ENDOFSTATS": '219',
2258
"RPL_STATSUPTIME": '242',
2259
"RPL_STATSOLINE": '243',
2260
"RPL_UMODEIS": '221',
2261
"RPL_SERVLIST": '234',
2262
"RPL_SERVLISTEND": '235',
2263
"RPL_LUSERCLIENT": '251',
2264
"RPL_LUSEROP": '252',
2265
"RPL_LUSERUNKNOWN": '253',
2266
"RPL_LUSERCHANNELS": '254',
2267
"RPL_LUSERME": '255',
2268
"RPL_ADMINME": '256',
2269
"RPL_ADMINLOC": '257',
2270
"RPL_ADMINLOC": '258',
2271
"RPL_ADMINEMAIL": '259',
2272
"RPL_TRYAGAIN": '263',
2273
"ERR_NOSUCHNICK": '401',
2274
"ERR_NOSUCHSERVER": '402',
2275
"ERR_NOSUCHCHANNEL": '403',
2276
"ERR_CANNOTSENDTOCHAN": '404',
2277
"ERR_TOOMANYCHANNELS": '405',
2278
"ERR_WASNOSUCHNICK": '406',
2279
"ERR_TOOMANYTARGETS": '407',
2280
"ERR_NOSUCHSERVICE": '408',
2281
"ERR_NOORIGIN": '409',
2282
"ERR_NORECIPIENT": '411',
2283
"ERR_NOTEXTTOSEND": '412',
2284
"ERR_NOTOPLEVEL": '413',
2285
"ERR_WILDTOPLEVEL": '414',
2286
"ERR_BADMASK": '415',
2287
"ERR_UNKNOWNCOMMAND": '421',
2288
"ERR_NOMOTD": '422',
2289
"ERR_NOADMININFO": '423',
2290
"ERR_FILEERROR": '424',
2291
"ERR_NONICKNAMEGIVEN": '431',
2292
"ERR_ERRONEUSNICKNAME": '432',
2293
"ERR_NICKNAMEINUSE": '433',
2294
"ERR_NICKCOLLISION": '436',
2295
"ERR_UNAVAILRESOURCE": '437',
2296
"ERR_USERNOTINCHANNEL": '441',
2297
"ERR_NOTONCHANNEL": '442',
2298
"ERR_USERONCHANNEL": '443',
2299
"ERR_NOLOGIN": '444',
2300
"ERR_SUMMONDISABLED": '445',
2301
"ERR_USERSDISABLED": '446',
2302
"ERR_NOTREGISTERED": '451',
2303
"ERR_NEEDMOREPARAMS": '461',
2304
"ERR_ALREADYREGISTRED": '462',
2305
"ERR_NOPERMFORHOST": '463',
2306
"ERR_PASSWDMISMATCH": '464',
2307
"ERR_YOUREBANNEDCREEP": '465',
2308
"ERR_YOUWILLBEBANNED": '466',
2309
"ERR_KEYSET": '467',
2310
"ERR_CHANNELISFULL": '471',
2311
"ERR_UNKNOWNMODE": '472',
2312
"ERR_INVITEONLYCHAN": '473',
2313
"ERR_BANNEDFROMCHAN": '474',
2314
"ERR_BADCHANNELKEY": '475',
2315
"ERR_BADCHANMASK": '476',
2316
"ERR_NOCHANMODES": '477',
2317
"ERR_BANLISTFULL": '478',
2318
"ERR_NOPRIVILEGES": '481',
2319
"ERR_CHANOPRIVSNEEDED": '482',
2320
"ERR_CANTKILLSERVER": '483',
2321
"ERR_RESTRICTED": '484',
2322
"ERR_UNIQOPPRIVSNEEDED": '485',
2323
"ERR_NOOPERHOST": '491',
2324
"ERR_NOSERVICEHOST": '492',
2325
"ERR_UMODEUNKNOWNFLAG": '501',
2326
"ERR_USERSDONTMATCH": '502',
2329
numeric_to_symbolic = {}
2330
for k, v in symbolic_to_numeric.items():
2331
numeric_to_symbolic[v] = k