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