1 | # -*- test-case-name: wokkel.test.test_xmppim -*- |
---|
2 | # |
---|
3 | # Copyright (c) 2003-2008 Ralph Meijer |
---|
4 | # See LICENSE for details. |
---|
5 | |
---|
6 | """ |
---|
7 | XMPP IM protocol support. |
---|
8 | |
---|
9 | This module provides generic implementations for the protocols defined in |
---|
10 | U{RFC 3921<http://www.xmpp.org/rfcs/rfc3921.html>} (XMPP IM). |
---|
11 | |
---|
12 | All of it should eventually move to Twisted. |
---|
13 | """ |
---|
14 | |
---|
15 | from twisted.words.protocols.jabber.jid import JID |
---|
16 | from twisted.words.protocols.jabber.xmlstream import IQ |
---|
17 | from twisted.words.xish import domish |
---|
18 | |
---|
19 | from wokkel.subprotocols import XMPPHandler |
---|
20 | |
---|
21 | NS_XML = 'http://www.w3.org/XML/1998/namespace' |
---|
22 | NS_ROSTER = 'jabber:iq:roster' |
---|
23 | |
---|
24 | class Presence(domish.Element): |
---|
25 | def __init__(self, to=None, type=None): |
---|
26 | domish.Element.__init__(self, (None, "presence")) |
---|
27 | if type: |
---|
28 | self["type"] = type |
---|
29 | |
---|
30 | if to is not None: |
---|
31 | self["to"] = to.full() |
---|
32 | |
---|
33 | class AvailablePresence(Presence): |
---|
34 | def __init__(self, to=None, show=None, statuses=None, priority=0): |
---|
35 | Presence.__init__(self, to, type=None) |
---|
36 | |
---|
37 | if show in ['away', 'xa', 'chat', 'dnd']: |
---|
38 | self.addElement('show', content=show) |
---|
39 | |
---|
40 | if statuses is not None: |
---|
41 | for lang, status in statuses.iteritems(): |
---|
42 | s = self.addElement('status', content=status) |
---|
43 | if lang: |
---|
44 | s[(NS_XML, "lang")] = lang |
---|
45 | |
---|
46 | if priority != 0: |
---|
47 | self.addElement('priority', int(priority)) |
---|
48 | |
---|
49 | class UnavailablePresence(Presence): |
---|
50 | def __init__(self, to=None, statuses=None): |
---|
51 | Presence.__init__(self, to, type='unavailable') |
---|
52 | |
---|
53 | if statuses is not None: |
---|
54 | for lang, status in statuses.iteritems(): |
---|
55 | s = self.addElement('status', content=status) |
---|
56 | if lang: |
---|
57 | s[(NS_XML, "lang")] = lang |
---|
58 | |
---|
59 | class PresenceClientProtocol(XMPPHandler): |
---|
60 | |
---|
61 | def connectionInitialized(self): |
---|
62 | self.xmlstream.addObserver('/presence', self._onPresence) |
---|
63 | |
---|
64 | def _getStatuses(self, presence): |
---|
65 | statuses = {} |
---|
66 | for element in presence.elements(): |
---|
67 | if element.name == 'status': |
---|
68 | lang = element.getAttribute((NS_XML, 'lang')) |
---|
69 | text = unicode(element) |
---|
70 | statuses[lang] = text |
---|
71 | return statuses |
---|
72 | |
---|
73 | def _onPresence(self, presence): |
---|
74 | type = presence.getAttribute("type", "available") |
---|
75 | try: |
---|
76 | handler = getattr(self, '_onPresence%s' % (type.capitalize())) |
---|
77 | except AttributeError: |
---|
78 | return |
---|
79 | else: |
---|
80 | handler(presence) |
---|
81 | |
---|
82 | def _onPresenceAvailable(self, presence): |
---|
83 | entity = JID(presence["from"]) |
---|
84 | |
---|
85 | show = unicode(presence.show or '') |
---|
86 | if show not in ['away', 'xa', 'chat', 'dnd']: |
---|
87 | show = None |
---|
88 | |
---|
89 | statuses = self._getStatuses(presence) |
---|
90 | |
---|
91 | try: |
---|
92 | priority = int(unicode(presence.priority or '')) or 0 |
---|
93 | except ValueError: |
---|
94 | priority = 0 |
---|
95 | |
---|
96 | self.availableReceived(entity, show, statuses, priority) |
---|
97 | |
---|
98 | def _onPresenceUnavailable(self, presence): |
---|
99 | entity = JID(presence["from"]) |
---|
100 | |
---|
101 | statuses = self._getStatuses(presence) |
---|
102 | |
---|
103 | self.unavailableReceived(entity, statuses) |
---|
104 | |
---|
105 | def _onPresenceSubscribed(self, presence): |
---|
106 | self.subscribedReceived(JID(presence["from"])) |
---|
107 | |
---|
108 | def _onPresenceUnsubscribed(self, presence): |
---|
109 | self.unsubscribedReceived(JID(presence["from"])) |
---|
110 | |
---|
111 | def _onPresenceSubscribe(self, presence): |
---|
112 | self.subscribeReceived(JID(presence["from"])) |
---|
113 | |
---|
114 | def _onPresenceUnsubscribe(self, presence): |
---|
115 | self.unsubscribeReceived(JID(presence["from"])) |
---|
116 | |
---|
117 | def availableReceived(self, entity, show=None, statuses=None, priority=0): |
---|
118 | """ |
---|
119 | Available presence was received. |
---|
120 | |
---|
121 | @param entity: entity from which the presence was received. |
---|
122 | @type entity: {JID} |
---|
123 | @param show: detailed presence information. One of C{'away'}, C{'xa'}, |
---|
124 | C{'chat'}, C{'dnd'} or C{None}. |
---|
125 | @type show: C{str} or C{NoneType} |
---|
126 | @param statuses: dictionary of natural language descriptions of the |
---|
127 | availability status, keyed by the language |
---|
128 | descriptor. A status without a language |
---|
129 | specified, is keyed with C{None}. |
---|
130 | @type statuses: C{dict} |
---|
131 | @param priority: priority level of the resource. |
---|
132 | @type priority: C{int} |
---|
133 | """ |
---|
134 | |
---|
135 | def unavailableReceived(self, entity, statuses=None): |
---|
136 | """ |
---|
137 | Unavailable presence was received. |
---|
138 | |
---|
139 | @param entity: entity from which the presence was received. |
---|
140 | @type entity: {JID} |
---|
141 | @param statuses: dictionary of natural language descriptions of the |
---|
142 | availability status, keyed by the language |
---|
143 | descriptor. A status without a language |
---|
144 | specified, is keyed with C{None}. |
---|
145 | @type statuses: C{dict} |
---|
146 | """ |
---|
147 | |
---|
148 | def subscribedReceived(self, entity): |
---|
149 | """ |
---|
150 | Subscription approval confirmation was received. |
---|
151 | |
---|
152 | @param entity: entity from which the confirmation was received. |
---|
153 | @type entity: {JID} |
---|
154 | """ |
---|
155 | |
---|
156 | def unsubscribedReceived(self, entity): |
---|
157 | """ |
---|
158 | Unsubscription confirmation was received. |
---|
159 | |
---|
160 | @param entity: entity from which the confirmation was received. |
---|
161 | @type entity: {JID} |
---|
162 | """ |
---|
163 | |
---|
164 | def subscribeReceived(self, entity): |
---|
165 | """ |
---|
166 | Subscription request was received. |
---|
167 | |
---|
168 | @param entity: entity from which the request was received. |
---|
169 | @type entity: {JID} |
---|
170 | """ |
---|
171 | |
---|
172 | def unsubscribeReceived(self, entity): |
---|
173 | """ |
---|
174 | Unsubscription request was received. |
---|
175 | |
---|
176 | @param entity: entity from which the request was received. |
---|
177 | @type entity: {JID} |
---|
178 | """ |
---|
179 | |
---|
180 | def available(self, entity=None, show=None, statuses=None, priority=0): |
---|
181 | """ |
---|
182 | Send available presence. |
---|
183 | |
---|
184 | @param entity: optional entity to which the presence should be sent. |
---|
185 | @type entity: {JID} |
---|
186 | @param show: optional detailed presence information. One of C{'away'}, |
---|
187 | C{'xa'}, C{'chat'}, C{'dnd'}. |
---|
188 | @type show: C{str} |
---|
189 | @param statuses: dictionary of natural language descriptions of the |
---|
190 | availability status, keyed by the language |
---|
191 | descriptor. A status without a language |
---|
192 | specified, is keyed with C{None}. |
---|
193 | @type statuses: C{dict} |
---|
194 | @param priority: priority level of the resource. |
---|
195 | @type priority: C{int} |
---|
196 | """ |
---|
197 | self.send(AvailablePresence(entity, show, statuses, priority)) |
---|
198 | |
---|
199 | def unavailable(self, entity=None, statuses=None): |
---|
200 | """ |
---|
201 | Send unavailable presence. |
---|
202 | |
---|
203 | @param entity: optional entity to which the presence should be sent. |
---|
204 | @type entity: {JID} |
---|
205 | @param statuses: dictionary of natural language descriptions of the |
---|
206 | availability status, keyed by the language |
---|
207 | descriptor. A status without a language |
---|
208 | specified, is keyed with C{None}. |
---|
209 | @type statuses: C{dict} |
---|
210 | """ |
---|
211 | self.send(UnavailablePresence(entity, statuses)) |
---|
212 | |
---|
213 | def subscribe(self, entity): |
---|
214 | """ |
---|
215 | Send subscription request |
---|
216 | |
---|
217 | @param entity: entity to subscribe to. |
---|
218 | @type entity: {JID} |
---|
219 | """ |
---|
220 | self.send(Presence(to=entity, type='subscribe')) |
---|
221 | |
---|
222 | def unsubscribe(self, entity): |
---|
223 | """ |
---|
224 | Send unsubscription request |
---|
225 | |
---|
226 | @param entity: entity to unsubscribe from. |
---|
227 | @type entity: {JID} |
---|
228 | """ |
---|
229 | self.send(Presence(to=entity, type='unsubscribe')) |
---|
230 | |
---|
231 | def subscribed(self, entity): |
---|
232 | """ |
---|
233 | Send subscription confirmation. |
---|
234 | |
---|
235 | @param entity: entity that subscribed. |
---|
236 | @type entity: {JID} |
---|
237 | """ |
---|
238 | self.send(Presence(to=entity, type='subscribed')) |
---|
239 | |
---|
240 | def unsubscribed(self, entity): |
---|
241 | """ |
---|
242 | Send unsubscription confirmation. |
---|
243 | |
---|
244 | @param entity: entity that unsubscribed. |
---|
245 | @type entity: {JID} |
---|
246 | """ |
---|
247 | self.send(Presence(to=entity, type='unsubscribed')) |
---|
248 | |
---|
249 | |
---|
250 | class RosterItem(object): |
---|
251 | """ |
---|
252 | Roster item. |
---|
253 | |
---|
254 | This represents one contact from an XMPP contact list known as roster. |
---|
255 | |
---|
256 | @ivar jid: The JID of the contact. |
---|
257 | @type jid: L{JID} |
---|
258 | @ivar name: The optional associated nickname for this contact. |
---|
259 | @type name: C{unicode} |
---|
260 | @ivar subscriptionTo: Subscription state to contact's presence. If C{True}, |
---|
261 | the roster owner is subscribed to the presence |
---|
262 | information of the contact. |
---|
263 | @type subscriptionTo: C{bool} |
---|
264 | @ivar subscriptionFrom: Contact's subscription state. If C{True}, the |
---|
265 | contact is subscribed to the presence information |
---|
266 | of the roster owner. |
---|
267 | @type subscriptionTo: C{bool} |
---|
268 | @ivar ask: Whether subscription is pending. |
---|
269 | @type ask: C{bool} |
---|
270 | @ivar groups: Set of groups this contact is categorized in. Groups are |
---|
271 | represented by an opaque identifier of type C{unicode}. |
---|
272 | @type groups: C{set} |
---|
273 | """ |
---|
274 | |
---|
275 | def __init__(self, jid): |
---|
276 | self.jid = jid |
---|
277 | self.name = None |
---|
278 | self.subscriptionTo = False |
---|
279 | self.subscriptionFrom = False |
---|
280 | self.ask = None |
---|
281 | self.groups = set() |
---|
282 | |
---|
283 | |
---|
284 | class RosterClientProtocol(XMPPHandler): |
---|
285 | """ |
---|
286 | Client side XMPP roster protocol. |
---|
287 | """ |
---|
288 | |
---|
289 | def connectionInitialized(self): |
---|
290 | ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER |
---|
291 | self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet) |
---|
292 | |
---|
293 | def _parseRosterItem(self, element): |
---|
294 | jid = JID(element['jid']) |
---|
295 | item = RosterItem(jid) |
---|
296 | item.name = element.getAttribute('name') |
---|
297 | subscription = element.getAttribute('subscription') |
---|
298 | item.subscriptionTo = subscription in ('to', 'both') |
---|
299 | item.subscriptionFrom = subscription in ('from', 'both') |
---|
300 | item.ask = element.getAttribute('ask') == 'subscribe' |
---|
301 | for subElement in domish.generateElementsQNamed(element.children, |
---|
302 | 'group', NS_ROSTER): |
---|
303 | item.groups.add(unicode(subElement)) |
---|
304 | |
---|
305 | return item |
---|
306 | |
---|
307 | def getRoster(self): |
---|
308 | """ |
---|
309 | Retrieve contact list. |
---|
310 | |
---|
311 | @return: Roster as a mapping from L{JID} to L{RosterItem}. |
---|
312 | @rtype: L{twisted.internet.defer.Deferred} |
---|
313 | """ |
---|
314 | |
---|
315 | def processRoster(result): |
---|
316 | roster = {} |
---|
317 | for element in domish.generateElementsQNamed(result.query.children, |
---|
318 | 'item', NS_ROSTER): |
---|
319 | item = self._parseRosterItem(element) |
---|
320 | roster[item.jid.userhost()] = item |
---|
321 | |
---|
322 | return roster |
---|
323 | |
---|
324 | iq = IQ(self.xmlstream, 'get') |
---|
325 | iq.addElement((NS_ROSTER, 'query')) |
---|
326 | d = iq.send() |
---|
327 | d.addCallback(processRoster) |
---|
328 | return d |
---|
329 | |
---|
330 | def _onRosterSet(self, iq): |
---|
331 | if iq.handled or \ |
---|
332 | iq.hasAttribute('from') and iq['from'] != self.xmlstream: |
---|
333 | return |
---|
334 | |
---|
335 | iq.handled = True |
---|
336 | |
---|
337 | itemElement = iq.query.item |
---|
338 | |
---|
339 | if unicode(itemElement['subscription']) == 'remove': |
---|
340 | self.onRosterRemove(JID(itemElement['jid'])) |
---|
341 | else: |
---|
342 | item = self._parseRosterItem(iq.query.item) |
---|
343 | self.onRosterSet(item) |
---|
344 | |
---|
345 | def onRosterSet(self, item): |
---|
346 | """ |
---|
347 | Called when a roster push for a new or update item was received. |
---|
348 | |
---|
349 | @param item: The pushed roster item. |
---|
350 | @type item: L{RosterItem} |
---|
351 | """ |
---|
352 | |
---|
353 | def onRosterRemove(self, entity): |
---|
354 | """ |
---|
355 | Called when a roster push for the removal of an item was received. |
---|
356 | |
---|
357 | @param entity: The entity for which the roster item has been removed. |
---|
358 | @type entity: L{JID} |
---|
359 | """ |
---|
360 | |
---|
361 | class MessageProtocol(XMPPHandler): |
---|
362 | """ |
---|
363 | Generic XMPP subprotocol handler for incoming message stanzas. |
---|
364 | """ |
---|
365 | |
---|
366 | messageTypes = None, 'normal', 'chat', 'headline', 'groupchat' |
---|
367 | |
---|
368 | def connectionInitialized(self): |
---|
369 | self.xmlstream.addObserver("/message", self._onMessage) |
---|
370 | |
---|
371 | def _onMessage(self, message): |
---|
372 | if message.handled: |
---|
373 | return |
---|
374 | |
---|
375 | messageType = message.getAttribute("type") |
---|
376 | |
---|
377 | if messageType == 'error': |
---|
378 | return |
---|
379 | |
---|
380 | if messageType not in self.messageTypes: |
---|
381 | message["type"] = 'normal' |
---|
382 | |
---|
383 | self.onMessage(message) |
---|
384 | |
---|
385 | def onMessage(self, message): |
---|
386 | """ |
---|
387 | Called when a message stanza was received. |
---|
388 | """ |
---|