Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_misc_attach.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_misc_attach.py@524856bd7b19 |
children | 0d7bb4df2343 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 # SàT plugin for attaching files | |
4 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) | |
5 | |
6 # This program is free software: you can redistribute it and/or modify | |
7 # it under the terms of the GNU Affero General Public License as published by | |
8 # the Free Software Foundation, either version 3 of the License, or | |
9 # (at your option) any later version. | |
10 | |
11 # This program is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU Affero General Public License for more details. | |
15 | |
16 # You should have received a copy of the GNU Affero General Public License | |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 from collections import namedtuple | |
20 import mimetypes | |
21 from pathlib import Path | |
22 import shutil | |
23 import tempfile | |
24 from typing import Callable, Optional | |
25 | |
26 from twisted.internet import defer | |
27 | |
28 from libervia.backend.core import exceptions | |
29 from libervia.backend.core.constants import Const as C | |
30 from libervia.backend.core.core_types import SatXMPPEntity | |
31 from libervia.backend.core.i18n import _ | |
32 from libervia.backend.core.log import getLogger | |
33 from libervia.backend.tools import utils | |
34 from libervia.backend.tools import image | |
35 | |
36 | |
37 log = getLogger(__name__) | |
38 | |
39 | |
40 PLUGIN_INFO = { | |
41 C.PI_NAME: "File Attach", | |
42 C.PI_IMPORT_NAME: "ATTACH", | |
43 C.PI_TYPE: C.PLUG_TYPE_MISC, | |
44 C.PI_MODES: C.PLUG_MODE_BOTH, | |
45 C.PI_DEPENDENCIES: ["UPLOAD"], | |
46 C.PI_MAIN: "AttachPlugin", | |
47 C.PI_HANDLER: "no", | |
48 C.PI_DESCRIPTION: _("""Attachments handler"""), | |
49 } | |
50 | |
51 | |
52 AttachmentHandler = namedtuple('AttachmentHandler', ['can_handle', 'attach', 'priority']) | |
53 | |
54 | |
55 class AttachPlugin: | |
56 | |
57 def __init__(self, host): | |
58 log.info(_("plugin Attach initialization")) | |
59 self.host = host | |
60 self._u = host.plugins["UPLOAD"] | |
61 host.trigger.add("sendMessage", self._send_message_trigger) | |
62 host.trigger.add("sendMessageComponent", self._send_message_trigger) | |
63 self._attachments_handlers = {'clear': [], 'encrypted': []} | |
64 self.register(self.default_can_handle, self.default_attach, False, -1000) | |
65 | |
66 def register(self, can_handle, attach, encrypted=False, priority=0): | |
67 """Register an attachments handler | |
68 | |
69 @param can_handle(callable, coroutine, Deferred): a method which must return True | |
70 if this plugin can handle the upload, otherwise next ones will be tried. | |
71 This method will get client and mess_data as arguments, before the XML is | |
72 generated | |
73 @param attach(callable, coroutine, Deferred): attach the file | |
74 this method will get client and mess_data as arguments, after XML is | |
75 generated. Upload operation must be handled | |
76 hint: "UPLOAD" plugin can be used | |
77 @param encrypted(bool): True if the handler manages encrypted files | |
78 A handler can be registered twice if it handle both encrypted and clear | |
79 attachments | |
80 @param priority(int): priority of this handler, handler with higher priority will | |
81 be tried first | |
82 """ | |
83 handler = AttachmentHandler(can_handle, attach, priority) | |
84 handlers = ( | |
85 self._attachments_handlers['encrypted'] | |
86 if encrypted else self._attachments_handlers['clear'] | |
87 ) | |
88 if handler in handlers: | |
89 raise exceptions.InternalError( | |
90 'Attachment handler has been registered twice, this should never happen' | |
91 ) | |
92 | |
93 handlers.append(handler) | |
94 handlers.sort(key=lambda h: h.priority, reverse=True) | |
95 log.debug(f"new attachments handler: {handler}") | |
96 | |
97 async def attach_files(self, client, data): | |
98 """Main method to attach file | |
99 | |
100 It will do generic pre-treatment, and call the suitable attachments handler | |
101 """ | |
102 # we check attachment for pre-treatment like large image resizing | |
103 # media_type will be added if missing (and if it can be guessed from path) | |
104 attachments = data["extra"][C.KEY_ATTACHMENTS] | |
105 tmp_dirs_to_clean = [] | |
106 for attachment in attachments: | |
107 if attachment.get(C.KEY_ATTACHMENTS_RESIZE, False): | |
108 path = Path(attachment["path"]) | |
109 try: | |
110 media_type = attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE] | |
111 except KeyError: | |
112 media_type = mimetypes.guess_type(path, strict=False)[0] | |
113 if media_type is None: | |
114 log.warning( | |
115 _("Can't resize attachment of unknown type: {attachment}") | |
116 .format(attachment=attachment)) | |
117 continue | |
118 attachment[C.KEY_ATTACHMENTS_MEDIA_TYPE] = media_type | |
119 | |
120 main_type = media_type.split('/')[0] | |
121 if main_type == "image": | |
122 report = image.check(self.host, path) | |
123 if report['too_large']: | |
124 tmp_dir = Path(tempfile.mkdtemp()) | |
125 tmp_dirs_to_clean.append(tmp_dir) | |
126 new_path = tmp_dir / path.name | |
127 await image.resize( | |
128 path, report["recommended_size"], dest=new_path) | |
129 attachment["path"] = new_path | |
130 log.info( | |
131 _("Attachment {path!r} has been resized at {new_path!r}") | |
132 .format(path=str(path), new_path=str(new_path))) | |
133 else: | |
134 log.warning( | |
135 _("Can't resize attachment of type {main_type!r}: {attachment}") | |
136 .format(main_type=main_type, attachment=attachment)) | |
137 | |
138 if client.encryption.is_encryption_requested(data): | |
139 handlers = self._attachments_handlers['encrypted'] | |
140 else: | |
141 handlers = self._attachments_handlers['clear'] | |
142 | |
143 for handler in handlers: | |
144 can_handle = await utils.as_deferred(handler.can_handle, client, data) | |
145 if can_handle: | |
146 break | |
147 else: | |
148 raise exceptions.NotFound( | |
149 _("No plugin can handle attachment with {destinee}").format( | |
150 destinee = data['to'] | |
151 )) | |
152 | |
153 await utils.as_deferred(handler.attach, client, data) | |
154 | |
155 for dir_path in tmp_dirs_to_clean: | |
156 log.debug(f"Cleaning temporary directory at {dir_path}") | |
157 shutil.rmtree(dir_path) | |
158 | |
159 return data | |
160 | |
161 async def upload_files( | |
162 self, | |
163 client: SatXMPPEntity, | |
164 data: dict, | |
165 upload_cb: Optional[Callable] = None | |
166 ): | |
167 """Upload file, and update attachments | |
168 | |
169 invalid attachments will be removed | |
170 @param client: | |
171 @param data(dict): message data | |
172 @param upload_cb(coroutine, Deferred, None): method to use for upload | |
173 if None, upload method from UPLOAD plugin will be used. | |
174 Otherwise, following kwargs will be used with the cb: | |
175 - client | |
176 - filepath | |
177 - filename | |
178 - options | |
179 the method must return a tuple similar to UPLOAD plugin's upload method, | |
180 it must contain: | |
181 - progress_id | |
182 - a deferred which fire download URL | |
183 """ | |
184 if upload_cb is None: | |
185 upload_cb = self._u.upload | |
186 | |
187 uploads_d = [] | |
188 to_delete = [] | |
189 attachments = data["extra"]["attachments"] | |
190 | |
191 for attachment in attachments: | |
192 if "url" in attachment and not "path" in attachment: | |
193 log.debug(f"attachment is external, we don't upload it: {attachment}") | |
194 continue | |
195 try: | |
196 # we pop path because we don't want it to be stored, as the file can be | |
197 # only in a temporary location | |
198 path = Path(attachment.pop("path")) | |
199 except KeyError: | |
200 log.warning("no path in attachment: {attachment}") | |
201 to_delete.append(attachment) | |
202 continue | |
203 | |
204 if "url" in attachment: | |
205 url = attachment.pop('url') | |
206 log.warning( | |
207 f"unexpected URL in attachment: {url!r}\nattachment: {attachment}" | |
208 ) | |
209 | |
210 try: | |
211 name = attachment["name"] | |
212 except KeyError: | |
213 name = attachment["name"] = path.name | |
214 | |
215 attachment["size"] = path.stat().st_size | |
216 | |
217 extra = { | |
218 "attachment": attachment | |
219 } | |
220 progress_id = attachment.pop("progress_id", None) | |
221 if progress_id: | |
222 extra["progress_id"] = progress_id | |
223 check_certificate = self.host.memory.param_get_a( | |
224 "check_certificate", "Connection", profile_key=client.profile) | |
225 if not check_certificate: | |
226 extra['ignore_tls_errors'] = True | |
227 log.warning( | |
228 _("certificate check disabled for upload, this is dangerous!")) | |
229 | |
230 __, upload_d = await upload_cb( | |
231 client=client, | |
232 filepath=path, | |
233 filename=name, | |
234 extra=extra, | |
235 ) | |
236 uploads_d.append(upload_d) | |
237 | |
238 for attachment in to_delete: | |
239 attachments.remove(attachment) | |
240 | |
241 upload_results = await defer.DeferredList(uploads_d) | |
242 for idx, (success, ret) in enumerate(upload_results): | |
243 attachment = attachments[idx] | |
244 | |
245 if not success: | |
246 # ret is a failure here | |
247 log.warning(f"error while uploading {attachment}: {ret}") | |
248 continue | |
249 | |
250 attachment["url"] = ret | |
251 | |
252 return data | |
253 | |
254 def _attach_files(self, data, client): | |
255 return defer.ensureDeferred(self.attach_files(client, data)) | |
256 | |
257 def _send_message_trigger( | |
258 self, client, mess_data, pre_xml_treatments, post_xml_treatments): | |
259 if mess_data['extra'].get(C.KEY_ATTACHMENTS): | |
260 post_xml_treatments.addCallback(self._attach_files, client=client) | |
261 return True | |
262 | |
263 async def default_can_handle(self, client, data): | |
264 return True | |
265 | |
266 async def default_attach(self, client, data): | |
267 await self.upload_files(client, data) | |
268 # TODO: handle xhtml-im | |
269 body_elt = data["xml"].body | |
270 if body_elt is None: | |
271 body_elt = data["xml"].addElement("body") | |
272 attachments = data["extra"][C.KEY_ATTACHMENTS] | |
273 if attachments: | |
274 body_links = '\n'.join(a['url'] for a in attachments) | |
275 if str(body_elt).strip(): | |
276 # if there is already a body, we add a line feed before the first link | |
277 body_elt.addContent('\n') | |
278 body_elt.addContent(body_links) |