Mercurial > libervia-backend
comparison src/plugins/plugin_xep_0095.py @ 385:41fdaeb005bc
plugins: Stream initiation (xep-0095) implementation
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 29 Sep 2011 12:07:11 +0200 |
parents | |
children | c34fd9d6242e |
comparison
equal
deleted
inserted
replaced
384:785420cd63f7 | 385:41fdaeb005bc |
---|---|
1 #!/usr/bin/python | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 SAT plugin for managing xep-0095 | |
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 | |
23 from twisted.words.xish import domish | |
24 from twisted.internet import protocol | |
25 from twisted.words.protocols.jabber import client, jid | |
26 from twisted.words.protocols.jabber import error as jab_error | |
27 import os.path | |
28 from twisted.internet import reactor | |
29 import uuid | |
30 | |
31 from zope.interface import implements | |
32 | |
33 try: | |
34 from twisted.words.protocols.xmlstream import XMPPHandler | |
35 except ImportError: | |
36 from wokkel.subprotocols import XMPPHandler | |
37 | |
38 from wokkel import disco, iwokkel | |
39 | |
40 IQ_SET = '/iq[@type="set"]' | |
41 NS_SI = 'http://jabber.org/protocol/si' | |
42 SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]' | |
43 SI_PROFILE_HEADER = "http://jabber.org/protocol/si/profile/" | |
44 | |
45 PLUGIN_INFO = { | |
46 "name": "XEP 0095 Plugin", | |
47 "import_name": "XEP-0095", | |
48 "type": "XEP", | |
49 "protocols": ["XEP-0095"], | |
50 "main": "XEP_0095", | |
51 "handler": "yes", | |
52 "description": _("""Implementation of Stream Initiation""") | |
53 } | |
54 | |
55 class XEP_0095(): | |
56 | |
57 def __init__(self, host): | |
58 info(_("Plugin XEP_0095 initialization")) | |
59 self.host = host | |
60 self.si_profiles = {} #key: SI profile, value: callback | |
61 | |
62 def getHandler(self, profile): | |
63 return XEP_0095_handler(self) | |
64 | |
65 def registerSIProfile(self, si_profile, callback): | |
66 """Add a callback for a SI Profile | |
67 param si_profile: SI profile name (e.g. file-transfer) | |
68 param callback: method to call when the profile name is asked""" | |
69 self.si_profiles[si_profile] = callback | |
70 | |
71 def streamInit(self, iq_el, profile): | |
72 """This method is called on stream initiation (XEP-0095 #3.2) | |
73 @param iq_el: IQ element | |
74 @param profile: %(doc_profile)s""" | |
75 info (_("XEP-0095 Stream initiation")) | |
76 iq_el.handled=True | |
77 si_el = iq_el.firstChildElement() | |
78 si_id = iq_el.getAttribute('id') | |
79 si_mime_type = iq_el.getAttribute('mime-type', 'application/octet-stream') | |
80 si_profile = si_el.getAttribute('profile') | |
81 si_profile_key = si_profile[len(SI_PROFILE_HEADER):] if si_profile.startswith(SI_PROFILE_HEADER) else si_profile | |
82 if self.si_profiles.has_key(si_profile_key): | |
83 #We know this SI profile, we call the callback | |
84 self.si_profiles[si_profile_key](iq_el['from'], si_id, si_mime_type, si_el, profile) | |
85 else: | |
86 #We don't know this profile, we send an error | |
87 self.sendBadProfileError(iq_el['id'], iq_el['from'], profile) | |
88 | |
89 def sendRejectedError(self, id, to_jid, reason = 'Offer Declined', profile='@NONE@'): | |
90 """Helper method to send when the stream is rejected | |
91 @param id: IQ id | |
92 @param to_jid: recipient | |
93 @param reason: human readable reason (string) | |
94 @param profile: %(doc_profile)s""" | |
95 self.sendError(id, to_jid, 403, 'cancel', {'text':reason}, profile=profile) | |
96 | |
97 def sendBadProfileError(self, id, to_jid, profile): | |
98 """Helper method to send when we don't know the SI profile | |
99 @param id: IQ id | |
100 @param to_jid: recipient | |
101 @param profile: %(doc_profile)s""" | |
102 self.sendError(id, to_jid, 400, 'modify', profile=profile) | |
103 | |
104 def sendBadRequestError(self, id, to_jid, profile): | |
105 """Helper method to send when we don't know the SI profile | |
106 @param id: IQ id | |
107 @param to_jid: recipient | |
108 @param profile: %(doc_profile)s""" | |
109 self.sendError(id, to_jid, 400, 'cancel', profile=profile) | |
110 | |
111 def sendFailedError(self, id, to_jid, profile): | |
112 """Helper method to send when we transfert failed | |
113 @param id: IQ id | |
114 @param to_jid: recipient | |
115 @param profile: %(doc_profile)s""" | |
116 self.sendError(id, to_jid, 500, 'cancel', {'custom':'failed'}, profile=profile) #as there is no error code for failed transfert, we use 500 (undefined-condition) | |
117 | |
118 def sendError(self, id, to_jid, err_code, err_type='cancel', data={}, profile='@NONE@'): | |
119 """Send IQ error as a result | |
120 @param id: IQ id | |
121 @param to_jid: recipient | |
122 @param err_code: error err_code (see XEP-0095 #4.2) | |
123 @param err_type: one of cancel, modify | |
124 @param data: error specific data (dictionary) | |
125 @param profile: %(doc_profile)s | |
126 """ | |
127 client = self.host.getClient(profile) | |
128 assert(client) | |
129 result = domish.Element(('', 'iq')) | |
130 result['type'] = 'result' | |
131 result['id'] = id | |
132 result['to'] = to_jid | |
133 error_el = result.addElement('error') | |
134 error_el['err_code'] = str(err_code) | |
135 error_el['type'] = err_type | |
136 if err_code==400 and err_type=='cancel': | |
137 error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas','bad-request')) | |
138 error_el.addElement((NS_SI,'no-valid-streams')) | |
139 elif err_code==400 and err_type=='modify': | |
140 error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas','bad-request')) | |
141 error_el.addElement((NS_SI,'bad-profile')) | |
142 elif err_code==403 and err_type=='cancel': | |
143 error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas','forbidden')) | |
144 if data.has_key('text'): | |
145 error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas','text'), content=data['text']) | |
146 elif err_code==500 and err_type=='cancel': | |
147 condition_el = error_el.addElement((NS_SI,'undefined-condition')) | |
148 if data.has_key('custom') and data['custom']=='failed': | |
149 condition_el.addContent('Stream failed') | |
150 | |
151 client.xmlstream.send(result) | |
152 | |
153 def acceptStream(self, id, to_jid, feature_elt, misc_elts=[], profile='@NONE@'): | |
154 """Send the accept stream initiation answer | |
155 @param id: stream initiation id | |
156 @param feature_elt: domish element 'feature' containing stream method to use | |
157 @param misc_elts: list of domish element to add | |
158 @param profile: %(doc_profile)s""" | |
159 client = self.host.getClient(profile) | |
160 assert(client) | |
161 info (_("sending stream initiation accept answer")) | |
162 result = domish.Element(('', 'iq')) | |
163 result['type'] = 'result' | |
164 result['id'] = id | |
165 result['to'] = to_jid | |
166 si = result.addElement('si', NS_SI) | |
167 si.addChild(feature_elt) | |
168 for elt in misc_elts: | |
169 si.addChild(elt) | |
170 client.xmlstream.send(result) | |
171 | |
172 def proposeStream(self, to_jid, si_profile, feature_elt, misc_elts, mime_type='application/octet-stream', profile_key='@NONE@'): | |
173 """Propose a stream initiation | |
174 @param to_jid: recipient (JID) | |
175 @param si_profile: Stream initiation profile (XEP-0095) | |
176 @param feature_elt: feature domish element, according to XEP-0020 | |
177 @param misc_elts: list of domish element to add for this profile | |
178 @param mime_type: stream mime type | |
179 @param profile: %(doc_profile)s | |
180 @return: session id, offer""" | |
181 current_jid, xmlstream = self.host.getJidNStream(profile_key) | |
182 if not xmlstream: | |
183 error (_('Asking for an non-existant or not connected profile')) | |
184 return "" | |
185 | |
186 offer = client.IQ(xmlstream,'set') | |
187 sid = str(uuid.uuid4()) | |
188 debug (_("Stream Session ID: %s") % offer["id"]) | |
189 | |
190 offer["from"] = current_jid.full() | |
191 offer["to"] = to_jid.full() | |
192 si=offer.addElement('si',NS_SI) | |
193 si['id'] = sid | |
194 si["mime-type"] = mime_type | |
195 si["profile"] = si_profile | |
196 for elt in misc_elts: | |
197 si.addChild(elt) | |
198 si.addChild(feature_elt) | |
199 | |
200 offer.send() | |
201 return sid, offer | |
202 | |
203 | |
204 class XEP_0095_handler(XMPPHandler): | |
205 implements(iwokkel.IDisco) | |
206 | |
207 def __init__(self, plugin_parent): | |
208 self.plugin_parent = plugin_parent | |
209 self.host = plugin_parent.host | |
210 | |
211 def connectionInitialized(self): | |
212 self.xmlstream.addObserver(SI_REQUEST, self.plugin_parent.streamInit, profile = self.parent.profile) | |
213 | |
214 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
215 return [disco.DiscoFeature(NS_SI)] + [disco.DiscoFeature("http://jabber.org/protocol/si/profile/%s" % profile_name) for profile_name in self.plugin_parent.si_profiles] | |
216 | |
217 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
218 return [] | |
219 |