[9] | 1 | # -*- test-case-name: wokkel.test.test_xmppim -*- |
---|
| 2 | # |
---|
| 3 | # Copyright (c) 2003-2008 Ralph Meijer |
---|
[2] | 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 | |
---|
[9] | 199 | def unavailable(self, entity=None, statuses=None): |
---|
[2] | 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 | """ |
---|
[9] | 211 | self.send(UnavailablePresence(entity, statuses)) |
---|
[2] | 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, |
---|
[9] | 318 | 'item', NS_ROSTER): |
---|
[2] | 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 | """ |
---|