comparison src/plugins/plugin_xep_0234.py @ 1528:1c71d7335d02

plugin XEP-0234: jingle file transfer first draft
author Goffi <goffi@goffi.org>
date Fri, 25 Sep 2015 19:24:00 +0200
parents
children a151f3a5a2d0
comparison
equal deleted inserted replaced
1527:bfef1934a8f3 1528:1c71d7335d02
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # SAT plugin for Jingle File Transfer (XEP-0234)
5 # Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015 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 from sat.core.i18n import _, D_
21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger
23 log = getLogger(__name__)
24 from sat.core import exceptions
25 from sat.tools import xml_tools
26 from wokkel import disco, iwokkel
27 from zope.interface import implements
28 from sat.tools import utils
29 import os.path
30 from twisted.words.xish import domish
31 from twisted.words.protocols.jabber import jid
32 from twisted.python import failure
33
34 try:
35 from twisted.words.protocols.xmlstream import XMPPHandler
36 except ImportError:
37 from wokkel.subprotocols import XMPPHandler
38
39
40 NS_JINGLE_FT = 'urn:xmpp:jingle:apps:file-transfer:4'
41 CONFIRM = D_(u'{entity} wants to send the file "{name}" to you:\n{desc}\n\nThe file has a size of {size_human}\n\nDo you accept ?')
42 CONFIRM_TITLE = D_(u'Confirm file transfer')
43 CONFIRM_OVERWRITE = D_(u'File {} already exists, are you sure you want to overwrite ?')
44 CONFIRM_OVERWRITE_TITLE = D_(u'File exists')
45
46 PLUGIN_INFO = {
47 "name": "Jingle File Transfer",
48 "import_name": "XEP-0234",
49 "type": "XEP",
50 "protocols": ["XEP-0234"],
51 "dependencies": ["XEP-0166", "XEP-0300", "FILE"],
52 "main": "XEP_0234",
53 "handler": "yes",
54 "description": _("""Implementation of Jingle File Transfer""")
55 }
56
57
58 class XEP_0234(object):
59
60 def __init__(self, host):
61 log.info(_("plugin Jingle File Transfer initialization"))
62 self.host = host
63 self._j = host.plugins["XEP-0166"] # shortcut to access jingle
64 self._j.registerApplication(NS_JINGLE_FT, self)
65 self._f = host.plugins["FILE"]
66 host.bridge.addMethod("__test", ".plugin", in_sign='', out_sign='', method=self.__test)
67 host.bridge.addMethod("fileJingleSend", ".plugin", in_sign='sssss', out_sign='', method=self._fileJingleSend)
68
69 def getHandler(self, profile):
70 return XEP_0234_handler()
71
72 def _fileJingleSend(self, to_jid, filepath, name="", file_desc="", profile=C.PROF_KEY_NONE):
73 return self.fileJingleSend(jid.JID(to_jid), filepath, name or None, file_desc or None, profile)
74
75 def fileJingleSend(self, to_jid, filepath, name=None, file_desc=None, profile=C.PROF_KEY_NONE):
76 self._j.initiate(to_jid,
77 [{'app_ns': NS_JINGLE_FT,
78 'app_kwargs': {'filepath': filepath,
79 'name': name,
80 'file_desc': file_desc},
81 }],
82 profile=profile)
83
84
85 # Dialogs with user
86 # the overwrite check is done here
87
88 def _getDestDir(self, session, content_data, profile):
89 """Request confirmation and destination dir to user
90
91 if transfer is confirmed, session is filled
92 @param session(dict): jingle session data
93 @param content_data(dict): content informations
94 @param profile: %(doc_profile)s
95 return (defer.Deferred): True if transfer is accepted
96 """
97 file_data = content_data['file_data']
98 d = xml_tools.deferDialog(self.host,
99 _(CONFIRM).format(entity=session['to_jid'].full(), **file_data),
100 _(CONFIRM_TITLE),
101 type_=C.XMLUI_DIALOG_FILE,
102 options={C.XMLUI_DATA_FILETYPE: C.XMLUI_DATA_FILETYPE_DIR},
103 profile=profile)
104 d.addCallback(self._gotConfirmation, session, content_data, profile)
105 return d
106
107 def _gotConfirmation(self, data, session, content_data, profile):
108 """Called when the permission and dest path have been received
109
110 @param data(dict): xmlui data received from file dialog
111 return (bool): True if copy is wanted and OK
112 False if user wants to cancel
113 if fill exists ask confirmation and call again self._getDestDir if needed
114 """
115 if data.get('cancelled', False):
116 return False
117 file_data = content_data['file_data']
118 path = data['path']
119 file_data['file_path'] = file_path = os.path.join(path, file_data['name'])
120 log.debug(u'destination file path set to {}'.format(file_path))
121
122 # we manage case where file already exists
123 if os.path.exists(file_path):
124 def check_overwrite(overwrite):
125 if overwrite:
126 assert 'file_obj' not in content_data
127 content_data['file_obj'] = open(file_path, 'w')
128 return True
129 else:
130 return self._getDestDir(session, content_data, profile)
131
132 exists_d = xml_tools.deferConfirm(
133 self.host,
134 _(CONFIRM_OVERWRITE).format(file_path),
135 _(CONFIRM_OVERWRITE_TITLE),
136 profile=profile)
137 exists_d.addCallback(check_overwrite)
138 return exists_d
139
140 assert 'file_obj' not in content_data
141 content_data['file_obj'] = open(file_path, 'w')
142 return True
143
144 # jingle callbacks
145
146 def jingleSessionInit(self, session, content_name, filepath, name=None, file_desc=None, profile=C.PROF_KEY_NONE):
147 content_data = session['contents'][content_name]
148 assert 'file_path' not in content_data
149 content_data['file_path'] = filepath
150 file_data = content_data['file_data'] = {}
151 file_data['date'] = utils.xmpp_date()
152 file_data['desc'] = file_desc or ''
153 file_data['media-type'] = "application/octet-stream" # TODO
154 file_data['name'] = os.path.basename(filepath) if name is None else name
155 file_data['size'] = os.path.getsize(filepath)
156 desc_elt = domish.Element((NS_JINGLE_FT, 'description'))
157 file_elt = desc_elt.addElement("file")
158 for name in ('date', 'desc', 'media-type', 'name', 'size'):
159 file_elt.addElement(name, content=unicode(file_data[name]))
160 file_elt.addElement("range") # TODO
161 file_elt.addChild(self.host.plugins["XEP-0300"].buidHash())
162 return desc_elt
163
164 def jingleRequestConfirmation(self, action, session, content_name, desc_elt, profile):
165 """This method request confirmation for a jingle session"""
166 content_data = session['contents'][content_name]
167 # first we grab file informations
168 try:
169 file_elt = desc_elt.elements(NS_JINGLE_FT, 'file').next()
170 except StopIteration:
171 raise failure.Failure(exceptions.DataError)
172 file_data = {}
173 for name in ('date', 'desc', 'media-type', 'name', 'range', 'size'):
174 try:
175 file_data[name] = unicode(file_elt.elements(NS_JINGLE_FT, name).next())
176 except StopIteration:
177 file_data[name] = ''
178
179 try:
180 size = file_data['size'] = int(file_data['size'])
181 except ValueError:
182 raise failure.Failure(exceptions.DataError)
183 else:
184 # human readable size
185 file_data['size_human'] = u'{:.6n} Mio'.format(float(size)/(1024**2))
186
187 name = file_data['name']
188 if '/' in name or '\\' in name:
189 log.warning(u"File name contain path characters, we replace them: {}".format(name))
190 file_data['name'] = name.replace('/', '_').replace('\\', '_')
191
192 # TODO: parse hash using plugin XEP-0300
193
194 content_data['file_data'] = file_data
195
196 # now we actualy request permission to user
197 return self._getDestDir(session, content_data, profile)
198
199
200 def jingleHandler(self, action, session, content_name, desc_elt, profile):
201 content_data = session['contents'][content_name]
202 if action in (self._j.A_SESSION_INITIATE, self._j.A_ACCEPTED_ACK):
203 pass
204 elif action == self._j.A_SESSION_ACCEPT:
205 assert not 'file_obj' in content_data
206 file_path = content_data['file_path']
207 size = content_data['file_data']['size']
208 file_obj = content_data['file_obj'] = self._f.File(self.host,
209 file_path,
210 size=size,
211 profile=profile
212 )
213 file_obj.eof.addCallback(lambda dummy: file_obj.close())
214 else:
215 log.warning(u"FIXME: unmanaged action {}".format(action))
216 return desc_elt
217
218
219 class XEP_0234_handler(XMPPHandler):
220 implements(iwokkel.IDisco)
221
222 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
223 return [disco.DiscoFeature(NS_JINGLE_FT)]
224
225 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
226 return []