2
# Twisted, the Framework of Your Internet
3
# Copyright (C) 2001 Matthew W. Lefkowitz
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.
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.
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
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
31
# Status "enumeration"
37
statuses = ["Offline","Online","Away"]
39
class WordsError(pb.Error):
42
class NotInCollectionError(WordsError):
45
class NotInGroupError(NotInCollectionError):
46
def __init__(self, groupName, pName=None):
47
WordsError.__init__(self, groupName, pName)
48
self.group = groupName
53
pName = "'%s' is" % (self.pName,)
56
s = ("%s not in group '%s'." % (pName, self.group))
59
class UserNonexistantError(NotInCollectionError):
60
def __init__(self, pName):
61
WordsError.__init__(self, pName)
65
return "'%s' does not exist." % (self.pName,)
67
class WrongStatusError(WordsError):
68
def __init__(self, status, pName=None):
69
WordsError.__init__(self, status, pName)
75
pName = "'%s'" % (self.pName,)
79
if self.status in statuses:
82
status = 'unknown? (%s)' % self.status
83
s = ("%s status is '%s'." % (pName, status))
87
class WordsClientInterface:
88
"""A client to a perspective on the twisted.words service.
90
I attach to that participant with Participant.attached(),
91
and detatch with Participant.detached().
94
def receiveContactList(self, contactList):
95
"""Receive a list of contacts and their status.
97
The list is composed of 2-tuples, of the form
98
(contactName, contactStatus)
101
def notifyStatusChanged(self, name, status):
102
"""Notify me of a change in status of one of my contacts.
105
def receiveGroupMembers(self, names, group):
106
"""Receive a list of members in a group.
108
'names' is a list of participant names in the group named 'group'.
111
def setGroupMetadata(self, metadata, name):
112
"""Some metadata on a group has been set.
114
XXX: Should this be receiveGroupMetadata(name, metedata)?
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.
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.
129
def memberJoined(self, member, group):
130
"""Tells me a member has joined a group.
133
def memberLeft(self, member, group):
134
"""Tells me a member has left a group.
138
class Participant(pb.Perspective, styles.Versioned):
139
def __init__(self, name):
140
pb.Perspective.__init__(self, name)
142
self.status = OFFLINE
144
self.reverseContacts = []
149
persistenceVersion = 1
151
def __getstate__(self):
152
state = styles.Versioned.__getstate__(self)
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.
164
def attached(self, client, identity):
165
"""Attach a client which implements WordsClientInterface to me.
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)
172
client.callRemote('receiveContactList', map(lambda contact: (contact.name,
175
self.changeStatus(ONLINE)
178
def changeStatus(self, newStatus):
179
self.status = newStatus
180
for contact in self.reverseContacts:
181
contact.notifyStatusChanged(self)
183
def notifyStatusChanged(self, contact):
185
self.client.callRemote('notifyStatusChanged', contact.name, contact.status)
187
def detached(self, client, identity):
188
log.msg("detached: %s" % self.name)
190
for group in self.groups[:]:
192
self.leaveGroup(group.name)
193
except NotInGroupError:
195
self.changeStatus(OFFLINE)
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)
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)
211
raise NotInCollectionError("No such contact '%s'."
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.
219
group.addMember(self)
220
self.groups.append(group)
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)
228
raise NotInGroupError(name)
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,
236
raise NotInGroupError(groupName)
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)
243
def receiveDirectMessage(self, sender, message, metadata):
246
d = self.client.callRemote('receiveDirectMessage', sender.name, message,
248
#If the client doesn't support metadata, call this function
249
#again with no metadata, so none is sent
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.)
255
d.addErrback(self.receiveDirectMessage,
256
sender.name, message, None)
258
self.client.callRemote('receiveDirectMessage', sender.name, message)
260
raise WrongStatusError(self.status, self.name)
263
def receiveGroupMessage(self, sender, group, message, metadata):
264
if sender is not self and self.client:
266
d = self.client.receiveGroupMessage(sender.name, group.name,
269
d.addErrback(self.receiveGroupMessage,
270
sender, group, message, None)
272
self.client.receiveGroupMessage(sender.name, group.name,
275
def memberJoined(self, member, group):
276
self.client.memberJoined(member.name, group.name)
278
def memberLeft(self, member, group):
279
self.client.memberLeft(member.name, group.name)
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 {})
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 {})
293
raise NotInGroupError(groupName)
295
def setGroupMetadata(self, dict_, groupName):
297
self.client.callRemote('setGroupMetadata', dict_, groupName)
299
def perspective_setGroupMetadata(self, dict_, groupName):
301
if dict_.has_key('topic'):
302
#don't want topic-spoofing, now
303
dict_["topic_author"] = self.name
305
for group in self.groups:
306
if group.name == groupName:
307
group.setMetadata(dict_)
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
320
if self.identityName != "Nobody":
321
id_s = '(id:%s)' % (self.identityName, )
324
s = ("<%s '%s'%s on %s at %x>"
325
% (self.__class__, self.name, id_s,
326
self.service.serviceName, id(self)))
329
class Group(styles.Versioned):
331
def __init__(self, name):
334
self.metadata = {'topic': 'Welcome to %s!' % self.name,
335
'topic_author': 'admin'}
337
def __getstate__(self):
338
state = styles.Versioned.__getstate__(self)
339
state['members'] = []
342
def addMember(self, participant):
343
if participant in self.members:
345
for member in self.members:
346
member.memberJoined(participant, self)
347
participant.setGroupMetadata(self.metadata, self.name)
348
self.members.append(participant)
350
def removeMember(self, participant):
352
self.members.remove(participant)
354
raise NotInGroupError(self.name, participant.name)
356
for member in self.members:
357
member.memberLeft(participant, self)
359
def sendMessage(self, sender, message, metadata):
360
for member in self.members:
361
member.receiveGroupMessage(sender, self, message, metadata)
363
def setMetadata(self, dict_):
364
self.metadata.update(dict_)
365
for member in self.members:
366
member.setGroupMetadata(dict_, self.name)
369
s = "<%s '%s' at %x>" % (self.__class__, self.name, id(self))
373
##Persistence Versioning
375
persistenceVersion = 1
377
def upgradeToVersion1(self):
378
self.metadata = {'topic': self.topic}
380
self.metadata['topic_author'] = 'admin'
382
class Service(pb.Service, styles.Versioned, coil.Configurable):
383
"""I am a chat service.
385
def __init__(self, name, app):
386
pb.Service.__init__(self, name, app)
387
self.participants = {}
389
self._setConfigDispensers()
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]
399
def makeWebAccounts(self):
401
return webwords.WordsGadget(self)
403
def makeIRCGateway(self):
405
return ircservice.IRCGateway(self)
407
def configInit(self, container, name):
408
self.__init__(name, container.app)
410
def getConfiguration(self):
411
return {"name": self.serviceName}
414
'name': types.StringType
417
configName = 'Twisted Words PB Service'
419
def config_name(self, name):
420
raise coil.InvalidConfiguration("You can't change a Service's name.")
422
## Persistence versioning.
423
persistenceVersion = 2
425
def upgradeToVersion1(self):
426
from twisted.internet.app import theApplication
427
styles.requireUpgrade(theApplication)
428
pb.Service.__init__(self, 'twisted.words', theApplication)
430
def upgradeToVersion2(self):
431
self._setConfigDispensers()
433
## Service functionality.
435
def getGroup(self, name):
436
group = self.groups.get(name)
439
self.groups[name] = group
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)
447
self.participants[name] = p
450
def getPerspectiveNamed(self, name):
452
p = self.participants[name]
454
raise UserNonexistantError(name)
459
s = "<%s in app '%s' at %x>" % (self.serviceName,
460
self.application.name,
464
coil.registerClass(Service)