~cbehrens/nova/lp844160-build-works-with-zones

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/words/service.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- test-case-name: twisted.words.test.test_service -*-
 
2
# Copyright (c) 2001-2008 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, targetName,
 
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.IRC_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
    ]