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:
parent
c23af9c9e7
commit
3a3be94aa8
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, '' ],
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
@ -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"
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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),
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)) + "%")
|
||||||
|
|
|
@ -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!
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue