root/trunk/twisted/words/service.py

Revision 31007, 36.0 KB (checked in by lvh, 15 months ago)

twisted.words.services no longer references nonexistent twisted.words.protocols.irc.IRC_NOSUCHCHANNEL.

Author: devinj
Reviewer: lvh, exarkun
Fixes: #4915

twisted.words.services is no longer fundamentally wrong, and a bunch of test got added in the related code. Yay!

Line 
1# -*- test-case-name: twisted.words.test.test_service -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6A module that needs a better name.
7
8Implements new cred things for words.
9
10How 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
30from time import time, ctime
31
32from zope.interface import implements
33
34from twisted.words import iwords, ewords
35
36from twisted.python.components import registerAdapter
37from twisted.cred import portal, credentials, error as ecred
38from twisted.spread import pb
39from twisted.words.protocols import irc
40from twisted.internet import defer, protocol
41from twisted.python import log, failure, reflect
42from twisted import copyright
43
44
45class 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
131class 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
177NICKSERV = 'NickServ!NickServ@services'
178
179
180class 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
872class 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
894class 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
914class 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)
947pb.setUnjellyableForClass(PBMind, PBMindReference)
948
949
950class 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
973class 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)
986pb.setUnjellyableForClass(PBGroup, PBGroupReference)
987
988class 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
998class 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
1020registerAdapter(ChatAvatar, iwords.IUser, pb.IPerspective)
1021
1022class 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
1032pb.setUnjellyableForClass(ChatAvatar, AvatarReference)
1033
1034
1035class 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
1173class 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    ]
Note: See TracBrowser for help on using the browser.