comparison src/plugins/plugin_xep_0115.py @ 282:6a0c6d8e119d

added plugin xep-0115: entity capabilities
author Goffi <goffi@goffi.org>
date Thu, 03 Feb 2011 01:27:57 +0100
parents
children 68cd30d982a5
comparison
equal deleted inserted replaced
281:1e3e169955b2 282:6a0c6d8e119d
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 """
5 SAT plugin for managing xep-0115
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 logging import debug, info, error, warning
23 from twisted.words.xish import domish
24 from twisted.words.protocols.jabber import client, jid, xmlstream
25 from twisted.words.protocols.jabber import error as jab_error
26 from twisted.words.protocols.jabber.xmlstream import IQ
27 import os.path
28 import types
29
30 from zope.interface import implements
31
32 from wokkel import disco, iwokkel
33
34 from hashlib import sha1
35 from base64 import b64encode
36
37 try:
38 from twisted.words.protocols.xmlstream import XMPPHandler
39 except ImportError:
40 from wokkel.subprotocols import XMPPHandler
41
42 PRESENCE = '/presence'
43 NS_ENTITY_CAPABILITY = 'http://jabber.org/protocol/caps'
44 CAPABILITY_UPDATE = PRESENCE + '/c[@xmlns="' + NS_ENTITY_CAPABILITY + '"]'
45
46 PLUGIN_INFO = {
47 "name": "XEP 0115 Plugin",
48 "import_name": "XEP_0115",
49 "type": "XEP",
50 "protocols": ["XEP-0115"],
51 "dependencies": [],
52 "main": "XEP_0115",
53 "handler": "yes",
54 "description": _("""Implementation of entity capabilities""")
55 }
56
57 class HashGenerationError(Exception):
58 pass
59
60 class ByteIdentity(object):
61 """This class manage identity as bytes (needed for i;octet sort),
62 it is used for the hash generation"""
63
64 def __init__(self, identity, lang=None):
65 assert isinstance(identity, disco.DiscoIdentity)
66 self.category = identity.category.encode('utf-8')
67 self.idType = identity.type.encode('utf-8')
68 self.name = identity.name.encode('utf-8') if identity.name else ''
69 self.lang = lang.encode('utf-8') if lang else ''
70
71 def __str__(self):
72 return "%s/%s/%s/%s" % (self.category, self.idType, self.lang, self.name)
73
74
75 class XEP_0115():
76 cap_hash = None
77
78 def __init__(self, host):
79 info(_("Plugin XEP_0115 initialization"))
80 self.host = host
81 host.trigger.add("Disco Handled", self.checkHash)
82 self.hash_cache = host.memory.getPrivate("entity_capabilities_cache") or {} #key = hash or jid
83 self.jid_hash = {} #jid to hash mapping, map to a discoInfo if the hash method is unknown
84
85 def checkHash(self, profile):
86 if not XEP_0115.cap_hash:
87 XEP_0115.cap_hash = self.generateHash(profile)
88 else:
89 self.presenceHack(profile)
90 return True
91
92 def getHandler(self, profile):
93 return XEP_0115_handler(self, profile)
94
95 def presenceHack(self, profile):
96 """modify SatPresenceProtocol to add capabilities data"""
97 client=self.host.getClient(profile)
98 presenceInst = client.presence
99 def hacked_send(self, obj):
100 obj.addChild(self._c_elt)
101 self._legacy_send(obj)
102 new_send = types.MethodType(hacked_send, presenceInst, presenceInst.__class__)
103 presenceInst._legacy_send = presenceInst.send
104 presenceInst.send = new_send
105 c_elt = domish.Element((NS_ENTITY_CAPABILITY,'c'))
106 c_elt['hash']='sha-1'
107 c_elt['node']='http://wiki.goffi.org/wiki/Salut_%C3%A0_Toi'
108 c_elt['ver']=XEP_0115.cap_hash
109 presenceInst._c_elt = c_elt
110
111
112 def generateHash(self, profile_key="@DEFAULT@"):
113 """This method generate a sha1 hash as explained in xep-0115 #5.1
114 it then store it in XEP_0115.cap_hash"""
115 profile = self.host.memory.getProfileName(profile_key)
116 if not profile:
117 error ('Requesting hash for an inexistant profile')
118 raise HashGenerationError
119
120 client = self.host.getClient(profile_key)
121 if not client:
122 error ('Requesting hash for an inexistant client')
123 raise HashGenerationError
124
125 def generateHash_2(services, profile):
126 _s=[]
127 byte_identities = [ByteIdentity(identity) for identity in filter(lambda x:isinstance(x,disco.DiscoIdentity),services)] #FIXME: lang must be managed here
128 byte_identities.sort(key=lambda i:i.lang)
129 byte_identities.sort(key=lambda i:i.idType)
130 byte_identities.sort(key=lambda i:i.category)
131 for identity in byte_identities:
132 _s.append(str(identity))
133 _s.append('<')
134 byte_features = [feature.encode('utf-8') for feature in filter(lambda x:isinstance(x,disco.DiscoFeature),services)]
135 byte_features.sort() #XXX: the default sort has the same behaviour as the requested RFC 4790 i;octet sort
136 for feature in byte_features:
137 _s.append(feature)
138 _s.append('<')
139 #TODO: manage XEP-0128 data form here
140 XEP_0115.cap_hash = b64encode(sha1(''.join(_s)).digest())
141 debug(_('Capability hash generated: [%s]') % XEP_0115.cap_hash)
142 self.presenceHack(profile)
143
144 services = client.discoHandler.info(client.jid, client.jid, '').addCallback(generateHash_2, profile)
145
146 class XEP_0115_handler(XMPPHandler):
147 implements(iwokkel.IDisco)
148
149 def __init__(self, plugin_parent, profile):
150 self.plugin_parent = plugin_parent
151 self.host = plugin_parent.host
152 self.profile = profile
153
154 def connectionInitialized(self):
155 self.xmlstream.addObserver(CAPABILITY_UPDATE, self.update)
156
157 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
158 return [disco.DiscoFeature(NS_ENTITY_CAPABILITY)]
159
160 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
161 return []
162
163 def _updateCache(self, discoResult, from_jid, key):
164 """Actually update the cache
165 @param discoResult: result of the requestInfo
166 @param known_hash: True if it's a hash method we know, we don't save the cache else"""
167 if key:
168 self.plugin_parent.jid_hash[from_jid] = key
169 self.plugin_parent.hash_cache[key] = discoResult
170 else:
171 #No key, that mean unknown hash method
172 self.plugin_parent.jid_hash[from_jid] = discoResult
173 self.host.memory.setPrivate("entity_capabilities_cache", self.plugin_parent.hash_cache)
174
175
176 def update(self, presence):
177 """
178 Manage the capabilities of the entity
179 Check if we know the version of this capatilities
180 and get the capibilities if necessary
181 """
182 from_jid = jid.JID(presence['from'])
183 c_elem = filter (lambda x:x.name == "c", presence.elements())[0] #We only want the "c" element
184 try:
185 ver=c_elem['ver']
186 hash=c_elem['hash']
187 node=c_elem['node']
188 except KeyError:
189 warning('Received invalid capabilities tag')
190 return
191 if not self.plugin_parent.jid_hash.has_key(from_jid):
192 if self.plugin_parent.hash_cache.has_key(ver):
193 #we know that hash, we just link it with the jid
194 self.plugin_parent.jid_hash[from_jid] = ver
195 else:
196 if hash!='sha-1':
197 #unknown hash method
198 warning('Unknown hash for entity capabilities: [%s]' % hash)
199 self.parent.disco.requestInfo(from_jid).addCallback(self._updateCache, from_jid, ver if hash=='sha-1' else None )
200 #TODO: me must manage the full algorithm described at XEP-0115 #5.4 part 3
201