comparison src/plugins/plugin_xep_0234.py @ 2512:025afb04c10b

plugin XEP-0234: some cleaning + added triggers to allow plugins to change parsing/generation of <file> element
author Goffi <goffi@goffi.org>
date Fri, 02 Mar 2018 17:53:19 +0100
parents 7ad5f2c4e34a
children a19b2c43e719
comparison
equal deleted inserted replaced
2511:20a5e7db0609 2512:025afb04c10b
34 from twisted.internet import defer 34 from twisted.internet import defer
35 from twisted.internet import reactor 35 from twisted.internet import reactor
36 from twisted.internet import error as internet_error 36 from twisted.internet import error as internet_error
37 from collections import namedtuple 37 from collections import namedtuple
38 import regex 38 import regex
39 import mimetypes
39 40
40 41
41 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:5' 42 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:5'
42 43
43 PLUGIN_INFO = { 44 PLUGIN_INFO = {
57 58
58 59
59 class XEP_0234(object): 60 class XEP_0234(object):
60 # TODO: assure everything is closed when file is sent or session terminate is received 61 # TODO: assure everything is closed when file is sent or session terminate is received
61 # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end) 62 # TODO: call self._f.unregister when unloading order will be managing (i.e. when dependencies will be unloaded at the end)
63 Range = Range # we copy the class here, so it can be used by other plugins
62 64
63 def __init__(self, host): 65 def __init__(self, host):
64 log.info(_("plugin Jingle File Transfer initialization")) 66 log.info(_("plugin Jingle File Transfer initialization"))
65 self.host = host 67 self.host = host
66 host.registerNamespace('jingle-ft', NS_JINGLE_FT) 68 host.registerNamespace('jingle-ft', NS_JINGLE_FT)
84 """ 86 """
85 return u'{}_{}'.format(session['id'], content_name) 87 return u'{}_{}'.format(session['id'], content_name)
86 88
87 # generic methods 89 # generic methods
88 90
89 def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, media_type=None, desc=None, 91 def buildFileElement(self, name, file_hash=None, hash_algo=None, size=None, mime_type=None, desc=None,
90 date=None, range_offset=None, range_length=None, path=None, namespace=None, file_elt=None): 92 modified=None, transfer_range=None, path=None, namespace=None, file_elt=None, **kwargs):
91 """Generate a <file> element with available metadata 93 """Generate a <file> element with available metadata
92 94
93 @param file_hash(unicode, None): hash of the file 95 @param file_hash(unicode, None): hash of the file
94 empty string to set <hash-used/> element 96 empty string to set <hash-used/> element
95 @param hash_algo(unicode, None): hash algorithm used 97 @param hash_algo(unicode, None): hash algorithm used
96 if file_hash is None and hash_algo is set, a <hash-used/> element will be generated 98 if file_hash is None and hash_algo is set, a <hash-used/> element will be generated
97 @param range_offset(int, None): offset where transfer must start 99 @param transfer_range(Range, None): where transfer must start/stop
98 use -1 to add an empty <range/> element 100 @param modified(int, unicode, None): date of last modification
99 @param date(int, unicode, None): date of last modification
100 0 to use current date 101 0 to use current date
101 int to use an unix timestamp 102 int to use an unix timestamp
102 else must be an unicode string which will be used as it (it must be an XMPP time) 103 else must be an unicode string which will be used as it (it must be an XMPP time)
103 @param file_elt(domish.Element, None): element to use 104 @param file_elt(domish.Element, None): element to use
104 None to create a new one 105 None to create a new one
106 @param **kwargs: data for plugin extension (ignored by default)
105 @return (domish.Element): generated element 107 @return (domish.Element): generated element
108 @trigger XEP-0234_buildFileElement(file_elt, extra_args): can be used to extend elements to add
106 """ 109 """
107 if file_elt is None: 110 if file_elt is None:
108 file_elt = domish.Element((NS_JINGLE_FT, u'file')) 111 file_elt = domish.Element((NS_JINGLE_FT, u'file'))
109 for name, value in ((u'name', name), (u'size', size), ('media-type', media_type), 112 for name, value in ((u'name', name), (u'size', size), ('media-type', mime_type),
110 (u'desc', desc), (u'path', path), (u'namespace', namespace)): 113 (u'desc', desc), (u'path', path), (u'namespace', namespace)):
111 if value is not None: 114 if value is not None:
112 file_elt.addElement(name, content=unicode(value)) 115 file_elt.addElement(name, content=unicode(value))
113 if date is not None: 116
114 if isinstance(date, int): 117 if modified is not None:
115 file_elt.addElement(u'date', utils.xmpp_date(date or None)) 118 if isinstance(modified, int):
119 file_elt.addElement(u'date', utils.xmpp_date(modified or None))
116 else: 120 else:
117 file_elt.addElement(u'date', date) 121 file_elt.addElement(u'date', modified)
118 if range_offset or range_length: 122 elif 'created' in kwargs:
119 range_elt = file_elt.addElement(u'range') 123 file_elt.addElement(u'date', utils.xmpp_date(kwargs.pop('created')))
120 if range_offset is not None and range_offset != -1: 124
121 range_elt[u'offset'] = range_offset 125 range_elt = file_elt.addElement(u'range')
122 if range_length is not None: 126 if transfer_range is not None:
123 range_elt[u'length'] = range_length 127 if transfer_range.offset is not None:
128 range_elt[u'offset'] = transfer_range.offset
129 if transfer_range.length is not None:
130 range_elt[u'length'] = transfer_range.length
124 if file_hash is not None: 131 if file_hash is not None:
125 if not file_hash: 132 if not file_hash:
126 file_elt.addChild(self._hash.buildHashUsedElt()) 133 file_elt.addChild(self._hash.buildHashUsedElt())
127 else: 134 else:
128 file_elt.addChild(self._hash.buildHashElt(file_hash, hash_algo)) 135 file_elt.addChild(self._hash.buildHashElt(file_hash, hash_algo))
129 elif hash_algo is not None: 136 elif hash_algo is not None:
130 file_elt.addChild(self._hash.buildHashUsedElt(hash_algo)) 137 file_elt.addChild(self._hash.buildHashUsedElt(hash_algo))
138 self.host.trigger.point(u'XEP-0234_buildFileElement', file_elt, extra_args=kwargs)
139 if kwargs:
140 for kw in kwargs:
141 log.debug('ignored keyword: {}'.format(kw))
131 return file_elt 142 return file_elt
132 143
133 def buildFileElementFromDict(self, file_data, file_elt = None, **kwargs): 144 def buildFileElementFromDict(self, file_data, **kwargs):
134 """like buildFileElement but get values from a file_data dict 145 """like buildFileElement but get values from a file_data dict
135 146
136 @param file_data(dict): metadata to use 147 @param file_data(dict): metadata to use
137 @param **kwargs: data to override 148 @param **kwargs: data to override
138 """ 149 """
139 if kwargs: 150 if kwargs:
140 file_data = file_data.copy() 151 file_data = file_data.copy()
141 file_data.update(kwargs) 152 file_data.update(kwargs)
142 (name, file_hash, hash_algo, size, media_type, 153 return self. buildFileElement(**file_data)
143 desc, date, path, namespace) = (file_data.get(u'name'), 154
144 file_data.get(u'file_hash'), 155
145 file_data.get(u'hash_algo'), 156 def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None, keep_empty_range=False):
146 file_data.get(u'size'),
147 file_data.get(u'media-type'),
148 file_data.get(u'desc'),
149 file_data.get(u'date'),
150 file_data.get(u'path'),
151 file_data.get(u'namespace'))
152 try:
153 range_offset, range_length = file_data[u'range']
154 except KeyError:
155 range_offset = range_length = None
156 return self. buildFileElement(name = name, file_hash = file_hash, hash_algo = hash_algo, size = size,
157 media_type = media_type, desc = desc, date = date,
158 range_offset = range_offset, range_length = range_length, path = path,
159 namespace = namespace, file_elt = file_elt)
160
161
162 def parseFileElement(self, file_elt, file_data=None, given=False, parent_elt=None):
163 """Parse a <file> element and file dictionary accordingly 157 """Parse a <file> element and file dictionary accordingly
164 158
165 @param file_data(dict, None): dict where the data will be set 159 @param file_data(dict, None): dict where the data will be set
166 following keys will be set (and overwritten if they already exist): 160 following keys will be set (and overwritten if they already exist):
167 name, file_hash, hash_algo, size, media-type, desc, path, namespace, range 161 name, file_hash, hash_algo, size, mime_type, desc, path, namespace, range
168 if None, a new dict is created 162 if None, a new dict is created
169 @param given(bool): if True, prefix hash key with "given_" 163 @param given(bool): if True, prefix hash key with "given_"
170 @param parent_elt(domish.Element, None): parent of the file element 164 @param parent_elt(domish.Element, None): parent of the file element
171 if set, file_elt must not be set 165 if set, file_elt must not be set
166 @param keep_empty_range(bool): if True, keep empty range (i.e. range when offset and length are None)
167 empty range are useful to know if a peer_jid can handle range
172 @return (dict): file_data 168 @return (dict): file_data
169 @trigger XEP-0234_parseFileElement(file_elt, file_data): can be used to parse new elements
173 @raise exceptions.NotFound: there is not <file> element in parent_elt 170 @raise exceptions.NotFound: there is not <file> element in parent_elt
174 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT 171 @raise exceptions.DataError: if file_elt uri is not NS_JINGLE_FT
175 """ 172 """
176 if parent_elt is not None: 173 if parent_elt is not None:
177 if file_elt is not None: 174 if file_elt is not None:
185 raise exceptions.DataError(u'invalid <file> element: {stanza}'.format(stanza = file_elt.toXml())) 182 raise exceptions.DataError(u'invalid <file> element: {stanza}'.format(stanza = file_elt.toXml()))
186 183
187 if file_data is None: 184 if file_data is None:
188 file_data = {} 185 file_data = {}
189 186
190 for name in (u'name', u'media-type', u'desc', u'path', u'namespace'): 187 for name in (u'name', u'desc', u'path', u'namespace'):
191 try: 188 try:
192 file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next()) 189 file_data[name] = unicode(next(file_elt.elements(NS_JINGLE_FT, name)))
193 except StopIteration: 190 except StopIteration:
194 pass 191 pass
192
195 193
196 name = file_data.get(u'name') 194 name = file_data.get(u'name')
197 if name == u'..': 195 if name == u'..':
198 # we don't want to go to parent dir when joining to a path 196 # we don't want to go to parent dir when joining to a path
199 name = u'--' 197 name = u'--'
200 file_data[u'name'] = name 198 file_data[u'name'] = name
201 elif name is not None and u'/' in name or u'\\' in name: 199 elif name is not None and u'/' in name or u'\\' in name:
202 file_data[u'name'] = regex.pathEscape(name) 200 file_data[u'name'] = regex.pathEscape(name)
203 201
204 try: 202 try:
205 file_data[u'size'] = int(unicode(file_elt.elements(NS_JINGLE_FT, u'size').next())) 203 file_data[u'mime_type'] = unicode(next(file_elt.elements(NS_JINGLE_FT, u'media-type')))
204 except StopIteration:
205 pass
206
207 try:
208 file_data[u'size'] = int(unicode(next(file_elt.elements(NS_JINGLE_FT, u'size'))))
209 except StopIteration:
210 pass
211
212 try:
213 file_data[u'modified'] = utils.date_parse(next(file_elt.elements(NS_JINGLE_FT, u'date')))
206 except StopIteration: 214 except StopIteration:
207 pass 215 pass
208 216
209 try: 217 try:
210 range_elt = file_elt.elements(NS_JINGLE_FT, u'range').next() 218 range_elt = file_elt.elements(NS_JINGLE_FT, u'range').next()
211 except StopIteration: 219 except StopIteration:
212 pass 220 pass
213 else: 221 else:
214 offset = range_elt.getAttribute('offset') 222 offset = range_elt.getAttribute('offset')
215 length = range_elt.getAttribute('length') 223 length = range_elt.getAttribute('length')
216 file_data[u'range'] = Range(offset=offset, length=length) 224 if offset or length or keep_empty_range:
225 file_data[u'transfer_range'] = Range(offset=offset, length=length)
217 226
218 prefix = u'given_' if given else u'' 227 prefix = u'given_' if given else u''
219 hash_algo_key, hash_key = u'hash_algo', prefix + u'file_hash' 228 hash_algo_key, hash_key = u'hash_algo', prefix + u'file_hash'
220 try: 229 try:
221 file_data[hash_algo_key], file_data[hash_key] = self._hash.parseHashElt(file_elt) 230 file_data[hash_algo_key], file_data[hash_key] = self._hash.parseHashElt(file_elt)
222 except exceptions.NotFound: 231 except exceptions.NotFound:
223 pass 232 pass
233
234 self.host.trigger.point(u'XEP-0234_parseFileElement', file_elt, file_data)
224 235
225 return file_data 236 return file_data
226 237
227 # bridge methods 238 # bridge methods
228 239
312 desc_elt = domish.Element((NS_JINGLE_FT, 'description')) 323 desc_elt = domish.Element((NS_JINGLE_FT, 'description'))
313 file_elt = desc_elt.addElement("file") 324 file_elt = desc_elt.addElement("file")
314 325
315 if content_data[u'senders'] == self._j.ROLE_INITIATOR: 326 if content_data[u'senders'] == self._j.ROLE_INITIATOR:
316 # we send a file 327 # we send a file
328 if name is None:
329 name = os.path.basename(filepath)
317 file_data[u'date'] = utils.xmpp_date() 330 file_data[u'date'] = utils.xmpp_date()
318 file_data[u'desc'] = extra.pop(u'file_desc', u'') 331 file_data[u'desc'] = extra.pop(u'file_desc', u'')
319 file_data[u'media-type'] = "application/octet-stream" # TODO 332 file_data[u'name'] = name
320 file_data[u'name'] = os.path.basename(filepath) if name is None else name 333 mime_type = mimetypes.guess_type(name, strict=False)[0]
334 if mime_type is not None:
335 file_data[u'mime_type'] = mime_type
321 file_data[u'size'] = os.path.getsize(filepath) 336 file_data[u'size'] = os.path.getsize(filepath)
322 if u'namespace' in extra: 337 if u'namespace' in extra:
323 file_data[u'namespace'] = extra[u'namespace'] 338 file_data[u'namespace'] = extra[u'namespace']
324 if u'path' in extra: 339 if u'path' in extra:
325 file_data[u'path'] = extra[u'path'] 340 file_data[u'path'] = extra[u'path']
326 self.buildFileElementFromDict(file_data, file_elt=file_elt, file_hash=u'') 341 self.buildFileElementFromDict(file_data, file_elt=file_elt, file_hash=u'')
327 file_elt.addElement("range") # TODO
328 else: 342 else:
329 # we request a file 343 # we request a file
330 file_hash = extra.pop(u'file_hash', u'') 344 file_hash = extra.pop(u'file_hash', u'')
331 if not name and not file_hash: 345 if not name and not file_hash:
332 raise ValueError(_(u'you need to provide at least name or file hash')) 346 raise ValueError(_(u'you need to provide at least name or file hash'))