comparison sat/plugins/plugin_xep_0095.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/plugins/plugin_xep_0095.py@e2a7bb875957
children 56f94936df1e
comparison
equal deleted inserted replaced
2561:bd30dc3ffe5a 2562:26edcf3a30eb
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for managing xep-0095
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from sat.core.i18n import _
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from sat.core import exceptions
25 from twisted.words.protocols.jabber import xmlstream
26 from twisted.words.protocols.jabber import error
27 from zope.interface import implements
28 from wokkel import disco
29 from wokkel import iwokkel
30 import uuid
31
32
33 PLUGIN_INFO = {
34 C.PI_NAME: "XEP 0095 Plugin",
35 C.PI_IMPORT_NAME: "XEP-0095",
36 C.PI_TYPE: "XEP",
37 C.PI_PROTOCOLS: ["XEP-0095"],
38 C.PI_MAIN: "XEP_0095",
39 C.PI_HANDLER: "yes",
40 C.PI_DESCRIPTION: _("""Implementation of Stream Initiation""")
41 }
42
43
44 IQ_SET = '/iq[@type="set"]'
45 NS_SI = 'http://jabber.org/protocol/si'
46 SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]'
47 SI_PROFILE_HEADER = "http://jabber.org/protocol/si/profile/"
48 SI_ERROR_CONDITIONS = ('bad-profile', 'no-valid-streams')
49
50
51 class XEP_0095(object):
52
53 def __init__(self, host):
54 log.info(_("Plugin XEP_0095 initialization"))
55 self.host = host
56 self.si_profiles = {} # key: SI profile, value: callback
57
58 def getHandler(self, client):
59 return XEP_0095_handler(self)
60
61 def registerSIProfile(self, si_profile, callback):
62 """Add a callback for a SI Profile
63
64 @param si_profile(unicode): SI profile name (e.g. file-transfer)
65 @param callback(callable): method to call when the profile name is asked
66 """
67 self.si_profiles[si_profile] = callback
68
69 def unregisterSIProfile(self, si_profile):
70 try:
71 del self.si_profiles[si_profile]
72 except KeyError:
73 log.error(u"Trying to unregister SI profile [{}] which was not registered".format(si_profile))
74
75 def streamInit(self, iq_elt, client):
76 """This method is called on stream initiation (XEP-0095 #3.2)
77
78 @param iq_elt: IQ element
79 """
80 log.info(_("XEP-0095 Stream initiation"))
81 iq_elt.handled = True
82 si_elt = iq_elt.elements(NS_SI, 'si').next()
83 si_id = si_elt['id']
84 si_mime_type = iq_elt.getAttribute('mime-type', 'application/octet-stream')
85 si_profile = si_elt['profile']
86 si_profile_key = si_profile[len(SI_PROFILE_HEADER):] if si_profile.startswith(SI_PROFILE_HEADER) else si_profile
87 if si_profile_key in self.si_profiles:
88 #We know this SI profile, we call the callback
89 self.si_profiles[si_profile_key](client, iq_elt, si_id, si_mime_type, si_elt)
90 else:
91 #We don't know this profile, we send an error
92 self.sendError(client, iq_elt, 'bad-profile')
93
94 def sendError(self, client, request, condition):
95 """Send IQ error as a result
96
97 @param request(domish.Element): original IQ request
98 @param condition(str): error condition
99 """
100 if condition in SI_ERROR_CONDITIONS:
101 si_condition = condition
102 condition = 'bad-request'
103 else:
104 si_condition = None
105
106 iq_error_elt = error.StanzaError(condition).toResponse(request)
107 if si_condition is not None:
108 iq_error_elt.error.addElement((NS_SI, si_condition))
109
110 client.send(iq_error_elt)
111
112 def acceptStream(self, client, iq_elt, feature_elt, misc_elts=None):
113 """Send the accept stream initiation answer
114
115 @param iq_elt(domish.Element): initial SI request
116 @param feature_elt(domish.Element): 'feature' element containing stream method to use
117 @param misc_elts(list[domish.Element]): list of elements to add
118 """
119 log.info(_("sending stream initiation accept answer"))
120 if misc_elts is None:
121 misc_elts = []
122 result_elt = xmlstream.toResponse(iq_elt, 'result')
123 si_elt = result_elt.addElement((NS_SI, 'si'))
124 si_elt.addChild(feature_elt)
125 for elt in misc_elts:
126 si_elt.addChild(elt)
127 client.send(result_elt)
128
129 def _parseOfferResult(self, iq_elt):
130 try:
131 si_elt = iq_elt.elements(NS_SI, "si").next()
132 except StopIteration:
133 log.warning(u"No <si/> element found in result while expected")
134 raise exceptions.DataError
135 return (iq_elt, si_elt)
136
137
138 def proposeStream(self, client, to_jid, si_profile, feature_elt, misc_elts, mime_type='application/octet-stream'):
139 """Propose a stream initiation
140
141 @param to_jid(jid.JID): recipient
142 @param si_profile(unicode): Stream initiation profile (XEP-0095)
143 @param feature_elt(domish.Element): feature element, according to XEP-0020
144 @param misc_elts(list[domish.Element]): list of elements to add
145 @param mime_type(unicode): stream mime type
146 @return (tuple): tuple with:
147 - session id (unicode)
148 - (D(domish_elt, domish_elt): offer deferred which returl a tuple
149 with iq_elt and si_elt
150 """
151 offer = client.IQ()
152 sid = str(uuid.uuid4())
153 log.debug(_(u"Stream Session ID: %s") % offer["id"])
154
155 offer["from"] = client.jid.full()
156 offer["to"] = to_jid.full()
157 si = offer.addElement('si', NS_SI)
158 si['id'] = sid
159 si["mime-type"] = mime_type
160 si["profile"] = si_profile
161 for elt in misc_elts:
162 si.addChild(elt)
163 si.addChild(feature_elt)
164
165 offer_d = offer.send()
166 offer_d.addCallback(self._parseOfferResult)
167 return sid, offer_d
168
169
170 class XEP_0095_handler(xmlstream.XMPPHandler):
171 implements(iwokkel.IDisco)
172
173 def __init__(self, plugin_parent):
174 self.plugin_parent = plugin_parent
175 self.host = plugin_parent.host
176
177 def connectionInitialized(self):
178 self.xmlstream.addObserver(SI_REQUEST, self.plugin_parent.streamInit, client=self.parent)
179
180 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
181 return [disco.DiscoFeature(NS_SI)] + [disco.DiscoFeature(u"http://jabber.org/protocol/si/profile/{}".format(profile_name)) for profile_name in self.plugin_parent.si_profiles]
182
183 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
184 return []