comparison libervia/backend/plugins/plugin_xep_0231.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_xep_0231.py@524856bd7b19
children 0d7bb4df2343
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3
4 # SAT plugin for Bit of Binary handling (XEP-0231)
5 # Copyright (C) 2009-2021 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 import base64
21 import time
22 from pathlib import Path
23 from functools import partial
24 from zope.interface import implementer
25 from twisted.python import failure
26 from twisted.words.protocols.jabber import xmlstream
27 from twisted.words.protocols.jabber import jid
28 from twisted.words.protocols.jabber import error as jabber_error
29 from twisted.internet import defer
30 from wokkel import disco, iwokkel
31 from libervia.backend.tools import xml_tools
32 from libervia.backend.core.i18n import _
33 from libervia.backend.core.constants import Const as C
34 from libervia.backend.core import exceptions
35 from libervia.backend.core.log import getLogger
36
37 log = getLogger(__name__)
38
39
40 PLUGIN_INFO = {
41 C.PI_NAME: "Bits of Binary",
42 C.PI_IMPORT_NAME: "XEP-0231",
43 C.PI_TYPE: "XEP",
44 C.PI_MODES: C.PLUG_MODE_BOTH,
45 C.PI_PROTOCOLS: ["XEP-0231"],
46 C.PI_MAIN: "XEP_0231",
47 C.PI_HANDLER: "yes",
48 C.PI_DESCRIPTION: _(
49 """Implementation of bits of binary (used for small images/files)"""
50 ),
51 }
52
53 NS_BOB = "urn:xmpp:bob"
54 IQ_BOB_REQUEST = C.IQ_GET + '/data[@xmlns="' + NS_BOB + '"]'
55
56
57 class XEP_0231(object):
58 def __init__(self, host):
59 log.info(_("plugin Bits of Binary initialization"))
60 self.host = host
61 host.register_namespace("bob", NS_BOB)
62 host.trigger.add("xhtml_post_treat", self.xhtml_trigger)
63 host.bridge.add_method(
64 "bob_get_file",
65 ".plugin",
66 in_sign="sss",
67 out_sign="s",
68 method=self._get_file,
69 async_=True,
70 )
71
72 def dump_data(self, cache, data_elt, cid):
73 """save file encoded in data_elt to cache
74
75 @param cache(memory.cache.Cache): cache to use to store the data
76 @param data_elt(domish.Element): <data> as in XEP-0231
77 @param cid(unicode): content-id
78 @return(unicode): full path to dumped file
79 """
80 #  FIXME: is it needed to use a separate thread?
81 # probably not with the little data expected with BoB
82 try:
83 max_age = int(data_elt["max-age"])
84 if max_age < 0:
85 raise ValueError
86 except (KeyError, ValueError):
87 log.warning("invalid max-age found")
88 max_age = None
89
90 with cache.cache_data(
91 PLUGIN_INFO[C.PI_IMPORT_NAME], cid, data_elt.getAttribute("type"), max_age
92 ) as f:
93
94 file_path = Path(f.name)
95 f.write(base64.b64decode(str(data_elt)))
96
97 return file_path
98
99 def get_handler(self, client):
100 return XEP_0231_handler(self)
101
102 def _request_cb(self, iq_elt, cache, cid):
103 for data_elt in iq_elt.elements(NS_BOB, "data"):
104 if data_elt.getAttribute("cid") == cid:
105 file_path = self.dump_data(cache, data_elt, cid)
106 return file_path
107
108 log.warning(
109 "invalid data stanza received, requested cid was not found:\n{iq_elt}\nrequested cid: {cid}".format(
110 iq_elt=iq_elt, cid=cid
111 )
112 )
113 raise failure.Failure(exceptions.DataError("missing data"))
114
115 def _request_eb(self, failure_):
116 """Log the error and continue errback chain"""
117 log.warning("Can't get requested data:\n{reason}".format(reason=failure_))
118 return failure_
119
120 def request_data(self, client, to_jid, cid, cache=None):
121 """Request data if we don't have it in cache
122
123 @param to_jid(jid.JID): jid to request the data to
124 @param cid(unicode): content id
125 @param cache(memory.cache.Cache, None): cache to use
126 client.cache will be used if None
127 @return D(unicode): path to file with data
128 """
129 if cache is None:
130 cache = client.cache
131 iq_elt = client.IQ("get")
132 iq_elt["to"] = to_jid.full()
133 data_elt = iq_elt.addElement((NS_BOB, "data"))
134 data_elt["cid"] = cid
135 d = iq_elt.send()
136 d.addCallback(self._request_cb, cache, cid)
137 d.addErrback(self._request_eb)
138 return d
139
140 def _set_img_elt_src(self, path, img_elt):
141 img_elt["src"] = "file://{}".format(path)
142
143 def xhtml_trigger(self, client, message_elt, body_elt, lang, treat_d):
144 for img_elt in xml_tools.find_all(body_elt, C.NS_XHTML, "img"):
145 source = img_elt.getAttribute("src", "")
146 if source.startswith("cid:"):
147 cid = source[4:]
148 file_path = client.cache.get_file_path(cid)
149 if file_path is not None:
150 #  image is in cache, we change the url
151 img_elt["src"] = "file://{}".format(file_path)
152 continue
153 else:
154 # image is not in cache, is it given locally?
155 for data_elt in message_elt.elements(NS_BOB, "data"):
156 if data_elt.getAttribute("cid") == cid:
157 file_path = self.dump_data(client.cache, data_elt, cid)
158 img_elt["src"] = "file://{}".format(file_path)
159 break
160 else:
161 # cid not found locally, we need to request it
162 # so we use the deferred
163 d = self.request_data(client, jid.JID(message_elt["from"]), cid)
164 d.addCallback(partial(self._set_img_elt_src, img_elt=img_elt))
165 treat_d.addCallback(lambda __: d)
166
167 def on_component_request(self, iq_elt, client):
168 """cache data is retrieve from common cache for components"""
169 # FIXME: this is a security/privacy issue as no access check is done
170 # but this is mitigated by the fact that the cid must be known.
171 # An access check should be implemented though.
172
173 iq_elt.handled = True
174 data_elt = next(iq_elt.elements(NS_BOB, "data"))
175 try:
176 cid = data_elt["cid"]
177 except KeyError:
178 error_elt = jabber_error.StanzaError("not-acceptable").toResponse(iq_elt)
179 client.send(error_elt)
180 return
181
182 metadata = self.host.common_cache.get_metadata(cid)
183 if metadata is None:
184 error_elt = jabber_error.StanzaError("item-not-found").toResponse(iq_elt)
185 client.send(error_elt)
186 return
187
188 with open(metadata["path"], 'rb') as f:
189 data = f.read()
190
191 result_elt = xmlstream.toResponse(iq_elt, "result")
192 data_elt = result_elt.addElement(
193 (NS_BOB, "data"), content=base64.b64encode(data).decode())
194 data_elt["cid"] = cid
195 data_elt["type"] = metadata["mime_type"]
196 data_elt["max-age"] = str(int(max(0, metadata["eol"] - time.time())))
197 client.send(result_elt)
198
199 def _get_file(self, peer_jid_s, cid, profile):
200 peer_jid = jid.JID(peer_jid_s)
201 assert cid
202 client = self.host.get_client(profile)
203 d = self.get_file(client, peer_jid, cid)
204 d.addCallback(lambda path: str(path))
205 return d
206
207 def get_file(self, client, peer_jid, cid, parent_elt=None):
208 """Retrieve a file from it's content-id
209
210 @param peer_jid(jid.JID): jid of the entity offering the data
211 @param cid(unicode): content-id of file data
212 @param parent_elt(domish.Element, None): if file is not in cache,
213 data will be looked after in children of this elements.
214 None to ignore
215 @return D(Path): path to cached data
216 """
217 file_path = client.cache.get_file_path(cid)
218 if file_path is not None:
219 #  file is in cache
220 return defer.succeed(file_path)
221 else:
222 # file not in cache, is it given locally?
223 if parent_elt is not None:
224 for data_elt in parent_elt.elements(NS_BOB, "data"):
225 if data_elt.getAttribute("cid") == cid:
226 return defer.succeed(self.dump_data(client.cache, data_elt, cid))
227
228 # cid not found locally, we need to request it
229 # so we use the deferred
230 return self.request_data(client, peer_jid, cid)
231
232
233 @implementer(iwokkel.IDisco)
234 class XEP_0231_handler(xmlstream.XMPPHandler):
235
236 def __init__(self, plugin_parent):
237 self.plugin_parent = plugin_parent
238 self.host = plugin_parent.host
239
240 def connectionInitialized(self):
241 if self.parent.is_component:
242 self.xmlstream.addObserver(
243 IQ_BOB_REQUEST, self.plugin_parent.on_component_request, client=self.parent
244 )
245
246 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
247 return [disco.DiscoFeature(NS_BOB)]
248
249 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
250 return []