Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_attach.py @ 3219:2ba602aef90e
plugin attach, aesgcm: attachments refactoring:
attachment handling has been simplified, and now use a "register" method similar as the
ones used for download or upload.
A default method (for unencrypted messages) will try a simple upload and will copy the
links to body.
AESGCM plugin has been adapted to be used for encrypted files. If more than one file is
sent with AESGCM plugin, they will be split in several messages as current de-facto
standard (OMEMO media sharing) doesn't support several files per message.
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 18 Mar 2020 20:25:02 +0100 |
parents | 2e892f9f54f6 |
children | 163014f09bf4 |
comparison
equal
deleted
inserted
replaced
3218:806a7936a591 | 3219:2ba602aef90e |
---|---|
16 # You should have received a copy of the GNU Affero General Public License | 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/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 from functools import partial | 19 from functools import partial |
20 from pathlib import Path | 20 from pathlib import Path |
21 from collections import namedtuple | |
21 from twisted.internet import defer | 22 from twisted.internet import defer |
22 from sat.core.i18n import _ | 23 from sat.core.i18n import _ |
24 from sat.core import exceptions | |
23 from sat.core.constants import Const as C | 25 from sat.core.constants import Const as C |
24 from sat.core.log import getLogger | 26 from sat.core.log import getLogger |
27 from sat.tools import utils | |
28 | |
25 | 29 |
26 log = getLogger(__name__) | 30 log = getLogger(__name__) |
27 | 31 |
28 | 32 |
29 PLUGIN_INFO = { | 33 PLUGIN_INFO = { |
35 C.PI_HANDLER: "no", | 39 C.PI_HANDLER: "no", |
36 C.PI_DESCRIPTION: _("""Attachments handler"""), | 40 C.PI_DESCRIPTION: _("""Attachments handler"""), |
37 } | 41 } |
38 | 42 |
39 | 43 |
44 AttachmentHandler = namedtuple('AttachmentHandler', ['can_handle', 'attach', 'priority']) | |
45 | |
46 | |
40 class AttachPlugin: | 47 class AttachPlugin: |
41 | 48 |
42 def __init__(self, host): | 49 def __init__(self, host): |
43 log.info(_("plugin Attach initialization")) | 50 log.info(_("plugin Attach initialization")) |
44 self.host = host | 51 self.host = host |
45 self._u = host.plugins["UPLOAD"] | 52 self._u = host.plugins["UPLOAD"] |
46 host.trigger.add("sendMessage", self._sendMessageTrigger) | 53 host.trigger.add("sendMessage", self._sendMessageTrigger) |
47 | 54 self._attachments_handlers = {'clear': [], 'encrypted': []} |
48 def _attachFiles(self, client, data): | 55 self.register(self.defaultCanHandle, self.defaultAttach, False, -1000) |
49 # TODO: handle xhtml-im | 56 |
50 body_elt = next(data["xml"].elements((C.NS_CLIENT, "body"))) | 57 def register(self, can_handle, attach, encrypted=False, priority=0): |
51 for attachment in data["extra"][C.MESS_KEY_ATTACHMENTS]: | 58 """Register an attachments handler |
52 body_elt.addContent(f'\n{attachment["url"]}') | 59 |
60 @param can_handle(callable, coroutine, Deferred): a method which must return True | |
61 if this plugin can handle the upload, otherwise next ones will be tried. | |
62 This method will get client and mess_data as arguments, before the XML is | |
63 generated | |
64 @param attach(callable, coroutine, Deferred): attach the file | |
65 this method will get client and mess_data as arguments, after XML is | |
66 generated. Upload operation must be handled | |
67 hint: "UPLOAD" plugin can be used | |
68 @param encrypted(bool): True if the handler manages encrypted files | |
69 A handler can be registered twice if it handle both encrypted and clear | |
70 attachments | |
71 @param priority(int): priority of this handler, handler with higher priority will | |
72 be tried first | |
73 """ | |
74 handler = AttachmentHandler(can_handle, attach, priority) | |
75 handlers = ( | |
76 self._attachments_handlers['encrypted'] | |
77 if encrypted else self._attachments_handlers['clear'] | |
78 ) | |
79 if handler in handlers: | |
80 raise exceptions.InternalError( | |
81 'Attachment handler has been registered twice, this should never happen' | |
82 ) | |
83 | |
84 handlers.append(handler) | |
85 handlers.sort(key=lambda h: h.priority, reverse=True) | |
86 log.debug(f"new attachments handler: {handler}") | |
87 | |
88 async def attachFiles(self, client, data): | |
89 if client.encryption.isEncryptionRequested(data): | |
90 handlers = self._attachments_handlers['encrypted'] | |
91 else: | |
92 handlers = self._attachments_handlers['clear'] | |
93 | |
94 for handler in handlers: | |
95 can_handle = await utils.asDeferred(handler.can_handle, client, data) | |
96 if can_handle: | |
97 break | |
98 else: | |
99 raise exceptions.NotFound( | |
100 _("No plugin can handle attachment with {destinee}").format( | |
101 destinee = data['to'] | |
102 )) | |
103 | |
104 await utils.asDeferred(handler.attach, client, data) | |
105 | |
53 return data | 106 return data |
54 | 107 |
55 async def uploadFiles(self, client, data): | 108 async def uploadFiles(self, client, data, upload_cb=None): |
109 """Upload file, and update attachments | |
110 | |
111 invalid attachments will be removed | |
112 @param client: | |
113 @param data(dict): message data | |
114 @param upload_cb(coroutine, Deferred, None): method to use for upload | |
115 if None, upload method from UPLOAD plugin will be used. | |
116 Otherwise, following kwargs will be use with the cb: | |
117 - client | |
118 - filepath | |
119 - filename | |
120 - options | |
121 the method must return a tuple similar to UPLOAD plugin's upload method, | |
122 it must contain: | |
123 - progress_id | |
124 - a deferred which fire download URL | |
125 """ | |
126 if upload_cb is None: | |
127 upload_cb = self._u.upload | |
128 | |
56 uploads_d = [] | 129 uploads_d = [] |
57 to_delete = [] | 130 to_delete = [] |
58 attachments = data["extra"]["attachments"] | 131 attachments = data["extra"]["attachments"] |
59 | 132 |
60 for attachment in attachments: | 133 for attachment in attachments: |
77 name = attachment["name"] | 150 name = attachment["name"] |
78 except KeyError: | 151 except KeyError: |
79 name = attachment["name"] = path.name | 152 name = attachment["name"] = path.name |
80 | 153 |
81 options = {} | 154 options = {} |
82 progress_id = attachment.get("progress_id") | 155 progress_id = attachment.pop("progress_id", None) |
83 if progress_id: | 156 if progress_id: |
84 options["progress_id"] = attachment["progress_id"] | 157 options["progress_id"] = progress_id |
85 check_certificate = self.host.memory.getParamA( | 158 check_certificate = self.host.memory.getParamA( |
86 "check_certificate", "Connection", profile_key=client.profile) | 159 "check_certificate", "Connection", profile_key=client.profile) |
87 if not check_certificate: | 160 if not check_certificate: |
88 options['ignore_tls_errors'] = True | 161 options['ignore_tls_errors'] = True |
89 log.warning( | 162 log.warning( |
90 _("certificate check disabled for upload, this is dangerous!")) | 163 _("certificate check disabled for upload, this is dangerous!")) |
91 if client.encryption.isEncryptionRequested(data): | 164 |
92 # FIXME: we should not use implementation specific value here | 165 __, upload_d = await upload_cb( |
93 # but for now it's the only file encryption method available with | 166 client=client, |
94 # with upload. | |
95 options['encryption'] = C.ENC_AES_GCM | |
96 | |
97 __, upload_d = await self._u.upload( | |
98 client, | |
99 filepath=path, | 167 filepath=path, |
100 filename=name, | 168 filename=name, |
101 options=options, | 169 options=options, |
102 ) | 170 ) |
103 uploads_d.append(upload_d) | 171 uploads_d.append(upload_d) |
116 | 184 |
117 attachment["url"] = ret | 185 attachment["url"] = ret |
118 | 186 |
119 return data | 187 return data |
120 | 188 |
121 def _uploadFiles(self, client, data): | 189 def _attachFiles(self, client, data): |
122 return defer.ensureDeferred(self.uploadFiles(client, data)) | 190 return defer.ensureDeferred(self.attachFiles(client, data)) |
123 | 191 |
124 def _sendMessageTrigger( | 192 def _sendMessageTrigger( |
125 self, client, mess_data, pre_xml_treatments, post_xml_treatments): | 193 self, client, mess_data, pre_xml_treatments, post_xml_treatments): |
126 if mess_data['extra'].get(C.MESS_KEY_ATTACHMENTS): | 194 if mess_data['extra'].get(C.MESS_KEY_ATTACHMENTS): |
127 pre_xml_treatments.addCallback(partial(self._uploadFiles, client)) | |
128 post_xml_treatments.addCallback(partial(self._attachFiles, client)) | 195 post_xml_treatments.addCallback(partial(self._attachFiles, client)) |
129 return True | 196 return True |
197 | |
198 async def defaultCanHandle(self, client, data): | |
199 return True | |
200 | |
201 async def defaultAttach(self, client, data): | |
202 await self.uploadFiles(client, data) | |
203 # TODO: handle xhtml-im | |
204 body_elt = next(data["xml"].elements((C.NS_CLIENT, "body"))) | |
205 attachments = data["extra"][C.MESS_KEY_ATTACHMENTS] | |
206 if attachments: | |
207 body_links = '\n'.join(a['url'] for a in attachments) | |
208 if str(body_elt).strip(): | |
209 # if there is already a body, we add a line feed before the first link | |
210 body_elt.addContent('\n') | |
211 body_elt.addContent(body_links) |