- core: 2 new convenient methods: getJidNStream and getClient - new param in plugin info: "handler" to know if there is a handler to plug on profiles clients - plugins with handler now use an other class which is returned to profile client with the new method "getHandler" and pluged when connecting
208 lines
8.1 KiB
Python
Executable file
208 lines
8.1 KiB
Python
Executable file
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
SAT plugin for managing xep-0096
|
|
Copyright (C) 2009, 2010 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
from logging import debug, info, error
|
|
from twisted.words.xish import domish
|
|
from twisted.internet import protocol
|
|
from twisted.words.protocols.jabber import client, jid
|
|
from twisted.words.protocols.jabber import error as jab_error
|
|
import os.path
|
|
from twisted.internet import reactor #FIXME best way ???
|
|
import pdb
|
|
|
|
from zope.interface import implements
|
|
|
|
try:
|
|
from twisted.words.protocols.xmlstream import XMPPHandler
|
|
except ImportError:
|
|
from wokkel.subprotocols import XMPPHandler
|
|
|
|
from wokkel import disco, iwokkel
|
|
|
|
IQ_SET = '/iq[@type="set"]'
|
|
NS_SI = 'http://jabber.org/protocol/si'
|
|
SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]'
|
|
|
|
PLUGIN_INFO = {
|
|
"name": "XEP 0096 Plugin",
|
|
"import_name": "XEP_0096",
|
|
"type": "XEP",
|
|
"protocols": ["XEP-0096"],
|
|
"dependencies": ["XEP_0065"],
|
|
"main": "XEP_0096",
|
|
"handler": "yes",
|
|
"description": """Implementation of SI File Transfert"""
|
|
}
|
|
|
|
class XEP_0096():
|
|
|
|
def __init__(self, host):
|
|
info("Plugin XEP_0096 initialization")
|
|
self.host = host
|
|
self._waiting_for_approval = {}
|
|
host.bridge.addMethod("sendFile", ".communication", in_sign='sss', out_sign='s', method=self.sendFile)
|
|
|
|
def getHandler(self):
|
|
return XEP_0096_handler(self)
|
|
|
|
def xep_96(self, IQ, profile):
|
|
info ("XEP-0096 management")
|
|
IQ.handled=True
|
|
SI_elem = IQ.firstChildElement()
|
|
debug(SI_elem.toXml())
|
|
filename = ""
|
|
file_size = ""
|
|
for element in SI_elem.elements():
|
|
if element.name == "file":
|
|
info ("File proposed: name=[%s] size=%s", element['name'], element['size'])
|
|
filename = element["name"]
|
|
file_size = element["size"]
|
|
elif element.name == "feature":
|
|
from_jid = IQ["from"]
|
|
self._waiting_for_approval[IQ["id"]] = (element, from_jid, file_size, profile)
|
|
data={ "filename":filename, "from":from_jid, "size":file_size }
|
|
self.host.askConfirmation(IQ["id"], "FILE_TRANSFERT", data, self.confirmationCB)
|
|
|
|
def confirmationCB(self, id, accepted, data):
|
|
"""Called on confirmation answer"""
|
|
if accepted:
|
|
data['size'] = self._waiting_for_approval[id][2]
|
|
self.host.plugins["XEP_0065"].setData(data, id)
|
|
self.approved(id)
|
|
else:
|
|
debug ("Transfert [%s] refused", id)
|
|
del(self._waiting_for_approval[id])
|
|
|
|
def approved(self, id):
|
|
"""must be called when a file transfert has be accepted by client"""
|
|
debug ("Transfert [%s] accepted", id)
|
|
|
|
if ( not self._waiting_for_approval.has_key(id) ):
|
|
error ("Approved unknow id !")
|
|
#TODO: manage this (maybe approved by several frontends)
|
|
else:
|
|
element, from_id, size, profile = self._waiting_for_approval[id]
|
|
del(self._waiting_for_approval[id])
|
|
self.negociate(element, id, from_id, profile)
|
|
|
|
def negociate(self, feat_elem, id, to_jid, profile):
|
|
#TODO: put this in a plugin
|
|
#FIXME: over ultra mega ugly, need to be generic
|
|
client = self.host.getClient(profile)
|
|
assert(client)
|
|
info ("Feature negociation")
|
|
data = feat_elem.firstChildElement()
|
|
field = data.firstChildElement()
|
|
#FIXME: several options ! Q&D code for test only
|
|
option = field.firstChildElement()
|
|
value = option.firstChildElement()
|
|
if unicode(value) == "http://jabber.org/protocol/bytestreams":
|
|
#ugly, as usual, need to be entirely rewritten (just for test !)
|
|
result = domish.Element(('', 'iq'))
|
|
result['type'] = 'result'
|
|
result['id'] = id
|
|
result['to'] = to_jid
|
|
si = result.addElement('si', 'http://jabber.org/protocol/si')
|
|
file = si.addElement('file', 'http://jabber.org/protocol/si/profile/file-transfer')
|
|
feature = si.addElement('feature', 'http://jabber.org/protocol/feature-neg')
|
|
x = feature.addElement('x', 'jabber:x:data')
|
|
x['type'] = 'submit'
|
|
field = x.addElement('field')
|
|
field['var'] = 'stream-method'
|
|
value = field.addElement('value')
|
|
value.addContent('http://jabber.org/protocol/bytestreams')
|
|
client.xmlstream.send(result)
|
|
|
|
def fileCB(self, answer, xmlstream, current_jid):
|
|
if answer['type']=="result": #FIXME FIXME FIXME ugly ugly ugly ! and temp FIXME FIXME FIXME
|
|
info("SENDING UGLY ANSWER")
|
|
offer=client.IQ(xmlstream,'set')
|
|
offer["from"]=current_jid.full()
|
|
offer["to"]=answer['from']
|
|
query=offer.addElement('query', 'http://jabber.org/protocol/bytestreams')
|
|
query['mode']='tcp'
|
|
streamhost=query.addElement('streamhost')
|
|
streamhost['host']=self.host.memory.getParamA("IP", "File Transfert")
|
|
streamhost['port']=self.host.memory.getParamA("Port", "File Transfert")
|
|
streamhost['jid']=current_jid.full()
|
|
offer.send()
|
|
|
|
def sendFile(self, to, filepath, profile_key='@DEFAULT@'):
|
|
"""send a file using XEP-0096
|
|
Return an unique id to identify the transfert
|
|
"""
|
|
current_jid, xmlstream = self.host.getJidNStream(profile_key)
|
|
if not xmlstream:
|
|
error ('Asking profile for an non-existant or not connected profile')
|
|
return ""
|
|
debug ("sendfile (%s) to %s", filepath, to )
|
|
print type(filepath), type(to)
|
|
|
|
statinfo = os.stat(filepath)
|
|
|
|
offer=client.IQ(xmlstream,'set')
|
|
debug ("Transfert ID: %s", offer["id"])
|
|
|
|
self.host.plugins["XEP_0065"].sendFile(offer["id"], filepath, str(statinfo.st_size))
|
|
|
|
offer["from"]=current_jid.full()
|
|
offer["to"]=jid.JID(to).full()
|
|
si=offer.addElement('si','http://jabber.org/protocol/si')
|
|
si["mime-type"]='text/plain'
|
|
si["profile"]='http://jabber.org/protocol/si/profile/file-transfer'
|
|
file = si.addElement('file', 'http://jabber.org/protocol/si/profile/file-transfer')
|
|
file['name']=os.path.basename(filepath)
|
|
file['size']=str(statinfo.st_size)
|
|
|
|
###
|
|
# FIXME: Ugly temporary hard coded implementation of XEP-0020 & XEP-0004,
|
|
# Need to be recoded elsewhere in a more generic way
|
|
###
|
|
|
|
feature=si.addElement('feature', "http://jabber.org/protocol/feature-neg")
|
|
x=feature.addElement('x', "jabber:x:data")
|
|
x['type']='form'
|
|
field=x.addElement('field')
|
|
field['type']='list-single'
|
|
field['var']='stream-method'
|
|
option = field.addElement('option')
|
|
value = option.addElement('value', content='http://jabber.org/protocol/bytestreams')
|
|
|
|
offer.addCallback(self.fileCB, current_jid = current_jid, xmlstream = xmlstream)
|
|
offer.send()
|
|
return offer["id"] #XXX: using IQ id as file transfert id seems OK as IQ id are required
|
|
|
|
class XEP_0096_handler(XMPPHandler):
|
|
implements(iwokkel.IDisco)
|
|
|
|
def __init__(self, plugin_parent):
|
|
self.plugin_parent = plugin_parent
|
|
self.host = plugin_parent.host
|
|
|
|
def connectionInitialized(self):
|
|
self.xmlstream.addObserver(SI_REQUEST, self.plugin_parent.xep_96, profile = self.parent.profile)
|
|
|
|
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
|
|
return [disco.DiscoFeature(NS_SI)]
|
|
|
|
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
|
|
return []
|
|
|