~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/words/service.py

  • Committer: Bazaar Package Importer
  • Author(s): Moshe Zadka
  • Date: 2002-03-08 07:14:16 UTC
  • Revision ID: james.westby@ubuntu.com-20020308071416-oxvuw76tpcpi5v1q
Tags: upstream-0.15.5
ImportĀ upstreamĀ versionĀ 0.15.5

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
 
 
2
# Twisted, the Framework of Your Internet
 
3
# Copyright (C) 2001 Matthew W. Lefkowitz
 
4
#
 
5
# This library is free software; you can redistribute it and/or
 
6
# modify it under the terms of version 2.1 of the GNU Lesser General Public
 
7
# License as published by the Free Software Foundation.
 
8
#
 
9
# This library is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 
12
# Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public
 
15
# License along with this library; if not, write to the Free Software
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
 
 
19
# System Imports
 
20
import string
 
21
import types
 
22
 
 
23
# Twisted Imports
 
24
from twisted.spread import pb
 
25
from twisted.python import log, roots
 
26
from twisted.manhole import coil
 
27
from twisted.persisted import styles
 
28
from twisted import copyright
 
29
from twisted.cred import authorizer
 
30
 
 
31
# Status "enumeration"
 
32
 
 
33
OFFLINE = 0
 
34
ONLINE  = 1
 
35
AWAY = 2
 
36
 
 
37
statuses = ["Offline","Online","Away"]
 
38
 
 
39
class WordsError(pb.Error):
 
40
    pass
 
41
 
 
42
class NotInCollectionError(WordsError):
 
43
    pass
 
44
 
 
45
class NotInGroupError(NotInCollectionError):
 
46
    def __init__(self, groupName, pName=None):
 
47
        WordsError.__init__(self, groupName, pName)
 
48
        self.group = groupName
 
49
        self.pName = pName
 
50
 
 
51
    def __str__(self):
 
52
        if self.pName:
 
53
            pName = "'%s' is" % (self.pName,)
 
54
        else:
 
55
            pName = "You are"
 
56
        s = ("%s not in group '%s'." % (pName, self.group))
 
57
        return s
 
58
 
 
59
class UserNonexistantError(NotInCollectionError):
 
60
    def __init__(self, pName):
 
61
        WordsError.__init__(self, pName)
 
62
        self.pName = pName
 
63
 
 
64
    def __str__(self):
 
65
        return "'%s' does not exist." % (self.pName,)
 
66
 
 
67
class WrongStatusError(WordsError):
 
68
    def __init__(self, status, pName=None):
 
69
        WordsError.__init__(self, status, pName)
 
70
        self.status = status
 
71
        self.pName = pName
 
72
 
 
73
    def __str__(self):
 
74
        if self.pName:
 
75
            pName = "'%s'" % (self.pName,)
 
76
        else:
 
77
            pName = "User"
 
78
 
 
79
        if self.status in statuses:
 
80
            status = self.status
 
81
        else:
 
82
            status = 'unknown? (%s)' % self.status
 
83
        s = ("%s status is '%s'." % (pName, status))
 
84
        return s
 
85
 
 
86
 
 
87
class WordsClientInterface:
 
88
    """A client to a perspective on the twisted.words service.
 
89
 
 
90
    I attach to that participant with Participant.attached(),
 
91
    and detatch with Participant.detached().
 
92
    """
 
93
 
 
94
    def receiveContactList(self, contactList):
 
95
        """Receive a list of contacts and their status.
 
96
 
 
97
        The list is composed of 2-tuples, of the form
 
98
        (contactName, contactStatus)
 
99
        """
 
100
 
 
101
    def notifyStatusChanged(self, name, status):
 
102
        """Notify me of a change in status of one of my contacts.
 
103
        """
 
104
 
 
105
    def receiveGroupMembers(self, names, group):
 
106
        """Receive a list of members in a group.
 
107
 
 
108
        'names' is a list of participant names in the group named 'group'.
 
109
        """
 
110
 
 
111
    def setGroupMetadata(self, metadata, name):
 
112
        """Some metadata on a group has been set.
 
113
 
 
114
        XXX: Should this be receiveGroupMetadata(name, metedata)?
 
115
        """
 
116
 
 
117
    def receiveDirectMessage(self, sender, message, metadata=None):
 
118
        """Receive a message from someone named 'sender'.
 
119
        'metadata' is a dict of special flags. So far 'style': 'emote'
 
120
        is defined. Note that 'metadata' *must* be optional.
 
121
        """
 
122
 
 
123
    def receiveGroupMessage(self, sender, group, message, metadata=None):
 
124
        """Receive a message from 'sender' directed to a group.
 
125
        'metadata' is a dict of special flags. So far 'style': 'emote'
 
126
        is defined. Note that 'metadata' *must* be optional.
 
127
        """
 
128
 
 
129
    def memberJoined(self, member, group):
 
130
        """Tells me a member has joined a group.
 
131
        """
 
132
 
 
133
    def memberLeft(self, member, group):
 
134
        """Tells me a member has left a group.
 
135
        """
 
136
 
 
137
 
 
138
class Participant(pb.Perspective, styles.Versioned):
 
139
    def __init__(self, name):
 
140
        pb.Perspective.__init__(self, name)
 
141
        self.name = name
 
142
        self.status = OFFLINE
 
143
        self.contacts = []
 
144
        self.reverseContacts = []
 
145
        self.groups = []
 
146
        self.client = None
 
147
        self.info = ""
 
148
 
 
149
    persistenceVersion = 1
 
150
 
 
151
    def __getstate__(self):
 
152
        state = styles.Versioned.__getstate__(self)
 
153
        # Assumptions:
 
154
        # * self.client is a RemoteReference, or otherwise represents
 
155
        #   a transient presence.
 
156
        state["client"] = None
 
157
        # * Because we have no client, we are not online.
 
158
        state["status"] = OFFLINE
 
159
        # * Because we are not online, we are in no groups.
 
160
        state["groups"] = []
 
161
 
 
162
        return state
 
163
 
 
164
    def attached(self, client, identity):
 
165
        """Attach a client which implements WordsClientInterface to me.
 
166
        """
 
167
        if ((self.client is not None)
 
168
            and self.client.__class__ != styles.Ephemeral):
 
169
            self.detached(client, identity)
 
170
        log.msg("attached: %s" % self.name)
 
171
        self.client = client
 
172
        client.callRemote('receiveContactList', map(lambda contact: (contact.name,
 
173
                                                                     contact.status),
 
174
                                                    self.contacts))
 
175
        self.changeStatus(ONLINE)
 
176
        return self
 
177
 
 
178
    def changeStatus(self, newStatus):
 
179
        self.status = newStatus
 
180
        for contact in self.reverseContacts:
 
181
            contact.notifyStatusChanged(self)
 
182
 
 
183
    def notifyStatusChanged(self, contact):
 
184
        if self.client:
 
185
            self.client.callRemote('notifyStatusChanged', contact.name, contact.status)
 
186
 
 
187
    def detached(self, client, identity):
 
188
        log.msg("detached: %s" % self.name)
 
189
        self.client = None
 
190
        for group in self.groups[:]:
 
191
            try:
 
192
                self.leaveGroup(group.name)
 
193
            except NotInGroupError:
 
194
                pass
 
195
        self.changeStatus(OFFLINE)
 
196
 
 
197
    def addContact(self, contactName):
 
198
        # XXX This should use a database or something.  Doing it synchronously
 
199
        # like this won't work.
 
200
        contact = self.service.getPerspectiveNamed(contactName)
 
201
        self.contacts.append(contact)
 
202
        contact.reverseContacts.append(self)
 
203
        self.notifyStatusChanged(contact)
 
204
 
 
205
    def removeContact(self, contactName):
 
206
        for contact in self.contacts:
 
207
            if contact.name == contactName:
 
208
                self.contacts.remove(contact)
 
209
                contact.reverseContacts.remove(self)
 
210
                return
 
211
        raise NotInCollectionError("No such contact '%s'."
 
212
                                   % (contactName,))
 
213
 
 
214
    def joinGroup(self, name):
 
215
        group = self.service.getGroup(name)
 
216
        if group in self.groups:
 
217
            # We're in that group.  Don't make a fuss.
 
218
            return
 
219
        group.addMember(self)
 
220
        self.groups.append(group)
 
221
 
 
222
    def leaveGroup(self, name):
 
223
        for group in self.groups:
 
224
            if group.name == name:
 
225
                self.groups.remove(group)
 
226
                group.removeMember(self)
 
227
                return
 
228
        raise NotInGroupError(name)
 
229
 
 
230
    def getGroupMembers(self, groupName):
 
231
        for group in self.groups:
 
232
            if group.name == groupName:
 
233
                self.client.callRemote('receiveGroupMembers', map(lambda m: m.name,
 
234
                                                                  group.members),
 
235
                                       group.name)
 
236
        raise NotInGroupError(groupName)
 
237
 
 
238
    def getGroupMetadata(self, groupName):
 
239
        for group in self.groups:
 
240
            if group.name == groupName:
 
241
                self.client.callRemote('setGroupMetadata', group.metadata, group.name)
 
242
 
 
243
    def receiveDirectMessage(self, sender, message, metadata):
 
244
        if self.client:
 
245
            if metadata:
 
246
                d = self.client.callRemote('receiveDirectMessage', sender.name, message,
 
247
                                           metadata)
 
248
                #If the client doesn't support metadata, call this function
 
249
                #again with no metadata, so none is sent
 
250
                #
 
251
                #note on the 'if d:' - the IRC service is in-process and
 
252
                #won't return a Deferred, but rather it returns None.
 
253
                #silently ignore it. (This is really evil and terrible.)
 
254
                if d:
 
255
                    d.addErrback(self.receiveDirectMessage,
 
256
                                 sender.name, message, None)
 
257
            else:
 
258
                self.client.callRemote('receiveDirectMessage', sender.name, message)
 
259
        else:
 
260
            raise WrongStatusError(self.status, self.name)
 
261
 
 
262
 
 
263
    def receiveGroupMessage(self, sender, group, message, metadata):
 
264
        if sender is not self and self.client:
 
265
            if metadata:
 
266
                d = self.client.receiveGroupMessage(sender.name, group.name,
 
267
                                                    message, metadata)
 
268
                if d:
 
269
                    d.addErrback(self.receiveGroupMessage,
 
270
                                 sender, group, message, None)
 
271
            else:
 
272
                self.client.receiveGroupMessage(sender.name, group.name,
 
273
                                                message)
 
274
 
 
275
    def memberJoined(self, member, group):
 
276
        self.client.memberJoined(member.name, group.name)
 
277
 
 
278
    def memberLeft(self, member, group):
 
279
        self.client.memberLeft(member.name, group.name)
 
280
 
 
281
    def directMessage(self, recipientName, message, metadata=None):
 
282
        # XXX getPerspectiveNamed is misleading here -- this ought to look up
 
283
        # the user to make sure they're *online*, and if they're not, it may
 
284
        # need to query a database.
 
285
        recipient = self.service.getPerspectiveNamed(recipientName)
 
286
        recipient.receiveDirectMessage(self, message, metadata or {})
 
287
 
 
288
    def groupMessage(self, groupName, message, metadata=None):
 
289
        for group in self.groups:
 
290
            if group.name == groupName:
 
291
                group.sendMessage(self, message, metadata or {})
 
292
                return
 
293
        raise NotInGroupError(groupName)
 
294
 
 
295
    def setGroupMetadata(self, dict_, groupName):
 
296
        if self.client:
 
297
            self.client.callRemote('setGroupMetadata', dict_, groupName)
 
298
 
 
299
    def perspective_setGroupMetadata(self, dict_, groupName):
 
300
        #pre-processing
 
301
        if dict_.has_key('topic'):
 
302
            #don't want topic-spoofing, now
 
303
            dict_["topic_author"] = self.name
 
304
 
 
305
        for group in self.groups:
 
306
            if group.name == groupName:
 
307
                group.setMetadata(dict_)
 
308
 
 
309
    # Establish client protocol for PB.
 
310
    perspective_changeStatus = changeStatus
 
311
    perspective_joinGroup = joinGroup
 
312
    perspective_directMessage = directMessage
 
313
    perspective_addContact = addContact
 
314
    perspective_removeContact = removeContact
 
315
    perspective_groupMessage = groupMessage
 
316
    perspective_leaveGroup = leaveGroup
 
317
    perspective_getGroupMembers = getGroupMembers
 
318
 
 
319
    def __repr__(self):
 
320
        if self.identityName != "Nobody":
 
321
            id_s = '(id:%s)' % (self.identityName, )
 
322
        else:
 
323
            id_s = ''
 
324
        s = ("<%s '%s'%s on %s at %x>"
 
325
             % (self.__class__, self.name, id_s,
 
326
                self.service.serviceName, id(self)))
 
327
        return s
 
328
 
 
329
class Group(styles.Versioned):
 
330
 
 
331
    def __init__(self, name):
 
332
        self.name = name
 
333
        self.members = []
 
334
        self.metadata = {'topic': 'Welcome to %s!' % self.name,
 
335
                         'topic_author': 'admin'}
 
336
 
 
337
    def __getstate__(self):
 
338
        state = styles.Versioned.__getstate__(self)
 
339
        state['members'] = []
 
340
        return state
 
341
 
 
342
    def addMember(self, participant):
 
343
        if participant in self.members:
 
344
            return
 
345
        for member in self.members:
 
346
            member.memberJoined(participant, self)
 
347
        participant.setGroupMetadata(self.metadata, self.name)
 
348
        self.members.append(participant)
 
349
 
 
350
    def removeMember(self, participant):
 
351
        try:
 
352
            self.members.remove(participant)
 
353
        except ValueError:
 
354
            raise NotInGroupError(self.name, participant.name)
 
355
        else:
 
356
            for member in self.members:
 
357
                member.memberLeft(participant, self)
 
358
 
 
359
    def sendMessage(self, sender, message, metadata):
 
360
        for member in self.members:
 
361
            member.receiveGroupMessage(sender, self, message, metadata)
 
362
 
 
363
    def setMetadata(self, dict_):
 
364
        self.metadata.update(dict_)
 
365
        for member in self.members:
 
366
            member.setGroupMetadata(dict_, self.name)
 
367
 
 
368
    def __repr__(self):
 
369
        s = "<%s '%s' at %x>" % (self.__class__, self.name, id(self))
 
370
        return s
 
371
 
 
372
 
 
373
    ##Persistence Versioning
 
374
 
 
375
    persistenceVersion = 1
 
376
 
 
377
    def upgradeToVersion1(self):
 
378
        self.metadata = {'topic': self.topic}
 
379
        del self.topic
 
380
        self.metadata['topic_author'] = 'admin'
 
381
 
 
382
class Service(pb.Service, styles.Versioned, coil.Configurable):
 
383
    """I am a chat service.
 
384
    """
 
385
    def __init__(self, name, app):
 
386
        pb.Service.__init__(self, name, app)
 
387
        self.participants = {}
 
388
        self.groups = {}
 
389
        self._setConfigDispensers()
 
390
 
 
391
    # Configuration stuff.
 
392
    def _setConfigDispensers(self):
 
393
        import ircservice, webwords
 
394
        self.configDispensers = [
 
395
            ['makeIRCGateway', ircservice.IRCGateway, "IRC chat gateway to %s" % self.serviceName],
 
396
            ['makeWebAccounts', webwords.WordsGadget, "Public Words Website for %s" % self.serviceName]
 
397
            ]
 
398
 
 
399
    def makeWebAccounts(self):
 
400
        import webwords
 
401
        return webwords.WordsGadget(self)
 
402
 
 
403
    def makeIRCGateway(self):
 
404
        import ircservice
 
405
        return ircservice.IRCGateway(self)
 
406
 
 
407
    def configInit(self, container, name):
 
408
        self.__init__(name, container.app)
 
409
 
 
410
    def getConfiguration(self):
 
411
        return {"name": self.serviceName}
 
412
 
 
413
    configTypes = {
 
414
        'name': types.StringType
 
415
        }
 
416
 
 
417
    configName = 'Twisted Words PB Service'
 
418
 
 
419
    def config_name(self, name):
 
420
        raise coil.InvalidConfiguration("You can't change a Service's name.")
 
421
 
 
422
    ## Persistence versioning.
 
423
    persistenceVersion = 2
 
424
 
 
425
    def upgradeToVersion1(self):
 
426
        from twisted.internet.app import theApplication
 
427
        styles.requireUpgrade(theApplication)
 
428
        pb.Service.__init__(self, 'twisted.words', theApplication)
 
429
 
 
430
    def upgradeToVersion2(self):
 
431
        self._setConfigDispensers()
 
432
 
 
433
    ## Service functionality.
 
434
 
 
435
    def getGroup(self, name):
 
436
        group = self.groups.get(name)
 
437
        if not group:
 
438
            group = Group(name)
 
439
            self.groups[name] = group
 
440
        return group
 
441
 
 
442
    def createParticipant(self, name):
 
443
        if not self.participants.has_key(name):
 
444
            log.msg("Created New Participant: %s" % name)
 
445
            p = Participant(name)
 
446
            p.setService(self)
 
447
            self.participants[name] = p
 
448
            return p
 
449
 
 
450
    def getPerspectiveNamed(self, name):
 
451
        try:
 
452
            p = self.participants[name]
 
453
        except KeyError:
 
454
            raise UserNonexistantError(name)
 
455
        else:
 
456
            return p
 
457
 
 
458
    def __str__(self):
 
459
        s = "<%s in app '%s' at %x>" % (self.serviceName,
 
460
                                        self.application.name,
 
461
                                        id(self))
 
462
        return s
 
463
 
 
464
coil.registerClass(Service)