1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4
5"""
6L{twisted.words} support for Instance Messenger.
7"""
8
9from twisted.internet import defer
10from twisted.internet import error
11from twisted.python import log
12from twisted.python.failure import Failure
13from twisted.spread import pb
14
15from twisted.words.im.locals import ONLINE, OFFLINE, AWAY
16
17from twisted.words.im import basesupport, interfaces
18from zope.interface import implements
19
20
21class TwistedWordsPerson(basesupport.AbstractPerson):
22    """I a facade for a person you can talk to through a twisted.words service.
23    """
24    def __init__(self, name, wordsAccount):
25        basesupport.AbstractPerson.__init__(self, name, wordsAccount)
26        self.status = OFFLINE
27
28    def isOnline(self):
29        return ((self.status == ONLINE) or
30                (self.status == AWAY))
31
32    def getStatus(self):
33        return self.status
34
35    def sendMessage(self, text, metadata):
36        """Return a deferred...
37        """
38        if metadata:
39            d=self.account.client.perspective.directMessage(self.name,
40                                                            text, metadata)
41            d.addErrback(self.metadataFailed, "* "+text)
42            return d
43        else:
44            return self.account.client.perspective.callRemote('directMessage',self.name, text)
45
46    def metadataFailed(self, result, text):
47        print "result:",result,"text:",text
48        return self.account.client.perspective.directMessage(self.name, text)
49
50    def setStatus(self, status):
51        self.status = status
52        self.chat.getContactsList().setContactStatus(self)
53
54class TwistedWordsGroup(basesupport.AbstractGroup):
55    implements(interfaces.IGroup)
56    def __init__(self, name, wordsClient):
57        basesupport.AbstractGroup.__init__(self, name, wordsClient)
58        self.joined = 0
59
60    def sendGroupMessage(self, text, metadata=None):
61        """Return a deferred.
62        """
63        #for backwards compatibility with older twisted.words servers.
64        if metadata:
65            d=self.account.client.perspective.callRemote(
66                'groupMessage', self.name, text, metadata)
67            d.addErrback(self.metadataFailed, "* "+text)
68            return d
69        else:
70            return self.account.client.perspective.callRemote('groupMessage',
71                                                              self.name, text)
72
73    def setTopic(self, text):
74        self.account.client.perspective.callRemote(
75            'setGroupMetadata',
76            {'topic': text, 'topic_author': self.client.name},
77            self.name)
78
79    def metadataFailed(self, result, text):
80        print "result:",result,"text:",text
81        return self.account.client.perspective.callRemote('groupMessage',
82                                                          self.name, text)
83
84    def joining(self):
85        self.joined = 1
86
87    def leaving(self):
88        self.joined = 0
89
90    def leave(self):
91        return self.account.client.perspective.callRemote('leaveGroup',
92                                                          self.name)
93
94
95
96class TwistedWordsClient(pb.Referenceable, basesupport.AbstractClientMixin):
97    """In some cases, this acts as an Account, since it a source of text
98    messages (multiple Words instances may be on a single PB connection)
99    """
100    def __init__(self, acct, serviceName, perspectiveName, chatui,
101                 _logonDeferred=None):
102        self.accountName = "%s (%s:%s)" % (acct.accountName, serviceName, perspectiveName)
103        self.name = perspectiveName
104        print "HELLO I AM A PB SERVICE", serviceName, perspectiveName
105        self.chat = chatui
106        self.account = acct
107        self._logonDeferred = _logonDeferred
108
109    def getPerson(self, name):
110        return self.chat.getPerson(name, self)
111
112    def getGroup(self, name):
113        return self.chat.getGroup(name, self)
114
115    def getGroupConversation(self, name):
116        return self.chat.getGroupConversation(self.getGroup(name))
117
118    def addContact(self, name):
119        self.perspective.callRemote('addContact', name)
120
121    def remote_receiveGroupMembers(self, names, group):
122        print 'received group members:', names, group
123        self.getGroupConversation(group).setGroupMembers(names)
124
125    def remote_receiveGroupMessage(self, sender, group, message, metadata=None):
126        print 'received a group message', sender, group, message, metadata
127        self.getGroupConversation(group).showGroupMessage(sender, message, metadata)
128
129    def remote_memberJoined(self, member, group):
130        print 'member joined', member, group
131        self.getGroupConversation(group).memberJoined(member)
132
133    def remote_memberLeft(self, member, group):
134        print 'member left'
135        self.getGroupConversation(group).memberLeft(member)
136
137    def remote_notifyStatusChanged(self, name, status):
138        self.chat.getPerson(name, self).setStatus(status)
139
140    def remote_receiveDirectMessage(self, name, message, metadata=None):
141        self.chat.getConversation(self.chat.getPerson(name, self)).showMessage(message, metadata)
142
143    def remote_receiveContactList(self, clist):
144        for name, status in clist:
145            self.chat.getPerson(name, self).setStatus(status)
146
147    def remote_setGroupMetadata(self, dict_, groupName):
148        if dict_.has_key("topic"):
149            self.getGroupConversation(groupName).setTopic(dict_["topic"], dict_.get("topic_author", None))
150
151    def joinGroup(self, name):
152        self.getGroup(name).joining()
153        return self.perspective.callRemote('joinGroup', name).addCallback(self._cbGroupJoined, name)
154
155    def leaveGroup(self, name):
156        self.getGroup(name).leaving()
157        return self.perspective.callRemote('leaveGroup', name).addCallback(self._cbGroupLeft, name)
158
159    def _cbGroupJoined(self, result, name):
160        groupConv = self.chat.getGroupConversation(self.getGroup(name))
161        groupConv.showGroupMessage("sys", "you joined")
162        self.perspective.callRemote('getGroupMembers', name)
163
164    def _cbGroupLeft(self, result, name):
165        print 'left',name
166        groupConv = self.chat.getGroupConversation(self.getGroup(name), 1)
167        groupConv.showGroupMessage("sys", "you left")
168
169    def connected(self, perspective):
170        print 'Connected Words Client!', perspective
171        if self._logonDeferred is not None:
172            self._logonDeferred.callback(self)
173        self.perspective = perspective
174        self.chat.getContactsList()
175
176
177pbFrontEnds = {
178    "twisted.words": TwistedWordsClient,
179    "twisted.reality": None
180    }
181
182
183class PBAccount(basesupport.AbstractAccount):
184    implements(interfaces.IAccount)
185    gatewayType = "PB"
186    _groupFactory = TwistedWordsGroup
187    _personFactory = TwistedWordsPerson
188
189    def __init__(self, accountName, autoLogin, username, password, host, port,
190                 services=None):
191        """
192        @param username: The name of your PB Identity.
193        @type username: string
194        """
195        basesupport.AbstractAccount.__init__(self, accountName, autoLogin,
196                                             username, password, host, port)
197        self.services = []
198        if not services:
199            services = [('twisted.words', 'twisted.words', username)]
200        for serviceType, serviceName, perspectiveName in services:
201            self.services.append([pbFrontEnds[serviceType], serviceName,
202                                  perspectiveName])
203
204    def logOn(self, chatui):
205        """
206        @returns: this breaks with L{interfaces.IAccount}
207        @returntype: DeferredList of L{interfaces.IClient}s
208        """
209        # Overriding basesupport's implementation on account of the
210        # fact that _startLogOn tends to return a deferredList rather
211        # than a simple Deferred, and we need to do registerAccountClient.
212        if (not self._isConnecting) and (not self._isOnline):
213            self._isConnecting = 1
214            d = self._startLogOn(chatui)
215            d.addErrback(self._loginFailed)
216            def registerMany(results):
217                for success, result in results:
218                    if success:
219                        chatui.registerAccountClient(result)
220                        self._cb_logOn(result)
221                    else:
222                        log.err(result)
223            d.addCallback(registerMany)
224            return d
225        else:
226            raise error.ConnectionError("Connection in progress")
227
228
229    def _startLogOn(self, chatui):
230        print 'Connecting...',
231        d = pb.getObjectAt(self.host, self.port)
232        d.addCallbacks(self._cbConnected, self._ebConnected,
233                       callbackArgs=(chatui,))
234        return d
235
236    def _cbConnected(self, root, chatui):
237        print 'Connected!'
238        print 'Identifying...',
239        d = pb.authIdentity(root, self.username, self.password)
240        d.addCallbacks(self._cbIdent, self._ebConnected,
241                       callbackArgs=(chatui,))
242        return d
243
244    def _cbIdent(self, ident, chatui):
245        if not ident:
246            print 'falsely identified.'
247            return self._ebConnected(Failure(Exception("username or password incorrect")))
248        print 'Identified!'
249        dl = []
250        for handlerClass, sname, pname in self.services:
251            d = defer.Deferred()
252            dl.append(d)
253            handler = handlerClass(self, sname, pname, chatui, d)
254            ident.callRemote('attach', sname, pname, handler).addCallback(handler.connected)
255        return defer.DeferredList(dl)
256
257    def _ebConnected(self, error):
258        print 'Not connected.'
259        return error
260
261