...
 
Commits (61)
......@@ -14,7 +14,7 @@ run-tests:
- . ./civenv/bin/activate
- pip3 install mypy
- pip3 install git+https://dev.gajim.org/gajim/python-nbxmpp.git@master
- pip3 install pylint==2.1.1
- pip3 install pylint==2.3.1
- mypy gajim
- scripts/dev/pylint-ci.sh --jobs=2 gajim
- python3 setup.py test -s test.no_gui
......
......@@ -16,8 +16,7 @@
### Optional Runtime Requirements
- python3-pil (pillow) for support of webp avatars
- python3-gnupg to enable GPG encryption
- For zeroconf you need python3-dbus on Linux or [pybonjour](https://dev.gajim.org/lovetox/pybonjour-python3) on Windows/macOS
- gir1.2-avahi-0.6 for zeroconf on Linux or [pybonjour](https://dev.gajim.org/lovetox/pybonjour-python3) on Windows/macOS
- gir1.2-gspell-1 and hunspell-LANG where lang is your locale eg. en, fr etc
- gir1.2-secret-1 for GNOME Keyring or KDE support as password storage
- D-Bus running to have gajim-remote working
......@@ -36,7 +35,7 @@
#### Packages
- [Arch](https://aur.archlinux.org/packages/gajim-git/)
- [Debian](https://packages.debian.org/source/experimental/gajim) (tested with Debian ``testing`` and ``unstable``)
- [Debian](https://packages.debian.org/stretch-backports/gajim)
#### Snapshots
......
......@@ -179,11 +179,6 @@ def on_merge_accounts(action, param):
app.interface.roster.setup_and_draw_roster()
def on_use_pgp_agent(action, param):
action.set_state(param)
app.config.set('use_gpg_agent', param.get_boolean())
def on_add_account(action, param):
if 'account_creation_wizard' in app.interface.instances:
app.interface.instances['account_creation_wizard'].window.present()
......
......@@ -388,11 +388,6 @@ class GajimApplication(Gtk.Application):
act.connect('change-state', app_actions.on_merge_accounts)
self.add_action(act)
act = Gio.SimpleAction.new_stateful(
'agent', None,
GLib.Variant.new_boolean(app.config.get('use_gpg_agent')))
self.add_action(act)
# General Actions
general_actions = [
......
......@@ -399,12 +399,8 @@ class ChatControl(ChatControlBase):
'Show a list of formattings'))
else:
self._formattings_button.set_sensitive(False)
if self.contact.supports(NS_XHTML_IM):
self._formattings_button.set_tooltip_text(_('Formatting is not '
'available so long as GPG is active'))
else:
self._formattings_button.set_tooltip_text(_('This contact does '
'not support HTML'))
self._formattings_button.set_tooltip_text(
_('This contact does not support HTML'))
# Jingle detection
if self.contact.supports(NS_JINGLE_ICE_UDP) and \
......@@ -628,7 +624,8 @@ class ChatControl(ChatControlBase):
else:
sha = self.gc_contact.avatar_sha
name = self.gc_contact.get_shown_name()
gui_menu_builder.show_save_as_menu(sha, name)
if sha is not None:
gui_menu_builder.show_save_as_menu(sha, name)
return True
def on_location_eventbox_button_release_event(self, widget, event):
......@@ -824,13 +821,14 @@ class ChatControl(ChatControlBase):
if authenticated:
authenticated_string = _('and authenticated')
self.lock_image.set_from_icon_name(
'security-high', Gtk.IconSize.MENU)
'security-high-symbolic', Gtk.IconSize.MENU)
else:
authenticated_string = _('and NOT authenticated')
self.lock_image.set_from_icon_name(
'security-low', Gtk.IconSize.MENU)
'security-low-symbolic', Gtk.IconSize.MENU)
tooltip = _('%(type)s encryption is active %(authenticated)s.') % {'type': enc_type, 'authenticated': authenticated_string}
tooltip = _('%(type)s encryption is active %(authenticated)s') % \
{'type': enc_type, 'authenticated': authenticated_string}
self.authentication_button.set_tooltip_text(tooltip)
self.widget_set_visible(self.authentication_button, not visible)
......@@ -920,7 +918,7 @@ class ChatControl(ChatControlBase):
correct_id=obj.correct_id,
additional_data=obj.additional_data)
def send_message(self, message, keyID='', xhtml=None,
def send_message(self, message, xhtml=None,
process_commands=True, attention=False):
"""
Send a message to contact
......@@ -937,12 +935,8 @@ class ChatControl(ChatControlBase):
if message in ('', None, '\n'):
return None
contact = self.contact
keyID = contact.keyID
ChatControlBase.send_message(self,
message,
keyID,
type_='chat',
xhtml=xhtml,
process_commands=process_commands,
......@@ -1424,9 +1418,6 @@ class ChatControl(ChatControlBase):
self.update_actions()
def update_status_display(self, name, uf_show, status):
"""
Print the contact's status and update the status/GPG image
"""
self.update_ui()
self.parent_win.redraw_tab(self)
......
......@@ -75,7 +75,7 @@ from gajim.command_system.implementation import execute
# pylint: enable=unused-import
if app.is_installed('GSPELL'):
from gi.repository import Gspell
from gi.repository import Gspell # pylint: disable=ungrouped-imports
################################################################################
......@@ -131,7 +131,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
"""
Derived types SHOULD implement this
"""
pass
def update_ui(self):
"""
......@@ -149,13 +148,11 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
"""
Derived types MAY implement this
"""
pass
def _update_toolbar(self):
"""
Derived types MAY implement this
"""
pass
def _nec_our_status(self, obj):
if self.account != obj.conn.name:
......@@ -326,7 +323,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
self.set_emoticon_popover()
# Attach speller
self.spell_checker = None
self.set_speller()
self.conv_textview.tv.show()
......@@ -457,15 +453,15 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
if gspell_lang is None:
return
self.spell_checker = Gspell.Checker.new(gspell_lang)
spell_checker = Gspell.Checker.new(gspell_lang)
spell_buffer = Gspell.TextBuffer.get_from_gtk_text_buffer(
self.msg_textview.get_buffer())
spell_buffer.set_spell_checker(self.spell_checker)
spell_buffer.set_spell_checker(spell_checker)
spell_view = Gspell.TextView.get_from_gtk_text_view(self.msg_textview)
spell_view.set_inline_spell_checking(False)
spell_view.set_enable_language_menu(True)
self.spell_checker.connect('notify::language', self.on_language_changed)
spell_checker.connect('notify::language', self.on_language_changed)
def get_speller_language(self):
per_type = 'contacts'
......@@ -500,7 +496,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
item = Gtk.SeparatorMenuItem.new()
menu.prepend(item)
menu2 = self.prepare_context_menu()
menu2 = self.prepare_context_menu() # pylint: disable=assignment-from-none
i = 0
for item in menu2:
menu2.remove(item)
......@@ -544,14 +540,27 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
id_ = item.connect('activate', self.msg_textview.clear)
self.handlers[id_] = item
paste_item = Gtk.MenuItem.new_with_label(_('Paste as quote'))
id_ = paste_item.connect('activate', self.paste_clipboard_as_quote)
self.handlers[id_] = paste_item
menu.append(paste_item)
menu.show_all()
def on_quote(self, widget, text):
def insert_as_quote(self, text: str) -> None:
self.msg_textview.remove_placeholder()
text = '>' + text.replace('\n', '\n>') + '\n'
text = '> ' + text.replace('\n', '\n> ') + '\n'
message_buffer = self.msg_textview.get_buffer()
message_buffer.insert_at_cursor(text)
def paste_clipboard_as_quote(self, _item: Gtk.MenuItem) -> None:
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
text = clipboard.wait_for_text()
self.insert_as_quote(text)
def on_quote(self, widget, text):
self.insert_as_quote(text)
# moved from ChatControl
def _on_banner_eventbox_button_press_event(self, widget, event):
"""
......@@ -698,7 +707,6 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
"""
Derived types SHOULD implement this
"""
pass
def _on_drag_leave(self, *args):
# FIXME: DND on non editable TextView, find a better way
......@@ -765,7 +773,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
label = labels[lname]
return label
def send_message(self, message, keyID='', type_='chat',
def send_message(self, message, type_='chat',
resource=None, xhtml=None, process_commands=True, attention=False):
"""
Send the given message to the active tab. Doesn't return None if error
......@@ -789,7 +797,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
app.nec.push_outgoing_event(MessageOutgoingEvent(None,
account=self.account, jid=self.contact.jid, message=message,
keyID=keyID, type_=type_, chatstate=chatstate,
type_=type_, chatstate=chatstate,
resource=resource, user_nick=self.user_nick, xhtml=xhtml,
label=label, control=self, attention=attention, correct_id=correct_id,
automatic_message=False, encryption=self.encryption))
......@@ -840,7 +848,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
history = self.received_history
pos = self.received_history_pos
size = len(history)
scroll = False if pos == size else True # are we scrolling?
scroll = pos != size
# we don't want size of the buffer to grow indefinitely
max_size = app.config.get('key_up_lines')
for _i in range(size - max_size + 1):
......
......@@ -38,16 +38,13 @@ class DefinitionError(BaseError):
"""
Used to indicate errors occurred on command definition.
"""
pass
class CommandError(BaseError):
"""
Used to indicate errors occurred during command execution.
"""
pass
class NoCommandError(BaseError):
"""
Used to indicate an inability to find the specified command.
"""
pass
......@@ -114,14 +114,12 @@ class CommandProcessor:
If returns True then command execution will be interrupted and
command will not be executed.
"""
pass
def command_postprocessor(self, cmd, name, arguments, args, kwargs, value):
"""
Redefine this method in the subclass to execute custom code
after command gets executed.
"""
pass
def looks_like_command(self, text, body, name, arguments):
"""
......@@ -132,7 +130,6 @@ class CommandProcessor:
be interrupted and that value will be used to return from
process_as_command.
"""
pass
def get_command(self, name):
cmd = get_command(self.COMMAND_HOST, name)
......
......@@ -35,7 +35,6 @@ import sys
import logging
import uuid
from pathlib import Path
from distutils.version import LooseVersion as V
from collections import namedtuple
import nbxmpp
......@@ -189,10 +188,8 @@ gajim_optional_features = {} # type: Dict[str, List[str]]
caps_hash = {} # type: Dict[str, List[str]]
_dependencies = {
'PYTHON-DBUS': False,
'AVAHI': False,
'PYBONJOUR': False,
'PYGPG': False,
'GPG_BINARY': False,
'FARSTREAM': False,
'GEOCLUE': False,
'UPNP': False,
......@@ -203,12 +200,9 @@ _dependencies = {
def is_installed(dependency):
if dependency == 'GPG':
# Alias for checking python-gnupg and the GPG binary
return _dependencies['PYGPG'] and _dependencies['GPG_BINARY']
if dependency == 'ZEROCONF':
# Alias for checking zeroconf libs
return _dependencies['PYTHON-DBUS'] or _dependencies['PYBONJOUR']
return _dependencies['AVAHI'] or _dependencies['PYBONJOUR']
return _dependencies[dependency]
def is_flatpak():
......@@ -234,51 +228,18 @@ def detect_dependencies():
# ZEROCONF
try:
import pybonjour # pylint: disable=unused-variable
import pybonjour # pylint: disable=unused-import
_dependencies['PYBONJOUR'] = True
except Exception:
pass
try:
import dbus # pylint: disable=unused-variable
_dependencies['PYTHON-DBUS'] = True
gi.require_version('Avahi', '0.6')
from gi.repository import Avahi # pylint: disable=unused-import
_dependencies['AVAHI'] = True
except Exception:
pass
# python-gnupg
try:
import gnupg
# We need https://pypi.python.org/pypi/python-gnupg
# but https://pypi.python.org/pypi/gnupg shares the same package name.
# It cannot be used as a drop-in replacement.
# We test with a version check if python-gnupg is installed as it is
# on a much lower version number than gnupg
# Also we need at least python-gnupg 0.3.8
v_gnupg = gnupg.__version__
if V(v_gnupg) < V('0.3.8') or V(v_gnupg) > V('1.0.0'):
log('gajim').info('Gajim needs python-gnupg >= 0.3.8')
raise ImportError
_dependencies['PYGPG'] = True
except ImportError:
pass
# GPG BINARY
import subprocess
def test_gpg(binary='gpg'):
if os.name == 'nt':
gpg_cmd = binary + ' -h >nul 2>&1'
else:
gpg_cmd = binary + ' -h >/dev/null 2>&1'
if subprocess.call(gpg_cmd, shell=True):
return False
return True
if test_gpg(binary='gpg2'):
_dependencies['GPG_BINARY'] = 'gpg2'
elif test_gpg(binary='gpg'):
_dependencies['GPG_BINARY'] = 'gpg'
# FARSTREAM
try:
if os.name == 'nt':
......@@ -304,7 +265,7 @@ def detect_dependencies():
# GEOCLUE
try:
gi.require_version('Geoclue', '2.0')
from gi.repository import Geoclue # pylint: disable=unused-variable
from gi.repository import Geoclue # pylint: disable=unused-import
_dependencies['GEOCLUE'] = True
except (ImportError, ValueError):
pass
......@@ -321,7 +282,7 @@ def detect_dependencies():
# PYCURL
try:
import pycurl # pylint: disable=unused-variable
import pycurl # pylint: disable=unused-import
_dependencies['PYCURL'] = True
except ImportError:
pass
......@@ -353,9 +314,6 @@ def detect_dependencies():
log('gajim').info('Used language: %s', LANG)
def get_gpg_binary():
return _dependencies['GPG_BINARY']
def get_an_id():
return str(uuid.uuid4())
......
......@@ -29,10 +29,8 @@ through ClientCaps objects which are hold by contact instances.
import base64
import hashlib
from collections import namedtuple
import logging
log = logging.getLogger('gajim.c.caps_cache')
from collections import namedtuple
import nbxmpp
from nbxmpp.const import Affiliation
......@@ -44,6 +42,8 @@ FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_ESESSION,
NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO,
NS_JINGLE_FILE_TRANSFER_5]
log = logging.getLogger('gajim.c.caps_cache')
# Query entry status codes
NEW = 0
QUERIED = 1
......
......@@ -167,7 +167,6 @@ class Config:
'time_stamp': [opt_str, '[%X] ', _('This option let you customize timestamp that is printed in conversation. For example "[%H:%M] " will show "[hour:minute] ". See python doc on strftime for full documentation: http://docs.python.org/lib/module-time.html')],
'before_nickname': [opt_str, '', _('Characters that are printed before the nickname in conversations')],
'after_nickname': [opt_str, ':', _('Characters that are printed after the nickname in conversations')],
'use_gpg_agent': [opt_bool, False],
'change_roster_title': [opt_bool, True, _('Add * and [n] in roster title?')],
'restore_lines': [opt_int, 10, _('How many history messages should be restored when a chat tab/window is reopened?')],
'restore_timeout': [opt_int, -1, _('How far back in time (minutes) history is restored. -1 means no limit.')],
......@@ -285,7 +284,6 @@ class Config:
'positive_184_ack': [opt_bool, False, _('If enabled, Gajim will show an icon to show that sent message has been received by your contact')],
'show_avatar_in_tabs': [opt_bool, False, _('Show a mini avatar in chat window tabs and in window icon')],
'use_keyring': [opt_bool, True, _('If true, Gajim will use the Systems Keyring to store account passwords.')],
'pgp_encoding': [opt_str, '', _('Sets the encoding used by python-gnupg'), True],
'remote_commands': [opt_bool, False, _('If true, Gajim will execute XEP-0146 Commands.')],
'dark_theme': [opt_int, 2, _('2: System, 1: Enabled, 0: Disabled')],
'threshold_options': [opt_str, '1, 2, 4, 10, 0', _('Options in days which can be chosen in the sync threshold menu'), True],
......@@ -325,7 +323,6 @@ class Config:
'active': [opt_bool, True, _('If False, this account will be disabled and will not appear in roster window.'), True],
'proxy': [opt_str, '', '', True],
'keyid': [opt_str, '', '', True],
'gpg_sign_presence': [opt_bool, True, _('If disabled, don\'t sign presences with GPG key, even if GPG is configured.')],
'keyname': [opt_str, '', '', True],
'allow_plaintext_connection': [opt_bool, False, _('Allow plaintext connections')],
'tls_version': [opt_str, '1.2', ''],
......
This diff is collapsed.
......@@ -153,15 +153,6 @@ class NewAccountConnectedEvent(nec.NetworkIncomingEvent):
class NewAccountNotConnectedEvent(nec.NetworkIncomingEvent):
name = 'new-account-not-connected'
class BadGPGPassphraseEvent(nec.NetworkIncomingEvent):
name = 'bad-gpg-passphrase'
def generate(self):
self.account = self.conn.name
self.use_gpg_agent = app.config.get('use_gpg_agent')
self.keyID = app.config.get_per('accounts', self.conn.name, 'keyid')
return True
class ConnectionLostEvent(nec.NetworkIncomingEvent):
name = 'connection-lost'
......@@ -170,13 +161,6 @@ class ConnectionLostEvent(nec.NetworkIncomingEvent):
show='offline'))
return True
class GPGPasswordRequiredEvent(nec.NetworkIncomingEvent):
name = 'gpg-password-required'
def generate(self):
self.keyid = app.config.get_per('accounts', self.conn.name, 'keyid')
return True
class FileRequestReceivedEvent(nec.NetworkIncomingEvent):
name = 'file-request-received'
......@@ -610,7 +594,6 @@ class MessageOutgoingEvent(nec.NetworkOutgoingEvent):
def init(self):
self.additional_data = AdditionalDataDict()
self.message = None
self.keyID = None
self.type_ = 'chat'
self.kind = None
self.timestamp = None
......
......@@ -23,7 +23,6 @@ class OptionKind(IntEnum):
PRIORITY = 9
FILECHOOSER = 10
CHANGEPASSWORD = 11
GPG = 12
@unique
......
......@@ -137,7 +137,7 @@ class Contact(CommonContact):
Information concerning a contact
"""
def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0, keyID='', client_caps=None,
sub='', ask='', resource='', priority=0, client_caps=None,
chatstate=None, idle_time=None, avatar_sha=None, groupchat=False,
is_pm_contact=False):
if not isinstance(jid, str):
......@@ -159,7 +159,6 @@ class Contact(CommonContact):
self.ask = ask
self.priority = priority
self.keyID = keyID
self.idle_time = idle_time
self.pep = {}
......@@ -306,7 +305,7 @@ class LegacyContactsAPI:
self._metacontact_manager.remove_account(account)
def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0, keyID='',
status='', sub='', ask='', resource='', priority=0,
client_caps=None, chatstate=None, idle_time=None,
avatar_sha=None, groupchat=False):
if groups is None:
......@@ -315,36 +314,36 @@ class LegacyContactsAPI:
account = self._accounts.get(account, account)
return Contact(jid=jid, account=account, name=name, groups=groups,
show=show, status=status, sub=sub, ask=ask, resource=resource,
priority=priority, keyID=keyID, client_caps=client_caps,
priority=priority, client_caps=client_caps,
chatstate=chatstate, idle_time=idle_time, avatar_sha=avatar_sha,
groupchat=groupchat)
def create_self_contact(self, jid, account, resource, show, status, priority,
name='', keyID=''):
name=''):
conn = common.app.connections[account]
nick = name or common.app.nicks[account]
account = self._accounts.get(account, account) # Use Account object if available
self_contact = self.create_contact(jid=jid, account=account,
name=nick, groups=['self_contact'], show=show, status=status,
sub='both', ask='none', priority=priority, keyID=keyID,
sub='both', ask='none', priority=priority,
resource=resource)
self_contact.pep = conn.pep
return self_contact
def create_not_in_roster_contact(self, jid, account, resource='', name='',
keyID='', groupchat=False):
groupchat=False):
# Use Account object if available
account = self._accounts.get(account, account)
return self.create_contact(jid=jid, account=account, resource=resource,
name=name, groups=[_('Not in Roster')], show='not in roster',
status='', sub='none', keyID=keyID, groupchat=groupchat)
status='', sub='none', groupchat=groupchat)
def copy_contact(self, contact):
return self.create_contact(contact.jid, contact.account,
name=contact.name, groups=contact.groups, show=contact.show,
status=contact.status, sub=contact.sub, ask=contact.ask,
resource=contact.resource, priority=contact.priority,
keyID=contact.keyID, client_caps=contact.client_caps,
client_caps=contact.client_caps,
chatstate=contact.chatstate_enum,
idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
......
......@@ -23,7 +23,7 @@ from nbxmpp.structs import LocationData
from gajim.common import app
if app.is_installed('GEOCLUE'):
from gi.repository import Geoclue
from gi.repository import Geoclue # pylint: disable=ungrouped-imports
log = logging.getLogger('gajim.c.dbus.location')
......
......@@ -261,8 +261,7 @@ class Events:
self._events[account][jid].remove(event)
self.fire_event_removed([event])
return
else:
return True
return True
if types:
new_list = [] # list of events to keep
removed_list = [] # list of removed events
......
......@@ -97,13 +97,11 @@ class NegotiationError(Exception):
"""
A session negotiation failed
"""
pass
class Cancelled(Exception):
"""
The user cancelled an operation
"""
pass
class LatexError(Exception):
"""
......
......@@ -90,8 +90,6 @@ class FuzzyClock:
'0': self.HOUR_NAMES[now.tm_hour % 12],
'1': self.HOUR_NAMES[(now.tm_hour + 1) % 12]}
elif fuzzyness == 3:
if fuzzyness == 3:
return self.FUZZY_DAYTIME[int(round(now.tm_hour / 3.0))]
else:
return self.FUZZY_WEEK[now.tm_wday]
return self.FUZZY_WEEK[now.tm_wday]
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jean-Marie Traissard <jim AT lapin.org>
# Jonathan Schleifer <js-gajim AT webkeks.org>
#
# 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
from gajim.common import app
if app.is_installed('GPG'):
import gnupg
class GnuPG(gnupg.GPG):
def __init__(self):
use_agent = app.config.get('use_gpg_agent')
gnupg.GPG.__init__(self, gpgbinary=app.get_gpg_binary(), use_agent=use_agent)
encoding = app.config.get('pgp_encoding')
if encoding:
self.encoding = encoding
self.decode_errors = 'replace'
self.passphrase = None
self.always_trust = [] # list of keyID to always trust
def encrypt(self, str_, recipients, always_trust=False):
trust = always_trust
if not trust:
# check if we trust all keys
trust = True
for key in recipients:
if key not in self.always_trust:
trust = False
if not trust:
# check that we'll be able to encrypt
result = super(GnuPG, self).list_keys(keys=recipients)
for key in result:
if key['trust'] not in ('f', 'u'):
if key['keyid'][-8:] not in self.always_trust:
return '', 'NOT_TRUSTED ' + key['keyid'][-8:]
trust = True
result = super(GnuPG, self).encrypt(str_.encode('utf8'), recipients,
always_trust=trust, passphrase=self.passphrase)
if result.ok:
error = ''
else:
error = result.status
return self._stripHeaderFooter(str(result)), error
def decrypt(self, str_, keyID):
data = self._addHeaderFooter(str_, 'MESSAGE')
result = super(GnuPG, self).decrypt(data.encode('utf8'),
passphrase=self.passphrase)
return result.data.decode('utf8')
def sign(self, str_, keyID):
result = super(GnuPG, self).sign(str_.encode('utf8'), keyid=keyID, detach=True,
passphrase=self.passphrase)
if result.fingerprint:
return self._stripHeaderFooter(str(result))
if hasattr(result, 'status') and result.status == 'key expired':
return 'KEYEXPIRED'
return 'BAD_PASSPHRASE'
def verify(self, str_, sign):
if str_ is None:
return ''
# Hash algorithm is not transfered in the signed presence stanza so try
# all algorithms. Text name for hash algorithms from RFC 4880 - section 9.4
hash_algorithms = ['SHA512', 'SHA384', 'SHA256', 'SHA224', 'SHA1', 'RIPEMD160']
for algo in hash_algorithms:
data = os.linesep.join(
['-----BEGIN PGP SIGNED MESSAGE-----',
'Hash: ' + algo,
'',
str_,
self._addHeaderFooter(sign, 'SIGNATURE')]
)
result = super(GnuPG, self).verify(data.encode('utf8'))
if result.valid:
return result.key_id
return ''
def get_key(self, keyID):
return super(GnuPG, self).list_keys(keys=[keyID])
def get_keys(self, secret=False):
keys = {}
result = super(GnuPG, self).list_keys(secret=secret)
for key in result:
# Take first not empty uid
keys[key['keyid'][8:]] = [uid for uid in key['uids'] if uid][0]
return keys
def get_secret_keys(self):
return self.get_keys(True)
def _stripHeaderFooter(self, data):
"""
Remove header and footer from data
"""
if not data:
return ''
lines = data.splitlines()
while lines[0] != '':
lines.remove(lines[0])
while lines[0] == '':
lines.remove(lines[0])
i = 0
for line in lines:
if line:
if line[0] == '-':
break
i = i+1
line = '\n'.join(lines[0:i])
return line
def _addHeaderFooter(self, data, type_):
"""
Add header and footer from data
"""
out = "-----BEGIN PGP %s-----" % type_ + os.linesep
out = out + "Version: PGP" + os.linesep
out = out + os.linesep
out = out + data + os.linesep
out = out + "-----END PGP %s-----" % type_ + os.linesep
return out
......@@ -25,6 +25,9 @@
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
import sys
import re
import os
......@@ -47,9 +50,11 @@ from datetime import datetime, timedelta
from distutils.version import LooseVersion as V
from encodings.punycode import punycode_encode
from string import Template
from functools import wraps
import nbxmpp
from nbxmpp.stringprepare import nameprep
from gi.repository import GLib
import precis_i18n.codec # pylint: disable=unused-import
from gajim.common import app
......@@ -1103,54 +1108,6 @@ def get_current_show(account):
status = app.connections[account].connected
return app.SHOW_LIST[status]
def prepare_and_validate_gpg_keyID(account, jid, keyID):
"""
Return an eight char long keyID that can be used with for GPG encryption
with this contact
If the given keyID is None, return UNKNOWN; if the key does not match the
assigned key XXXXXXXXMISMATCH is returned. If the key is trusted and not yet
assigned, assign it.
"""
if app.connections[account].USE_GPG:
if keyID and len(keyID) == 16:
keyID = keyID[8:]
attached_keys = app.config.get_per('accounts', account,
'attached_gpg_keys').split()
if jid in attached_keys and keyID:
attachedkeyID = attached_keys[attached_keys.index(jid) + 1]
if attachedkeyID != keyID:
# Get signing subkeys for the attached key
subkeys = []
for key in app.connections[account].gpg.list_keys():
if key['keyid'][8:] == attachedkeyID:
subkeys = [subkey[0][8:] for subkey in key['subkeys'] \
if subkey[1] == 's']
break
if keyID not in subkeys:
# Mismatch! Another gpg key was expected
keyID += 'MISMATCH'
elif jid in attached_keys:
# An unsigned presence, just use the assigned key
keyID = attached_keys[attached_keys.index(jid) + 1]
elif keyID:
full_key = app.connections[account].ask_gpg_keys(keyID=keyID)
# Assign the corresponding key, if we have it in our keyring
if full_key:
for u in app.contacts.get_contacts(account, jid):
u.keyID = keyID
keys_str = app.config.get_per('accounts', account,
'attached_gpg_keys')
keys_str += jid + ' ' + keyID + ' '
app.config.set_per('accounts', account, 'attached_gpg_keys',
keys_str)
elif keyID is None:
keyID = 'UNKNOWN'
return keyID
def update_optional_features(account=None):
if account is not None:
accounts = [account]
......@@ -1520,3 +1477,30 @@ def save_roster_position(window):
log.debug('Save roster position: %s %s', x_pos, y_pos)
app.config.set('roster_x-position', x_pos)
app.config.set('roster_y-position', y_pos)
class Singleton(type):
_instances = {} # type: Dict[Any, Any]
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(
*args, **kwargs)
return cls._instances[cls]
def delay_execution(milliseconds):
# Delay the first call for `milliseconds`
# ignore all other calls while the delay is active
def delay_execution_decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
def timeout_wrapper():
func(*args, **kwargs)
delattr(func_wrapper, 'source_id')
if hasattr(func_wrapper, 'source_id'):
return
func_wrapper.source_id = GLib.timeout_add(
milliseconds, timeout_wrapper)
return func_wrapper
return delay_execution_decorator
......@@ -170,7 +170,9 @@ try:
if os.name == 'nt':
# Set the env var on Windows because gettext.find() uses it to
# find the translation
os.environ['LANG'] = LANG
# Use LANGUAGE instead of LANG, LANG sets LC_ALL and thus
# doesn't retain other region settings like LC_TIME
os.environ['LANGUAGE'] = LANG
except Exception as error:
print('Failed to determine default language', file=sys.stderr)
import traceback
......
......@@ -307,8 +307,7 @@ class JingleFileTransfer(JingleContent):
self.__state_changed(State.CAND_SENT_AND_RECEIVED, args)
self.__state_changed(State.TRANSFERING)
raise nbxmpp.NodeProcessed
else:
self.__state_changed(State.CAND_RECEIVED, args)
self.__state_changed(State.CAND_RECEIVED, args)
def __on_iq_result(self, stanza, content, error, action):
log.info("__on_iq_result")
......
......@@ -12,12 +12,14 @@
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import logging
import nbxmpp
from gajim.common import app
from gajim.common.jingle_transport import TransportType
from gajim.common.socks5 import Socks5ReceiverClient, Socks5SenderClient
from gajim.common.socks5 import Socks5ReceiverClient
from gajim.common.socks5 import Socks5SenderClient
import logging
log = logging.getLogger('gajim.c.jingle_ftstates')
......
......@@ -18,22 +18,22 @@ Handles Jingle RTP sessions (XEP 0167)
import logging
import socket
from collections import deque
import nbxmpp
import gi
from gi.repository import Farstream
gi.require_version('Gst', '1.0')
from gi.repository import Gst
from gi.repository import GLib
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.jingle_transport import JingleTransportICEUDP
from gajim.common.jingle_content import contents, JingleContent, JingleContentSetupException
from gajim.common.jingle_content import contents
from gajim.common.jingle_content import JingleContent
from gajim.common.jingle_content import JingleContentSetupException
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.jingle_session import FailedApplication
from collections import deque
log = logging.getLogger('gajim.c.jingle_rtp')
......
......@@ -593,9 +593,9 @@ class JingleSession:
text = 'Content %s (created by %s) does not exist' % (name, creator)
self.__send_error(stanza, 'bad-request', text=text, type_='_modify')
raise nbxmpp.NodeProcessed
else:
cn = self.contents[(creator, name)]
cn.on_stanza(stanza, content, error, action)
cn = self.contents[(creator, name)]
cn.on_stanza(stanza, content, error, action)
def __on_session_terminate(self, stanza, jingle, error, action):
self.connection.delete_jingle_session(self.sid)
......
......@@ -74,7 +74,6 @@ class JingleTransport:
"""
Build a candidate stanza for the given candidate
"""
pass
def make_transport(self, candidates=None):
"""
......
......@@ -296,6 +296,13 @@ class LeaveGroupchatsCommand(AdHocCommand):
class AdHocCommands(BaseModule):
_nbxmpp_extends = 'AdHoc'
_nbxmpp_methods = [
'request_command_list',
'execute_command',
]
def __init__(self, con):
BaseModule.__init__(self, con)
......@@ -431,75 +438,43 @@ class AdHocCommands(BaseModule):
self._sessions[(jid, sessionid, node)] = obj
self._log.info('Comand %s executed: %s', node, jid)
raise nbxmpp.NodeProcessed
else:
# the command is already running, check for it
magictuple = (jid, sessionid, node)
if magictuple not in self._sessions:
# we don't have this session... ha!
self._log.warning('Invalid session %s', magictuple)
raise nbxmpp.NodeProcessed
action = cmd.getAttr('action')
obj = self._sessions[magictuple]
try:
if action == 'cancel':
rc = obj.cancel(stanza)
elif action == 'prev':
rc = obj.prev(stanza)
elif action == 'next':
rc = obj.next(stanza)
elif action == 'execute' or action is None:
rc = obj.execute(stanza)
elif action == 'complete':
rc = obj.complete(stanza)
else:
# action is wrong. stop the session, send error
raise AttributeError
except AttributeError:
# the command probably doesn't handle invoked action...
# stop the session, return error
del self._sessions[magictuple]
self._log.warning('Wrong action %s %s', node, jid)
raise nbxmpp.NodeProcessed
# delete the session if rc is False
if not rc:
del self._sessions[magictuple]
# the command is already running, check for it
magictuple = (jid, sessionid, node)
if magictuple not in self._sessions:
# we don't have this session... ha!
self._log.warning('Invalid session %s', magictuple)
raise nbxmpp.NodeProcessed
def request_command_list(self, jid):
"""
Request the command list.
"""
self._log.info('Request Command List: %s', jid)
query = nbxmpp.Iq(typ='get', to=jid, queryNS=nbxmpp.NS_DISCO_ITEMS)
query.setQuerynode(nbxmpp.NS_COMMANDS)
self._con.connection.SendAndCallForResponse(
query, self._command_list_received)
def _command_list_received(self, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Error: %s', stanza.getError())
action = cmd.getAttr('action')
obj = self._sessions[magictuple]
app.nec.push_incoming_event(
AdHocCommandError(None, conn=self._con,
error=stanza.getError()))
return
try:
if action == 'cancel':
rc = obj.cancel(stanza)
elif action == 'prev':
rc = obj.prev(stanza)
elif action == 'next':
rc = obj.next(stanza)
elif action == 'execute' or action is None:
rc = obj.execute(stanza)
elif action == 'complete':
rc = obj.complete(stanza)
else:
# action is wrong. stop the session, send error
raise AttributeError
except AttributeError:
# the command probably doesn't handle invoked action...
# stop the session, return error
del self._sessions[magictuple]
self._log.warning('Wrong action %s %s', node, jid)
raise nbxmpp.NodeProcessed
items = stanza.getQueryPayload()
commandlist = []
if items:
commandlist = [
(t.getAttr('node'), t.getAttr('name')) for t in items
]
# delete the session if rc is False
if not rc:
del self._sessions[magictuple]
self._log.info('Received: %s', commandlist)
app.nec.push_incoming_event(
AdHocCommandListReceived(
None, conn=self._con, commandlist=commandlist))
raise nbxmpp.NodeProcessed
def send_command(self, jid, node, session_id,
form, action='execute'):
......@@ -563,10 +538,6 @@ class AdHocCommandError(NetworkIncomingEvent):
name = 'adhoc-command-error'
class AdHocCommandListReceived(NetworkIncomingEvent):
name = 'adhoc-command-list'
class AdHocCommandActionResponse(NetworkIncomingEvent):
name = 'adhoc-command-action-response'
......
......@@ -85,4 +85,4 @@ class BaseModule:
if self._stored_publish is None:
return
self._log.info('Send stored publish')
self._stored_publish()
self._stored_publish() # pylint: disable=not-callable
......@@ -329,7 +329,7 @@ class HTTPUpload(BaseModule):
else:
app.nec.push_outgoing_event(MessageOutgoingEvent(
None, account=self._account, jid=file.contact.jid,
message=message, keyID=file.key_id, type_='chat',
message=message, type_='chat',
automatic_message=False, session=file.session))
else:
......@@ -353,9 +353,6 @@ class File:
setattr(self, key, val)
self.encrypted = False
self.contact = contact
self.key_id = None
if hasattr(contact, 'keyID'):
self.key_id = contact.keyID
self.stream = None
self.path = path
self.put = None
......
......@@ -25,7 +25,6 @@ from gajim.common.i18n import _
from gajim.common.nec import NetworkEvent
from gajim.common.const import KindConstant
from gajim.common.const import ShowConstant
from gajim.common.helpers import prepare_and_validate_gpg_keyID
from gajim.common.modules.base import BaseModule
......@@ -94,12 +93,6 @@ class Presence(BaseModule):
self._log.warning(stanza)
return
key_id = ''
if properties.signed is not None and self._con.USE_GPG:
key_id = self._con.gpg.verify(properties.status, properties.signed)
key_id = prepare_and_validate_gpg_keyID(
self._account, properties.jid.getBare(), key_id)
show = properties.show.value
if properties.type.is_unavailable:
show = 'offline'
......@@ -107,7 +100,6 @@ class Presence(BaseModule):
event_attrs = {
'conn': self._con,
'stanza': stanza,
'keyID': key_id,
'prio': properties.priority,
'need_add_in_roster': False,
'popup': False,
......@@ -185,13 +177,6 @@ class Presence(BaseModule):
contact.show = event.show
contact.status = properties.status
contact.priority = properties.priority
attached_keys = app.config.get_per('accounts', self._account,
'attached_gpg_keys').split()
if jid in attached_keys:
contact.keyID = attached_keys[attached_keys.index(jid) + 1]
else:
# Do not override assigned key
contact.keyID = event.keyID
contact.idle_time = properties.idle_timestamp
event.contact = contact
......@@ -355,15 +340,16 @@ class Presence(BaseModule):
def get_presence(self, to=None, typ=None, priority=None,
show=None, status=None, nick=None, caps=True,
sign=None, idle_time=None):
idle_time=None):
if show not in ('chat', 'away', 'xa', 'dnd'):
# Gajim sometimes passes invalid show values here
# until this is fixed this is a workaround
show = None
presence = nbxmpp.Presence(to, typ, priority, show, status)
if nick is not None:
nick_tag = presence.setTag('nick', namespace=nbxmpp.NS_NICK)
nick_tag.setData(nick)
if sign:
presence.setTag(nbxmpp.NS_SIGNED + ' x').setData(sign)
if idle_time is not None:
idle_node = presence.setTag('idle', namespace=nbxmpp.NS_IDLE)
idle_node.setAttr('since', idle_time)
......@@ -385,6 +371,8 @@ class Presence(BaseModule):
if not app.account_is_connected(self._account):
return
presence = self.get_presence(*args, **kwargs)
app.plugin_manager.extension_point(
'send-presence', self._account, presence)
self._log.debug('Send presence:\n%s', presence)
self._con.connection.send(presence)
......
......@@ -87,7 +87,10 @@ class Register(BaseModule):
error = stanza.getErrorMsg()
self._log.info('Error: %s', error)
if error_cb() is not None:
error_cb()(error)
form = is_form = None
if stanza.getErrorType() == 'modify':
form, is_form = self._get_register_form(stanza)
error_cb()(error, form, is_form)
return
self._con.get_module('Presence').subscribe(agent, auto_auth=True)
......@@ -119,19 +122,25 @@ class Register(BaseModule):
error_cb()(error)
else:
self._log.info('Register form received')
parse_bob_data(stanza.getQuery())
form = stanza.getQuery().getTag('x', namespace=nbxmpp.NS_DATA)
is_form = form is not None
if not is_form:
form = {}
for field in stanza.getQueryPayload():
if not isinstance(field, nbxmpp.Node):
continue
form[field.getName()] = field.getData()
if success_cb() is not None:
form, is_form = self._get_register_form(stanza)
success_cb()(form, is_form)
@staticmethod
def _get_register_form(stanza):