integrate httpupload plugin into gajim core

add new config option 'filetransfer_preference'
add drag and drop support for file upload
This commit is contained in:
Bronko 2017-12-27 13:17:13 +01:00 committed by Philipp Hörist
parent c23af9c9e7
commit 3a3be94aa8
15 changed files with 880 additions and 80 deletions

View File

@ -97,6 +97,10 @@ class ChatControl(ChatControlBase):
self._on_authentication_button_clicked) self._on_authentication_button_clicked)
self.handlers[id_] = self.authentication_button self.handlers[id_] = self.authentication_button
self.sendfile_button = self.xml.get_object('sendfile_button')
self.sendfile_button.set_action_name('win.send-file-' + \
self.control_id)
# Add lock image to show chat encryption # Add lock image to show chat encryption
self.lock_image = self.xml.get_object('lock_image') self.lock_image = self.xml.get_object('lock_image')
@ -242,7 +246,6 @@ class ChatControl(ChatControlBase):
def add_actions(self): def add_actions(self):
actions = [ actions = [
('send-file-', self._on_send_file),
('invite-contacts-', self._on_invite_contacts), ('invite-contacts-', self._on_invite_contacts),
('add-to-roster-', self._on_add_to_roster), ('add-to-roster-', self._on_add_to_roster),
('information-', self._on_information), ('information-', self._on_information),
@ -288,16 +291,42 @@ class ChatControl(ChatControlBase):
win.lookup_action('toggle-video-' + self.control_id).set_enabled( win.lookup_action('toggle-video-' + self.control_id).set_enabled(
online and self.video_available) online and self.video_available)
# Send file (HTTP File Upload)
httpupload = win.lookup_action(
'send-file-httpupload-' + self.control_id)
httpupload.set_enabled(
online and app.connections[self.account].httpupload)
# Send file (Jingle)
jingle_conditions = (
(self.contact.supports(NS_FILE) or
self.contact.supports(NS_JINGLE_FILE_TRANSFER_5)) and
self.contact.show != 'offline')
jingle = win.lookup_action('send-file-jingle-' + self.control_id)
jingle.set_enabled(online and jingle_conditions)
# Send file # Send file
if ((self.contact.supports(NS_FILE) or \ win.lookup_action(
self.contact.supports(NS_JINGLE_FILE_TRANSFER_5)) and \ 'send-file-' + self.control_id).set_enabled(
(self.type_id == 'chat' or self.gc_contact.resource)) and \ jingle.get_enabled() or httpupload.get_enabled())
self.contact.show != 'offline' and online:
win.lookup_action('send-file-' + self.control_id).set_enabled( # Set File Transfer Button tooltip
True) ft_pref = app.config.get_per('accounts', self.account,
else: 'filetransfer_preference')
win.lookup_action('send-file-' + self.control_id).set_enabled(
False) tooltip_text = None
if httpupload.get_enabled() and jingle.get_enabled():
if ft_pref == 'httpupload':
tooltip_text = _('HTTP File Upload')
else:
tooltip_text = _('Jingle File Transfer')
elif httpupload.get_enabled():
tooltip_text = _('HTTP File Upload')
elif jingle.get_enabled():
tooltip_text = _('Jingle File Transfer')
elif online:
tooltip_text = _('No File Transfer available')
self.sendfile_button.set_tooltip_text(tooltip_text)
# Convert to GC # Convert to GC
if app.config.get_per('accounts', self.account, 'is_zeroconf'): if app.config.get_per('accounts', self.account, 'is_zeroconf'):
@ -315,9 +344,6 @@ class ChatControl(ChatControlBase):
win.lookup_action( win.lookup_action(
'information-' + self.control_id).set_enabled(online) 'information-' + self.control_id).set_enabled(online)
def _on_send_file(self, action, param):
super()._on_send_file()
def _on_add_to_roster(self, action, param): def _on_add_to_roster(self, action, param):
dialogs.AddNewContactWindow(self.account, self.contact.jid) dialogs.AddNewContactWindow(self.account, self.contact.jid)
@ -1265,43 +1291,37 @@ class ChatControl(ChatControlBase):
self.show_avatar() self.show_avatar()
def _on_drag_data_received(self, widget, context, x, y, selection, def _on_drag_data_received(self, widget, context, x, y, selection,
target_type, timestamp): target_type, timestamp):
if not selection.get_data(): if not selection.get_data():
return return
# get contact info (check for PM = private chat)
if self.TYPE_ID == message_control.TYPE_PM: if self.TYPE_ID == message_control.TYPE_PM:
c = self.gc_contact c = self.gc_contact.as_contact()
else: else:
c = self.contact c = self.contact
if target_type == self.TARGET_TYPE_URI_LIST: if target_type == self.TARGET_TYPE_URI_LIST:
if not c.resource: # If no resource is known, we can't send a file # file drag and drop (handled in chat_control_base)
self.drag_data_file_transfer(c, selection, self)
else:
# chat2muc
treeview = app.interface.roster.tree
model = treeview.get_model()
data = selection.get_data()
path = treeview.get_selection().get_selected_rows()[1][0]
iter_ = model.get_iter(path)
type_ = model[iter_][2]
if type_ != 'contact': # source is not a contact
return return
dropped_jid = data
# we may have more than one file dropped dropped_transport = app.get_transport_name_from_jid(dropped_jid)
uri_splitted = selection.get_uris() c_transport = app.get_transport_name_from_jid(c.jid)
for uri in uri_splitted: if dropped_transport or c_transport:
path = helpers.get_file_path_from_dnd_dropped_uri(uri) return # transport contacts cannot be invited
if os.path.isfile(path): # is it file?
ft = app.interface.instances['file_transfers']
ft.send_file(self.account, c, path)
return
# chat2muc dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
treeview = app.interface.roster.tree
model = treeview.get_model()
data = selection.get_data()
path = treeview.get_selection().get_selected_rows()[1][0]
iter_ = model.get_iter(path)
type_ = model[iter_][2]
if type_ != 'contact': # source is not a contact
return
dropped_jid = data
dropped_transport = app.get_transport_name_from_jid(dropped_jid)
c_transport = app.get_transport_name_from_jid(c.jid)
if dropped_transport or c_transport:
return # transport contacts cannot be invited
dialogs.TransformChatToMUC(self.account, [c.jid], [dropped_jid])
def _on_message_tv_buffer_changed(self, textbuffer): def _on_message_tv_buffer_changed(self, textbuffer):
super()._on_message_tv_buffer_changed(textbuffer) super()._on_message_tv_buffer_changed(textbuffer)

View File

@ -27,6 +27,7 @@
## along with Gajim. If not, see <http://www.gnu.org/licenses/>. ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
## ##
import os
import time import time
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
@ -403,6 +404,24 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
action.connect('activate', self._on_history) action.connect('activate', self._on_history)
self.parent_win.window.add_action(action) self.parent_win.window.add_action(action)
action = Gio.SimpleAction.new(
'send-file-%s' % self.control_id, None)
action.connect('activate', self._on_send_file)
action.set_enabled(False)
self.parent_win.window.add_action(action)
action = Gio.SimpleAction.new(
'send-file-httpupload-%s' % self.control_id, None)
action.connect('activate', self._on_send_httpupload)
action.set_enabled(False)
self.parent_win.window.add_action(action)
action = Gio.SimpleAction.new(
'send-file-jingle-%s' % self.control_id, None)
action.connect('activate', self._on_send_jingle)
action.set_enabled(False)
self.parent_win.window.add_action(action)
# Actions # Actions
def _on_history(self, action, param): def _on_history(self, action, param):
@ -730,6 +749,44 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self.drag_entered_conv = True self.drag_entered_conv = True
self.conv_textview.tv.set_editable(True) self.conv_textview.tv.set_editable(True)
def drag_data_file_transfer(self, contact, selection, widget):
# get file transfer preference
ft_pref = app.config.get_per('accounts', self.account,
'filetransfer_preference')
win = self.parent_win.window
con = app.connections[self.account]
httpupload = win.lookup_action(
'send-file-httpupload-%s' % self.control_id)
jingle = win.lookup_action('send-file-jingle-%s' % self.control_id)
# we may have more than one file dropped
uri_splitted = selection.get_uris()
for uri in uri_splitted:
path = helpers.get_file_path_from_dnd_dropped_uri(uri)
if not os.path.isfile(path): # is it a file?
continue
if self.type_id == message_control.TYPE_GC:
# groupchat only supports httpupload on drag and drop
if httpupload.get_enabled():
# use httpupload
con.check_file_before_transfer(
path, self.encryption, contact,
self.session, groupchat=True)
else:
if httpupload.get_enabled() and jingle.get_enabled():
if ft_pref == 'httpupload':
con.check_file_before_transfer(
path, self.encryption, contact, self.session)
else:
ft = app.interface.instances['file_transfers']
ft.send_file(self.account, contact, path)
elif httpupload.get_enabled():
con.check_file_before_transfer(
path, self.encryption, contact, self.session)
elif jingle.get_enabled():
ft = app.interface.instances['file_transfers']
ft.send_file(self.account, contact, path)
def get_seclabel(self): def get_seclabel(self):
label = None label = None
if self.seclabel_combo is not None: if self.seclabel_combo is not None:
@ -1065,14 +1122,40 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
app.interface.instances['logs'] = \ app.interface.instances['logs'] = \
history_window.HistoryWindow(jid, self.account) history_window.HistoryWindow(jid, self.account)
def _on_send_file(self, gc_contact=None): def _on_send_file(self, action, param):
# get file transfer preference
ft_pref = app.config.get_per('accounts', self.account,
'filetransfer_preference')
win = self.parent_win.window
httpupload = win.lookup_action(
'send-file-httpupload-%s' % self.control_id)
jingle = win.lookup_action('send-file-jingle-%s' % self.control_id)
if httpupload.get_enabled() and jingle.get_enabled():
if ft_pref == 'httpupload':
httpupload.activate()
else:
jingle.activate()
elif httpupload.get_enabled():
httpupload.activate()
elif jingle.get_enabled():
jingle.activate()
def _on_send_httpupload(self, action, param):
app.interface.send_httpupload(self)
def _on_send_jingle(self, action, param):
self._on_send_file_jingle()
def _on_send_file_jingle(self, gc_contact=None):
""" """
gc_contact can be set when we are in a groupchat control gc_contact can be set when we are in a groupchat control
""" """
def _on_ok(c): def _on_ok(c):
app.interface.instances['file_transfers'].show_file_send_request( app.interface.instances['file_transfers'].show_file_send_request(
self.account, c) self.account, c)
if self.TYPE_ID == message_control.TYPE_PM: if self.type_id == message_control.TYPE_PM:
gc_contact = self.gc_contact gc_contact = self.gc_contact
if gc_contact: if gc_contact:
# gc or pm # gc or pm

View File

@ -408,6 +408,8 @@ class Config:
'oauth2_redirect_url': [ opt_str, 'https%3A%2F%2Fgajim.org%2Fmsnauth%2Findex.cgi', _('redirect_url for OAuth 2.0 authentication.')], 'oauth2_redirect_url': [ opt_str, 'https%3A%2F%2Fgajim.org%2Fmsnauth%2Findex.cgi', _('redirect_url for OAuth 2.0 authentication.')],
'opened_chat_controls': [opt_str, '', _('Space separated list of JIDs for which we want to re-open a chat window on next startup.')], 'opened_chat_controls': [opt_str, '', _('Space separated list of JIDs for which we want to re-open a chat window on next startup.')],
'recent_groupchats': [ opt_str, '' ], 'recent_groupchats': [ opt_str, '' ],
'httpupload_verify': [ opt_bool, True, _('HTTP Upload: Enable HTTPS Verification')],
'filetransfer_preference' : [ opt_str, 'httpupload', _('Preferred file transfer mechanism for file drag&drop on chat window. Can be \'httpupload\' (default) or \'jingle\'')],
}, {}), }, {}),
'statusmsg': ({ 'statusmsg': ({
'message': [ opt_str, '' ], 'message': [ opt_str, '' ],

View File

@ -422,7 +422,10 @@ class CommonConnection:
if not obj.is_loggable: if not obj.is_loggable:
return return
if obj.forward_from or not obj.session or not obj.session.is_loggable(): if obj.forward_from:
return
if obj.session and not obj.session.is_loggable():
return return
if not app.config.should_log(self.name, jid): if not app.config.should_log(self.name, jid):

View File

@ -54,6 +54,7 @@ from gajim.common.protocol.caps import ConnectionCaps
from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream
from gajim.common.protocol.bytestream import ConnectionIBBytestream from gajim.common.protocol.bytestream import ConnectionIBBytestream
from gajim.common.message_archiving import ConnectionArchive313 from gajim.common.message_archiving import ConnectionArchive313
from gajim.common.httpupload import ConnectionHTTPUpload
from gajim.common.connection_handlers_events import * from gajim.common.connection_handlers_events import *
from gajim.common import ged from gajim.common import ged
@ -1249,7 +1250,8 @@ class ConnectionHandlersBase:
class ConnectionHandlers(ConnectionArchive313, class ConnectionHandlers(ConnectionArchive313,
ConnectionVcard, ConnectionSocks5Bytestream, ConnectionDisco, ConnectionVcard, ConnectionSocks5Bytestream, ConnectionDisco,
ConnectionCommands, ConnectionPubSub, ConnectionPEP, ConnectionCaps, ConnectionCommands, ConnectionPubSub, ConnectionPEP, ConnectionCaps,
ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream): ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream,
ConnectionHTTPUpload):
def __init__(self): def __init__(self):
ConnectionArchive313.__init__(self) ConnectionArchive313.__init__(self)
ConnectionVcard.__init__(self) ConnectionVcard.__init__(self)
@ -1259,6 +1261,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
ConnectionPubSub.__init__(self) ConnectionPubSub.__init__(self)
ConnectionPEP.__init__(self, account=self.name, dispatcher=self, ConnectionPEP.__init__(self, account=self.name, dispatcher=self,
pubsub_connection=self) pubsub_connection=self)
ConnectionHTTPUpload.__init__(self, account=self.name)
# Handle presences BEFORE caps # Handle presences BEFORE caps
app.nec.register_incoming_event(PresenceReceivedEvent) app.nec.register_incoming_event(PresenceReceivedEvent)
@ -1343,6 +1346,7 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
ConnectionCaps.cleanup(self) ConnectionCaps.cleanup(self)
ConnectionArchive313.cleanup(self) ConnectionArchive313.cleanup(self)
ConnectionPubSub.cleanup(self) ConnectionPubSub.cleanup(self)
ConnectionHTTPUpload.cleanup(self)
app.ged.remove_event_handler('http-auth-received', ged.CORE, app.ged.remove_event_handler('http-auth-received', ged.CORE,
self._nec_http_auth_received) self._nec_http_auth_received)
app.ged.remove_event_handler('version-request-received', ged.CORE, app.ged.remove_event_handler('version-request-received', ged.CORE,

View File

@ -2958,3 +2958,17 @@ class BlockingEvent(nec.NetworkIncomingEvent):
app.log('blocking').info( app.log('blocking').info(
'Blocking Push - unblocked JIDs: %s', self.unblocked_jids) 'Blocking Push - unblocked JIDs: %s', self.unblocked_jids)
return True return True
class HTTPUploadStartEvent(nec.NetworkIncomingEvent):
name = 'httpupload-start'
base_network_events = []
def generate(self):
return True
class HTTPUploadProgressEvent(nec.NetworkIncomingEvent):
name = 'httpupload-progress'
base_network_events = []
def generate(self):
return True

384
gajim/common/httpupload.py Normal file
View File

@ -0,0 +1,384 @@
# -*- coding: utf-8 -*-
#
# This file is part of Gajim.
#
# Gajim 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; version 3 only.
#
# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
import os
import threading
import ssl
import urllib
from urllib.request import Request, urlopen
from urllib.parse import urlparse
import io
import mimetypes
import logging
import nbxmpp
from nbxmpp import NS_HTTPUPLOAD
from gi.repository import GLib
from gajim.common import app
from gajim.common import ged
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.connection_handlers_events import HTTPUploadProgressEvent
from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
if os.name == 'nt':
import certifi
log = logging.getLogger('gajim.c.httpupload')
class ConnectionHTTPUpload:
"""
Implement HTTP File Upload
(XEP-0363, https://xmpp.org/extensions/xep-0363.html)
"""
def __init__(self, account):
self.name = account
self.encrypted_upload = False
self.component = None
self.max_file_size = None # maximum file size in bytes
app.ged.register_event_handler('agent-info-received',
ged.GUI1,
self.handle_agent_info_received)
app.ged.register_event_handler('stanza-message-outgoing',
ged.OUT_PREGUI,
self.handle_outgoing_stanza)
app.ged.register_event_handler('gc-stanza-message-outgoing',
ged.OUT_PREGUI,
self.handle_outgoing_stanza)
self.messages = []
def cleanup(self):
app.ged.remove_event_handler('agent-info-received',
ged.GUI1,
self.handle_agent_info_received)
app.ged.remove_event_handler('stanza-message-outgoing',
ged.OUT_PREGUI,
self.handle_outgoing_stanza)
app.ged.remove_event_handler('gc-stanza-message-outgoing',
ged.OUT_PREGUI,
self.handle_outgoing_stanza)
def handle_agent_info_received(self, event):
if (NS_HTTPUPLOAD not in event.features or not
app.jid_is_transport(event.jid)):
return
if not event.id_.startswith('Gajim_'):
return
account = event.conn.name
self.component = event.jid
for form in event.data:
form_dict = form.asDict()
if form_dict.get('FORM_TYPE', None) != NS_HTTPUPLOAD:
continue
size = form_dict.get('max-file-size', None)
if size is not None:
self.max_file_size = int(size)
break
if self.max_file_size is None:
log.warning('%s does not provide maximum file size', account)
else:
log.info('%s has a maximum file size of: %s MiB',
account, self.max_file_size/(1024*1024))
def handle_outgoing_stanza(self, event):
message = event.msg_iq.getTagData('body')
if message and message in self.messages:
self.messages.remove(message)
# Add oob information before sending message to recipient,
# to distinguish HTTP File Upload Link from pasted URL
oob = event.msg_iq.addChild('x', namespace=nbxmpp.NS_X_OOB)
oob.addChild('url').setData(message)
if 'gajim' in event.additional_data:
event.additional_data['gajim']['oob_url'] = message
else:
event.additional_data['gajim'] = {'oob_url': message}
def check_file_before_transfer(self, path, encryption, contact, session,
groupchat=False):
if not path or not os.path.exists(path):
return
invalid_file = False
stat = os.stat(path)
if os.path.isfile(path):
if stat[6] == 0:
invalid_file = True
msg = _('File is empty')
else:
invalid_file = True
msg = _('File does not exist')
if self.max_file_size is not None and \
stat.st_size > self.max_file_size:
invalid_file = True
size = GLib.format_size_full(self.max_file_size,
GLib.FormatSizeFlags.IEC_UNITS)
msg = _('File is too large, '
'maximum allowed file size is: %s') % size
if invalid_file:
self.raise_information_event('open-file-error2', msg)
return
mime = mimetypes.MimeTypes().guess_type(path)[0]
if not mime:
mime = 'application/octet-stream' # fallback mime type
log.info("Detected MIME type of file: %s", mime)
try:
file = File(path, contact, mime=mime, encryption=encryption,
update_progress=self.raise_progress_event,
session=session, groupchat=groupchat)
app.interface.show_httpupload_progress(file)
except Exception as error:
log.exception('Error while loading file')
self.raise_information_event('open-file-error2', str(error))
return
if encryption is not None:
app.interface.encrypt_file(file, self.request_slot)
else:
self.request_slot(file)
@staticmethod
def raise_progress_event(status, file, seen=None, total=None):
app.nec.push_incoming_event(HTTPUploadProgressEvent(
None, status=status, file=file, seen=seen, total=total))
@staticmethod
def raise_information_event(dialog_name, args=None):
app.nec.push_incoming_event(InformationEvent(
None, dialog_name=dialog_name, args=args))
def request_slot(self, file):
GLib.idle_add(self.raise_progress_event, 'request', file)
iq = nbxmpp.Iq(typ='get', to=self.component)
id_ = app.get_an_id()
iq.setID(id_)
request = iq.setTag(name="request", namespace=NS_HTTPUPLOAD)
request.addChild('filename', payload=os.path.basename(file.path))
request.addChild('size', payload=file.size)
request.addChild('content-type', payload=file.mime)
log.info("Sending request for slot")
app.connections[self.name].connection.SendAndCallForResponse(
iq, self.received_slot, {'file': file})
@staticmethod
def get_slot_error_message(stanza):
tmp = stanza.getTag('error').getTag('file-too-large')
if tmp is not None:
max_file_size = int(tmp.getTag('max-file-size').getData())
return _('File is too large, maximum allowed file size is: %s') % \
GLib.format_size_full(max_file_size,
GLib.FormatSizeFlags.IEC_UNITS)
return stanza.getErrorMsg()
def received_slot(self, conn, stanza, file):
log.info("Received slot")
if stanza.getType() == 'error':
self.raise_progress_event('close', file)
self.raise_information_event('request-upload-slot-error',
self.get_slot_error_message(stanza))
log.error(stanza)
return
try:
file.put = stanza.getTag("slot").getTag("put").getData()
file.get = stanza.getTag("slot").getTag("get").getData()
except Exception:
log.error("Got unexpected stanza: %s", stanza)
log.exception('Error')
self.raise_progress_event('close', file)
self.raise_information_event('request-upload-slot-error2')
return
if (urlparse(file.put).scheme != 'https' or
urlparse(file.get).scheme != 'https'):
self.raise_progress_event('close', file)
self.raise_information_event('unsecure-error')
return
try:
file.stream = StreamFileWithProgress(file)
except Exception:
log.exception('Error')
self.raise_progress_event('close', file)
self.raise_information_event('open-file-error')
return
log.info('Uploading file to %s', file.put)
log.info('Please download from %s', file.get)
thread = threading.Thread(target=self.upload_file, args=(file,))
thread.daemon = True
thread.start()
def upload_file(self, file):
GLib.idle_add(self.raise_progress_event, 'upload', file)
try:
headers = {'User-Agent': 'Gajim %s' % app.version,
'Content-Type': file.mime,
'Content-Length': file.size}
request = Request(
file.put, data=file.stream, headers=headers, method='PUT')
log.info("Opening Urllib upload request...")
if not app.config.get_per('accounts', self.name, 'httpupload_verify'):
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
log.warning('CERT Verification disabled')
transfer = urlopen(request, timeout=30, context=context)
else:
if os.name == 'nt':
transfer = urlopen(
request, cafile=certifi.where(), timeout=30)
else:
transfer = urlopen(request, timeout=30)
file.stream.close()
log.info('Urllib upload request done, response code: %s',
transfer.getcode())
GLib.idle_add(self.upload_complete, transfer.getcode(), file)
return
except UploadAbortedException as exc:
log.info(exc)
error_msg = exc
except urllib.error.URLError as exc:
if isinstance(exc.reason, ssl.SSLError):
error_msg = exc.reason.reason
if error_msg == 'CERTIFICATE_VERIFY_FAILED':
log.exception('Certificate verify failed')
else:
log.exception('URLError')
error_msg = exc.reason
except Exception as exc:
log.exception("Exception during upload")
error_msg = exc
GLib.idle_add(self.raise_progress_event, 'close', file)
GLib.idle_add(self.on_upload_error, file, error_msg)
def upload_complete(self, response_code, file):
self.raise_progress_event('close', file)
if 200 <= response_code < 300:
log.info("Upload completed successfully")
message = file.get
if file.user_data:
message += '#' + file.user_data
message = self.convert_to_aegscm(message)
else:
self.messages.append(message)
if file.groupchat:
app.nec.push_outgoing_event(GcMessageOutgoingEvent(
None, account=self.name, jid=file.contact.jid,
message=message, automatic_message=False,
session=file.session))
else:
app.nec.push_outgoing_event(MessageOutgoingEvent(
None, account=self.name, jid=file.contact.jid,
message=message, keyID=file.keyID, type_='chat',
automatic_message=False, session=file.session))
else:
log.error('Got unexpected http upload response code: %s',
response_code)
self.raise_information_event('httpupload-response-error',
response_code)
def on_upload_error(self, file, reason):
self.raise_progress_event('close', file)
self.raise_information_event('httpupload-error', str(reason))
@staticmethod
def convert_to_aegscm(url):
return 'aesgcm' + url[5:]
class File:
def __init__(self, path, contact, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
self.encrypted = False
self.contact = contact
self.keyID = None
if hasattr(contact, 'keyID'):
self.keyID = contact.keyID
self.stream = None
self.path = path
self.put = None
self.get = None
self.data = None
self.user_data = None
self.size = None
self.event = threading.Event()
self.load_data()
def load_data(self):
with open(self.path, 'rb') as content:
self.data = content.read()
self.size = len(self.data)
def get_data(self, full=False):
if full:
return io.BytesIO(self.data).getvalue()
return io.BytesIO(self.data)
class StreamFileWithProgress:
def __init__(self, file):
self.file = file
self.event = file.event
self.backing = file.get_data()
self.backing.seek(0, os.SEEK_END)
self._total = self.backing.tell()
self.backing.seek(0)
self._callback = file.update_progress
self._seen = 0
def __len__(self):
return self._total
def read(self, size):
if self.event.isSet():
raise UploadAbortedException
data = self.backing.read(size)
self._seen += len(data)
if self._callback:
GLib.idle_add(self._callback, 'update',
self.file, self._seen, self._total)
return data
def close(self):
return self.backing.close()
class UploadAbortedException(Exception):
def __str__(self):
return "Upload Aborted"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 --> <!-- Generated with glade 3.20.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.20"/>
<object class="GtkAdjustment" id="adjustment1"> <object class="GtkAdjustment" id="adjustment1">
@ -821,9 +821,6 @@ audio-mic-volume-low</property>
<property name="position">4</property> <property name="position">4</property>
</packing> </packing>
</child> </child>
<child>
<placeholder/>
</child>
<child> <child>
<object class="GtkMenuButton" id="encryption_menu"> <object class="GtkMenuButton" id="encryption_menu">
<property name="visible">True</property> <property name="visible">True</property>
@ -846,9 +843,39 @@ audio-mic-volume-low</property>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="pack_type">end</property> <property name="pack_type">end</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkButton" id="sendfile_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">mail-attachment-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
<style>
<class name="chatcontrol-actionbar-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">6</property> <property name="position">6</property>
</packing> </packing>
</child> </child>
<child>
<placeholder/>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 --> <!-- Generated with glade 3.20.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.20"/>
<object class="GtkMenu" id="formattings_menu"> <object class="GtkMenu" id="formattings_menu">
@ -340,6 +340,31 @@
<property name="position">5</property> <property name="position">5</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkButton" id="sendfile_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">mail-attachment-symbolic</property>
</object>
</child>
<style>
<class name="chatcontrol-actionbar-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">6</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 -->
<interface>
<requires lib="gtk+" version="3.14"/>
<object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkAlignment" id="alignment1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">8</property>
<property name="bottom_padding">4</property>
<property name="left_padding">8</property>
<property name="right_padding">8</property>
<child>
<object class="GtkLabel" id="label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="variant" value="normal"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkAlignment" id="alignment2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="top_padding">4</property>
<property name="bottom_padding">4</property>
<property name="left_padding">8</property>
<property name="right_padding">8</property>
<child>
<object class="GtkProgressBar" id="progressbar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pulse_step">0.10000000149</property>
<property name="show_text">True</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@ -133,6 +133,46 @@ messages = {
_('%s\nLink-local messaging might not work properly.'), _('%s\nLink-local messaging might not work properly.'),
ErrorDialog), ErrorDialog),
'request-upload-slot-error': Message(
_('Could not request upload slot'),
'%s',
ErrorDialog),
'request-upload-slot-error2': Message(
_('Could not request upload slot'),
_('Got unexpected response from server (see log)'),
ErrorDialog),
'open-file-error': Message(
_('Could not open file'),
_('Exception raised while opening file (see log)'),
ErrorDialog),
'open-file-error2': Message(
_('Could not open file'),
'%s',
ErrorDialog),
'unsecure-error': Message(
_('Unsecure'),
_('Server returned unsecure transport (http)'),
ErrorDialog),
'httpupload-response-error': Message(
_('Could not upload file'),
_('HTTP response code from server: %s'),
ErrorDialog),
'httpupload-error': Message(
_('Upload Error'),
'%s',
ErrorDialog),
'httpupload-encryption-not-available': Message(
_('Encryption Error'),
_('For the choosen encryption is no encryption method available'),
ErrorDialog),
} }

View File

@ -5417,3 +5417,65 @@ class SSLErrorDialog(ConfirmationDialogDoubleCheck):
def on_cert_clicked(self, button): def on_cert_clicked(self, button):
d = CertificatDialog(self, self.account, self.cert) d = CertificatDialog(self, self.account, self.cert)
class ProgressWindow(Gtk.ApplicationWindow):
def __init__(self, file):
Gtk.ApplicationWindow.__init__(self)
self.set_name('HTTPUploadProgressWindow')
self.set_application(app.app)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_show_menubar(False)
self.set_title(_('File Transfer'))
self.set_default_size(250, -1)
self.event = file.event
self.file = file
self.xml = gtkgui_helpers.get_gtk_builder(
'httpupload_progress_dialog.ui')
self.label = self.xml.get_object('label')
self.progressbar = self.xml.get_object('progressbar')
self.add(self.xml.get_object('box'))
self.pulse = GLib.timeout_add(100, self._pulse_progressbar)
self.show_all()
self.connect('destroy', self._on_destroy)
app.ged.register_event_handler('httpupload-progress', ged.CORE,
self._on_httpupload_progress)
def _on_httpupload_progress(self, obj):
if self.file != obj.file:
return
if obj.status == 'request':
self.label.set_text(_('Requesting HTTP Upload Slot...'))
elif obj.status == 'close':
self.destroy()
elif obj.status == 'upload':
self.label.set_text(_('Uploading file via HTTP File Upload...'))
elif obj.status == 'update':
self.update_progress(obj.seen, obj.total)
elif obj.status == 'encrypt':
self.label.set_text(_('Encrypting file...'))
def _pulse_progressbar(self):
self.progressbar.pulse()
return True
def _on_destroy(self, *args):
self.event.set()
if self.pulse:
GLib.source_remove(self.pulse)
app.ged.remove_event_handler('httpupload-progress', ged.CORE,
self._on_httpupload_progress)
def update_progress(self, seen, total):
if self.event.isSet():
return
if self.pulse:
GLib.source_remove(self.pulse)
self.pulse = None
pct = (float(seen) / total) * 100.0
self.progressbar.set_fraction(float(seen) / total)
self.progressbar.set_text(str(int(pct)) + "%")

View File

@ -58,6 +58,7 @@ from gajim.common import helpers
from gajim.common import dataforms from gajim.common import dataforms
from gajim.common import ged from gajim.common import ged
from gajim.common import i18n from gajim.common import i18n
from gajim.common import contacts
from gajim.chat_control import ChatControl from gajim.chat_control import ChatControl
from gajim.chat_control_base import ChatControlBase from gajim.chat_control_base import ChatControlBase
@ -444,6 +445,11 @@ class GroupchatControl(ChatControlBase):
self.form_widget = None self.form_widget = None
# Send file
self.sendfile_button = self.xml.get_object('sendfile_button')
self.sendfile_button.set_action_name('win.send-file-' + \
self.control_id)
# Encryption # Encryption
self.lock_image = self.xml.get_object('lock_image') self.lock_image = self.xml.get_object('lock_image')
self.authentication_button = self.xml.get_object( self.authentication_button = self.xml.get_object(
@ -589,6 +595,24 @@ class GroupchatControl(ChatControlBase):
win.lookup_action('execute-command-' + self.control_id).set_enabled( win.lookup_action('execute-command-' + self.control_id).set_enabled(
online) online)
# Send file (HTTP File Upload)
httpupload = win.lookup_action(
'send-file-httpupload-' + self.control_id)
httpupload.set_enabled(
online and app.connections[self.account].httpupload)
win.lookup_action('send-file-' + self.control_id).set_enabled(
httpupload.get_enabled())
tooltip_text = None
if online:
if httpupload.get_enabled():
tooltip_text = _('HTTP File Upload')
else:
tooltip_text = _('HTTP File Upload not supported '
'by your server')
self.sendfile_button.set_tooltip_text(tooltip_text)
# Actions # Actions
def _on_change_subject(self, action, param): def _on_change_subject(self, action, param):
@ -1565,12 +1589,6 @@ class GroupchatControl(ChatControlBase):
if ctrl and msg: if ctrl and msg:
ctrl.send_message(msg) ctrl.send_message(msg)
def on_send_file(self, widget, gc_contact):
"""
Send a file to a contact in the room
"""
self._on_send_file(gc_contact)
def draw_contact(self, nick, selected=False, focus=False): def draw_contact(self, nick, selected=False, focus=False):
iter_ = self.get_contact_iter(nick) iter_ = self.get_contact_iter(nick)
if not iter_: if not iter_:
@ -2294,23 +2312,31 @@ class GroupchatControl(ChatControlBase):
ok_handler=on_ok, transient_for=self.parent_win.window) ok_handler=on_ok, transient_for=self.parent_win.window)
def _on_drag_data_received(self, widget, context, x, y, selection, def _on_drag_data_received(self, widget, context, x, y, selection,
target_type, timestamp): target_type, timestamp):
# Invite contact to groupchat if not selection.get_data():
treeview = app.interface.roster.tree
model = treeview.get_model()
if not selection.get_data() or target_type == 80:
# target_type = 80 means a file is dropped
return return
data = selection.get_data()
path = treeview.get_selection().get_selected_rows()[1][0] # get contact info
iter_ = model.get_iter(path) contact = contacts.Contact(jid=self.room_jid, account=self.account)
type_ = model[iter_][2]
if type_ != 'contact': # source is not a contact if target_type == self.TARGET_TYPE_URI_LIST:
return # file drag and drop (handled in chat_control_base)
contact_jid = data self.drag_data_file_transfer(contact, selection, self)
app.connections[self.account].send_invite(self.room_jid, contact_jid) else:
self.print_conversation(_('%(jid)s has been invited in this room') % { # Invite contact to groupchat
'jid': contact_jid}, graphics=False) treeview = app.interface.roster.tree
model = treeview.get_model()
data = selection.get_data()
path = treeview.get_selection().get_selected_rows()[1][0]
iter_ = model.get_iter(path)
type_ = model[iter_][2]
if type_ != 'contact': # source is not a contact
return
contact_jid = data
app.connections[self.account].send_invite(self.room_jid, contact_jid)
self.print_conversation(_('%(jid)s has been invited in this room') %
{'jid': contact_jid}, graphics=False)
def _on_message_textview_key_press_event(self, widget, event): def _on_message_textview_key_press_event(self, widget, event):
res = ChatControlBase._on_message_textview_key_press_event(self, widget, res = ChatControlBase._on_message_textview_key_press_event(self, widget,
@ -2605,7 +2631,9 @@ class GroupchatControl(ChatControlBase):
if not c.resource: if not c.resource:
item.set_sensitive(False) item.set_sensitive(False)
else: else:
id_ = item.connect('activate', self.on_send_file, c) item.set_sensitive(False)
# ToDo: integrate HTTP File Upload
id_ = item.connect('activate', self._on_send_file_jingle, c)
self.handlers[id_] = item self.handlers[id_] = item
# show the popup now! # show the popup now!

View File

@ -64,6 +64,8 @@ from gajim import dialogs
from gajim import notify from gajim import notify
from gajim import message_control from gajim import message_control
from gajim.dialog_messages import get_dialog from gajim.dialog_messages import get_dialog
from gajim.dialogs import ProgressWindow
from gajim.dialogs import FileChooserDialog
from gajim.chat_control_base import ChatControlBase from gajim.chat_control_base import ChatControlBase
from gajim.chat_control import ChatControl from gajim.chat_control import ChatControl
@ -88,7 +90,7 @@ from gajim.common import passwords
from gajim.common import logging_helpers from gajim.common import logging_helpers
from gajim.common.connection_handlers_events import ( from gajim.common.connection_handlers_events import (
OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent, OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent,
UpdateRosterAvatarEvent, UpdateGCAvatarEvent) UpdateRosterAvatarEvent, UpdateGCAvatarEvent, HTTPUploadProgressEvent)
from gajim.common.connection import Connection from gajim.common.connection import Connection
from gajim.common.file_props import FilesProp from gajim.common.file_props import FilesProp
from gajim.common import pep from gajim.common import pep
@ -1139,6 +1141,45 @@ class Interface:
app.config.get_per('accounts', account, 'publish_location')): app.config.get_per('accounts', account, 'publish_location')):
location_listener.enable() location_listener.enable()
@staticmethod
def show_httpupload_progress(file):
ProgressWindow(file)
def send_httpupload(self, chat_control):
FileChooserDialog(
on_response_ok=lambda widget: self.on_file_dialog_ok(widget,
chat_control),
title_text=_('Choose file to send'),
action=Gtk.FileChooserAction.OPEN,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OPEN, Gtk.ResponseType.OK),
default_response=Gtk.ResponseType.OK,
transient_for=chat_control.parent_win.window)
@staticmethod
def on_file_dialog_ok(widget, chat_control):
path = widget.get_filename()
widget.destroy()
con = app.connections[chat_control.account]
groupchat = chat_control.type_id == message_control.TYPE_GC
con.check_file_before_transfer(path,
chat_control.encryption,
chat_control.contact,
chat_control.session,
groupchat)
def encrypt_file(self, file, callback):
app.nec.push_incoming_event(HTTPUploadProgressEvent(
None, status='encrypt', file=file))
encryption = file.encryption
plugin = app.plugin_manager.encryption_plugins[encryption]
if hasattr(plugin, 'encrypt_file'):
plugin.encrypt_file(file, None, callback)
else:
app.nec.push_incoming_event(HTTPUploadProgressEvent(
None, status='close', file=file))
self.raise_dialog('httpupload-encryption-not-available')
@staticmethod @staticmethod
def handle_event_metacontacts(obj): def handle_event_metacontacts(obj):
app.contacts.define_metacontacts(obj.conn.name, obj.meta_list) app.contacts.define_metacontacts(obj.conn.name, obj.meta_list)

View File

@ -608,7 +608,10 @@ Build dynamic Application Menus
def get_singlechat_menu(control_id): def get_singlechat_menu(control_id):
singlechat_menu = [ singlechat_menu = [
('win.send-file-', _('Send File...')), (_('Send File...'), [
('win.send-file-httpupload-', 'HTTP Upload'),
('win.send-file-jingle-', 'Jingle'),
]),
('win.invite-contacts-', _('Invite Contacts')), ('win.invite-contacts-', _('Invite Contacts')),
('win.add-to-roster-', _('Add to Roster')), ('win.add-to-roster-', _('Add to Roster')),
('win.toggle-audio-', _('Audio Session')), ('win.toggle-audio-', _('Audio Session')),
@ -620,11 +623,17 @@ def get_singlechat_menu(control_id):
def build_menu(preset): def build_menu(preset):
menu = Gio.Menu() menu = Gio.Menu()
for item in preset: for item in preset:
action_name, label = item if isinstance(item[1], str):
if action_name == 'win.browse-history-': action_name, label = item
menu.append(label, action_name + control_id + '::none') if action_name == 'win.browse-history-':
menu.append(label, action_name + control_id + '::none')
else:
menu.append(label, action_name + control_id)
else: else:
menu.append(label, action_name + control_id) label, sub_menu = item
# This is a submenu
submenu = build_menu(sub_menu)
menu.append_submenu(label, submenu)
return menu return menu
return build_menu(singlechat_menu) return build_menu(singlechat_menu)