OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.words.test.test_service -*- | |
2 # Copyright (c) 2001-2005 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 """ | |
6 A module that needs a better name. | |
7 | |
8 Implements new cred things for words. | |
9 | |
10 How does this thing work? | |
11 | |
12 - Network connection on some port expecting to speak some protocol | |
13 | |
14 - Protocol-specific authentication, resulting in some kind of credentials obje
ct | |
15 | |
16 - twisted.cred.portal login using those credentials for the interface | |
17 IUser and with something implementing IChatClient as the mind | |
18 | |
19 - successful login results in an IUser avatar the protocol can call | |
20 methods on, and state added to the realm such that the mind will have | |
21 methods called on it as is necessary | |
22 | |
23 - protocol specific actions lead to calls onto the avatar; remote events | |
24 lead to calls onto the mind | |
25 | |
26 - protocol specific hangup, realm is notified, user is removed from active | |
27 play, the end. | |
28 """ | |
29 | |
30 from time import time, ctime | |
31 | |
32 from zope.interface import implements | |
33 | |
34 from twisted.words import iwords, ewords | |
35 | |
36 from twisted.python.components import registerAdapter | |
37 from twisted.cred import portal, credentials, error as ecred | |
38 from twisted.spread import pb | |
39 from twisted.words.protocols import irc | |
40 from twisted.internet import defer, protocol | |
41 from twisted.python import log, failure, reflect | |
42 from twisted import copyright | |
43 | |
44 | |
45 class Group(object): | |
46 implements(iwords.IGroup) | |
47 | |
48 def __init__(self, name): | |
49 self.name = name | |
50 self.users = {} | |
51 self.meta = { | |
52 "topic": "", | |
53 "topic_author": "", | |
54 } | |
55 | |
56 | |
57 def _ebUserCall(self, err, p): | |
58 return failure.Failure(Exception(p, err)) | |
59 | |
60 | |
61 def _cbUserCall(self, results): | |
62 for (success, result) in results: | |
63 if not success: | |
64 user, err = result.value # XXX | |
65 self.remove(user, err.getErrorMessage()) | |
66 | |
67 | |
68 def add(self, user): | |
69 assert iwords.IChatClient.providedBy(user), "%r is not a chat client" %
(user,) | |
70 if user.name not in self.users: | |
71 additions = [] | |
72 self.users[user.name] = user | |
73 for p in self.users.itervalues(): | |
74 if p is not user: | |
75 d = defer.maybeDeferred(p.userJoined, self, user) | |
76 d.addErrback(self._ebUserCall, p=p) | |
77 additions.append(d) | |
78 defer.DeferredList(additions).addCallback(self._cbUserCall) | |
79 return defer.succeed(None) | |
80 | |
81 | |
82 def remove(self, user, reason=None): | |
83 assert reason is None or isinstance(reason, unicode) | |
84 try: | |
85 del self.users[user.name] | |
86 except KeyError: | |
87 pass | |
88 else: | |
89 removals = [] | |
90 for p in self.users.itervalues(): | |
91 if p is not user: | |
92 d = defer.maybeDeferred(p.userLeft, self, user, reason) | |
93 d.addErrback(self._ebUserCall, p=p) | |
94 removals.append(d) | |
95 defer.DeferredList(removals).addCallback(self._cbUserCall) | |
96 return defer.succeed(None) | |
97 | |
98 | |
99 def size(self): | |
100 return defer.succeed(len(self.users)) | |
101 | |
102 | |
103 def receive(self, sender, recipient, message): | |
104 assert recipient is self | |
105 receives = [] | |
106 for p in self.users.itervalues(): | |
107 if p is not sender: | |
108 d = defer.maybeDeferred(p.receive, sender, self, message) | |
109 d.addErrback(self._ebUserCall, p=p) | |
110 receives.append(d) | |
111 defer.DeferredList(receives).addCallback(self._cbUserCall) | |
112 return defer.succeed(None) | |
113 | |
114 | |
115 def setMetadata(self, meta): | |
116 self.meta = meta | |
117 sets = [] | |
118 for p in self.users.itervalues(): | |
119 d = defer.maybeDeferred(p.groupMetaUpdate, self, meta) | |
120 d.addErrback(self._ebUserCall, p=p) | |
121 sets.append(d) | |
122 defer.DeferredList(sets).addCallback(self._cbUserCall) | |
123 return defer.succeed(None) | |
124 | |
125 | |
126 def iterusers(self): | |
127 # XXX Deferred? | |
128 return iter(self.users.values()) | |
129 | |
130 | |
131 class User(object): | |
132 implements(iwords.IUser) | |
133 | |
134 realm = None | |
135 mind = None | |
136 | |
137 def __init__(self, name): | |
138 self.name = name | |
139 self.groups = [] | |
140 self.lastMessage = time() | |
141 | |
142 | |
143 def loggedIn(self, realm, mind): | |
144 self.realm = realm | |
145 self.mind = mind | |
146 self.signOn = time() | |
147 | |
148 | |
149 def join(self, group): | |
150 def cbJoin(result): | |
151 self.groups.append(group) | |
152 return result | |
153 return group.add(self.mind).addCallback(cbJoin) | |
154 | |
155 | |
156 def leave(self, group, reason=None): | |
157 def cbLeave(result): | |
158 self.groups.remove(group) | |
159 return result | |
160 return group.remove(self.mind, reason).addCallback(cbLeave) | |
161 | |
162 | |
163 def send(self, recipient, message): | |
164 self.lastMessage = time() | |
165 return recipient.receive(self.mind, recipient, message) | |
166 | |
167 | |
168 def itergroups(self): | |
169 return iter(self.groups) | |
170 | |
171 | |
172 def logout(self): | |
173 for g in self.groups[:]: | |
174 self.leave(g) | |
175 | |
176 | |
177 NICKSERV = 'NickServ!NickServ@services' | |
178 class IRCUser(irc.IRC): | |
179 implements(iwords.IChatClient) | |
180 | |
181 # A list of IGroups in which I am participating | |
182 groups = None | |
183 | |
184 # A no-argument callable I should invoke when I go away | |
185 logout = None | |
186 | |
187 # An IUser we use to interact with the chat service | |
188 avatar = None | |
189 | |
190 # To whence I belong | |
191 realm = None | |
192 | |
193 # How to handle unicode (TODO: Make this customizable on a per-user basis) | |
194 encoding = 'utf-8' | |
195 | |
196 # Twisted callbacks | |
197 def connectionMade(self): | |
198 self.irc_PRIVMSG = self.irc_NICKSERV_PRIVMSG | |
199 | |
200 | |
201 def connectionLost(self, reason): | |
202 if self.logout is not None: | |
203 self.logout() | |
204 self.avatar = None | |
205 | |
206 | |
207 # Make sendMessage a bit more useful to us | |
208 def sendMessage(self, command, *parameter_list, **kw): | |
209 if not kw.has_key('prefix'): | |
210 kw['prefix'] = self.hostname | |
211 if not kw.has_key('to'): | |
212 kw['to'] = self.name.encode(self.encoding) | |
213 | |
214 arglist = [self, command, kw['to']] + list(parameter_list) | |
215 irc.IRC.sendMessage(*arglist, **kw) | |
216 | |
217 | |
218 # IChatClient implementation | |
219 def userJoined(self, group, user): | |
220 self.join( | |
221 "%s!%s@%s" % (user.name, user.name, self.hostname), | |
222 '#' + group.name) | |
223 | |
224 | |
225 def userLeft(self, group, user, reason=None): | |
226 assert reason is None or isinstance(reason, unicode) | |
227 self.part( | |
228 "%s!%s@%s" % (user.name, user.name, self.hostname), | |
229 '#' + group.name, | |
230 (reason or u"leaving").encode(self.encoding, 'replace')) | |
231 | |
232 | |
233 def receive(self, sender, recipient, message): | |
234 #>> :glyph!glyph@adsl-64-123-27-108.dsl.austtx.swbell.net PRIVMSG glyph_
:hello | |
235 | |
236 # omg??????????? | |
237 if iwords.IGroup.providedBy(recipient): | |
238 recipientName = '#' + recipient.name | |
239 else: | |
240 recipientName = recipient.name | |
241 | |
242 text = message.get('text', '<an unrepresentable message>') | |
243 for L in text.splitlines(): | |
244 self.privmsg( | |
245 '%s!%s@%s' % (sender.name, sender.name, self.hostname), | |
246 recipientName, | |
247 L) | |
248 | |
249 | |
250 def groupMetaUpdate(self, group, meta): | |
251 if 'topic' in meta: | |
252 topic = meta['topic'] | |
253 author = meta.get('topic_author', '') | |
254 self.topic( | |
255 self.name, | |
256 '#' + group.name, | |
257 topic, | |
258 '%s!%s@%s' % (author, author, self.hostname) | |
259 ) | |
260 | |
261 # irc.IRC callbacks - starting with login related stuff. | |
262 nickname = None | |
263 password = None | |
264 | |
265 def irc_PASS(self, prefix, params): | |
266 """Password message -- Register a password. | |
267 | |
268 Parameters: <password> | |
269 | |
270 [REQUIRED] | |
271 | |
272 Note that IRC requires the client send this *before* NICK | |
273 and USER. | |
274 """ | |
275 self.password = params[-1] | |
276 | |
277 | |
278 def irc_NICK(self, prefix, params): | |
279 """Nick message -- Set your nickname. | |
280 | |
281 Parameters: <nickname> | |
282 | |
283 [REQUIRED] | |
284 """ | |
285 try: | |
286 nickname = params[0].decode(self.encoding) | |
287 except UnicodeDecodeError: | |
288 self.privmsg( | |
289 NICKSERV, | |
290 nickname, | |
291 'Your nickname is cannot be decoded. Please use ASCII or UTF-8.
') | |
292 self.transport.loseConnection() | |
293 return | |
294 | |
295 if self.password is None: | |
296 self.nickname = nickname | |
297 self.privmsg( | |
298 NICKSERV, | |
299 nickname, | |
300 'Password?') | |
301 else: | |
302 password = self.password | |
303 self.password = None | |
304 self.logInAs(nickname, password) | |
305 | |
306 | |
307 def irc_USER(self, prefix, params): | |
308 """User message -- Set your realname. | |
309 | |
310 Parameters: <user> <mode> <unused> <realname> | |
311 """ | |
312 # Note: who gives a crap about this? The IUser has the real | |
313 # information we care about. Save it anyway, I guess, just | |
314 # for fun. | |
315 self.realname = params[-1] | |
316 | |
317 | |
318 def irc_NICKSERV_PRIVMSG(self, prefix, params): | |
319 """Send a (private) message. | |
320 | |
321 Parameters: <msgtarget> <text to be sent> | |
322 """ | |
323 target = params[0] | |
324 password = params[-1] | |
325 | |
326 if self.nickname is None: | |
327 # XXX Send an error response here | |
328 self.transport.loseConnection() | |
329 elif target.lower() != "nickserv": | |
330 self.privmsg( | |
331 NICKSERV, | |
332 self.nickname, | |
333 "Denied. Please send me (NickServ) your password.") | |
334 else: | |
335 nickname = self.nickname | |
336 self.nickname = None | |
337 self.logInAs(nickname, password) | |
338 | |
339 | |
340 def logInAs(self, nickname, password): | |
341 d = self.factory.portal.login( | |
342 credentials.UsernamePassword(nickname, password), | |
343 self, | |
344 iwords.IUser) | |
345 d.addCallbacks(self._cbLogin, self._ebLogin, errbackArgs=(nickname,)) | |
346 | |
347 | |
348 _welcomeMessages = [ | |
349 (irc.RPL_WELCOME, | |
350 ":connected to Twisted IRC"), | |
351 (irc.RPL_YOURHOST, | |
352 ":Your host is %(serviceName)s, running version %(serviceVersion)s"), | |
353 (irc.RPL_CREATED, | |
354 ":This server was created on %(creationDate)s"), | |
355 | |
356 # "Bummer. This server returned a worthless 004 numeric. | |
357 # I'll have to guess at all the values" | |
358 # -- epic | |
359 (irc.RPL_MYINFO, | |
360 # w and n are the currently supported channel and user modes | |
361 # -- specify this better | |
362 "%(serviceName)s %(serviceVersion)s w n"), | |
363 ] | |
364 | |
365 | |
366 def _cbLogin(self, (iface, avatar, logout)): | |
367 assert iface is iwords.IUser, "Realm is buggy, got %r" % (iface,) | |
368 | |
369 # Let them send messages to the world | |
370 del self.irc_PRIVMSG | |
371 | |
372 self.avatar = avatar | |
373 self.logout = logout | |
374 self.realm = avatar.realm | |
375 self.hostname = self.realm.name | |
376 | |
377 info = { | |
378 "serviceName": self.hostname, | |
379 "serviceVersion": copyright.version, | |
380 "creationDate": ctime(), # XXX | |
381 } | |
382 for code, text in self._welcomeMessages: | |
383 self.sendMessage(code, text % info) | |
384 | |
385 | |
386 def _ebLogin(self, err, nickname): | |
387 if err.check(ewords.AlreadyLoggedIn): | |
388 self.privmsg( | |
389 NICKSERV, | |
390 nickname, | |
391 "Already logged in. No pod people allowed!") | |
392 elif err.check(ecred.UnauthorizedLogin): | |
393 self.privmsg( | |
394 NICKSERV, | |
395 nickname, | |
396 "Login failed. Goodbye.") | |
397 else: | |
398 log.msg("Unhandled error during login:") | |
399 log.err(err) | |
400 self.privmsg( | |
401 NICKSERV, | |
402 nickname, | |
403 "Server error during login. Sorry.") | |
404 self.transport.loseConnection() | |
405 | |
406 | |
407 # Great, now that's out of the way, here's some of the interesting | |
408 # bits | |
409 def irc_PING(self, prefix, params): | |
410 """Ping message | |
411 | |
412 Parameters: <server1> [ <server2> ] | |
413 """ | |
414 if self.realm is not None: | |
415 self.sendMessage('PONG', self.hostname) | |
416 | |
417 | |
418 def irc_QUIT(self, prefix, params): | |
419 """Quit | |
420 | |
421 Parameters: [ <Quit Message> ] | |
422 """ | |
423 self.transport.loseConnection() | |
424 | |
425 | |
426 def _channelMode(self, group, modes=None, *args): | |
427 if modes: | |
428 self.sendMessage( | |
429 irc.ERR_UNKNOWNMODE, | |
430 ":Unknown MODE flag.") | |
431 else: | |
432 self.channelMode(self.name, '#' + group.name, '+') | |
433 | |
434 | |
435 def _userMode(self, user, modes=None): | |
436 if modes: | |
437 self.sendMessage( | |
438 irc.ERR_UNKNOWNMODE, | |
439 ":Unknown MODE flag.") | |
440 elif user is self.avatar: | |
441 self.sendMessage( | |
442 irc.RPL_UMODEIS, | |
443 "+") | |
444 else: | |
445 self.sendMessage( | |
446 irc.ERR_USERSDONTMATCH, | |
447 ":You can't look at someone else's modes.") | |
448 | |
449 | |
450 def irc_MODE(self, prefix, params): | |
451 """User mode message | |
452 | |
453 Parameters: <nickname> | |
454 *( ( "+" / "-" ) *( "i" / "w" / "o" / "O" / "r" ) ) | |
455 | |
456 """ | |
457 try: | |
458 channelOrUser = params[0].decode(self.encoding) | |
459 except UnicodeDecodeError: | |
460 self.sendMessage( | |
461 irc.ERR_NOSUCHNICK, params[0], | |
462 ":No such nickname (could not decode your unicode!)") | |
463 return | |
464 | |
465 if channelOrUser.startswith('#'): | |
466 def ebGroup(err): | |
467 err.trap(ewords.NoSuchGroup) | |
468 self.sendMessage( | |
469 irc.ERR_NOSUCHCHANNEL, params[0], | |
470 ":That channel doesn't exist.") | |
471 d = self.realm.lookupGroup(channelOrUser[1:]) | |
472 d.addCallbacks( | |
473 self._channelMode, | |
474 ebGroup, | |
475 callbackArgs=tuple(params[1:])) | |
476 else: | |
477 def ebUser(err): | |
478 self.sendMessage( | |
479 irc.ERR_NOSUCHNICK, | |
480 ":No such nickname.") | |
481 | |
482 d = self.realm.lookupUser(channelOrUser) | |
483 d.addCallbacks( | |
484 self._userMode, | |
485 ebUser, | |
486 callbackArgs=tuple(params[1:])) | |
487 | |
488 | |
489 def irc_USERHOST(self, prefix, params): | |
490 """Userhost message | |
491 | |
492 Parameters: <nickname> *( SPACE <nickname> ) | |
493 | |
494 [Optional] | |
495 """ | |
496 pass | |
497 | |
498 | |
499 def irc_PRIVMSG(self, prefix, params): | |
500 """Send a (private) message. | |
501 | |
502 Parameters: <msgtarget> <text to be sent> | |
503 """ | |
504 try: | |
505 targetName = params[0].decode(self.encoding) | |
506 except UnicodeDecodeError: | |
507 self.sendMessage( | |
508 irc.ERR_NOSUCHNICK, targetName, | |
509 ":No such nick/channel (could not decode your unicode!)") | |
510 return | |
511 | |
512 messageText = params[-1] | |
513 if targetName.startswith('#'): | |
514 target = self.realm.lookupGroup(targetName[1:]) | |
515 else: | |
516 target = self.realm.lookupUser(targetName).addCallback(lambda user:
user.mind) | |
517 | |
518 def cbTarget(targ): | |
519 if targ is not None: | |
520 return self.avatar.send(targ, {"text": messageText}) | |
521 | |
522 def ebTarget(err): | |
523 self.sendMessage( | |
524 irc.ERR_NOSUCHNICK, targetName, | |
525 ":No such nick/channel.") | |
526 | |
527 target.addCallbacks(cbTarget, ebTarget) | |
528 | |
529 | |
530 def irc_JOIN(self, prefix, params): | |
531 """Join message | |
532 | |
533 Parameters: ( <channel> *( "," <channel> ) [ <key> *( "," <key> ) ] ) | |
534 """ | |
535 try: | |
536 groupName = params[0].decode(self.encoding) | |
537 except UnicodeDecodeError: | |
538 self.sendMessage( | |
539 irc.IRC_NOSUCHCHANNEL, params[0], | |
540 ":No such channel (could not decode your unicode!)") | |
541 return | |
542 | |
543 if groupName.startswith('#'): | |
544 groupName = groupName[1:] | |
545 | |
546 def cbGroup(group): | |
547 def cbJoin(ign): | |
548 self.userJoined(group, self) | |
549 self.names( | |
550 self.name, | |
551 '#' + group.name, | |
552 [user.name for user in group.iterusers()]) | |
553 self._sendTopic(group) | |
554 return self.avatar.join(group).addCallback(cbJoin) | |
555 | |
556 def ebGroup(err): | |
557 self.sendMessage( | |
558 irc.ERR_NOSUCHCHANNEL, '#' + groupName, | |
559 ":No such channel.") | |
560 | |
561 self.realm.getGroup(groupName).addCallbacks(cbGroup, ebGroup) | |
562 | |
563 | |
564 def irc_PART(self, prefix, params): | |
565 """Part message | |
566 | |
567 Parameters: <channel> *( "," <channel> ) [ <Part Message> ] | |
568 """ | |
569 try: | |
570 groupName = params[0].decode(self.encoding) | |
571 except UnicodeDecodeError: | |
572 self.sendMessage( | |
573 irc.ERR_NOTONCHANNEL, params[0], | |
574 ":Could not decode your unicode!") | |
575 return | |
576 | |
577 if groupName.startswith('#'): | |
578 groupName = groupName[1:] | |
579 | |
580 if len(params) > 1: | |
581 reason = params[1].decode('utf-8') | |
582 else: | |
583 reason = None | |
584 | |
585 def cbGroup(group): | |
586 def cbLeave(result): | |
587 self.userLeft(group, self, reason) | |
588 return self.avatar.leave(group, reason).addCallback(cbLeave) | |
589 | |
590 def ebGroup(err): | |
591 err.trap(ewords.NoSuchGroup) | |
592 self.sendMessage( | |
593 irc.ERR_NOTONCHANNEL, | |
594 '#' + groupName, | |
595 ":" + err.getErrorMessage()) | |
596 | |
597 self.realm.lookupGroup(groupName).addCallbacks(cbGroup, ebGroup) | |
598 | |
599 | |
600 def irc_NAMES(self, prefix, params): | |
601 """Names message | |
602 | |
603 Parameters: [ <channel> *( "," <channel> ) [ <target> ] ] | |
604 """ | |
605 #<< NAMES #python | |
606 #>> :benford.openprojects.net 353 glyph = #python :Orban ... @glyph ...
Zymurgy skreech | |
607 #>> :benford.openprojects.net 366 glyph #python :End of /NAMES list. | |
608 try: | |
609 channel = params[-1].decode(self.encoding) | |
610 except UnicodeDecodeError: | |
611 self.sendMessage( | |
612 irc.ERR_NOSUCHCHANNEL, params[-1], | |
613 ":No such channel (could not decode your unicode!)") | |
614 return | |
615 | |
616 if channel.startswith('#'): | |
617 channel = channel[1:] | |
618 | |
619 def cbGroup(group): | |
620 self.names( | |
621 self.name, | |
622 '#' + group.name, | |
623 [user.name for user in group.iterusers()]) | |
624 | |
625 def ebGroup(err): | |
626 err.trap(ewords.NoSuchGroup) | |
627 # No group? Fine, no names! | |
628 self.names( | |
629 self.name, | |
630 '#' + channel, | |
631 []) | |
632 | |
633 self.realm.lookupGroup(channel).addCallbacks(cbGroup, ebGroup) | |
634 | |
635 | |
636 def irc_TOPIC(self, prefix, params): | |
637 """Topic message | |
638 | |
639 Parameters: <channel> [ <topic> ] | |
640 """ | |
641 try: | |
642 channel = params[0].decode(self.encoding) | |
643 except UnicodeDecodeError: | |
644 self.sendMessage( | |
645 irc.ERR_NOSUCHCHANNEL, | |
646 ":That channel doesn't exist (could not decode your unicode!)") | |
647 return | |
648 | |
649 if channel.startswith('#'): | |
650 channel = channel[1:] | |
651 | |
652 if len(params) > 1: | |
653 self._setTopic(channel, params[1]) | |
654 else: | |
655 self._getTopic(channel) | |
656 | |
657 | |
658 def _sendTopic(self, group): | |
659 topic = group.meta.get("topic") | |
660 author = group.meta.get("topic_author") or "<noone>" | |
661 date = group.meta.get("topic_date", 0) | |
662 self.topic(self.name, '#' + group.name, topic) | |
663 self.topicAuthor(self.name, '#' + group.name, author, date) | |
664 | |
665 | |
666 def _getTopic(self, channel): | |
667 #<< TOPIC #python | |
668 #>> :benford.openprojects.net 332 glyph #python :<churchr> I really did.
I sprained all my toes. | |
669 #>> :benford.openprojects.net 333 glyph #python itamar|nyc 994713482 | |
670 def ebGroup(err): | |
671 err.trap(ewords.NoSuchGroup) | |
672 self.sendMessage( | |
673 irc.ERR_NOSUCHCHANNEL, '=', channel, | |
674 ":That channel doesn't exist.") | |
675 | |
676 self.realm.lookupGroup(channel).addCallbacks(self._sendTopic, ebGroup) | |
677 | |
678 | |
679 def _setTopic(self, channel, topic): | |
680 #<< TOPIC #divunal :foo | |
681 #>> :glyph!glyph@adsl-64-123-27-108.dsl.austtx.swbell.net TOPIC #divunal
:foo | |
682 | |
683 def cbGroup(group): | |
684 newMeta = group.meta.copy() | |
685 newMeta['topic'] = topic | |
686 newMeta['topic_author'] = self.name | |
687 newMeta['topic_date'] = int(time()) | |
688 | |
689 def ebSet(err): | |
690 self.sendMessage( | |
691 irc.ERR_CHANOPRIVSNEEDED, | |
692 "#" + group.name, | |
693 ":You need to be a channel operator to do that.") | |
694 | |
695 return group.setMetadata(newMeta).addErrback(ebSet) | |
696 | |
697 def ebGroup(err): | |
698 err.trap(ewords.NoSuchGroup) | |
699 self.sendMessage( | |
700 irc.ERR_NOSUCHCHANNEL, '=', channel, | |
701 ":That channel doesn't exist.") | |
702 | |
703 self.realm.lookupGroup(channel).addCallbacks(cbGroup, ebGroup) | |
704 | |
705 | |
706 def list(self, channels): | |
707 """Send a group of LIST response lines | |
708 | |
709 @type channel: C{list} of C{(str, int, str)} | |
710 @param channel: Information about the channels being sent: | |
711 their name, the number of participants, and their topic. | |
712 """ | |
713 for (name, size, topic) in channels: | |
714 self.sendMessage(irc.RPL_LIST, name, str(size), ":" + topic) | |
715 self.sendMessage(irc.RPL_LISTEND, ":End of /LIST") | |
716 | |
717 | |
718 def irc_LIST(self, prefix, params): | |
719 """List query | |
720 | |
721 Return information about the indicated channels, or about all | |
722 channels if none are specified. | |
723 | |
724 Parameters: [ <channel> *( "," <channel> ) [ <target> ] ] | |
725 """ | |
726 #<< list #python | |
727 #>> :orwell.freenode.net 321 exarkun Channel :Users Name | |
728 #>> :orwell.freenode.net 322 exarkun #python 358 :The Python programming
language | |
729 #>> :orwell.freenode.net 323 exarkun :End of /LIST | |
730 if params: | |
731 # Return information about indicated channels | |
732 try: | |
733 channels = params[0].decode(self.encoding).split(',') | |
734 except UnicodeDecodeError: | |
735 self.sendMessage( | |
736 irc.ERR_NOSUCHCHANNEL, params[0], | |
737 ":No such channel (could not decode your unicode!)") | |
738 return | |
739 | |
740 groups = [] | |
741 for ch in channels: | |
742 if ch.startswith('#'): | |
743 ch = ch[1:] | |
744 groups.append(self.realm.lookupGroup(ch)) | |
745 | |
746 groups = defer.DeferredList(groups, consumeErrors=True) | |
747 groups.addCallback(lambda gs: [r for (s, r) in gs if s]) | |
748 else: | |
749 # Return information about all channels | |
750 groups = self.realm.itergroups() | |
751 | |
752 def cbGroups(groups): | |
753 def gotSize(size, group): | |
754 return group.name, size, group.meta.get('topic') | |
755 d = defer.DeferredList([ | |
756 group.size().addCallback(gotSize, group) for group in groups]) | |
757 d.addCallback(lambda results: self.list([r for (s, r) in results if
s])) | |
758 return d | |
759 groups.addCallback(cbGroups) | |
760 | |
761 | |
762 def _channelWho(self, group): | |
763 self.who(self.name, '#' + group.name, | |
764 [(m.name, self.hostname, self.realm.name, m.name, "H", 0, m.name) fo
r m in group.iterusers()]) | |
765 | |
766 | |
767 def _userWho(self, user): | |
768 self.sendMessage(irc.RPL_ENDOFWHO, | |
769 ":User /WHO not implemented") | |
770 | |
771 | |
772 def irc_WHO(self, prefix, params): | |
773 """Who query | |
774 | |
775 Parameters: [ <mask> [ "o" ] ] | |
776 """ | |
777 #<< who #python | |
778 #>> :x.opn 352 glyph #python aquarius pc-62-31-193-114-du.blueyonder.co.
uk y.opn Aquarius H :3 Aquarius | |
779 # ... | |
780 #>> :x.opn 352 glyph #python foobar europa.tranquility.net z.opn skreech
H :0 skreech | |
781 #>> :x.opn 315 glyph #python :End of /WHO list. | |
782 ### also | |
783 #<< who glyph | |
784 #>> :x.opn 352 glyph #python glyph adsl-64-123-27-108.dsl.austtx.swbell.
net x.opn glyph H :0 glyph | |
785 #>> :x.opn 315 glyph glyph :End of /WHO list. | |
786 if not params: | |
787 self.sendMessage(irc.RPL_ENDOFWHO, ":/WHO not supported.") | |
788 return | |
789 | |
790 try: | |
791 channelOrUser = params[0].decode(self.encoding) | |
792 except UnicodeDecodeError: | |
793 self.sendMessage( | |
794 irc.RPL_ENDOFWHO, params[0], | |
795 ":End of /WHO list (could not decode your unicode!)") | |
796 return | |
797 | |
798 if channelOrUser.startswith('#'): | |
799 def ebGroup(err): | |
800 err.trap(ewords.NoSuchGroup) | |
801 self.sendMessage( | |
802 irc.RPL_ENDOFWHO, channelOrUser, | |
803 ":End of /WHO list.") | |
804 d = self.realm.lookupGroup(channelOrUser[1:]) | |
805 d.addCallbacks(self._channelWho, ebGroup) | |
806 else: | |
807 def ebUser(err): | |
808 err.trap(ewords.NoSuchUser) | |
809 self.sendMessage( | |
810 irc.RPL_ENDOFWHO, channelOrUser, | |
811 ":End of /WHO list.") | |
812 d = self.realm.lookupUser(channelOrUser) | |
813 d.addCallbacks(self._userWho, ebUser) | |
814 | |
815 | |
816 | |
817 def irc_WHOIS(self, prefix, params): | |
818 """Whois query | |
819 | |
820 Parameters: [ <target> ] <mask> *( "," <mask> ) | |
821 """ | |
822 def cbUser(user): | |
823 self.whois( | |
824 self.name, | |
825 user.name, user.name, self.realm.name, | |
826 user.name, self.realm.name, 'Hi mom!', False, | |
827 int(time() - user.lastMessage), user.signOn, | |
828 ['#' + group.name for group in user.itergroups()]) | |
829 | |
830 def ebUser(err): | |
831 err.trap(ewords.NoSuchUser) | |
832 self.sendMessage( | |
833 irc.ERR_NOSUCHNICK, | |
834 params[0], | |
835 ":No such nick/channel") | |
836 | |
837 try: | |
838 user = params[0].decode(self.encoding) | |
839 except UnicodeDecodeError: | |
840 self.sendMessage( | |
841 irc.ERR_NOSUCHNICK, | |
842 params[0], | |
843 ":No such nick/channel") | |
844 return | |
845 | |
846 self.realm.lookupUser(user).addCallbacks(cbUser, ebUser) | |
847 | |
848 | |
849 # Unsupported commands, here for legacy compatibility | |
850 def irc_OPER(self, prefix, params): | |
851 """Oper message | |
852 | |
853 Parameters: <name> <password> | |
854 """ | |
855 self.sendMessage(irc.ERR_NOOPERHOST, ":O-lines not applicable") | |
856 | |
857 | |
858 class IRCFactory(protocol.ServerFactory): | |
859 protocol = IRCUser | |
860 | |
861 def __init__(self, realm, portal): | |
862 self.realm = realm | |
863 self.portal = portal | |
864 | |
865 | |
866 class PBMind(pb.Referenceable): | |
867 def __init__(self): | |
868 pass | |
869 | |
870 def jellyFor(self, jellier): | |
871 return reflect.qual(PBMind), jellier.invoker.registerReference(self) | |
872 | |
873 def remote_userJoined(self, user, group): | |
874 pass | |
875 | |
876 def remote_userLeft(self, user, group, reason): | |
877 pass | |
878 | |
879 def remote_receive(self, sender, recipient, message): | |
880 pass | |
881 | |
882 def remote_groupMetaUpdate(self, group, meta): | |
883 pass | |
884 | |
885 | |
886 class PBMindReference(pb.RemoteReference): | |
887 implements(iwords.IChatClient) | |
888 | |
889 def receive(self, sender, recipient, message): | |
890 if iwords.IGroup.providedBy(recipient): | |
891 rec = PBGroup(self.realm, self.avatar, recipient) | |
892 else: | |
893 rec = PBUser(self.realm, self.avatar, recipient) | |
894 return self.callRemote( | |
895 'receive', | |
896 PBUser(self.realm, self.avatar, sender), | |
897 rec, | |
898 message) | |
899 | |
900 def groupMetaUpdate(self, group, meta): | |
901 return self.callRemote( | |
902 'groupMetaUpdate', | |
903 PBGroup(self.realm, self.avatar, group), | |
904 meta) | |
905 | |
906 def userJoined(self, group, user): | |
907 return self.callRemote( | |
908 'userJoined', | |
909 PBGroup(self.realm, self.avatar, group), | |
910 PBUser(self.realm, self.avatar, user)) | |
911 | |
912 def userLeft(self, group, user, reason=None): | |
913 assert reason is None or isinstance(reason, unicode) | |
914 return self.callRemote( | |
915 'userLeft', | |
916 PBGroup(self.realm, self.avatar, group), | |
917 PBUser(self.realm, self.avatar, user), | |
918 reason) | |
919 pb.setUnjellyableForClass(PBMind, PBMindReference) | |
920 | |
921 | |
922 class PBGroup(pb.Referenceable): | |
923 def __init__(self, realm, avatar, group): | |
924 self.realm = realm | |
925 self.avatar = avatar | |
926 self.group = group | |
927 | |
928 | |
929 def processUniqueID(self): | |
930 return hash((self.realm.name, self.avatar.name, self.group.name)) | |
931 | |
932 | |
933 def jellyFor(self, jellier): | |
934 return reflect.qual(self.__class__), self.group.name.encode('utf-8'), je
llier.invoker.registerReference(self) | |
935 | |
936 | |
937 def remote_leave(self, reason=None): | |
938 return self.avatar.leave(self.group, reason) | |
939 | |
940 | |
941 def remote_send(self, message): | |
942 return self.avatar.send(self.group, message) | |
943 | |
944 | |
945 class PBGroupReference(pb.RemoteReference): | |
946 implements(iwords.IGroup) | |
947 | |
948 def unjellyFor(self, unjellier, unjellyList): | |
949 clsName, name, ref = unjellyList | |
950 self.name = name.decode('utf-8') | |
951 return pb.RemoteReference.unjellyFor(self, unjellier, [clsName, ref]) | |
952 | |
953 def leave(self, reason=None): | |
954 return self.callRemote("leave", reason) | |
955 | |
956 def send(self, message): | |
957 return self.callRemote("send", message) | |
958 pb.setUnjellyableForClass(PBGroup, PBGroupReference) | |
959 | |
960 class PBUser(pb.Referenceable): | |
961 def __init__(self, realm, avatar, user): | |
962 self.realm = realm | |
963 self.avatar = avatar | |
964 self.user = user | |
965 | |
966 def processUniqueID(self): | |
967 return hash((self.realm.name, self.avatar.name, self.user.name)) | |
968 | |
969 | |
970 class ChatAvatar(pb.Referenceable): | |
971 implements(iwords.IChatClient) | |
972 | |
973 def __init__(self, avatar): | |
974 self.avatar = avatar | |
975 | |
976 | |
977 def jellyFor(self, jellier): | |
978 return reflect.qual(self.__class__), jellier.invoker.registerReference(s
elf) | |
979 | |
980 | |
981 def remote_join(self, groupName): | |
982 assert isinstance(groupName, unicode) | |
983 def cbGroup(group): | |
984 def cbJoin(ignored): | |
985 return PBGroup(self.avatar.realm, self.avatar, group) | |
986 d = self.avatar.join(group) | |
987 d.addCallback(cbJoin) | |
988 return d | |
989 d = self.avatar.realm.getGroup(groupName) | |
990 d.addCallback(cbGroup) | |
991 return d | |
992 registerAdapter(ChatAvatar, iwords.IUser, pb.IPerspective) | |
993 | |
994 class AvatarReference(pb.RemoteReference): | |
995 def join(self, groupName): | |
996 return self.callRemote('join', groupName) | |
997 | |
998 def quit(self): | |
999 d = defer.Deferred() | |
1000 self.broker.notifyOnDisconnect(lambda: d.callback(None)) | |
1001 self.broker.transport.loseConnection() | |
1002 return d | |
1003 | |
1004 pb.setUnjellyableForClass(ChatAvatar, AvatarReference) | |
1005 | |
1006 | |
1007 class WordsRealm(object): | |
1008 implements(portal.IRealm, iwords.IChatService) | |
1009 | |
1010 _encoding = 'utf-8' | |
1011 | |
1012 def __init__(self, name): | |
1013 self.name = name | |
1014 | |
1015 | |
1016 def userFactory(self, name): | |
1017 return User(name) | |
1018 | |
1019 | |
1020 def groupFactory(self, name): | |
1021 return Group(name) | |
1022 | |
1023 | |
1024 def logoutFactory(self, avatar, facet): | |
1025 def logout(): | |
1026 # XXX Deferred support here | |
1027 getattr(facet, 'logout', lambda: None)() | |
1028 avatar.realm = avatar.mind = None | |
1029 return logout | |
1030 | |
1031 | |
1032 def requestAvatar(self, avatarId, mind, *interfaces): | |
1033 if isinstance(avatarId, str): | |
1034 avatarId = avatarId.decode(self._encoding) | |
1035 | |
1036 def gotAvatar(avatar): | |
1037 if avatar.realm is not None: | |
1038 raise ewords.AlreadyLoggedIn() | |
1039 for iface in interfaces: | |
1040 facet = iface(avatar, None) | |
1041 if facet is not None: | |
1042 avatar.loggedIn(self, mind) | |
1043 mind.name = avatarId | |
1044 mind.realm = self | |
1045 mind.avatar = avatar | |
1046 return iface, facet, self.logoutFactory(avatar, facet) | |
1047 raise NotImplementedError(self, interfaces) | |
1048 | |
1049 return self.getUser(avatarId).addCallback(gotAvatar) | |
1050 | |
1051 | |
1052 # IChatService, mostly. | |
1053 createGroupOnRequest = False | |
1054 createUserOnRequest = True | |
1055 | |
1056 def lookupUser(self, name): | |
1057 raise NotImplementedError | |
1058 | |
1059 | |
1060 def lookupGroup(self, group): | |
1061 raise NotImplementedError | |
1062 | |
1063 | |
1064 def addUser(self, user): | |
1065 """Add the given user to this service. | |
1066 | |
1067 This is an internal method intented to be overridden by | |
1068 L{WordsRealm} subclasses, not called by external code. | |
1069 | |
1070 @type user: L{IUser} | |
1071 | |
1072 @rtype: L{twisted.internet.defer.Deferred} | |
1073 @return: A Deferred which fires with C{None} when the user is | |
1074 added, or which fails with | |
1075 L{twisted.words.ewords.DuplicateUser} if a user with the | |
1076 same name exists already. | |
1077 """ | |
1078 raise NotImplementedError | |
1079 | |
1080 | |
1081 def addGroup(self, group): | |
1082 """Add the given group to this service. | |
1083 | |
1084 @type group: L{IGroup} | |
1085 | |
1086 @rtype: L{twisted.internet.defer.Deferred} | |
1087 @return: A Deferred which fires with C{None} when the group is | |
1088 added, or which fails with | |
1089 L{twisted.words.ewords.DuplicateGroup} if a group with the | |
1090 same name exists already. | |
1091 """ | |
1092 raise NotImplementedError | |
1093 | |
1094 | |
1095 def getGroup(self, name): | |
1096 assert isinstance(name, unicode) | |
1097 if self.createGroupOnRequest: | |
1098 def ebGroup(err): | |
1099 err.trap(ewords.DuplicateGroup) | |
1100 return self.lookupGroup(name) | |
1101 return self.createGroup(name).addErrback(ebGroup) | |
1102 return self.lookupGroup(name) | |
1103 | |
1104 | |
1105 def getUser(self, name): | |
1106 assert isinstance(name, unicode) | |
1107 if self.createUserOnRequest: | |
1108 def ebUser(err): | |
1109 err.trap(ewords.DuplicateUser) | |
1110 return self.lookupUser(name) | |
1111 return self.createUser(name).addErrback(ebUser) | |
1112 return self.lookupUser(name) | |
1113 | |
1114 | |
1115 def createUser(self, name): | |
1116 assert isinstance(name, unicode) | |
1117 def cbLookup(user): | |
1118 return failure.Failure(ewords.DuplicateUser(name)) | |
1119 def ebLookup(err): | |
1120 err.trap(ewords.NoSuchUser) | |
1121 return self.userFactory(name) | |
1122 | |
1123 name = name.lower() | |
1124 d = self.lookupUser(name) | |
1125 d.addCallbacks(cbLookup, ebLookup) | |
1126 d.addCallback(self.addUser) | |
1127 return d | |
1128 | |
1129 | |
1130 def createGroup(self, name): | |
1131 assert isinstance(name, unicode) | |
1132 def cbLookup(group): | |
1133 return failure.Failure(ewords.DuplicateGroup(name)) | |
1134 def ebLookup(err): | |
1135 err.trap(ewords.NoSuchGroup) | |
1136 return self.groupFactory(name) | |
1137 | |
1138 name = name.lower() | |
1139 d = self.lookupGroup(name) | |
1140 d.addCallbacks(cbLookup, ebLookup) | |
1141 d.addCallback(self.addGroup) | |
1142 return d | |
1143 | |
1144 | |
1145 class InMemoryWordsRealm(WordsRealm): | |
1146 def __init__(self, *a, **kw): | |
1147 super(InMemoryWordsRealm, self).__init__(*a, **kw) | |
1148 self.users = {} | |
1149 self.groups = {} | |
1150 | |
1151 | |
1152 def itergroups(self): | |
1153 return defer.succeed(self.groups.itervalues()) | |
1154 | |
1155 | |
1156 def addUser(self, user): | |
1157 if user.name in self.users: | |
1158 return defer.fail(failure.Failure(ewords.DuplicateUser())) | |
1159 self.users[user.name] = user | |
1160 return defer.succeed(user) | |
1161 | |
1162 | |
1163 def addGroup(self, group): | |
1164 if group.name in self.groups: | |
1165 return defer.fail(failure.Failure(ewords.DuplicateGroup())) | |
1166 self.groups[group.name] = group | |
1167 return defer.succeed(group) | |
1168 | |
1169 | |
1170 def lookupUser(self, name): | |
1171 assert isinstance(name, unicode) | |
1172 name = name.lower() | |
1173 try: | |
1174 user = self.users[name] | |
1175 except KeyError: | |
1176 return defer.fail(failure.Failure(ewords.NoSuchUser(name))) | |
1177 else: | |
1178 return defer.succeed(user) | |
1179 | |
1180 | |
1181 def lookupGroup(self, name): | |
1182 assert isinstance(name, unicode) | |
1183 name = name.lower() | |
1184 try: | |
1185 group = self.groups[name] | |
1186 except KeyError: | |
1187 return defer.fail(failure.Failure(ewords.NoSuchGroup(name))) | |
1188 else: | |
1189 return defer.succeed(group) | |
1190 | |
1191 __all__ = [ | |
1192 'Group', 'User', | |
1193 | |
1194 'WordsRealm', 'InMemoryWordsRealm', | |
1195 ] | |
OLD | NEW |