comparison src/core/xmpp.py @ 330:608a4a2ba94e

Core: created a new core module where xmpp classes are put
author Goffi <goffi@goffi.org>
date Mon, 23 May 2011 21:18:58 +0200
parents src/sat.tac@b069055320b1
children 4c835d614bdb
comparison
equal deleted inserted replaced
329:be9f682c53a5 330:608a4a2ba94e
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 """
5 SAT: a jabber client
6 Copyright (C) 2009, 2010, 2011 Jérôme Poisson (goffi@goffi.org)
7
8 This program is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with this program. If not, see <http://www.gnu.org/licenses/>.
20 """
21
22 from twisted.internet import task, defer
23 from twisted.words.protocols.jabber import jid, xmlstream
24 from wokkel import client, disco, xmppim, generic, compat
25 from logging import debug, info, error
26
27 class SatXMPPClient(client.XMPPClient):
28
29 def __init__(self, host_app, profile, user_jid, password, host=None, port=5222):
30 client.XMPPClient.__init__(self, user_jid, password, host, port)
31 self.factory.clientConnectionLost = self.connectionLost
32 self.__connected=False
33 self.profile = profile
34 self.host_app = host_app
35 self.client_initialized = defer.Deferred()
36
37 def _authd(self, xmlstream):
38 if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
39 return
40 client.XMPPClient._authd(self, xmlstream)
41 self.__connected=True
42 info (_("********** [%s] CONNECTED **********") % self.profile)
43 self.streamInitialized()
44 self.host_app.bridge.connected(self.profile) #we send the signal to the clients
45
46
47 def streamInitialized(self):
48 """Called after _authd"""
49 debug (_("XML stream is initialized"))
50 self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") #Needed to avoid disconnection (specially with openfire)
51 self.keep_alife.start(180)
52
53 self.disco = SatDiscoProtocol(self)
54 self.disco.setHandlerParent(self)
55 self.discoHandler = disco.DiscoHandler()
56 self.discoHandler.setHandlerParent(self)
57
58 if not self.host_app.trigger.point("Disco Handled", self.profile):
59 return
60
61 self.roster.requestRoster()
62
63 self.presence.available()
64
65 self.disco.requestInfo(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDisco, self.profile) #FIXME: use these informations
66 self.disco.requestItems(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDiscoItems, self.disco, self.profile, self.client_initialized)
67
68 def initializationFailed(self, reason):
69 print ("initializationFailed: %s" % reason)
70 self.host_app.bridge.connectionError("AUTH_ERROR", self.profile)
71 try:
72 client.XMPPClient.initializationFailed(self, reason)
73 except:
74 #we already send an error signal, no need to raise an exception
75 pass
76
77 def isConnected(self):
78 return self.__connected
79
80 def connectionLost(self, connector, unused_reason):
81 self.__connected=False
82 info (_("********** [%s] DISCONNECTED **********") % self.profile)
83 try:
84 self.keep_alife.stop()
85 except AttributeError:
86 debug (_("No keep_alife"))
87 self.host_app.bridge.disconnected(self.profile) #we send the signal to the clients
88
89
90 class SatMessageProtocol(xmppim.MessageProtocol):
91
92 def __init__(self, host):
93 xmppim.MessageProtocol.__init__(self)
94 self.host = host
95
96 def onMessage(self, message):
97 debug (_(u"got message from: %s"), message["from"])
98 if not self.host.trigger.point("MessageReceived",message, profile=self.parent.profile):
99 return
100 for e in message.elements():
101 if e.name == "body":
102 mess_type = message['type'] if message.hasAttribute('type') else 'normal'
103 self.host.bridge.newMessage(message["from"], e.children[0], mess_type, message['to'], profile=self.parent.profile)
104 self.host.memory.addToHistory(self.parent.jid, jid.JID(message["from"]), self.parent.jid, "chat", e.children[0])
105 break
106
107 class SatRosterProtocol(xmppim.RosterClientProtocol):
108
109 def __init__(self, host):
110 xmppim.RosterClientProtocol.__init__(self)
111 self.host = host
112 self._groups=set()
113
114 def rosterCb(self, roster):
115 for raw_jid, item in roster.iteritems():
116 self.onRosterSet(item)
117
118 def requestRoster(self):
119 """ ask the server for Roster list """
120 debug("requestRoster")
121 self.getRoster().addCallback(self.rosterCb)
122
123 def removeItem(self, to):
124 """Remove a contact from roster list"""
125 xmppim.RosterClientProtocol.removeItem(self, to)
126 #TODO: check IQ result
127
128 #XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
129 #def addItem(self, to):
130 #"""Add a contact to roster list"""
131 #xmppim.RosterClientProtocol.addItem(self, to)
132 #TODO: check IQ result"""
133
134 def onRosterSet(self, item):
135 """Called when a new/update roster item is received"""
136 #TODO: send a signal to frontends
137 item_attr = {'to': str(item.subscriptionTo),
138 'from': str(item.subscriptionFrom),
139 'ask': str(item.ask)
140 }
141 if item.name:
142 item_attr['name'] = item.name
143 info (_("new contact in roster list: %s"), item.jid.full())
144 self.host.memory.addContact(item.jid, item_attr, item.groups, self.parent.profile)
145 self.host.bridge.newContact(item.jid.full(), item_attr, item.groups, self.parent.profile)
146 self._groups.update(item.groups)
147
148 def onRosterRemove(self, entity):
149 """Called when a roster removal event is received"""
150 #TODO: send a signal to frontends
151 print _("removing %s from roster list") % entity.full()
152 self.host.memory.delContact(entity, self.parent.profile)
153
154 def getGroups(self):
155 """Return a set of groups"""
156 return self._groups
157
158 class SatPresenceProtocol(xmppim.PresenceClientProtocol):
159
160 def __init__(self, host):
161 xmppim.PresenceClientProtocol.__init__(self)
162 self.host = host
163
164 def availableReceived(self, entity, show=None, statuses=None, priority=0):
165 debug (_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity':entity, 'show':show, 'statuses':statuses, 'priority':priority})
166
167 if statuses.has_key(None): #we only want string keys
168 statuses["default"] = statuses[None]
169 del statuses[None]
170
171 self.host.memory.addPresenceStatus(entity, show or "",
172 int(priority), statuses, self.parent.profile)
173
174 #now it's time to notify frontends
175 self.host.bridge.presenceUpdate(entity.full(), show or "",
176 int(priority), statuses, self.parent.profile)
177
178 def unavailableReceived(self, entity, statuses=None):
179 debug (_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity':entity, 'statuses':statuses})
180 if statuses and statuses.has_key(None): #we only want string keys
181 statuses["default"] = statuses[None]
182 del statuses[None]
183 self.host.memory.addPresenceStatus(entity, "unavailable", 0, statuses, self.parent.profile)
184
185 #now it's time to notify frontends
186 self.host.bridge.presenceUpdate(entity.full(), "unavailable", 0, statuses, self.parent.profile)
187
188
189 def available(self, entity=None, show=None, statuses=None, priority=0):
190 if statuses and statuses.has_key('default'):
191 statuses[None] = statuses['default']
192 del statuses['default']
193 xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority)
194
195 def subscribedReceived(self, entity):
196 debug (_("subscription approved for [%s]") % entity.userhost())
197 self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
198 self.host.bridge.subscribe('subscribed', entity.userhost(), self.parent.profile)
199
200 def unsubscribedReceived(self, entity):
201 debug (_("unsubscription confirmed for [%s]") % entity.userhost())
202 self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
203 self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile)
204
205 def subscribeReceived(self, entity):
206 debug (_("subscription request for [%s]") % entity.userhost())
207 self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile)
208 self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile)
209
210 def unsubscribeReceived(self, entity):
211 debug (_("unsubscription asked for [%s]") % entity.userhost())
212 self.host.memory.addWaitingSub('unsubscribe', entity.userhost(), self.parent.profile)
213 self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile)
214
215 class SatDiscoProtocol(disco.DiscoClientProtocol):
216 def __init__(self, host):
217 disco.DiscoClientProtocol.__init__(self)
218
219 class SatFallbackHandler(generic.FallbackHandler):
220 def __init__(self, host):
221 generic.FallbackHandler.__init__(self)
222
223 def iqFallback(self, iq):
224 if iq.handled == True:
225 return
226 debug (u"iqFallback: xml = [%s]" % (iq.toXml()))
227 generic.FallbackHandler.iqFallback(self, iq)
228
229 class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
230
231 def __init__(self, host, jabber_host, user_login, user_pass, answer_id):
232 xmlstream.ConnectAuthenticator.__init__(self, jabber_host)
233 self.host = host
234 self.jabber_host = jabber_host
235 self.user_login = user_login
236 self.user_pass = user_pass
237 self.answer_id = answer_id
238 print _("Registration asked for"),user_login, user_pass, jabber_host
239
240 def connectionMade(self):
241 print "connectionMade"
242
243 self.xmlstream.namespace = "jabber:client"
244 self.xmlstream.sendHeader()
245
246 iq = compat.IQ(self.xmlstream, 'set')
247 iq["to"] = self.jabber_host
248 query = iq.addElement(('jabber:iq:register', 'query'))
249 _user = query.addElement('username')
250 _user.addContent(self.user_login)
251 _pass = query.addElement('password')
252 _pass.addContent(self.user_pass)
253 reg = iq.send(self.jabber_host).addCallbacks(self.registrationAnswer, self.registrationFailure)
254
255 def registrationAnswer(self, answer):
256 debug (_("registration answer: %s") % answer.toXml())
257 answer_type = "SUCCESS"
258 answer_data={"message":_("Registration successfull")}
259 self.host.bridge.actionResult(answer_type, self.answer_id, answer_data)
260 self.xmlstream.sendFooter()
261
262 def registrationFailure(self, failure):
263 info (_("Registration failure: %s") % str(failure.value))
264 answer_type = "ERROR"
265 answer_data = {}
266 if failure.value.condition == 'conflict':
267 answer_data['reason'] = 'conflict'
268 answer_data={"message":_("Username already exists, please choose an other one")}
269 else:
270 answer_data['reason'] = 'unknown'
271 answer_data={"message":_("Registration failed (%s)") % str(failure.value.condition)}
272 self.host.bridge.actionResult(answer_type, self.answer_id, answer_data)
273 self.xmlstream.sendFooter()
274
275 class SatVersionHandler(generic.VersionHandler):
276
277 def getDiscoInfo(self, requestor, target, node):
278 #XXX: We need to work around wokkel's behavious (namespace not added if there is a
279 # node) as it cause issues with XEP-0115 & PEP (XEP-0163): there is a node when server
280 # ask for disco info, and not when we generate the key, so the hash is used with different
281 # disco features, and when the server (seen on ejabberd) generate its own hash for security check
282 # it reject our features (resulting in e.g. no notification on PEP)
283 return generic.VersionHandler.getDiscoInfo(self, requestor, target, None)
284