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)