9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz

This commit is contained in:
emdee 2022-10-19 18:09:31 +00:00
parent a5b3822651
commit 4c1b226bff
1045 changed files with 753037 additions and 18 deletions

19
gajim/__init__.py Normal file
View file

@ -0,0 +1,19 @@
import subprocess
import sys
from pathlib import Path
__version__ = "1.3.3"
IS_FLATPAK = Path('/app/share/run-as-flatpak').exists()
portable_path = Path(sys.executable).parent / 'is_portable'
IS_PORTABLE = portable_path.exists()
try:
p = subprocess.Popen('git rev-parse --short=12 HEAD', shell=True,
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
node = p.communicate()[0]
if node:
__version__ += '+' + node.decode('utf-8').strip()
except Exception:
pass

325
gajim/app_actions.py Normal file
View file

@ -0,0 +1,325 @@
# Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com>
#
# 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/>.
from gi.repository import Gtk
from gi.repository import Gdk
from gajim.common import app
from gajim.common import helpers
from gajim.common.app import interface
from gajim.common.exceptions import GajimGeneralException
from gajim import dialogs
from gajim.gui.dialogs import ShortcutsWindow
from gajim.gui.single_message import SingleMessageWindow
from gajim.gui.about import AboutDialog
from gajim.gui.history import HistoryWindow
from gajim.gui.discovery import ServiceDiscoveryWindow
from gajim.gui.util import open_window
from gajim.gui.util import get_app_window
# General Actions
def on_add_contact_jid(_action, param):
contact_jid = param.get_string()
open_window('AddNewContactWindow', account=None, contact_jid=contact_jid)
# Application Menu Actions
def on_preferences(_action, _param):
open_window('Preferences')
def on_plugins(_action, _param):
open_window('PluginsWindow')
def on_accounts(_action, param):
window = open_window('AccountsWindow')
account = param.get_string()
if account:
window.select_account(account)
def on_history_manager(_action, _param):
open_window('HistoryManager')
def on_bookmarks(_action, param):
account = param.get_string()
open_window('Bookmarks', account=account)
def on_quit(_action, _param):
interface.roster.on_quit_request()
def on_new_chat(_action, param):
window = open_window('StartChatDialog')
search_text = param.get_string()
if search_text:
window.set_search_text(search_text)
# Accounts Actions
def on_profile(_action, param):
account = param.get_string()
open_window('ProfileWindow', account=account)
def on_send_server_message(_action, param):
account = param.get_string()
server = app.settings.get_account_setting(account, 'hostname')
server += '/announce/online'
SingleMessageWindow(account, server, 'send')
def on_service_disco(_action, param):
account = param.get_string()
server_jid = app.settings.get_account_setting(account, 'hostname')
if server_jid in interface.instances[account]['disco']:
interface.instances[account]['disco'][server_jid].\
window.present()
else:
try:
# Object will add itself to the window dict
ServiceDiscoveryWindow(account, address_entry=True)
except GajimGeneralException:
pass
def on_create_gc(_action, param):
account = param.get_string()
open_window('CreateGroupchatWindow', account=account or None)
def on_add_contact(_action, param):
account, contact_jid = param.get_strv()
if not contact_jid:
contact_jid = None
open_window('AddNewContactWindow', account=account, contact_jid=contact_jid)
def on_single_message(_action, param):
account = param.get_string()
open_window('SingleMessageWindow', account=account, action='send')
def on_merge_accounts(action, param):
action.set_state(param)
value = param.get_boolean()
app.settings.set('mergeaccounts', value)
# Do not merge accounts if only one active
if len(app.connections) >= 2:
app.interface.roster.regroup = value
else:
app.interface.roster.regroup = False
app.interface.roster.setup_and_draw_roster()
def on_add_account(action, _param):
open_window('AccountWizard')
def on_import_contacts(_action, param):
account = param.get_string()
if 'import_contacts' in app.interface.instances:
app.interface.instances['import_contacts'].dialog.present()
else:
app.interface.instances['import_contacts'] = \
dialogs.SynchroniseSelectAccountDialog(account)
# Advanced Actions
def on_pep_config(_action, param):
account = param.get_string()
open_window('PEPConfig', account=account)
def on_mam_preferences(_action, param):
account = param.get_string()
open_window('MamPreferences', account=account)
def on_blocking_list(_action, param):
account = param.get_string()
open_window('BlockingList', account=account)
def on_history_sync(_action, param):
account = param.get_string()
open_window('HistorySyncAssistant',
account=account,
parent=interface.roster.window)
def on_server_info(_action, param):
account = param.get_string()
open_window('ServerInfo', account=account)
def on_xml_console(_action, _param):
open_window('XMLConsoleWindow')
def on_manage_proxies(_action, _param):
open_window('ManageProxies')
# Admin Actions
def on_set_motd(_action, param):
account = param.get_string()
server = app.settings.get_account_setting(account, 'hostname')
server += '/announce/motd'
SingleMessageWindow(account, server, 'send')
def on_update_motd(_action, param):
account = param.get_string()
server = app.settings.get_account_setting(account, 'hostname')
server += '/announce/motd/update'
SingleMessageWindow(account, server, 'send')
def on_delete_motd(_action, param):
account = param.get_string()
app.connections[account].get_module('Announce').delete_motd()
# Help Actions
def on_contents(_action, _param):
helpers.open_uri('https://dev.gajim.org/gajim/gajim/wikis')
def on_faq(_action, _param):
helpers.open_uri('https://dev.gajim.org/gajim/gajim/wikis/help/gajimfaq')
def on_keyboard_shortcuts(_action, _param):
ShortcutsWindow()
def on_features(_action, _param):
open_window('Features')
def on_about(_action, _param):
AboutDialog()
# View Actions
def on_file_transfers(_action, _param):
if interface.instances['file_transfers']. \
window.get_property('visible'):
interface.instances['file_transfers'].window.present()
else:
interface.instances['file_transfers'].window.show_all()
def on_history(action, param):
on_browse_history(action, param)
def on_open_event(_action, param):
dict_ = param.unpack()
app.interface.handle_event(
dict_['account'], dict_['jid'], dict_['type_'])
def on_remove_event(_action, param):
dict_ = param.unpack()
account, jid, type_ = dict_['account'], dict_['jid'], dict_['type_']
event = app.events.get_first_event(account, jid, type_)
app.events.remove_events(account, jid, event)
win = app.interface.msg_win_mgr.get_window(jid, account)
if win:
win.redraw_tab(win.get_control(jid, account))
win.show_title()
# Other Actions
def toggle_ipython(_action, _param):
"""
Show/hide the ipython window
"""
win = app.ipython_window
if win and win.window.is_visible():
win.present()
else:
app.interface.create_ipython_window()
def show_next_pending_event(_action, _param):
"""
Show the window(s) with next pending event in tabbed/group chats
"""
if app.events.get_nb_events():
account, jid, event = app.events.get_first_systray_event()
if not event:
return
app.interface.handle_event(account, jid, event.type_)
def open_mail(_action, param):
uri = param.get_string()
if not uri.startswith('mailto:'):
uri = 'mailto:%s' % uri
helpers.open_uri(uri)
def open_link(_action, param):
account, uri = param.get_strv()
helpers.open_uri(uri, account=account)
def copy_text(_action, param):
text = param.get_string()
clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clip.set_text(text, -1)
def start_chat(_action, param):
account, jid = param.get_strv()
app.interface.new_chat_from_jid(account, jid)
def on_browse_history(_action, param):
jid, account = None, None
if param is not None:
dict_ = param.unpack()
jid = dict_.get('jid')
account = dict_.get('account')
window = get_app_window(HistoryWindow)
if window is None:
HistoryWindow(jid, account)
else:
window.present()
if jid is not None and account is not None:
window.open_history(jid, account)
def on_groupchat_join(_action, param):
account, jid = param.get_strv()
open_window('GroupchatJoin', account=account, jid=jid)

562
gajim/application.py Normal file
View file

@ -0,0 +1,562 @@
# Copyright (C) 2003-2017 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2005 Alex Podaras <bigpod AT gmail.com>
# Norman Rasmussen <norman AT rasmussen.co.za>
# Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
# Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
# James Newton <redshodan AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Julien Pivotto <roidelapluie AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2016-2017 Emmanuel Gil Peyrot <linkmauve AT linkmauve.fr>
# Philipp Hörist <philipp AT hoerist.com>
#
# 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 sys
from urllib.parse import unquote
from nbxmpp.namespaces import Namespace
from nbxmpp import JID
from nbxmpp.protocol import InvalidJid
from gi.repository import Gio
from gi.repository import GLib
from gi.repository import Gtk
import gajim
from gajim.common import app
from gajim.common import ged
from gajim.common import configpaths
from gajim.common import logging_helpers
from gajim.common import exceptions
from gajim.common.i18n import _
from gajim.common.contacts import LegacyContactsAPI
from gajim.common.task_manager import TaskManager
from gajim.common.storage.cache import CacheStorage
from gajim.common.storage.archive import MessageArchiveStorage
from gajim.common.settings import Settings
from gajim.common.settings import LegacyConfig
class GajimApplication(Gtk.Application):
'''Main class handling activation and command line.'''
def __init__(self):
flags = (Gio.ApplicationFlags.HANDLES_COMMAND_LINE |
Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID)
Gtk.Application.__init__(self,
application_id='org.gajim.Gajim',
flags=flags)
# required to track screensaver state
self.props.register_session = True
self.add_main_option(
'version',
ord('V'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show the application\'s version'))
self.add_main_option(
'quiet',
ord('q'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show only critical errors'))
self.add_main_option(
'separate',
ord('s'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Separate profile files completely '
'(even history database and plugins)'))
self.add_main_option(
'verbose',
ord('v'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Print XML stanzas and other debug information'))
self.add_main_option(
'profile',
ord('p'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Use defined profile in configuration directory'),
'NAME')
self.add_main_option(
'config-path',
ord('c'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Set configuration directory'),
'PATH')
self.add_main_option(
'loglevel',
ord('l'),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Configure logging system'),
'LEVEL')
self.add_main_option(
'warnings',
ord('w'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show all warnings'))
self.add_main_option(
'ipython',
ord('i'),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Open IPython shell'))
self.add_main_option(
'gdebug',
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Sets an environment variable so '
'GLib debug messages are printed'))
self.add_main_option(
'show-next-pending-event',
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Pops up a window with the next pending event'))
self.add_main_option(
'start-chat', 0,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Start a new chat'))
self.add_main_option_entries(self._get_remaining_entry())
self.connect('handle-local-options', self._handle_local_options)
self.connect('command-line', self._command_line)
self.connect('startup', self._startup)
self.interface = None
GLib.set_prgname('org.gajim.Gajim')
if GLib.get_application_name() != 'Gajim':
GLib.set_application_name('Gajim')
@staticmethod
def _get_remaining_entry():
option = GLib.OptionEntry()
option.arg = GLib.OptionArg.STRING_ARRAY
option.arg_data = None
option.arg_description = ('[URI …]')
option.flags = GLib.OptionFlags.NONE
option.long_name = GLib.OPTION_REMAINING
option.short_name = 0
return [option]
def _startup(self, _application):
# Create and initialize Application Paths & Databases
app.print_version()
app.detect_dependencies()
configpaths.create_paths()
app.settings = Settings()
app.settings.init()
app.config = LegacyConfig() # type: ignore
app.storage.cache = CacheStorage()
app.storage.cache.init()
app.storage.archive = MessageArchiveStorage()
app.storage.archive.init()
try:
app.contacts = LegacyContactsAPI()
except exceptions.DatabaseMalformed as error:
dlg = Gtk.MessageDialog(
transient_for=None,
destroy_with_parent=True,
modal=True,
message_type=Gtk.MessageType.ERROR,
buttons=Gtk.ButtonsType.OK,
text=_('Database Error'))
dlg.format_secondary_text(str(error))
dlg.run()
dlg.destroy()
sys.exit()
from gajim.gui.util import load_user_iconsets
load_user_iconsets()
from gajim.common.cert_store import CertificateStore
app.cert_store = CertificateStore()
app.task_manager = TaskManager()
# Set Application Menu
app.app = self
from gajim.gui.util import get_builder
builder = get_builder('application_menu.ui')
menubar = builder.get_object("menubar")
self.set_menubar(menubar)
from gajim.gui_interface import Interface
self.interface = Interface()
self.interface.run(self)
self.add_actions()
self._set_shortcuts()
from gajim import gui_menu_builder
gui_menu_builder.build_accounts_menu()
self.update_app_actions_state()
app.ged.register_event_handler('feature-discovered',
ged.CORE,
self._on_feature_discovered)
def _open_uris(self, uris):
accounts = list(app.connections.keys())
if not accounts:
return
for uri in uris:
app.log('uri_handler').info('open %s', uri)
if not uri.startswith('xmpp:'):
continue
# remove xmpp:
uri = uri[5:]
try:
jid, cmd = uri.split('?')
except ValueError:
# No query argument
jid, cmd = uri, 'message'
try:
jid = JID.from_string(jid)
except InvalidJid as error:
app.log('uri_handler').warning('Invalid JID %s: %s', uri, error)
continue
if cmd == 'join' and jid.resource:
app.log('uri_handler').warning('Invalid MUC JID %s', uri)
continue
jid = str(jid)
if cmd == 'join':
if len(accounts) == 1:
self.activate_action(
'groupchat-join',
GLib.Variant('as', [accounts[0], jid]))
else:
self.activate_action('start-chat', GLib.Variant('s', jid))
elif cmd == 'roster':
self.activate_action('add-contact', GLib.Variant('s', jid))
elif cmd.startswith('message'):
attributes = cmd.split(';')
message = None
for key in attributes:
if not key.startswith('body'):
continue
try:
message = unquote(key.split('=')[1])
except Exception:
app.log('uri_handler').error('Invalid URI: %s', cmd)
if len(accounts) == 1:
app.interface.new_chat_from_jid(accounts[0], jid, message)
else:
self.activate_action('start-chat', GLib.Variant('s', jid))
def do_shutdown(self, *args):
Gtk.Application.do_shutdown(self)
# Shutdown GUI and save config
if hasattr(self.interface, 'roster') and self.interface.roster:
self.interface.roster.prepare_quit()
# Commit any outstanding SQL transactions
app.storage.cache.shutdown()
app.storage.archive.shutdown()
def _command_line(self, _application, command_line):
options = command_line.get_options_dict()
remote_commands = [
('ipython', None),
('show-next-pending-event', None),
('start-chat', GLib.Variant('s', '')),
]
remaining = options.lookup_value(GLib.OPTION_REMAINING,
GLib.VariantType.new('as'))
for cmd, parameter in remote_commands:
if options.contains(cmd):
self.activate_action(cmd, parameter)
return 0
if remaining is not None:
self._open_uris(remaining.unpack())
return 0
return 0
def _handle_local_options(self,
_application: Gtk.Application,
options: GLib.VariantDict) -> int:
# Parse all options that have to be executed before ::startup
if options.contains('version'):
print(gajim.__version__)
return 0
if options.contains('profile'):
# Incorporate profile name into application id
# to have a single app instance for each profile.
profile = options.lookup_value('profile').get_string()
app_id = '%s.%s' % (self.get_application_id(), profile)
self.set_application_id(app_id)
configpaths.set_profile(profile)
if options.contains('separate'):
configpaths.set_separation(True)
if options.contains('config-path'):
path = options.lookup_value('config-path').get_string()
configpaths.set_config_root(path)
configpaths.init()
if options.contains('gdebug'):
os.environ['G_MESSAGES_DEBUG'] = 'all'
logging_helpers.init()
if options.contains('quiet'):
logging_helpers.set_quiet()
if options.contains('verbose'):
logging_helpers.set_verbose()
if options.contains('loglevel'):
loglevel = options.lookup_value('loglevel').get_string()
logging_helpers.set_loglevels(loglevel)
if options.contains('warnings'):
self.show_warnings()
return -1
@staticmethod
def show_warnings():
import traceback
import warnings
def warn_with_traceback(message, category, filename, lineno,
_file=None, line=None):
traceback.print_stack(file=sys.stderr)
sys.stderr.write(warnings.formatwarning(message, category,
filename, lineno, line))
warnings.showwarning = warn_with_traceback
warnings.filterwarnings(action="always")
def add_actions(self):
''' Build Application Actions '''
from gajim import app_actions
# General Stateful Actions
act = Gio.SimpleAction.new_stateful(
'merge', None,
GLib.Variant.new_boolean(app.settings.get('mergeaccounts')))
act.connect('change-state', app_actions.on_merge_accounts)
self.add_action(act)
actions = [
('quit', app_actions.on_quit),
('add-account', app_actions.on_add_account),
('manage-proxies', app_actions.on_manage_proxies),
('history-manager', app_actions.on_history_manager),
('preferences', app_actions.on_preferences),
('plugins', app_actions.on_plugins),
('xml-console', app_actions.on_xml_console),
('file-transfer', app_actions.on_file_transfers),
('history', app_actions.on_history),
('shortcuts', app_actions.on_keyboard_shortcuts),
('features', app_actions.on_features),
('content', app_actions.on_contents),
('about', app_actions.on_about),
('faq', app_actions.on_faq),
('ipython', app_actions.toggle_ipython),
('show-next-pending-event', app_actions.show_next_pending_event),
('start-chat', 's', app_actions.on_new_chat),
('accounts', 's', app_actions.on_accounts),
('add-contact', 's', app_actions.on_add_contact_jid),
('copy-text', 's', app_actions.copy_text),
('open-link', 'as', app_actions.open_link),
('open-mail', 's', app_actions.open_mail),
('create-groupchat', 's', app_actions.on_create_gc),
('browse-history', 'a{sv}', app_actions.on_browse_history),
('groupchat-join', 'as', app_actions.on_groupchat_join),
]
for action in actions:
if len(action) == 2:
action_name, func = action
variant = None
else:
action_name, variant, func = action
variant = GLib.VariantType.new(variant)
act = Gio.SimpleAction.new(action_name, variant)
act.connect('activate', func)
self.add_action(act)
accounts_list = sorted(app.settings.get_accounts())
if not accounts_list:
return
if len(accounts_list) > 1:
for acc in accounts_list:
self.add_account_actions(acc)
else:
self.add_account_actions(accounts_list[0])
@staticmethod
def _get_account_actions(account):
from gajim import app_actions as a
if account == 'Local':
return []
return [
('-bookmarks', a.on_bookmarks, 'online', 's'),
('-start-single-chat', a.on_single_message, 'online', 's'),
('-start-chat', a.start_chat, 'online', 'as'),
('-add-contact', a.on_add_contact, 'online', 'as'),
('-services', a.on_service_disco, 'online', 's'),
('-profile', a.on_profile, 'online', 's'),
('-server-info', a.on_server_info, 'online', 's'),
('-archive', a.on_mam_preferences, 'feature', 's'),
('-pep-config', a.on_pep_config, 'online', 's'),
('-sync-history', a.on_history_sync, 'online', 's'),
('-blocking', a.on_blocking_list, 'feature', 's'),
('-send-server-message', a.on_send_server_message, 'online', 's'),
('-set-motd', a.on_set_motd, 'online', 's'),
('-update-motd', a.on_update_motd, 'online', 's'),
('-delete-motd', a.on_delete_motd, 'online', 's'),
('-open-event', a.on_open_event, 'always', 'a{sv}'),
('-remove-event', a.on_remove_event, 'always', 'a{sv}'),
('-import-contacts', a.on_import_contacts, 'online', 's'),
]
def add_account_actions(self, account):
for action in self._get_account_actions(account):
action_name, func, state, type_ = action
action_name = account + action_name
if self.lookup_action(action_name):
# We already added this action
continue
act = Gio.SimpleAction.new(
action_name, GLib.VariantType.new(type_))
act.connect("activate", func)
if state != 'always':
act.set_enabled(False)
self.add_action(act)
def remove_account_actions(self, account):
for action in self._get_account_actions(account):
action_name = account + action[0]
self.remove_action(action_name)
def set_account_actions_state(self, account, new_state=False):
for action in self._get_account_actions(account):
action_name, _, state, _ = action
if not new_state and state in ('online', 'feature'):
# We go offline
self.lookup_action(account + action_name).set_enabled(False)
elif new_state and state == 'online':
# We go online
self.lookup_action(account + action_name).set_enabled(True)
def update_app_actions_state(self):
active_accounts = bool(app.get_connected_accounts(exclude_local=True))
self.lookup_action('create-groupchat').set_enabled(active_accounts)
enabled_accounts = app.contacts.get_accounts()
self.lookup_action('start-chat').set_enabled(enabled_accounts)
def _set_shortcuts(self):
shortcuts = {
'app.quit': ['<Primary>Q'],
'app.shortcuts': ['<Primary>question'],
'app.preferences': ['<Primary>P'],
'app.plugins': ['<Primary>E'],
'app.xml-console': ['<Primary><Shift>X'],
'app.file-transfer': ['<Primary>T'],
'app.ipython': ['<Primary><Alt>I'],
'app.start-chat::': ['<Primary>N'],
'app.create-groupchat::': ['<Primary>G'],
'win.show-roster': ['<Primary>R'],
'win.show-offline': ['<Primary>O'],
'win.show-active': ['<Primary>Y'],
'win.change-nickname': ['<Primary><Shift>N'],
'win.change-subject': ['<Primary><Shift>S'],
'win.escape': ['Escape'],
'win.browse-history': ['<Primary>H'],
'win.send-file': ['<Primary>F'],
'win.show-contact-info': ['<Primary>I'],
'win.show-emoji-chooser': ['<Primary><Shift>M'],
'win.clear-chat': ['<Primary>L'],
'win.delete-line': ['<Primary>U'],
'win.close-tab': ['<Primary>W'],
'win.move-tab-up': ['<Primary><Shift>Page_Up'],
'win.move-tab-down': ['<Primary><Shift>Page_Down'],
'win.switch-next-tab': ['<Primary>Page_Down'],
'win.switch-prev-tab': ['<Primary>Page_Up'],
'win.switch-next-unread-tab-right': ['<Primary>Tab'],
'win.switch-next-unread-tab-left': ['<Primary>ISO_Left_Tab'],
'win.switch-tab-1': ['<Alt>1', '<Alt>KP_1'],
'win.switch-tab-2': ['<Alt>2', '<Alt>KP_2'],
'win.switch-tab-3': ['<Alt>3', '<Alt>KP_3'],
'win.switch-tab-4': ['<Alt>4', '<Alt>KP_4'],
'win.switch-tab-5': ['<Alt>5', '<Alt>KP_5'],
'win.switch-tab-6': ['<Alt>6', '<Alt>KP_6'],
'win.switch-tab-7': ['<Alt>7', '<Alt>KP_7'],
'win.switch-tab-8': ['<Alt>8', '<Alt>KP_8'],
'win.switch-tab-9': ['<Alt>9', '<Alt>KP_9'],
}
for action, accels in shortcuts.items():
self.set_accels_for_action(action, accels)
def _on_feature_discovered(self, event):
if event.feature == Namespace.MAM_2:
action = '%s-archive' % event.account
self.lookup_action(action).set_enabled(True)
elif event.feature == Namespace.BLOCKING:
action = '%s-blocking' % event.account
self.lookup_action(action).set_enabled(True)

1746
gajim/chat_control.py Normal file

File diff suppressed because it is too large Load diff

1615
gajim/chat_control_base.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The command system providing scalable, clean and convenient architecture
in combination with declarative way of defining commands and a fair
amount of automatization for routine processes.
"""

View file

@ -0,0 +1,135 @@
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Backbone of the command system. Provides smart and controllable
dispatching mechanism with an auto-discovery functionality. In addition
to automatic discovery and dispatching, also features manual control
over the process.
"""
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from gajim.command_system.tools import remove
COMMANDS = {} # type: Dict[Any, Any]
CONTAINERS = {} # type: Dict[Any, Any]
def add_host(host):
CONTAINERS[host] = []
def remove_host(host):
remove(CONTAINERS, host)
def add_container(container):
for host in container.HOSTS:
CONTAINERS[host].append(container)
def remove_container(container):
for host in container.HOSTS:
remove(CONTAINERS[host], container)
def add_commands(container):
commands = COMMANDS.setdefault(container, {})
for command in traverse_commands(container):
for name in command.names:
commands[name] = command
def remove_commands(container):
remove(COMMANDS, container)
def traverse_commands(container):
for name in dir(container):
attribute = getattr(container, name)
if is_command(attribute):
yield attribute
def is_command(attribute):
from gajim.command_system.framework import Command
return isinstance(attribute, Command)
def is_root(namespace):
metaclass = namespace.get("__metaclass__", None)
if not metaclass:
return False
return issubclass(metaclass, Dispatchable)
def get_command(host, name):
for container in CONTAINERS[host]:
command = COMMANDS[container].get(name)
if command:
return command
def list_commands(host):
for container in CONTAINERS[host]:
commands = COMMANDS[container]
for name, command in commands.items():
yield name, command
class Dispatchable(type):
# pylint: disable=no-value-for-parameter
def __init__(cls, name, bases, namespace):
parents = super(Dispatchable, cls)
parents.__init__(name, bases, namespace)
if not is_root(namespace):
cls.dispatch()
def dispatch(cls):
if cls.AUTOMATIC:
cls.enable()
class Host(Dispatchable):
def enable(cls):
add_host(cls)
def disable(cls):
remove_host(cls)
class Container(Dispatchable):
def enable(cls):
add_container(cls)
add_commands(cls)
def disable(cls):
remove_commands(cls)
remove_container(cls)

View file

@ -0,0 +1,54 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
class BaseError(Exception):
"""
Common base for errors which relate to a specific command.
Encapsulates everything needed to identify a command, by either its
object or name.
"""
def __init__(self, message, command=None, name=None):
self.message = message
self.command = command
self.name = name
if command and not name:
self.name = command.first_name
super(BaseError, self).__init__()
def __str__(self):
return self.message
class DefinitionError(BaseError):
"""
Used to indicate errors occurred on command definition.
"""
class CommandError(BaseError):
"""
Used to indicate errors occurred during command execution.
"""
class NoCommandError(BaseError):
"""
Used to indicate an inability to find the specified command.
"""

View file

@ -0,0 +1,351 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Provides a tiny framework with simple, yet powerful and extensible
architecture to implement commands in a straight and flexible,
declarative way.
"""
from types import FunctionType
from inspect import getargspec, getdoc
from gajim.command_system.dispatcher import Host
from gajim.command_system.dispatcher import Container
from gajim.command_system.dispatcher import get_command
from gajim.command_system.dispatcher import list_commands
from gajim.command_system.mapping import parse_arguments
from gajim.command_system.mapping import adapt_arguments
from gajim.command_system.errors import DefinitionError
from gajim.command_system.errors import CommandError
from gajim.command_system.errors import NoCommandError
class CommandHost(metaclass=Host):
"""
Command host is a hub between numerous command processors and
command containers. Aimed to participate in a dispatching process in
order to provide clean and transparent architecture.
The AUTOMATIC class variable, which must be defined by a command
host, specifies whether the command host should be automatically
dispatched and enabled by the dispatcher or not.
"""
__metaclass__ = Host
class CommandContainer(metaclass=Container):
"""
Command container is an entity which holds defined commands,
allowing them to be dispatched and processed correctly. Each
command container may be bound to a one or more command hosts.
The AUTOMATIC class variable, which must be defined by a command
processor, specifies whether the command processor should be
automatically dispatched and enabled by the dispatcher or not.
Bounding is controlled by the HOSTS class variable, which must be
defined by the command container. This variable should contain a
sequence of hosts to bound to, as a tuple or list.
"""
__metaclass__ = Container
class CommandProcessor:
"""
Command processor is an immediate command emitter. It does not
participate in the dispatching process directly, but must define a
host to bound to.
Bounding is controlled by the COMMAND_HOST variable, which must be
defined in the body of the command processor. This variable should
be set to a specific command host.
"""
# This defines a command prefix (or an initializer), which should
# precede a text in order for it to be processed as a command.
COMMAND_PREFIX = '/'
def process_as_command(self, text):
"""
Try to process text as a command. Returns True if it has been
processed as a command and False otherwise.
"""
# pylint: disable=assignment-from-no-return
prefix = text.startswith(self.COMMAND_PREFIX)
length = len(text) > len(self.COMMAND_PREFIX)
if not (prefix and length):
return False
body = text[len(self.COMMAND_PREFIX):]
body = body.strip()
parts = body.split(None, 1)
name, arguments = parts if len(parts) > 1 else (parts[0], None)
flag = self.looks_like_command(text, body, name, arguments)
if flag is not None:
return flag
self.execute_command(name, arguments)
return True
def execute_command(self, name, arguments):
cmd = self.get_command(name)
args, opts = parse_arguments(arguments) if arguments else ([], [])
args, kwargs = adapt_arguments(cmd, arguments, args, opts)
if self.command_preprocessor(cmd, name, arguments, args, kwargs):
return
value = cmd(self, *args, **kwargs)
self.command_postprocessor(cmd, name, arguments, args, kwargs, value)
def command_preprocessor(self, cmd, name, arguments, args, kwargs):
"""
Redefine this method in the subclass to execute custom code
before command gets executed.
If returns True then command execution will be interrupted and
command will not be executed.
"""
def command_postprocessor(self, cmd, name, arguments, args, kwargs, value):
"""
Redefine this method in the subclass to execute custom code
after command gets executed.
"""
def looks_like_command(self, text, body, name, arguments):
"""
This hook is being called before any processing, but after it
was determined that text looks like a command.
If returns value other then None - then further processing will
be interrupted and that value will be used to return from
process_as_command.
"""
def get_command(self, name):
cmd = get_command(self.COMMAND_HOST, name)
if not cmd:
raise NoCommandError("Command does not exist", name=name)
return cmd
def list_commands(self):
commands = list_commands(self.COMMAND_HOST)
commands = dict(commands)
return sorted(set(commands.values()), key=lambda k: k.__repr__())
class Command:
def __init__(self, handler, *names, **properties):
self.handler = handler
self.names = names
# Automatically set all the properties passed to a constructor
# by the command decorator.
for key, value in properties.items():
setattr(self, key, value)
def __call__(self, *args, **kwargs):
try:
return self.handler(*args, **kwargs)
# This allows to use a shortcut way of raising an exception
# inside a handler. That is to raise a CommandError without
# command or name attributes set. They will be set to a
# corresponding values right here in case if they was not set by
# the one who raised an exception.
except CommandError as error:
if not error.command and not error.name:
raise CommandError(error.message, self)
raise
# This one is a little bit too wide, but as Python does not have
# anything more constrained - there is no other choice. Take a
# look here if command complains about invalid arguments while
# they are ok.
except TypeError:
raise CommandError("Command received invalid arguments", self)
def __repr__(self):
return "<Command %s>" % ', '.join(self.names)
def __cmp__(self, other):
if self.first_name > other.first_name:
return 1
if self.first_name < other.first_name:
return -1
return 0
@property
def first_name(self):
return self.names[0]
@property
def native_name(self):
return self.handler.__name__
def extract_documentation(self):
"""
Extract handler's documentation which is a doc-string and
transform it to a usable format.
"""
return getdoc(self.handler)
def extract_description(self):
"""
Extract handler's description (which is a first line of the
documentation). Try to keep them simple yet meaningful.
"""
documentation = self.extract_documentation()
return documentation.split('\n', 1)[0] if documentation else None
def extract_specification(self):
"""
Extract handler's arguments specification, as it was defined
preserving their order.
"""
names, var_args, var_kwargs, defaults = getargspec(self.handler) # pylint: disable=W1505
# Behavior of this code need to be checked. Might yield
# incorrect results on some rare occasions.
spec_args = names[:-len(defaults) if defaults else len(names)]
spec_kwargs = list(
zip(names[-len(defaults):], defaults)) if defaults else {}
# Removing self from arguments specification. Command handler
# should receive the processors as a first argument, which
# should be self by the canonical means.
if spec_args.pop(0) != 'self':
raise DefinitionError("First argument must be self", self)
return spec_args, spec_kwargs, var_args, var_kwargs
def command(*names, **properties):
"""
A decorator for defining commands in a declarative way. Provides
facilities for setting command's names and properties.
Names should contain a set of names (aliases) by which the command
can be reached. If no names are given - the native name (the one
extracted from the command handler) will be used.
If native=True is given (default) and names is non-empty - then the
native name of the command will be prepended in addition to the
given names.
If usage=True is given (default) - then command help will be
appended with autogenerated usage info, based of the command handler
arguments introspection.
If source=True is given - then the first argument of the command
will receive the source arguments, as a raw, unprocessed string. The
further mapping of arguments and options will not be affected.
If raw=True is given - then command considered to be raw and should
define positional arguments only. If it defines only one positional
argument - this argument will receive all the raw and unprocessed
arguments. If the command defines more then one positional argument
- then all the arguments except the last one will be processed
normally; the last argument will get what is left after the
processing as raw and unprocessed string.
If empty=True is given - this will allow to call a raw command
without arguments.
If extra=True is given - then all the extra arguments passed to a
command will be collected into a sequence and given to the last
positional argument.
If overlap=True is given - then all the extra arguments will be
mapped as if they were values for the keyword arguments.
If expand=True is given (default) - then short, one-letter options
will be expanded to a verbose ones, based of the comparison of the
first letter. If more then one option with the same first letter is
given - then only first one will be used in the expansion.
"""
names = list(names)
native = properties.get('native', True)
usage = properties.get('usage', True)
source = properties.get('source', False)
raw = properties.get('raw', False)
empty = properties.get('empty', False)
extra = properties.get('extra', False)
overlap = properties.get('overlap', False)
expand = properties.get('expand', True)
if empty and not raw:
raise DefinitionError("Empty option can be used only with raw commands")
if extra and overlap:
raise DefinitionError("Extra and overlap options can not be used "
"together")
properties = {
'usage': usage,
'source': source,
'raw': raw,
'extra': extra,
'overlap': overlap,
'empty': empty,
'expand': expand
}
def decorator(handler):
"""
Decorator which receives handler as a first argument and then
wraps it in the command which then returns back.
"""
cmd = Command(handler, *names, **properties)
# Extract and inject a native name if either no other names are
# specified or native property is enabled, while making
# sure it is going to be the first one in the list.
if not names or native:
names.insert(0, cmd.native_name)
cmd.names = tuple(names)
return cmd
# Workaround if we are getting called without parameters. Keep in
# mind that in that case - first item in the names will be the
# handler.
if names and isinstance(names[0], FunctionType):
return decorator(names.pop(0))
return decorator
def doc(text):
"""
This decorator is used to bind a documentation (a help) to a
command.
"""
def decorator(target):
if isinstance(target, Command):
target.handler.__doc__ = text
else:
target.__doc__ = text
return target
return decorator

View file

@ -0,0 +1,20 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The implementation and auxiliary systems which implement the standard
Gajim commands and also provide an infrastructure for adding custom
commands.
"""

View file

@ -0,0 +1,131 @@
# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
This module contains examples of how to create your own commands, by
creating a new command container, bounded to a specific command host,
and defining a set of commands inside of it.
Keep in mind that this module is not being loaded from anywhere, so the
code in here will not be executed and commands defined here will not be
detected.
"""
from gajim.common.i18n import _
from gajim.command_system.framework import CommandContainer
from gajim.command_system.framework import command
from gajim.command_system.framework import doc
from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.implementation.hosts import PrivateChatCommands
from gajim.command_system.implementation.hosts import GroupChatCommands
class CustomCommonCommands(CommandContainer):
"""
The AUTOMATIC class variable, set to a positive value, instructs the
command system to automatically discover the command container and
enable it.
This command container bounds to all three available in the default
implementation command hosts. This means that commands defined in
this container will be available to all: chat, private chat and a
group chat.
"""
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
@command
def dance(self):
"""
First line of the doc string is called a description and will be
programmatically extracted and formatted.
After that you can give more help, like explanation of the
options. This one will be programmatically extracted and
formatted too.
After all the documentation - there will be autogenerated (based
on the method signature) usage information appended. You can
turn it off, if you want.
"""
return "I don't dance."
class CustomChatCommands(CommandContainer):
"""
This command container bounds only to the ChatCommands command host.
Therefore commands defined inside of the container will be available
only to a chat.
"""
AUTOMATIC = True
HOSTS = (ChatCommands,)
@command("squal", "bawl")
def sing(self):
"""
This command has an additional aliases. It means the command will
be available under three names: sing (the native name), squal
(the first alias), bawl (the second alias).
You can turn off the usage of the native name, if you want, and
specify a name or a set of names, as aliases, under which a
command will be available.
"""
return "Buy yourself a stereo."
class CustomPrivateChatCommands(CommandContainer):
"""
This command container bounds only to the PrivateChatCommands
command host. Therefore commands defined inside of the container
will be available only to a private chat.
"""
AUTOMATIC = True
HOSTS = (PrivateChatCommands,)
@command
#Example string. Do not translate
@doc(_("The same as using a doc-string, except it supports translation"))
def make_coffee(self):
return "I'm not a coffee machine!"
class CustomGroupChatCommands(CommandContainer):
"""
This command container bounds only to the GroupChatCommands command
host. Therefore commands defined inside of the container will be
available only to a group chat.
"""
AUTOMATIC = True
HOSTS = (GroupChatCommands,)
@command
def fetch(self):
return "Buy yourself a dog."

View file

@ -0,0 +1,136 @@
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Provides facilities to safely execute expressions inside a shell process
and capture the resulting output, in an asynchronous fashion, avoiding
deadlocks. If the process execution time reaches the threshold - it is
forced to terminate. Consists of a tiny framework and a couple of
commands as a frontend.
"""
from subprocess import Popen, PIPE
from os.path import expanduser
from gi.repository import GLib
from gajim.common import app
from gajim.common.i18n import _
from gajim.command_system.framework import CommandContainer
from gajim.command_system.framework import command
from gajim.command_system.framework import doc
from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.implementation.hosts import PrivateChatCommands
from gajim.command_system.implementation.hosts import GroupChatCommands
class Execute(CommandContainer):
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
DIRECTORY = "~"
POLL_INTERVAL = 100
POLL_COUNT = 5
@command("exec", raw=True)
@doc(_("Execute expression inside a shell, show output"))
def execute(self, expression):
Execute.spawn(self, expression)
@classmethod
def spawn(cls, processor, expression):
command_system_execute = app.settings.get('command_system_execute')
if command_system_execute:
pipes = dict(stdout=PIPE, stderr=PIPE)
directory = expanduser(cls.DIRECTORY)
popen = Popen(expression, shell=True, cwd=directory, **pipes)
cls.monitor(processor, popen)
else:
processor.echo_error(
_('Command disabled. This command can be enabled by '
'setting \'command_system_execute\' to True in ACE '
'(Advanced Configuration Editor).'))
return
@classmethod
def monitor(cls, processor, popen):
poller = cls.poller(processor, popen)
GLib.timeout_add(cls.POLL_INTERVAL, next, poller)
@classmethod
def poller(cls, processor, popen):
for _ in range(cls.POLL_COUNT):
yield cls.brush(processor, popen)
cls.overdue(processor, popen)
yield False
@classmethod
def brush(cls, processor, popen):
if popen.poll() is not None:
cls.terminated(processor, popen)
return False
return True
@classmethod
def terminated(cls, processor, popen):
stdout, stderr = cls.fetch(popen)
success = popen.returncode == 0
if success and stdout:
processor.echo(stdout)
elif not success and stderr:
processor.echo_error(stderr)
@classmethod
def overdue(cls, processor, popen):
popen.terminate()
@classmethod
def fetch(cls, popen):
data = popen.communicate()
return map(cls.clean, data)
@staticmethod
def clean(text):
strip = chr(10) + chr(32)
return text.decode().strip(strip)
class Show(Execute):
@command("sh", raw=True)
@doc(_("Execute expression inside a shell, send output"))
def show(self, expression):
Show.spawn(self, expression)
@classmethod
def terminated(cls, processor, popen):
stdout, stderr = cls.fetch(popen)
success = popen.returncode == 0
if success and stdout:
processor.send(stdout)
elif not success and stderr:
processor.echo_error(stderr)

View file

@ -0,0 +1,45 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The module defines a set of command hosts, which are bound to a
different command processors, which are the source of commands.
"""
from gajim.command_system.framework import CommandHost
class ChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes
commands from a chat.
"""
AUTOMATIC = True
class PrivateChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes
commands from a private chat.
"""
AUTOMATIC = True
class GroupChatCommands(CommandHost):
"""
This command host is bound to the command processor which processes
commands from a group chat.
"""
AUTOMATIC = True

View file

@ -0,0 +1,195 @@
# Copyright (c) 2009-2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Provides a glue to tie command system framework and the actual code
where it would be dropped in. Defines a little bit of scaffolding to
support interaction between the two and a few utility methods so you
don't need to dig up the code itself to write basic commands.
"""
from traceback import print_exc
from gi.repository import Pango
from gajim.common import app
from gajim.common.i18n import _
from gajim.command_system.framework import CommandProcessor
from gajim.command_system.errors import CommandError
from gajim.command_system.errors import NoCommandError
class ChatCommandProcessor(CommandProcessor):
"""
A basic scaffolding to provide convenient interaction between the
command system and chat controls. It will be merged directly into
the controls, by ChatCommandProcessor being among superclasses of
the controls.
"""
def process_as_command(self, text):
self.command_succeeded = False
parents = super(ChatCommandProcessor, self)
flag = parents.process_as_command(text)
if flag and self.command_succeeded:
self.add_history(text)
self.clear_input()
return flag
def execute_command(self, name, arguments):
try:
parents = super(ChatCommandProcessor, self)
parents.execute_command(name, arguments)
except NoCommandError as error:
details = dict(name=error.name, message=error.message)
message = "%(name)s: %(message)s\n" % details
message += "Try using the //%(name)s or /say /%(name)s " % details
message += "construct if you intended to send it as a text."
self.echo_error(message)
except CommandError as error:
self.echo_error("%s: %s" % (error.name, error.message))
except Exception:
self.echo_error(_("Error during command execution!"))
print_exc()
else:
self.command_succeeded = True
def looks_like_command(self, text, body, name, arguments):
# Command escape stuff goes here. If text was prepended by the
# command prefix twice, like //not_a_command (if prefix is set
# to /) then it will be escaped, that is sent just as a regular
# message with one (only one) prefix removed, so message will be
# /not_a_command.
if body.startswith(self.COMMAND_PREFIX):
self.send(body)
return True
def command_preprocessor(self, command, name, arguments, args, kwargs):
# If command argument contain h or help option - forward it to
# the /help command. Don't forget to pass self, as all commands
# are unbound. And also don't forget to print output.
if 'h' in kwargs or 'help' in kwargs:
help_ = self.get_command('help')
self.echo(help_(self, name))
return True
def command_postprocessor(self, command, name, arguments, args, kwargs,
value):
# If command returns a string - print it to a user. A convenient
# and sufficient in most simple cases shortcut to a using echo.
if value and isinstance(value, str):
self.echo(value)
class CommandTools:
"""
Contains a set of basic tools and shortcuts you can use in your
commands to perform some simple operations. These will be merged
directly into the controls, by CommandTools being among superclasses
of the controls.
"""
def __init__(self):
self.install_tags()
def install_tags(self):
buffer_ = self.conv_textview.tv.get_buffer()
name = "Monospace"
font = Pango.FontDescription(name)
command_ok_tag = buffer_.create_tag("command_ok")
command_ok_tag.set_property("font-desc", font)
command_ok_tag.set_property("foreground", "#3465A4")
command_error_tag = buffer_.create_tag("command_error")
command_error_tag.set_property("font-desc", font)
command_error_tag.set_property("foreground", "#F57900")
def shift_line(self):
buffer_ = self.conv_textview.tv.get_buffer()
iter_ = buffer_.get_end_iter()
if iter_.ends_line() and not iter_.is_start():
buffer_.insert_with_tags_by_name(iter_, "\n", "eol")
def append_with_tags(self, text, *tags):
buffer_ = self.conv_textview.tv.get_buffer()
iter_ = buffer_.get_end_iter()
buffer_.insert_with_tags_by_name(iter_, text, *tags)
def echo(self, text, tag="command_ok"):
"""
Print given text to the user, as a regular command output.
"""
self.shift_line()
self.append_with_tags(text, tag)
def echo_error(self, text):
"""
Print given text to the user, as an error command output.
"""
self.echo(text, "command_error")
def send(self, text):
"""
Send a message to the contact.
"""
self.send_message(text, process_commands=False)
def set_input(self, text):
"""
Set given text into the input.
"""
buffer = self.msg_textview.get_buffer()
buffer.set_text(text)
def clear_input(self):
"""
Clear input.
"""
self.set_input(str())
def add_history(self, text):
"""
Add given text to the input history, so user can scroll through
it using ctrl + up/down arrow keys.
"""
self.save_message(text, 'sent')
@property
def connection(self):
"""
Get the current connection object.
"""
return app.connections[self.account]
@property
def full_jid(self):
"""
Get a full JID of the contact.
"""
return self.contact.get_full_jid()

View file

@ -0,0 +1,433 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Provides an actual implementation for the standard commands.
"""
from time import localtime
from time import strftime
from datetime import date
from gi.repository import GLib
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import _
from gajim.common.const import KindConstant
from gajim.command_system.errors import CommandError
from gajim.command_system.framework import CommandContainer
from gajim.command_system.framework import command
from gajim.command_system.framework import doc
from gajim.command_system.mapping import generate_usage
from gajim.command_system.implementation.hosts import ChatCommands
from gajim.command_system.implementation.hosts import PrivateChatCommands
from gajim.command_system.implementation.hosts import GroupChatCommands
class StandardCommonCommands(CommandContainer):
"""
This command container contains standard commands which are common
to all - chat, private chat, group chat.
"""
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands, GroupChatCommands
@command(overlap=True)
@doc(_("Show help on a given command or a list of available commands if "
"-a is given"))
def help(self, cmd=None, all_=False):
if cmd:
cmd = self.get_command(cmd)
documentation = _(cmd.extract_documentation())
usage = generate_usage(cmd)
text = []
if documentation:
text.append(documentation)
if cmd.usage:
text.append(usage)
return '\n\n'.join(text)
if all_:
for cmd_ in self.list_commands():
names = ', '.join(cmd_.names)
description = cmd_.extract_description()
self.echo("%s - %s" % (names, description))
else:
help_ = self.get_command('help')
self.echo(help_(self, 'help'))
@command(raw=True)
@doc(_("Send a message to the contact"))
def say(self, message):
self.send(message)
@command(raw=True)
@doc(_("Send action (in the third person) to the current chat"))
def me(self, action):
self.send("/me %s" % action)
@command('lastlog', overlap=True)
@doc(_("Show logged messages which mention given text"))
def grep(self, text, limit=None):
results = app.storage.archive.search_log(self.account, self.contact.jid, text)
if not results:
raise CommandError(_("%s: Nothing found") % text)
if limit:
try:
results = results[len(results) - int(limit):]
except ValueError:
raise CommandError(_("Limit must be an integer"))
for row in results:
contact = row.contact_name
if not contact:
if row.kind == KindConstant.CHAT_MSG_SENT:
contact = app.nicks[self.account]
else:
contact = self.contact.name
time_obj = localtime(row.time)
date_obj = date.fromtimestamp(row.time)
date_ = strftime('%Y-%m-%d', time_obj)
time_ = strftime('%H:%M:%S', time_obj)
if date_obj == date.today():
formatted = "[%s] %s: %s" % (time_, contact, row.message)
else:
formatted = "[%s, %s] %s: %s" % (
date_, time_, contact, row.message)
self.echo(formatted)
@command(raw=True, empty=True)
# Do not translate online, away, chat, xa, dnd
@doc(_("""
Set the current status
Status can be given as one of the following values:
online, away, chat, xa, dnd.
"""))
def status(self, status, message):
if status not in ('online', 'away', 'chat', 'xa', 'dnd'):
raise CommandError("Invalid status given")
for connection in app.connections.values():
if not app.settings.get_account_setting(connection.name,
'sync_with_global_status'):
continue
if not connection.state.is_available:
continue
connection.change_status(status, message)
@command(raw=True, empty=True)
@doc(_("Set the current status to away"))
def away(self, message):
if not message:
message = _("Away")
for connection in app.connections.values():
if not app.settings.get_account_setting(connection.name,
'sync_with_global_status'):
continue
if not connection.state.is_available:
continue
connection.change_status('away', message)
@command('back', raw=True, empty=True)
@doc(_("Set the current status to online"))
def online(self, message):
if not message:
message = _("Available")
for connection in app.connections.values():
if not app.settings.get_account_setting(connection.name,
'sync_with_global_status'):
continue
if not connection.state.is_available:
continue
connection.change_status('online', message)
@command
@doc(_("Send a disco info request"))
def disco(self):
client = app.get_client(self.account)
if not client.state.is_available:
return
client.get_module('Discovery').disco_contact(self.contact)
class StandardCommonChatCommands(CommandContainer):
"""
This command container contains standard commands, which are common
to a chat and a private chat only.
"""
AUTOMATIC = True
HOSTS = ChatCommands, PrivateChatCommands
@command
@doc(_("Clear the text window"))
def clear(self):
self.conv_textview.clear()
@command
@doc(_("Send a ping to the contact"))
def ping(self):
if self.account == app.ZEROCONF_ACC_NAME:
raise CommandError(
_('Command is not supported for zeroconf accounts'))
app.connections[self.account].get_module('Ping').send_ping(self.contact)
@command
@doc(_("Send DTMF sequence through an open voice chat"))
def dtmf(self, sequence):
if not self.audio_sid:
raise CommandError(_("No open voice chats with the contact"))
for tone in sequence:
if not (tone in ("*", "#") or tone.isdigit()):
raise CommandError(_("%s is not a valid tone") % tone)
gjs = self.connection.get_module('Jingle').get_jingle_session
session = gjs(self.full_jid, self.audio_sid)
content = session.get_content("audio")
content.batch_dtmf(sequence)
@command
@doc(_("Toggle Voice Chat"))
def audio(self):
if not self.audio_available:
raise CommandError(_("Voice chats are not available"))
# An audio session is toggled by inverting the state of the
# appropriate button.
state = self._audio_button.get_active()
self._audio_button.set_active(not state)
@command
@doc(_("Toggle Video Chat"))
def video(self):
if not self.video_available:
raise CommandError(_("Video chats are not available"))
# A video session is toggled by inverting the state of the
# appropriate button.
state = self._video_button.get_active()
self._video_button.set_active(not state)
@command(raw=True)
@doc(_("Send a message to the contact that will attract their attention"))
def attention(self, message):
self.send_message(message, process_commands=False, attention=True)
class StandardChatCommands(CommandContainer):
"""
This command container contains standard commands which are unique
to a chat.
"""
AUTOMATIC = True
HOSTS = (ChatCommands,)
class StandardPrivateChatCommands(CommandContainer):
"""
This command container contains standard commands which are unique
to a private chat.
"""
AUTOMATIC = True
HOSTS = (PrivateChatCommands,)
class StandardGroupChatCommands(CommandContainer):
"""
This command container contains standard commands which are unique
to a group chat.
"""
AUTOMATIC = True
HOSTS = (GroupChatCommands,)
@command
@doc(_("Clear the text window"))
def clear(self):
self.conv_textview.clear()
@command(raw=True)
@doc(_("Change your nickname in a group chat"))
def nick(self, new_nick):
try:
new_nick = helpers.parse_resource(new_nick)
except Exception:
raise CommandError(_("Invalid nickname"))
# FIXME: Check state of MUC
self.connection.get_module('MUC').change_nick(
self.room_jid, new_nick)
self.new_nick = new_nick
@command('query', raw=True)
@doc(_("Open a private chat window with a specified participant"))
def chat(self, nick):
nicks = app.contacts.get_nick_list(self.account, self.room_jid)
if nick in nicks:
self.send_pm(nick)
else:
raise CommandError(_("Nickname not found"))
@command('msg', raw=True)
@doc(_("Open a private chat window with a specified participant and send "
"him a message"))
def message(self, nick, message):
nicks = app.contacts.get_nick_list(self.account, self.room_jid)
if nick in nicks:
self.send_pm(nick, message)
else:
raise CommandError(_("Nickname not found"))
@command(raw=True, empty=True)
@doc(_("Display or change a group chat topic"))
def topic(self, new_topic):
if new_topic:
self.connection.get_module('MUC').set_subject(
self.room_jid, new_topic)
else:
return self.subject
@command(raw=True, empty=True)
@doc(_("Invite a user to a group chat for a reason"))
def invite(self, jid, reason):
control = app.get_groupchat_control(self.account, self.room_jid)
if control is not None:
control.invite(jid)
@command(raw=True, empty=True)
@doc(_("Join a group chat given by an XMPP Address"))
def join(self, jid):
if '@' not in jid:
jid = jid + '@' + app.get_server_from_jid(self.room_jid)
app.app.activate_action(
'groupchat-join',
GLib.Variant('as', [self.account, jid]))
@command('part', 'close', raw=True, empty=True)
@doc(_("Leave the group chat, optionally giving a reason, and close tab or "
"window"))
def leave(self, reason):
self.leave(reason=reason)
@command(raw=True, empty=True)
@doc(_("""
Ban user by a nick or a JID from a groupchat
If given nickname is not found it will be treated as a JID.
"""))
def ban(self, who, reason=''):
if who in app.contacts.get_nick_list(self.account, self.room_jid):
contact = app.contacts.get_gc_contact(
self.account, self.room_jid, who)
who = contact.jid
self.connection.get_module('MUC').set_affiliation(
self.room_jid,
{who: {'affiliation': 'outcast',
'reason': reason}})
@command(raw=True, empty=True)
@doc(_("Kick user from group chat by nickname"))
def kick(self, who, reason):
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
raise CommandError(_("Nickname not found"))
self.connection.get_module('MUC').set_role(
self.room_jid, who, 'none', reason)
@command(raw=True)
# Do not translate moderator, participant, visitor, none
@doc(_("""Set participant role in group chat.
Role can be given as one of the following values:
moderator, participant, visitor, none"""))
def role(self, who, role):
if role not in ('moderator', 'participant', 'visitor', 'none'):
raise CommandError(_("Invalid role given"))
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
raise CommandError(_("Nickname not found"))
self.connection.get_module('MUC').set_role(self.room_jid, who, role)
@command(raw=True)
# Do not translate owner, admin, member, outcast, none
@doc(_("""Set participant affiliation in group chat.
Affiliation can be given as one of the following values:
owner, admin, member, outcast, none"""))
def affiliate(self, who, affiliation):
if affiliation not in ('owner', 'admin', 'member', 'outcast', 'none'):
raise CommandError(_("Invalid affiliation given"))
if who not in app.contacts.get_nick_list(self.account, self.room_jid):
raise CommandError(_("Nickname not found"))
contact = app.contacts.get_gc_contact(self.account, self.room_jid, who)
self.connection.get_module('MUC').set_affiliation(
self.room_jid,
{contact.jid: {'affiliation': affiliation}})
@command
@doc(_("Display names of all group chat participants"))
def names(self, verbose=False):
ggc = app.contacts.get_gc_contact
gnl = app.contacts.get_nick_list
get_contact = lambda nick: ggc(self.account, self.room_jid, nick)
get_role = lambda nick: get_contact(nick).role
nicks = gnl(self.account, self.room_jid)
nicks = sorted(nicks)
nicks = sorted(nicks, key=get_role)
if not verbose:
return ", ".join(nicks)
for nick in nicks:
contact = get_contact(nick)
role = helpers.get_uf_role(contact.role)
affiliation = helpers.get_uf_affiliation(contact.affiliation)
self.echo("%s - %s - %s" % (nick, role, affiliation))
@command('ignore', raw=True)
@doc(_("Forbid a participant to send you public or private messages"))
def block(self, who):
self.on_block(None, who)
@command('unignore', raw=True)
@doc(_("Allow a participant to send you public or private messages"))
def unblock(self, who):
self.on_unblock(None, who)
@command
@doc(_("Send a ping to the contact"))
def ping(self, nick):
if self.account == app.ZEROCONF_ACC_NAME:
raise CommandError(
_('Command is not supported for zeroconf accounts'))
gc_c = app.contacts.get_gc_contact(self.account, self.room_jid, nick)
if gc_c is None:
raise CommandError(_("Unknown nickname"))
app.connections[self.account].get_module('Ping').send_ping(gc_c)

View file

@ -0,0 +1,349 @@
# Copyright (C) 2009-2010 Alexander Cherniuk <ts33kr@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
The module contains routines to parse command arguments and map them to
the command handler's positional and keyword arguments.
Mapping is done in two stages: 1) parse arguments into positional
arguments and options; 2) adapt them to the specific command handler
according to the command properties.
"""
import re
from operator import itemgetter
from gajim.common.i18n import _
from gajim.command_system.errors import DefinitionError
from gajim.command_system.errors import CommandError
# Quite complex piece of regular expression logic to parse options and
# arguments. Might need some tweaking along the way.
ARG_PATTERN = re.compile(r'(\'|")?(?P<body>(?(1).+?|\S+))(?(1)\1)')
OPT_PATTERN = re.compile(r'(?<!\w)--?(?P<key>[\w-]+)(?:(?:=|\s)(\'|")?(?P<value>(?(2)[^-]+?|[^-\s]+))(?(2)\2))?')
# Option keys needs to be encoded to a specific encoding as Python does
# not allow to expand dictionary with raw Unicode strings as keys from a
# **kwargs.
KEY_ENCODING = 'UTF-8'
# Defines how complete representation of command usage (generated based
# on command handler argument specification) will be rendered.
USAGE_PATTERN = 'Usage: %s %s'
def parse_arguments(arguments):
"""
Simple yet effective and sufficient in most cases parser which
parses command arguments and returns them as two lists.
First list represents positional arguments as (argument, position),
and second representing options as (key, value, position) tuples,
where position is a (start, end) span tuple of where it was found in
the string.
Options may be given in --long or -short format. As --option=value
or --option value or -option value. Keys without values will get
None as value.
Arguments and option values that contain spaces may be given as 'one
two three' or "one two three"; that is between single or double
quotes.
"""
args, opts = [], []
def intersects_opts(given_start, given_end):
"""
Check if given span intersects with any of options.
"""
for _key, _value, (start, end) in opts:
if given_start >= start and given_end <= end:
return True
return False
def intersects_args(given_start, given_end):
"""
Check if given span intersects with any of arguments.
"""
for _arg, (start, end) in args:
if given_start >= start and given_end <= end:
return True
return False
for match in re.finditer(OPT_PATTERN, arguments):
if match:
key = match.group('key')
value = match.group('value') or None
position = match.span()
opts.append((key, value, position))
for match in re.finditer(ARG_PATTERN, arguments):
if match:
body = match.group('body')
position = match.span()
args.append((body, position))
# Primitive but sufficiently effective way of disposing of
# conflicted sectors. Remove any arguments that intersect with
# options.
for arg, position in args[:]:
if intersects_opts(*position):
args.remove((arg, position))
# Primitive but sufficiently effective way of disposing of
# conflicted sectors. Remove any options that intersect with
# arguments.
for key, value, position in opts[:]:
if intersects_args(*position):
opts.remove((key, value, position))
return args, opts
def adapt_arguments(command, arguments, args, opts):
"""
Adapt args and opts got from the parser to a specific handler by
means of arguments specified on command definition. That is
transform them to *args and **kwargs suitable for passing to a
command handler.
Dashes (-) in the option names will be converted to underscores. So
you can map --one-more-option to a one_more_option=None.
If the initial value of a keyword argument is a boolean (False in
most cases) - then this option will be treated as a switch, that is
an option which does not take an argument. If a switch is followed
by an argument - then this argument will be treated just like a
normal positional argument.
"""
spec_args, spec_kwargs, var_args, _var_kwargs = command.extract_specification()
norm_kwargs = dict(spec_kwargs)
# Quite complex piece of neck-breaking logic to extract raw
# arguments if there is more, then one positional argument specified
# by the command. In case if it's just one argument which is the
# collector - this is fairly easy. But when it's more then one
# argument - the neck-breaking logic of how to retrieve residual
# arguments as a raw, all in one piece string, kicks in.
if command.raw:
if arguments:
spec_fix = 1 if command.source else 0
spec_len = len(spec_args) - spec_fix
arguments_end = len(arguments) - 1
# If there are any optional arguments given they should be
# either an unquoted positional argument or part of the raw
# argument. So we find all optional arguments that can
# possibly be unquoted argument and append them as is to the
# args.
for key, value, (start, end) in opts[:spec_len]:
if value:
end -= len(value) + 1
args.append((arguments[start:end], (start, end)))
args.append((value, (end, end + len(value) + 1)))
else:
args.append((arguments[start:end], (start, end)))
# We need in-place sort here because after manipulations
# with options order of arguments might be wrong and we just
# can't have more complex logic to not let that happen.
args.sort(key=itemgetter(1))
if spec_len > 1:
try:
_stopper, (start, end) = args[spec_len - 2]
except IndexError:
raise CommandError(_("Missing arguments"), command)
# The essential point of the whole play. After
# boundaries are being determined (supposedly correct)
# we separate raw part from the rest of arguments, which
# should be normally processed.
raw = arguments[end:]
raw = raw.strip() or None
if not raw and not command.empty:
raise CommandError(_("Missing arguments"), command)
# Discard residual arguments and all of the options as
# raw command does not support options and if an option
# is given it is rather a part of a raw argument.
args = args[:spec_len - 1]
opts = []
args.append((raw, (end, arguments_end)))
else:
# Substitute all of the arguments with only one, which
# contain raw and unprocessed arguments as a string. And
# discard all the options, as raw command does not
# support them.
args = [(arguments, (0, arguments_end))]
opts = []
else:
if command.empty:
args.append((None, (0, 0)))
else:
raise CommandError(_("Missing arguments"), command)
# The first stage of transforming options we have got to a format
# that can be used to associate them with declared keyword
# arguments. Substituting dashes (-) in their names with
# underscores (_).
for index, (key, value, position) in enumerate(opts):
if '-' in key:
opts[index] = (key.replace('-', '_'), value, position)
# The second stage of transforming options to an associable state.
# Expanding short, one-letter options to a verbose ones, if
# corresponding opt-in has been given.
if command.expand:
expanded = []
for spec_key in norm_kwargs.keys():
letter = spec_key[0] if len(spec_key) > 1 else None
if letter and letter not in expanded:
for index, (key, value, position) in enumerate(opts):
if key == letter:
expanded.append(letter)
opts[index] = (spec_key, value, position)
break
# Detect switches and set their values accordingly. If any of them
# carries a value - append it to args.
for index, (key, value, position) in enumerate(opts):
if isinstance(norm_kwargs.get(key), bool):
opts[index] = (key, True, position)
if value:
args.append((value, position))
# Sorting arguments and options (just to be sure) in regarding to
# their positions in the string.
args.sort(key=itemgetter(1))
opts.sort(key=itemgetter(2))
# Stripping down position information supplied with arguments and
# options as it won't be needed again.
args = list(map(lambda t: t[0], args))
opts = list(map(lambda t: (t[0], t[1]), opts))
# If command has extra option enabled - collect all extra arguments
# and pass them to a last positional argument command defines as a
# list.
if command.extra:
if not var_args:
spec_fix = 1 if not command.source else 2
spec_len = len(spec_args) - spec_fix
extra = args[spec_len:]
args = args[:spec_len]
args.append(extra)
else:
raise DefinitionError("Can not have both, extra and *args")
# Detect if positional arguments overlap keyword arguments. If so
# and this is allowed by command options - then map them directly to
# their options, so they can get proper further processing.
spec_fix = 1 if command.source else 0
spec_len = len(spec_args) - spec_fix
if len(args) > spec_len:
if command.overlap:
overlapped = args[spec_len:]
args = args[:spec_len]
for arg, spec_key, _spec_value in zip(overlapped, spec_kwargs):
opts.append((spec_key, arg))
else:
raise CommandError(_("Too many arguments"), command)
# Detect every switch and ensure it will not receive any arguments.
# Normally this does not happen unless overlapping is enabled.
for key, value in opts:
initial = norm_kwargs.get(key)
if isinstance(initial, bool):
if not isinstance(value, bool):
raise CommandError(
"%s: Switch can not take an argument" % key, command)
# Inject the source arguments as a string as a first argument, if
# command has enabled the corresponding option.
if command.source:
args.insert(0, arguments)
# Return *args and **kwargs in the form suitable for passing to a
# command handler and being expanded.
return tuple(args), dict(opts)
def generate_usage(command, complete=True):
"""
Extract handler's arguments specification and wrap them in a
human-readable format usage information. If complete is given - then
USAGE_PATTERN will be used to render the specification completely.
"""
spec_args, spec_kwargs, var_args, var_kwargs = command.extract_specification()
# Remove some special positional arguments from the specification,
# but store their names so they can be used for usage info
# generation.
_sp_source = spec_args.pop(0) if command.source else None
sp_extra = spec_args.pop() if command.extra else None
kwargs = []
letters = []
for key, value in spec_kwargs:
letter = key[0]
key = key.replace('_', '-')
if isinstance(value, bool):
value = str()
else:
value = '=%s' % value
if letter not in letters:
kwargs.append('-(-%s)%s%s' % (letter, key[1:], value))
letters.append(letter)
else:
kwargs.append('--%s%s' % (key, value))
usage = str()
args = str()
if command.raw:
spec_len = len(spec_args) - 1
if spec_len:
args += ('<%s>' % ', '.join(spec_args[:spec_len])) + ' '
args += ('(|%s|)' if command.empty else '|%s|') % spec_args[-1]
else:
if spec_args:
args += '<%s>' % ', '.join(spec_args)
if var_args or sp_extra:
args += (' ' if spec_args else str()) + '<<%s>>' % (
var_args or sp_extra)
usage += args
if kwargs or var_kwargs:
if kwargs:
usage += (' ' if args else str()) + '[%s]' % ', '.join(kwargs)
if var_kwargs:
usage += (' ' if args else str()) + '[[%s]]' % var_kwargs
# Native name will be the first one if it is included. Otherwise,
# names will be in the order they were specified.
if len(command.names) > 1:
names = '%s (%s)' % (command.first_name, ', '.join(command.names[1:]))
else:
names = command.first_name
return USAGE_PATTERN % (names, usage) if complete else usage

View file

@ -0,0 +1,34 @@
# Copyright (c) 2010, Alexander Cherniuk (ts33kr@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
def remove(sequence, target):
if isinstance(sequence, list):
if target in sequence:
sequence.remove(target)
elif isinstance(sequence, dict):
if target in sequence:
del sequence[target]

0
gajim/common/__init__.py Normal file
View file

32
gajim/common/account.py Normal file
View file

@ -0,0 +1,32 @@
# Copyright (C) 2009 Stephan Erb <steve-e AT h3c.de>
#
# 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/>.
class Account:
def __init__(self, name, contacts, gc_contacts):
self.name = name
self.contacts = contacts
self.gc_contacts = gc_contacts
def change_contact_jid(self, old_jid, new_jid):
self.contacts.change_contact_jid(old_jid, new_jid)
def __repr__(self):
return self.name
def __hash__(self):
return hash(self.name)

691
gajim/common/app.py Normal file
View file

@ -0,0 +1,691 @@
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
# Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2018 Philipp Hörist <philipp @ hoerist.com>
#
# 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/>.
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
from typing import Optional # pylint: disable=unused-import
from typing import cast
import os
import sys
import logging
import uuid
from collections import namedtuple
from collections import defaultdict
import nbxmpp
from gi.repository import Gdk
import gajim
from gajim.common import config as c_config
from gajim.common import configpaths
from gajim.common import ged as ged_module
from gajim.common.i18n import LANG
from gajim.common.const import Display
from gajim.common.events import Events
from gajim.common.types import NetworkEventsControllerT # pylint: disable=unused-import
from gajim.common.types import InterfaceT # pylint: disable=unused-import
from gajim.common.types import ConnectionT # pylint: disable=unused-import
from gajim.common.types import LegacyContactsAPIT # pylint: disable=unused-import
from gajim.common.types import SettingsT # pylint: disable=unused-import
interface = cast(InterfaceT, None)
thread_interface = lambda *args: None # Interface to run a thread and then a callback
config = c_config.Config()
settings = cast(SettingsT, None)
version = gajim.__version__
connections = {} # type: Dict[str, ConnectionT]
avatar_cache = {} # type: Dict[str, Dict[str, Any]]
bob_cache = {} # type: Dict[str, bytes]
ipython_window = None
app = None # Gtk.Application
ged = ged_module.GlobalEventsDispatcher() # Global Events Dispatcher
nec = cast(NetworkEventsControllerT, None)
plugin_manager = None # Plugins Manager
class Storage:
def __init__(self):
self.cache = None
self.archive = None
storage = Storage()
css_config = None
transport_type = {} # type: Dict[str, str]
# dict of time of the latest incoming message per jid
# {acct1: {jid1: time1, jid2: time2}, }
last_message_time = {} # type: Dict[str, Dict[str, float]]
contacts = cast(LegacyContactsAPIT, None)
# tell if we are connected to the room or not
# {acct: {room_jid: True}}
gc_connected = {} # type: Dict[str, Dict[str, bool]]
# dict of the pass required to enter a room
# {room_jid: password}
gc_passwords = {} # type: Dict[str, str]
# dict of rooms that must be automatically configured
# and for which we have a list of invities
# {account: {room_jid: {'invities': []}}}
automatic_rooms = {} # type: Dict[str, Dict[str, Dict[str, List[str]]]]
# dict of groups, holds if they are expanded or not
groups = {} # type: Dict[str, Dict[str, Dict[str, bool]]]
# list of contacts that has just signed in
newly_added = {} # type: Dict[str, List[str]]
# list of contacts that has just signed out
to_be_removed = {} # type: Dict[str, List[str]]
events = Events()
notification = None
# list of our nick names in each account
nicks = {} # type: Dict[str, str]
# should we block 'contact signed in' notifications for this account?
# this is only for the first 30 seconds after we change our show
# to something else than offline
# can also contain account/transport_jid to block notifications for contacts
# from this transport
block_signed_in_notifications = {} # type: Dict[str, bool]
proxy65_manager = None
cert_store = None
task_manager = None
# zeroconf account name
ZEROCONF_ACC_NAME = 'Local'
# These will be set in app.gui_interface.
idlequeue = None # type: nbxmpp.idlequeue.IdleQueue
socks5queue = None
gupnp_igd = None
gsound_ctx = None
_dependencies = {
'AVAHI': False,
'PYBONJOUR': False,
'FARSTREAM': False,
'GST': False,
'AV': False,
'GEOCLUE': False,
'UPNP': False,
'GSOUND': False,
'GSPELL': False,
'IDLE': False,
}
_tasks = defaultdict(list) # type: Dict[int, List[Any]]
def print_version():
log('gajim').info('Gajim Version: %s', gajim.__version__)
def get_client(account):
return connections[account]
def is_installed(dependency):
if dependency == 'ZEROCONF':
# Alias for checking zeroconf libs
return _dependencies['AVAHI'] or _dependencies['PYBONJOUR']
return _dependencies[dependency]
def is_flatpak():
return gajim.IS_FLATPAK
def is_portable():
return gajim.IS_PORTABLE
def is_display(display):
# XWayland reports as Display X11, so try with env var
is_wayland = os.environ.get('XDG_SESSION_TYPE') == 'wayland'
if is_wayland and display == Display.WAYLAND:
return True
default = Gdk.Display.get_default()
if default is None:
log('gajim').warning('Could not determine window manager')
return False
return default.__class__.__name__ == display.value
def disable_dependency(dependency):
_dependencies[dependency] = False
def detect_dependencies():
import gi
# ZEROCONF
try:
import pybonjour # pylint: disable=unused-import
_dependencies['PYBONJOUR'] = True
except Exception:
pass
try:
gi.require_version('Avahi', '0.6')
from gi.repository import Avahi # pylint: disable=unused-import
_dependencies['AVAHI'] = True
except Exception:
pass
try:
gi.require_version('Gst', '1.0')
from gi.repository import Gst
_dependencies['GST'] = True
except Exception:
pass
try:
gi.require_version('Farstream', '0.2')
from gi.repository import Farstream
_dependencies['FARSTREAM'] = True
except Exception:
pass
try:
if _dependencies['GST'] and _dependencies['FARSTREAM']:
Gst.init(None)
conference = Gst.ElementFactory.make('fsrtpconference', None)
conference.new_session(Farstream.MediaType.AUDIO)
from gajim.gui.gstreamer import create_gtk_widget
sink, _, _ = create_gtk_widget()
if sink is not None:
_dependencies['AV'] = True
except Exception as error:
log('gajim').warning('AV dependency test failed: %s', error)
# GEOCLUE
try:
gi.require_version('Geoclue', '2.0')
from gi.repository import Geoclue # pylint: disable=unused-import
_dependencies['GEOCLUE'] = True
except (ImportError, ValueError):
pass
# UPNP
try:
gi.require_version('GUPnPIgd', '1.0')
from gi.repository import GUPnPIgd
global gupnp_igd
gupnp_igd = GUPnPIgd.SimpleIgd()
_dependencies['UPNP'] = True
except ValueError:
pass
# IDLE
try:
from gajim.common import idle
if idle.Monitor.is_available():
_dependencies['IDLE'] = True
except Exception:
pass
# GSOUND
try:
gi.require_version('GSound', '1.0')
from gi.repository import GLib
from gi.repository import GSound
global gsound_ctx
gsound_ctx = GSound.Context()
try:
gsound_ctx.init()
_dependencies['GSOUND'] = True
except GLib.Error as error:
log('gajim').warning('GSound init failed: %s', error)
except (ImportError, ValueError):
pass
# GSPELL
try:
gi.require_version('Gspell', '1')
from gi.repository import Gspell
langs = Gspell.language_get_available()
for lang in langs:
log('gajim').info('%s (%s) dict available',
lang.get_name(), lang.get_code())
if langs:
_dependencies['GSPELL'] = True
except (ImportError, ValueError):
pass
# Print results
for dep, val in _dependencies.items():
log('gajim').info('%-13s %s', dep, val)
log('gajim').info('Used language: %s', LANG)
def detect_desktop_env():
if sys.platform in ('win32', 'darwin'):
return sys.platform
desktop = os.environ.get('XDG_CURRENT_DESKTOP')
if desktop is None:
return None
if 'gnome' in desktop.lower():
return 'gnome'
return desktop
desktop_env = detect_desktop_env()
def get_an_id():
return str(uuid.uuid4())
def get_nick_from_jid(jid):
pos = jid.find('@')
return jid[:pos]
def get_server_from_jid(jid):
pos = jid.find('@') + 1 # after @
return jid[pos:]
def get_name_and_server_from_jid(jid):
name = get_nick_from_jid(jid)
server = get_server_from_jid(jid)
return name, server
def get_room_and_nick_from_fjid(jid):
# fake jid is the jid for a contact in a room
# gaim@conference.jabber.no/nick/nick-continued
# return ('gaim@conference.jabber.no', 'nick/nick-continued')
l = jid.split('/', 1)
if len(l) == 1: # No nick
l.append('')
return l
def get_real_jid_from_fjid(account, fjid):
"""
Return real jid or returns None, if we don't know the real jid
"""
room_jid, nick = get_room_and_nick_from_fjid(fjid)
if not nick: # It's not a fake_jid, it is a real jid
return fjid # we return the real jid
real_jid = fjid
if interface.msg_win_mgr.get_gc_control(room_jid, account):
# It's a pm, so if we have real jid it's in contact.jid
gc_contact = contacts.get_gc_contact(account, room_jid, nick)
if not gc_contact:
return
# gc_contact.jid is None when it's not a real jid (we don't know real jid)
real_jid = gc_contact.jid
return real_jid
def get_room_from_fjid(jid):
return get_room_and_nick_from_fjid(jid)[0]
def get_contact_name_from_jid(account, jid):
c = contacts.get_first_contact_from_jid(account, jid)
return c.name
def get_jid_without_resource(jid):
return jid.split('/')[0]
def construct_fjid(room_jid, nick):
# fake jid is the jid for a contact in a room
# gaim@conference.jabber.org/nick
return room_jid + '/' + nick
def get_resource_from_jid(jid):
jids = jid.split('/', 1)
if len(jids) > 1:
return jids[1] # abc@doremi.org/res/res-continued
return ''
def get_number_of_accounts():
"""
Return the number of ALL accounts
"""
return len(connections.keys())
def get_number_of_connected_accounts(accounts_list=None):
"""
Returns the number of CONNECTED accounts. Uou can optionally pass an
accounts_list and if you do those will be checked, else all will be checked
"""
connected_accounts = 0
if accounts_list is None:
accounts = connections.keys()
else:
accounts = accounts_list
for account in accounts:
if account_is_connected(account):
connected_accounts = connected_accounts + 1
return connected_accounts
def get_available_clients():
clients = []
for client in connections.values():
if client.state.is_available:
clients.append(client)
return clients
def get_connected_accounts(exclude_local=False):
"""
Returns a list of CONNECTED accounts
"""
account_list = []
for account in connections:
if account == 'Local' and exclude_local:
continue
if account_is_connected(account):
account_list.append(account)
return account_list
def get_accounts_sorted():
'''
Get all accounts alphabetically sorted with Local first
'''
account_list = settings.get_accounts()
account_list.sort(key=str.lower)
if 'Local' in account_list:
account_list.remove('Local')
account_list.insert(0, 'Local')
return account_list
def get_enabled_accounts_with_labels(exclude_local=True, connected_only=False,
private_storage_only=False):
"""
Returns a list with [account, account_label] entries.
Order by account_label
"""
accounts = []
for acc in connections:
if exclude_local and account_is_zeroconf(acc):
continue
if connected_only and not account_is_connected(acc):
continue
if private_storage_only and not account_supports_private_storage(acc):
continue
accounts.append([acc, get_account_label(acc)])
accounts.sort(key=lambda xs: str.lower(xs[1]))
return accounts
def get_account_label(account):
return settings.get_account_setting(account, 'account_label') or account
def account_is_zeroconf(account):
return connections[account].is_zeroconf
def account_supports_private_storage(account):
# If Delimiter module is not available we can assume
# Private Storage is not available
return connections[account].get_module('Delimiter').available
def account_is_connected(account):
if account not in connections:
return False
return (connections[account].state.is_connected or
connections[account].state.is_available)
def account_is_available(account):
if account not in connections:
return False
return connections[account].state.is_available
def account_is_disconnected(account):
return not account_is_connected(account)
def zeroconf_is_connected():
return account_is_connected(ZEROCONF_ACC_NAME) and \
settings.get_account_setting(ZEROCONF_ACC_NAME, 'is_zeroconf')
def in_groupchat(account, room_jid):
room_jid = str(room_jid)
if room_jid not in gc_connected[account]:
return False
return gc_connected[account][room_jid]
def get_transport_name_from_jid(jid, use_config_setting=True):
"""
Returns 'gg', 'irc' etc
If JID is not from transport returns None.
"""
#FIXME: jid can be None! one TB I saw had this problem:
# in the code block # it is a groupchat presence in handle_event_notify
# jid was None. Yann why?
if not jid or (use_config_setting and not config.get('use_transports_iconsets')):
return
host = get_server_from_jid(jid)
if host in transport_type:
return transport_type[host]
# host is now f.e. icq.foo.org or just icq (sometimes on hacky transports)
host_splitted = host.split('.')
if host_splitted:
# now we support both 'icq.' and 'icq' but not icqsucks.org
host = host_splitted[0]
if host in ('irc', 'icq', 'sms', 'weather', 'mrim', 'facebook'):
return host
if host == 'gg':
return 'gadu-gadu'
if host == 'jit':
return 'icq'
if host == 'facebook':
return 'facebook'
return None
def jid_is_transport(jid):
# if not '@' or '@' starts the jid then it is transport
if jid.find('@') <= 0:
return True
return False
def get_jid_from_account(account_name):
"""
Return the jid we use in the given account
"""
name = settings.get_account_setting(account_name, 'name')
hostname = settings.get_account_setting(account_name, 'hostname')
jid = name + '@' + hostname
return jid
def get_account_from_jid(jid):
for account in settings.get_accounts():
if jid == get_jid_from_account(account):
return account
def get_our_jids():
"""
Returns a list of the jids we use in our accounts
"""
our_jids = list()
for account in contacts.get_accounts():
our_jids.append(get_jid_from_account(account))
return our_jids
def get_hostname_from_account(account_name, use_srv=False):
"""
Returns hostname (if custom hostname is used, that is returned)
"""
if use_srv and connections[account_name].connected_hostname:
return connections[account_name].connected_hostname
if settings.get_account_setting(account_name, 'use_custom_host'):
return settings.get_account_setting(account_name, 'custom_host')
return settings.get_account_setting(account_name, 'hostname')
def get_notification_image_prefix(jid):
"""
Returns the prefix for the notification images
"""
transport_name = get_transport_name_from_jid(jid)
if transport_name in ('icq', 'facebook'):
prefix = transport_name
else:
prefix = 'jabber'
return prefix
def get_name_from_jid(account, jid):
"""
Return from JID's shown name and if no contact returns jids
"""
contact = contacts.get_first_contact_from_jid(account, jid)
if contact:
actor = contact.get_shown_name()
else:
actor = jid
return actor
def get_recent_groupchats(account):
recent_groupchats = settings.get_account_setting(
account, 'recent_groupchats').split()
RecentGroupchat = namedtuple('RecentGroupchat',
['room', 'server', 'nickname'])
recent_list = []
for groupchat in recent_groupchats:
jid = nbxmpp.JID.from_string(groupchat)
recent = RecentGroupchat(jid.localpart, jid.domain, jid.resource)
recent_list.append(recent)
return recent_list
def add_recent_groupchat(account, room_jid, nickname):
recent = settings.get_account_setting(
account, 'recent_groupchats').split()
full_jid = room_jid + '/' + nickname
if full_jid in recent:
recent.remove(full_jid)
recent.insert(0, full_jid)
if len(recent) > 10:
recent = recent[0:9]
config_value = ' '.join(recent)
settings.set_account_setting(account, 'recent_groupchats', config_value)
def get_priority(account, show):
"""
Return the priority an account must have
"""
if not show:
show = 'online'
if show in ('online', 'chat', 'away', 'xa', 'dnd') and \
settings.get_account_setting(account, 'adjust_priority_with_status'):
prio = settings.get_account_setting(account, 'autopriority_' + show)
else:
prio = settings.get_account_setting(account, 'priority')
if prio < -128:
prio = -128
elif prio > 127:
prio = 127
return prio
def log(domain):
if domain != 'gajim':
domain = 'gajim.%s' % domain
return logging.getLogger(domain)
def prefers_app_menu():
if sys.platform == 'darwin':
return True
if sys.platform == 'win32':
return False
return app.prefers_app_menu()
def load_css_config():
global css_config
from gajim.gui.css_config import CSSConfig
css_config = CSSConfig()
def set_debug_mode(enable: bool) -> None:
debug_folder = configpaths.get('DEBUG')
debug_enabled = debug_folder / 'debug-enabled'
if enable:
debug_enabled.touch()
else:
if debug_enabled.exists():
debug_enabled.unlink()
def get_debug_mode() -> bool:
debug_folder = configpaths.get('DEBUG')
debug_enabled = debug_folder / 'debug-enabled'
return debug_enabled.exists()
def get_stored_bob_data(algo_hash: str) -> Optional[bytes]:
try:
return bob_cache[algo_hash]
except KeyError:
filepath = configpaths.get('BOB') / algo_hash
if filepath.exists():
with open(str(filepath), 'r+b') as file:
data = file.read()
return data
return None
def get_groupchat_control(account, jid):
control = app.interface.msg_win_mgr.get_gc_control(jid, account)
if control is not None:
return control
try:
return app.interface.minimized_controls[account][jid]
except Exception:
return None
def register_task(self, task):
_tasks[id(self)].append(task)
def remove_task(task, id_):
try:
_tasks[id_].remove(task)
except Exception:
pass
else:
if not _tasks[id_]:
del _tasks[id_]
def cancel_tasks(obj):
id_ = id(obj)
if id_ not in _tasks:
return
task_list = _tasks[id_]
for task in task_list:
task.cancel()

View file

@ -0,0 +1,89 @@
# 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 logging
from gi.repository import GLib
from gi.repository import Gio
from gajim.common import configpaths
from gajim.common.helpers import get_random_string
from gajim.common.helpers import write_file_async
log = logging.getLogger('gajim.c.cert_store')
class CertificateStore:
def __init__(self):
self._path = configpaths.get('CERT_STORE')
self._certs = []
self._load_certificates()
def _get_random_path(self):
filename = get_random_string()
path = self._path / filename
if path.exists():
return self._get_random_path()
return path
def _load_certificates(self):
for path in self._path.iterdir():
if path.is_dir():
continue
try:
cert = Gio.TlsCertificate.new_from_file(str(path))
except GLib.Error as error:
log.warning('Can\'t load certificate: %s, %s', path, error)
continue
log.info('Loaded: %s', path.stem)
self._certs.append(cert)
log.info('%s Certificates loaded', len(self._certs))
def get_certificates(self):
return list(self._certs)
def add_certificate(self, certificate):
log.info('Add certificate to trust store')
self._certs.append(certificate)
pem = certificate.props.certificate_pem
path = self._get_random_path()
write_file_async(path,
pem.encode(),
self._on_certificate_write_finished,
path)
def verify(self, certificate, tls_errors):
if Gio.TlsCertificateFlags.UNKNOWN_CA in tls_errors:
for trusted_certificate in self._certs:
if trusted_certificate.is_same(certificate):
tls_errors.remove(Gio.TlsCertificateFlags.UNKNOWN_CA)
break
if not tls_errors:
return True
return False
@staticmethod
def _on_certificate_write_finished(data, error, path):
if data is None:
log.error('Can\'t store certificate: %s', error)
return
log.info('Certificate stored: %s', path)

642
gajim/common/client.py Normal file
View file

@ -0,0 +1,642 @@
# 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 logging
import nbxmpp
from nbxmpp.client import Client as NBXMPPClient
from nbxmpp.const import StreamError
from nbxmpp.const import ConnectionType
from gi.repository import GLib
from gajim.common import passwords
from gajim.common.nec import NetworkEvent
from gajim.common import app
from gajim.common import helpers
from gajim.common import modules
from gajim.common.const import ClientState
from gajim.common.helpers import get_custom_host
from gajim.common.helpers import get_user_proxy
from gajim.common.helpers import warn_about_plain_connection
from gajim.common.helpers import get_resource
from gajim.common.helpers import get_idle_status_message
from gajim.common.idle import Monitor
from gajim.common.i18n import _
from gajim.common.connection_handlers import ConnectionHandlers
from gajim.common.connection_handlers_events import MessageSentEvent
from gajim.gui.util import open_window
log = logging.getLogger('gajim.client')
class Client(ConnectionHandlers):
def __init__(self, account):
self._client = None
self._account = account
self.name = account
self._hostname = app.settings.get_account_setting(self._account,
'hostname')
self._user = app.settings.get_account_setting(self._account, 'name')
self.password = None
self._priority = 0
self._connect_machine_calls = 0
self.addressing_supported = False
self.is_zeroconf = False
self.pep = {}
self.roster_supported = True
self._state = ClientState.DISCONNECTED
self._status_sync_on_resume = False
self._status = 'online'
self._status_message = ''
self._idle_status = 'online'
self._idle_status_enabled = True
self._idle_status_message = ''
self._reconnect = True
self._reconnect_timer_source = None
self._destroy_client = False
self._remove_account = False
self._destroyed = False
self.available_transports = {}
modules.register_modules(self)
self._create_client()
if Monitor.is_available():
self._idle_handler_id = Monitor.connect('state-changed',
self._idle_state_changed)
self._screensaver_handler_id = app.app.connect(
'notify::screensaver-active', self._screensaver_state_changed)
ConnectionHandlers.__init__(self)
def _set_state(self, state):
log.info('State: %s', state)
self._state = state
@property
def state(self):
return self._state
@property
def account(self):
return self._account
@property
def status(self):
return self._status
@property
def status_message(self):
if self._idle_status_active():
return self._idle_status_message
return self._status_message
@property
def priority(self):
return self._priority
@property
def certificate(self):
return self._client.peer_certificate[0]
@property
def features(self):
return self._client.features
@property
def local_address(self):
address = self._client.local_address
if address is not None:
return address.to_string().split(':')[0]
return None
def set_remove_account(self, value):
# Used by the RemoveAccount Assistant to make the Client
# not react to any stream errors that happen while the
# account is removed by the server and the connection is killed
self._remove_account = value
def _create_client(self):
if self._destroyed:
# If we disable an account cleanup() is called and all
# modules are unregistered. Because disable_account() does not wait
# for the client to properly disconnect, handlers of the
# nbxmpp.Client() are emitted after we called cleanup().
# After nbxmpp.Client() disconnects and is destroyed we create a
# new instance with this method but modules.get_handlers() fails
# because modules are already unregistered.
# TODO: Make this nicer
return
log.info('Create new nbxmpp client')
self._client = NBXMPPClient(log_context=self._account)
self.connection = self._client
self._client.set_domain(self._hostname)
self._client.set_username(self._user)
self._client.set_resource(get_resource(self._account))
pass_saved = app.settings.get_account_setting(self._account, 'savepass')
if pass_saved:
# Request password from keyring only if the user chose to save
# his password
self.password = passwords.get_password(self._account)
self._client.set_password(self.password)
self._client.set_accepted_certificates(
app.cert_store.get_certificates())
self._client.subscribe('resume-failed', self._on_resume_failed)
self._client.subscribe('resume-successful', self._on_resume_successful)
self._client.subscribe('disconnected', self._on_disconnected)
self._client.subscribe('connection-failed', self._on_connection_failed)
self._client.subscribe('connected', self._on_connected)
self._client.subscribe('stanza-sent', self._on_stanza_sent)
self._client.subscribe('stanza-received', self._on_stanza_received)
for handler in modules.get_handlers(self):
self._client.register_handler(handler)
def _on_resume_failed(self, _client, _signal_name):
log.info('Resume failed')
app.nec.push_incoming_event(NetworkEvent(
'our-show', account=self._account, show='offline'))
self.get_module('Chatstate').enabled = False
def _on_resume_successful(self, _client, _signal_name):
self._set_state(ClientState.CONNECTED)
self._set_client_available()
if self._status_sync_on_resume:
self._status_sync_on_resume = False
self.update_presence()
else:
# Normally show is updated when we receive a presence reflection.
# On resume, if show has not changed while offline, we dont send
# a new presence so we have to trigger the event here.
app.nec.push_incoming_event(
NetworkEvent('our-show',
account=self._account,
show=self._status))
def _set_client_available(self):
self._set_state(ClientState.AVAILABLE)
app.nec.push_incoming_event(NetworkEvent('account-connected',
account=self._account))
def disconnect(self, gracefully, reconnect, destroy_client=False):
if self._state.is_disconnecting:
log.warning('Disconnect already in progress')
return
self._set_state(ClientState.DISCONNECTING)
self._reconnect = reconnect
self._destroy_client = destroy_client
log.info('Starting to disconnect %s', self._account)
self._client.disconnect(immediate=not gracefully)
def _on_disconnected(self, _client, _signal_name):
log.info('Disconnect %s', self._account)
self._set_state(ClientState.DISCONNECTED)
domain, error, text = self._client.get_error()
if self._remove_account:
# Account was removed via RemoveAccount Assistant.
self._reconnect = False
elif domain == StreamError.BAD_CERTIFICATE:
self._reconnect = False
self._destroy_client = True
cert, errors = self._client.peer_certificate
open_window('SSLErrorDialog',
account=self._account,
client=self,
cert=cert,
error=errors.pop())
elif domain in (StreamError.STREAM, StreamError.BIND):
if error == 'conflict':
# Reset resource
app.settings.set_account_setting(self._account,
'resource',
'gajim.$rand')
elif domain == StreamError.SASL:
self._reconnect = False
self._destroy_client = True
if error in ('not-authorized', 'no-password'):
def _on_password(password):
self.password = password
self._client.set_password(password)
self._prepare_for_connect()
app.nec.push_incoming_event(NetworkEvent(
'password-required', conn=self, on_password=_on_password))
app.nec.push_incoming_event(
NetworkEvent('simple-notification',
account=self._account,
type_='connection-failed',
title=_('Authentication failed'),
text=text or error))
if self._reconnect:
self._after_disconnect()
self._schedule_reconnect()
app.nec.push_incoming_event(
NetworkEvent('our-show', account=self._account, show='error'))
else:
self.get_module('Chatstate').enabled = False
app.nec.push_incoming_event(NetworkEvent(
'our-show', account=self._account, show='offline'))
self._after_disconnect()
def _after_disconnect(self):
self._disable_reconnect_timer()
self.get_module('VCardAvatars').avatar_advertised = False
app.proxy65_manager.disconnect(self._client)
self.terminate_sessions()
self.get_module('Bytestream').remove_all_transfers()
if self._destroy_client:
self._client.destroy()
self._client = None
self._destroy_client = False
self._create_client()
app.nec.push_incoming_event(NetworkEvent('account-disconnected',
account=self._account))
def _on_connection_failed(self, _client, _signal_name):
self._schedule_reconnect()
def _on_connected(self, _client, _signal_name):
self._set_state(ClientState.CONNECTED)
self.get_module('MUC').get_manager().reset_state()
self.get_module('Discovery').discover_server_info()
self.get_module('Discovery').discover_account_info()
self.get_module('Discovery').discover_server_items()
self.get_module('Chatstate').enabled = True
self.get_module('MAM').reset_state()
def _on_stanza_sent(self, _client, _signal_name, stanza):
app.nec.push_incoming_event(NetworkEvent('stanza-sent',
account=self._account,
stanza=stanza))
def _on_stanza_received(self, _client, _signal_name, stanza):
app.nec.push_incoming_event(NetworkEvent('stanza-received',
account=self._account,
stanza=stanza))
def get_own_jid(self):
"""
Return the last full JID we received on a bind event.
In case we were never connected it returns the bare JID from config.
"""
if self._client is not None:
jid = self._client.get_bound_jid()
if jid is not None:
return jid
# This returns the bare jid
return nbxmpp.JID.from_string(app.get_jid_from_account(self._account))
def change_status(self, show, message):
if not message:
message = ''
self._idle_status_enabled = show == 'online'
self._status_message = message
if show != 'offline':
self._status = show
if self._state.is_disconnecting:
log.warning('Can\'t change status while '
'disconnect is in progress')
return
if self._state.is_disconnected:
if show == 'offline':
return
self._prepare_for_connect()
return
if self._state.is_connecting:
if show == 'offline':
self.disconnect(gracefully=False,
reconnect=False,
destroy_client=True)
return
if self._state.is_reconnect_scheduled:
if show == 'offline':
self._destroy_client = True
self._abort_reconnect()
else:
self._prepare_for_connect()
return
# We are connected
if show == 'offline':
self.set_user_activity(None)
self.set_user_mood(None)
self.set_user_tune(None)
self.set_user_location(None)
presence = self.get_module('Presence').get_presence(
typ='unavailable',
status=message,
caps=False)
self.send_stanza(presence)
self.disconnect(gracefully=True,
reconnect=False,
destroy_client=True)
return
self.update_presence()
def update_presence(self, include_muc=True):
status, message, idle = self.get_presence_state()
self._priority = app.get_priority(self._account, status)
self.get_module('Presence').send_presence(
priority=self._priority,
show=status,
status=message,
idle_time=idle)
if include_muc:
self.get_module('MUC').update_presence()
def set_user_activity(self, activity):
self.get_module('UserActivity').set_activity(activity)
def set_user_mood(self, mood):
self.get_module('UserMood').set_mood(mood)
def set_user_tune(self, tune):
self.get_module('UserTune').set_tune(tune)
def set_user_location(self, location):
self.get_module('UserLocation').set_location(location)
def get_module(self, name):
return modules.get(self._account, name)
@helpers.call_counter
def connect_machine(self):
log.info('Connect machine state: %s', self._connect_machine_calls)
if self._connect_machine_calls == 1:
self.get_module('MetaContacts').get_metacontacts()
elif self._connect_machine_calls == 2:
self.get_module('Delimiter').get_roster_delimiter()
elif self._connect_machine_calls == 3:
self.get_module('Roster').request_roster()
elif self._connect_machine_calls == 4:
self._finish_connect()
def _finish_connect(self):
self._status_sync_on_resume = False
self._set_client_available()
# We did not resume the stream, so we are not joined any MUCs
self.update_presence(include_muc=False)
self.get_module('Bookmarks').request_bookmarks()
self.get_module('SoftwareVersion').set_enabled(True)
self.get_module('Annotations').request_annotations()
self.get_module('Blocking').get_blocking_list()
# Inform GUI we just signed in
app.nec.push_incoming_event(NetworkEvent(
'signed-in', account=self._account, conn=self))
modules.send_stored_publish(self._account)
def send_stanza(self, stanza):
"""
Send a stanza untouched
"""
return self._client.send_stanza(stanza)
def send_message(self, message):
if not self._state.is_available:
log.warning('Trying to send message while offline')
return
stanza = self.get_module('Message').build_message_stanza(message)
message.stanza = stanza
if message.contact is None:
# Only Single Message should have no contact
self._send_message(message)
return
method = message.contact.settings.get('encryption')
if not method:
self._send_message(message)
return
# TODO: Make extension point return encrypted message
extension = 'encrypt'
if message.is_groupchat:
extension = 'gc_encrypt'
app.plugin_manager.extension_point(extension + method,
self,
message,
self._send_message)
def _send_message(self, message):
message.set_sent_timestamp()
message.message_id = self.send_stanza(message.stanza)
app.nec.push_incoming_event(
MessageSentEvent(None, jid=message.jid, **vars(message)))
if message.is_groupchat:
return
self.get_module('Message').log_message(message)
def send_messages(self, jids, message):
if not self._state.is_available:
log.warning('Trying to send message while offline')
return
for jid in jids:
message = message.copy()
message.contact = app.contacts.create_contact(jid, message.account)
stanza = self.get_module('Message').build_message_stanza(message)
message.stanza = stanza
self._send_message(message)
def _prepare_for_connect(self):
custom_host = get_custom_host(self._account)
if custom_host is not None:
self._client.set_custom_host(*custom_host)
gssapi = app.settings.get_account_setting(self._account,
'enable_gssapi')
if gssapi:
self._client.set_mechs(['GSSAPI'])
anonymous = app.settings.get_account_setting(self._account,
'anonymous_auth')
if anonymous:
self._client.set_mechs(['ANONYMOUS'])
if app.settings.get_account_setting(self._account,
'use_plain_connection'):
self._client.set_connection_types([ConnectionType.PLAIN])
proxy = get_user_proxy(self._account)
if proxy is not None:
self._client.set_proxy(proxy)
self.connect()
def connect(self, ignored_tls_errors=None):
if self._state not in (ClientState.DISCONNECTED,
ClientState.RECONNECT_SCHEDULED):
# Do not try to reco while we are already trying
return
log.info('Connect')
self._client.set_ignored_tls_errors(ignored_tls_errors)
self._reconnect = True
self._disable_reconnect_timer()
self._set_state(ClientState.CONNECTING)
if warn_about_plain_connection(self._account,
self._client.connection_types):
app.nec.push_incoming_event(NetworkEvent(
'plain-connection',
account=self._account,
connect=self._client.connect,
abort=self._abort_reconnect))
return
self._client.connect()
def _schedule_reconnect(self):
self._set_state(ClientState.RECONNECT_SCHEDULED)
log.info("Reconnect to %s in 3s", self._account)
self._reconnect_timer_source = GLib.timeout_add_seconds(
3, self._prepare_for_connect)
def _abort_reconnect(self):
self._set_state(ClientState.DISCONNECTED)
self._disable_reconnect_timer()
app.nec.push_incoming_event(
NetworkEvent('our-show', account=self._account, show='offline'))
if self._destroy_client:
self._client.destroy()
self._client = None
self._destroy_client = False
self._create_client()
def _disable_reconnect_timer(self):
if self._reconnect_timer_source is not None:
GLib.source_remove(self._reconnect_timer_source)
self._reconnect_timer_source = None
def _idle_state_changed(self, monitor):
state = monitor.state.value
if monitor.is_awake():
self._idle_status = state
self._idle_status_message = ''
self._update_status()
return
if not app.settings.get(f'auto{state}'):
return
if (state in ('away', 'xa') and self._status == 'online' or
state == 'xa' and self._idle_status == 'away'):
self._idle_status = state
self._idle_status_message = get_idle_status_message(
state, self._status_message)
self._update_status()
def _update_status(self):
if not self._idle_status_enabled:
return
self._status = self._idle_status
if self._state.is_available:
self.update_presence()
else:
self._status_sync_on_resume = True
def _idle_status_active(self):
if not Monitor.is_available():
return False
if not self._idle_status_enabled:
return False
return self._idle_status != 'online'
def get_presence_state(self):
if self._idle_status_active():
return self._idle_status, self._idle_status_message, True
return self._status, self._status_message, False
@staticmethod
def _screensaver_state_changed(application, _param):
active = application.get_property('screensaver-active')
Monitor.set_extended_away(active)
def cleanup(self):
self._destroyed = True
if Monitor.is_available():
Monitor.disconnect(self._idle_handler_id)
app.app.disconnect(self._screensaver_handler_id)
if self._client is not None:
# cleanup() is called before nbmxpp.Client has disconnected,
# when we disable the account. So we need to unregister
# handlers here.
# TODO: cleanup() should not be called before disconnect is finished
for handler in modules.get_handlers(self):
self._client.unregister_handler(handler)
modules.unregister_modules(self)
def quit(self, kill_core):
if kill_core and self._state in (ClientState.CONNECTING,
ClientState.CONNECTED,
ClientState.AVAILABLE):
self.disconnect(gracefully=True, reconnect=False)

643
gajim/common/config.py Normal file
View file

@ -0,0 +1,643 @@
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2004-2005 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2005 Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Alex Mauer <hawke AT hawkesnest.net>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2005-2007 Travis Shirk <travis AT pobox.com>
# Copyright (C) 2006 Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 James Newton <redshodan AT gmail.com>
# Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 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/>.
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
from typing import Tuple # pylint: disable=unused-import
import re
import copy
from enum import IntEnum, unique
from gi.repository import GLib
import gajim
from gajim.common.i18n import _
@unique
class Option(IntEnum):
TYPE = 0
VAL = 1
DESC = 2
# If Option.RESTART is True - we need restart to use our changed option
# Option.DESC also should be there
RESTART = 3
opt_int = ['integer', 0]
opt_str = ['string', 0]
opt_bool = ['boolean', 0]
opt_color = ['color', r'(#[0-9a-fA-F]{6})|rgb\(\d+,\d+,\d+\)|rgba\(\d+,\d+,\d+,[01]\.?\d*\)']
class Config:
DEFAULT_ICONSET = 'dcraven'
DEFAULT_MOOD_ICONSET = 'default'
DEFAULT_ACTIVITY_ICONSET = 'default'
__options = ({
# name: [ type, default_value, help_string, restart ]
'autopopup': [opt_bool, False],
'autopopupaway': [opt_bool, False],
'sounddnd': [opt_bool, False, _('Play sound even when being busy.')],
'showoffline': [opt_bool, True],
'show_only_chat_and_online': [opt_bool, False, _('Show only online and free for chat contacts in the contact list.')],
'show_transports_group': [opt_bool, True],
'autoaway': [opt_bool, True],
'autoawaytime': [opt_int, 5, _('Time in minutes, after which your status changes to away.')],
'autoaway_message': [opt_str, _('$S (Away: Idle more than $T min)'), _('$S will be replaced by current status message, $T by the \'autoawaytime\' value.')],
'autoxa': [opt_bool, True],
'autoxatime': [opt_int, 15, _('Time in minutes, after which your status changes to not available.')],
'autoxa_message': [opt_str, _('$S (Not available: Idle more than $T min)'), _('$S will be replaced by current status message, $T by the \'autoxatime\' value.')],
'ask_online_status': [opt_bool, False],
'ask_offline_status': [opt_bool, False],
'trayicon': [opt_str, 'always', _('When to show the notification area icon. Can be \'never\', \'on_event\', and \'always\'.'), False],
'allow_hide_roster': [opt_bool, False, _('Allow to hide the contact list window even if the notification area icon is not shown.'), False],
'iconset': [opt_str, DEFAULT_ICONSET, '', True],
'use_transports_iconsets': [opt_bool, True, '', True],
'collapsed_rows': [opt_str, '', _('List of rows (accounts and groups) that are collapsed (space separated).'), True],
'roster_theme': [opt_str, 'default', '', True],
'mergeaccounts': [opt_bool, False, '', True],
'sort_by_show_in_roster': [opt_bool, True, '', True],
'sort_by_show_in_muc': [opt_bool, False, '', True],
'use_speller': [opt_bool, False, ],
'show_xhtml': [opt_bool, True, ],
'speller_language': [opt_str, '', _('Language used for spell checking.')],
'print_time': [opt_str, 'always', _('\'always\' - print time for every message.\n\'sometimes\' - print time every print_ichat_every_foo_minutes minute.\n\'never\' - never print time.')],
'emoticons_theme': [opt_str, 'noto-emoticons', '', True],
'ascii_formatting': [opt_bool, True,
_('Treat * / _ pairs as possible formatting characters.'), True],
'show_ascii_formatting_chars': [opt_bool, True, _('If enabled, do not '
'remove */_ . So *abc* will be bold but with * * not removed.')],
'sounds_on': [opt_bool, True],
'gc_refer_to_nick_char': [opt_str, ',', _('Character to add after nickname when using nickname completion (tab) in group chat.')],
'msgwin-max-state': [opt_bool, False],
'msgwin-x-position': [opt_int, -1], # Default is to let the window manager decide
'msgwin-y-position': [opt_int, -1], # Default is to let the window manager decide
'msgwin-width': [opt_int, 500],
'msgwin-height': [opt_int, 440],
'chat-msgwin-x-position': [opt_int, -1], # Default is to let the window manager decide
'chat-msgwin-y-position': [opt_int, -1], # Default is to let the window manager decide
'chat-msgwin-width': [opt_int, 480],
'chat-msgwin-height': [opt_int, 440],
'gc-msgwin-x-position': [opt_int, -1], # Default is to let the window manager decide
'gc-msgwin-y-position': [opt_int, -1], # Default is to let the window manager decide
'gc-msgwin-width': [opt_int, 600],
'gc-msgwin-height': [opt_int, 440],
'pm-msgwin-x-position': [opt_int, -1], # Default is to let the window manager decide
'pm-msgwin-y-position': [opt_int, -1], # Default is to let the window manager decide
'pm-msgwin-width': [opt_int, 480],
'pm-msgwin-height': [opt_int, 440],
'single-msg-x-position': [opt_int, 0],
'single-msg-y-position': [opt_int, 0],
'single-msg-width': [opt_int, 400],
'single-msg-height': [opt_int, 280],
'save-roster-position': [opt_bool, True, _('If enabled, Gajim will save the contact list window position when hiding it, and restore it when showing the contact list window again.')],
'roster_x-position': [opt_int, 0],
'roster_y-position': [opt_int, 0],
'roster_width': [opt_int, 200],
'roster_height': [opt_int, 400],
'roster_hpaned_position': [opt_int, 200],
'roster_on_the_right': [opt_bool, False, _('Place the contact list on the right in single window mode'), True],
'history_window_width': [opt_int, -1],
'history_window_height': [opt_int, 450],
'history_window_x-position': [opt_int, 0],
'history_window_y-position': [opt_int, 0],
'latest_disco_addresses': [opt_str, ''],
'time_stamp': [opt_str, '%x | %X ', _('This option lets you customize the timestamp that is printed in conversation. For example \'[%H:%M] \' will show \'[hour:minute] \'. See python doc on strftime for full documentation (https://docs.python.org/3/library/time.html#time.strftime).')],
'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.')],
'change_roster_title': [opt_bool, True, _('If enabled, Gajim will add * and [n] in contact list window title.')],
'restore_lines': [opt_int, 10, _('Number of messages from chat history to be restored when a chat tab/window is reopened.')],
'restore_timeout': [opt_int, -1, _('How far back in time (minutes) chat history is restored. -1 means no limit.')],
'send_on_ctrl_enter': [opt_bool, False, _('Send message on Ctrl+Enter and make a new line with Enter.')],
'last_roster_visible': [opt_bool, True],
'key_up_lines': [opt_int, 25, _('How many lines to store for Ctrl+KeyUP (previously sent messages).')],
'version': [opt_str, gajim.__version__], # which version created the config
'search_engine': [opt_str, 'https://duckduckgo.com/?q=%s'],
'dictionary_url': [opt_str, 'WIKTIONARY', _('Either a custom URL with %%s in it (where %%s is the word/phrase) or \'WIKTIONARY\' (which means use Wikitionary).')],
'always_english_wikipedia': [opt_bool, False],
'always_english_wiktionary': [opt_bool, True],
'remote_control': [opt_bool, False, _('If checked, Gajim can be controlled remotely using gajim-remote.'), True],
'print_ichat_every_foo_minutes': [opt_int, 5, _('When not printing time for every message (\'print_time\'==sometimes), print it every x minutes.')],
'confirm_paste_image': [opt_bool, True, _('Ask before pasting an image.')],
'confirm_close_muc': [opt_bool, True, _('Ask before closing a group chat tab/window.')],
'confirm_close_multiple_tabs': [opt_bool, True, _('Ask before closing tabbed chat window if there are chats that can lose data (chat, private chat, group chat that will not be minimized).')],
'notify_on_file_complete': [opt_bool, True],
'file_transfers_port': [opt_int, 28011],
'ft_add_hosts_to_send': [opt_str, '', _('List of send hosts (comma separated) in addition to local interfaces for file transfers (in case of address translation/port forwarding).')],
'use_kib_mib': [opt_bool, False, _('IEC standard says KiB = 1024 bytes, KB = 1000 bytes.')],
'notify_on_all_muc_messages': [opt_bool, False],
'trayicon_notification_on_events': [opt_bool, True, _('Notify of events in the notification area.')],
'last_save_dir': [opt_str, ''],
'last_send_dir': [opt_str, ''],
'last_sounds_dir': [opt_str, ''],
'tabs_position': [opt_str, 'left'],
'tabs_always_visible': [opt_bool, False, _('Show tab when only one conversation?')],
'tabs_border': [opt_bool, False, _('Show tabbed notebook border in chat windows?')],
'tabs_close_button': [opt_bool, True, _('Show close button in tab?')],
'notification_preview_message': [opt_bool, True, _('Preview new messages in notification popup?')],
'notification_position_x': [opt_int, -1],
'notification_position_y': [opt_int, -1],
'muc_highlight_words': [opt_str, '', _('A list of words (semicolon separated) that will be highlighted in group chats.')],
'quit_on_roster_x_button': [opt_bool, False, _('If enabled, Gajim quits when clicking the X button of your Window Manager. This setting is taken into account only if the notification area icon is used.')],
'hide_on_roster_x_button': [opt_bool, False, _('If enabled, Gajim hides the contact list window when pressing the X button instead of minimizing into the notification area.')],
'show_status_msgs_in_roster': [opt_bool, True, _('If enabled, Gajim will display the status message (if not empty) underneath the contact name in the contact list window.'), True],
'show_avatars_in_roster': [opt_bool, True, '', True],
'show_mood_in_roster': [opt_bool, True, '', True],
'show_activity_in_roster': [opt_bool, True, '', True],
'show_tunes_in_roster': [opt_bool, True, '', True],
'show_location_in_roster': [opt_bool, True, '', True],
'avatar_position_in_roster': [opt_str, 'right', _('Define the position of avatars in the contact list. Can be \'left\' or \'right\'.'), True],
'print_status_in_chats': [opt_bool, False, _('If disabled, Gajim will no longer print status messages in chats when a contact changes their status (and/or their status message).')],
'print_join_left_default': [opt_bool, False, _('Default Setting: Show a status message for every join or leave in a group chat.')],
'print_status_muc_default': [opt_bool, False, _('Default Setting: Show a status message for all status changes (away, dnd, etc.) of users in a group chat.')],
'log_contact_status_changes': [opt_bool, False],
'roster_window_skip_taskbar': [opt_bool, False, _('Don\'t show contact list window in the system taskbar.')],
'use_urgency_hint': [opt_bool, True, _('If enabled, Gajim makes the window flash (the default behaviour in most Window Managers) when holding pending events.')],
'notification_timeout': [opt_int, 5],
'one_message_window': [opt_str, 'always',
#always, never, peracct, pertype should not be translated
_('Controls the window where new messages are placed.\n\'always\' - All messages are sent to a single window.\n\'always_with_roster\' - Like \'always\' but the messages are in a single window along with the contact list.\n\'never\' - All messages get their own window.\n\'peracct\' - Messages for each account are sent to a specific window.\n\'pertype\' - Each message type (e.g. chats vs. group chats) is sent to a specific window.')],
'show_roster_on_startup':[opt_str, 'always', _('Show contact list window on startup.\n\'always\' - Always show contact list window.\n\'never\' - Never show contact list window.\n\'last_state\' - Restore last state of the contact list window.')],
'escape_key_closes': [opt_bool, False, _('If enabled, pressing Esc closes a tab/window.')],
'hide_groupchat_banner': [opt_bool, False, _('Hides the banner in a group chat window.')],
'hide_chat_banner': [opt_bool, False, _('Hides the banner in a 1:1 chat window.')],
'hide_groupchat_occupants_list': [opt_bool, False, _('Hides the group chat participants list in a group chat window.')],
'chat_merge_consecutive_nickname': [opt_bool, False, _('In a chat, show the nickname at the beginning of a line only when it\'s not the same person talking as in the previous message.')],
'chat_merge_consecutive_nickname_indent': [opt_str, ' ', _('Indentation when using merge consecutive nickname.')],
'ctrl_tab_go_to_next_composing': [opt_bool, True, _('Ctrl+Tab switches to the next composing tab when there are no tabs with messages pending.')],
'confirm_metacontacts': [opt_str, '', _('Show a confirmation dialog to create metacontacts? Empty string means never show the dialog.')],
'confirm_block': [opt_str, '', _('Show a confirmation dialog to block a contact? Empty string means never show the dialog.')],
'enable_negative_priority': [opt_bool, False, _('If enabled, you will be able to set a negative priority to your account in the Accounts window. BE CAREFUL, when you are logged in with a negative priority, you will NOT receive any message from your server.')],
'show_contacts_number': [opt_bool, True, _('If enabled, Gajim will show both the number of online and total contacts in account rows as well as in group rows.')],
'scroll_roster_to_last_message': [opt_bool, True, _('If enabled, Gajim will scroll and select the contact who sent you the last message, if the chat window is not already opened.')],
'change_status_window_timeout': [opt_int, 15, _('Time of inactivity needed before the change status window closes down.')],
'max_conversation_lines': [opt_int, 500, _('Maximum number of lines that are printed in conversations. Oldest lines are cleared.')],
'uri_schemes': [opt_str, 'aaa:// aaas:// acap:// cap:// cid: crid:// data: dav: dict:// dns: fax: file:/ ftp:// geo: go: gopher:// h323: http:// https:// iax: icap:// im: imap:// info: ipp:// iris: iris.beep: iris.xpc: iris.xpcs: iris.lwz: ldap:// mid: modem: msrp:// msrps:// mtqp:// mupdate:// news: nfs:// nntp:// opaquelocktoken: pop:// pres: prospero:// rtsp:// service: sip: sips: sms: snmp:// soap.beep:// soap.beeps:// tag: tel: telnet:// tftp:// thismessage:/ tip:// tv: urn:// vemmi:// xmlrpc.beep:// xmlrpc.beeps:// z39.50r:// z39.50s:// about: apt: cvs:// daap:// ed2k:// feed: fish:// git:// iax2: irc:// ircs:// ldaps:// magnet: mms:// rsync:// ssh:// svn:// sftp:// smb:// webcal:// aesgcm://', _('Valid URI schemes. Only schemes in this list will be accepted as \'real\' URI (mailto and xmpp are handled separately).'), True],
'shell_like_completion': [opt_bool, False, _('If enabled, completion in group chats will be like a shell auto-completion.')],
'audio_input_device': [opt_str, 'autoaudiosrc ! volume name=gajim_vol'],
'audio_output_device': [opt_str, 'autoaudiosink'],
'video_input_device': [opt_str, 'autovideosrc'],
'video_framerate': [opt_str, '', _('Optionally fix Jingle output video framerate. Example: 10/1 or 25/2.')],
'video_size': [opt_str, '', _('Optionally resize Jingle output video. Example: 320x240.')],
'video_see_self': [opt_bool, True, _('If enabled, you will see your webcam\'s video stream as well.')],
'audio_input_volume': [opt_int, 50],
'audio_output_volume': [opt_int, 50],
'use_stun_server': [opt_bool, False, _('If enabled, Gajim will try to use a STUN server when using Jingle. The one in \'stun_server\' option, or the one given by the XMPP server.')],
'stun_server': [opt_str, '', _('STUN server to use when using Jingle')],
'global_proxy': [opt_str, '', _('Proxy used for all outgoing connections if the account does not have a specific proxy configured.')],
'ignore_incoming_attention': [opt_bool, False, _('If enabled, Gajim will ignore incoming attention requests (\'wizz\').')],
'remember_opened_chat_controls': [opt_bool, True, _('If enabled, Gajim will reopen chat windows that were opened last time Gajim was closed.')],
'positive_184_ack': [opt_bool, False, _('If enabled, Gajim will display an icon to show that sent messages have been received by your contact.')],
'use_keyring': [opt_bool, True, _('If enabled, Gajim will use the System\'s Keyring to store account passwords.')],
'remote_commands': [opt_bool, False, _('If enabled, Gajim will execute XEP-0146 Commands.')],
'dark_theme': [opt_int, 2, _('2: System, 1: Enabled, 0: Disabled')],
'public_room_sync_threshold': [opt_int, 1, _('Maximum history in days we request from a public group chat archive. 0: As much as possible.')],
'private_room_sync_threshold': [opt_int, 0, _('Maximum history in days we request from a private group chat archive. 0: As much as possible.')],
'show_subject_on_join': [opt_bool, True, _('If enabled, Gajim shows the group chat subject in the chat window when joining.')],
'show_chatstate_in_roster': [opt_bool, True, _('If enabled, the contact row is colored according to the current chat state of the contact.')],
'show_chatstate_in_tabs': [opt_bool, True, _('If enabled, the tab is colored according to the current chat state of the contact.')],
'show_chatstate_in_banner': [opt_bool, True, _('Shows a text in the banner that describes the current chat state of the contact.')],
'send_chatstate_default': [opt_str, 'composing_only', _('Chat state notifications that are sent to contacts. Possible values: all, composing_only, disabled')],
'send_chatstate_muc_default': [opt_str, 'composing_only', _('Chat state notifications that are sent to the group chat. Possible values: \'all\', \'composing_only\', \'disabled\'')],
'muclumbus_api_jid': [opt_str, 'api@search.jabber.network'],
'muclumbus_api_http_uri': [opt_str, 'https://search.jabber.network/api/1.0/search'],
'muclumbus_api_pref': [opt_str, 'http', _('API Preferences. Possible values: \'http\', \'iq\'')],
'command_system_execute': [opt_bool, False, _('If enabled, Gajim will execute commands (/show, /sh, /execute, /exec).')],
'groupchat_roster_width': [opt_int, 210, _('Width of group chat roster in pixel')],
'dev_force_bookmark_2': [opt_bool, False, _('Force Bookmark 2 usage')],
'show_help_start_chat': [opt_bool, True, _('Shows an info bar with helpful hints in the Start / Join Chat dialog')],
'check_for_update': [opt_bool, True, _('Check for Gajim updates periodically')],
'last_update_check': [opt_str, '', _('Date of the last update check')],
'always_ask_for_status_message': [opt_bool, False],
}, {}) # type: Tuple[Dict[str, List[Any]], Dict[Any, Any]]
__options_per_key = {
'accounts': ({
'name': [opt_str, '', '', True],
'account_label': [opt_str, '', '', False],
'account_color': [opt_color, 'rgb(85, 85, 85)'],
'hostname': [opt_str, '', '', True],
'anonymous_auth': [opt_bool, False],
'avatar_sha': [opt_str, '', '', False],
'client_cert': [opt_str, '', '', True],
'client_cert_encrypted': [opt_bool, False, '', False],
'savepass': [opt_bool, False],
'password': [opt_str, ''],
'resource': [opt_str, 'gajim.$rand', '', True],
'priority': [opt_int, 0, '', True],
'adjust_priority_with_status': [opt_bool, False, _('Priority will change automatically according to your status. Priorities are defined in \'autopriority_*\' options.')],
'autopriority_online': [opt_int, 50],
'autopriority_chat': [opt_int, 50],
'autopriority_away': [opt_int, 40],
'autopriority_xa': [opt_int, 30],
'autopriority_dnd': [opt_int, 20],
'autoconnect': [opt_bool, False, '', True],
'restore_last_status': [opt_bool, False, _('If enabled, the last status will be restored.')],
'autoauth': [opt_bool, False, _('If enabled, contacts requesting authorization will be accepted automatically.')],
'active': [opt_bool, True, _('If disabled, this account will be disabled and will not appear in the contact list window.'), True],
'proxy': [opt_str, '', '', True],
'keyid': [opt_str, '', '', True],
'keyname': [opt_str, '', '', True],
'use_plain_connection': [opt_bool, False, _('Use an unencrypted connection to the server')],
'confirm_unencrypted_connection': [opt_bool, True],
'use_custom_host': [opt_bool, False, '', True],
'custom_port': [opt_int, 5222, '', True],
'custom_host': [opt_str, '', '', True],
'custom_type': [opt_str, 'START TLS', _('ConnectionType: START TLS, DIRECT TLS or PLAIN'), True],
'sync_with_global_status': [opt_bool, False, ],
'no_log_for': [opt_str, '', _('List of XMPP Addresses (space separated) for which you do not want to store chat history. You can also add the name of an account to disable storing chat history for this account.')],
'attached_gpg_keys': [opt_str, ''],
'http_auth': [opt_str, 'ask'], # yes, no, ask
# proxy65 for FT
'file_transfer_proxies': [opt_str, ''],
'use_ft_proxies': [opt_bool, False, _('If enabled, Gajim will use your IP and proxies defined in \'file_transfer_proxies\' option for file transfers.'), True],
'test_ft_proxies_on_startup': [opt_bool, False, _('If enabled, Gajim will test file transfer proxies on startup to be sure they work. Openfire\'s proxies are known to fail this test even if they work.')],
'msgwin-x-position': [opt_int, -1], # Default is to let the wm decide
'msgwin-y-position': [opt_int, -1], # Default is to let the wm decide
'msgwin-width': [opt_int, 480],
'msgwin-height': [opt_int, 440],
'is_zeroconf': [opt_bool, False],
'last_status': [opt_str, 'online'],
'last_status_msg': [opt_str, ''],
'zeroconf_first_name': [opt_str, '', '', True],
'zeroconf_last_name': [opt_str, '', '', True],
'zeroconf_jabber_id': [opt_str, '', '', True],
'zeroconf_email': [opt_str, '', '', True],
'answer_receipts': [opt_bool, True, _('If enabled, Gajim will answer to message receipt requests.')],
'publish_tune': [opt_bool, False],
'publish_location': [opt_bool, False],
'request_user_data': [opt_bool, True],
'ignore_unknown_contacts': [opt_bool, False],
'send_os_info': [opt_bool, True, _('Allow Gajim to send information about the operating system you are running.')],
'send_time_info': [opt_bool, True, _('Allow Gajim to send your local time.')],
'send_idle_time': [opt_bool, True],
'roster_version': [opt_str, ''],
'subscription_request_msg': [opt_str, '', _('Message that is sent to contacts you want to add.')],
'ft_send_local_ips': [opt_bool, True, _('If enabled, Gajim will send your local IP so your contact can connect to your machine for file transfers.')],
'opened_chat_controls': [opt_str, '', _('List of XMPP Addresses (space separated) for which the chat window will be re-opened on next startup.')],
'recent_groupchats': [opt_str, ''],
'filetransfer_preference' : [opt_str, 'httpupload', _('Preferred file transfer mechanism for file drag&drop on a chat window. Can be \'httpupload\' (default) or \'jingle\'.')],
'allow_posh': [opt_bool, True, _('Allow certificate verification with POSH.')],
}, {}),
'statusmsg': ({
'message': [opt_str, ''],
'activity': [opt_str, ''],
'subactivity': [opt_str, ''],
'activity_text': [opt_str, ''],
'mood': [opt_str, ''],
'mood_text': [opt_str, ''],
}, {}),
'soundevents': ({
'enabled': [opt_bool, True],
'path': [opt_str, ''],
}, {}),
'proxies': ({
'type': [opt_str, 'http'],
'host': [opt_str, ''],
'port': [opt_int, 3128],
'useauth': [opt_bool, False],
'user': [opt_str, ''],
'pass': [opt_str, ''],
}, {}),
'contacts': ({
'speller_language': [opt_str, '', _('Language used for spell checking.')],
'send_chatstate': [opt_str, 'composing_only', _('Chat state notifications that are sent to contacts. Possible values: \'all\', \'composing_only\', \'disabled\'')],
}, {}),
'encryption': ({
'encryption': [opt_str, '', _('The currently active encryption for that contact.')],
}, {}),
'rooms': ({
'speller_language': [opt_str, '', _('Language used for spell checking.')],
'notify_on_all_messages': [opt_bool, False, _('If enabled, a notification is created for every message in this group chat.')],
'print_status': [opt_bool, False, _('Show a status message for all status changes (away, dnd, etc.) of users in a group chat.')],
'print_join_left': [opt_bool, False, _('Show a status message for every join or leave in a group chat.')],
'minimize_on_autojoin': [opt_bool, True, _('If enabled, the group chat is minimized into the contact list when joining automatically.')],
'minimize_on_close': [opt_bool, True, _('If enabled, the group chat is minimized into the contact list when closing it.')],
'send_chatstate': [opt_str, 'composing_only', _('Chat state notifications that are sent to the group chat. Possible values: \'all\', \'composing_only\' or \'disabled\'.')],
}, {}),
'plugins': ({
'active': [opt_bool, False, _('If enabled, plugins will be activated on startup (this is saved when exiting Gajim). This option SHOULD NOT be used to (de)activate plugins. Use the plugin window instead.')],
}, {}),
} # type: Dict[str, Tuple[Dict[str, List[Any]], Dict[Any, Any]]]
statusmsg_default = {
_('Sleeping'): ['ZZZZzzzzzZZZZZ', 'inactive', 'sleeping', '', 'sleepy', ''],
_('Back soon'): [_('Back in some minutes.'), '', '', '', '', ''],
_('Eating'): [_('I\'m eating.'), 'eating', 'other', '', '', ''],
_('Movie'): [_('I\'m watching a movie.'), 'relaxing', 'watching_a_movie', '', '', ''],
_('Working'): [_('I\'m working.'), 'working', 'other', '', '', ''],
_('Phone'): [_('I\'m on the phone.'), 'talking', 'on_the_phone', '', '', ''],
_('Out'): [_('I\'m out enjoying life.'), 'relaxing', 'going_out', '', '', ''],
}
soundevents_default = {
'attention_received': [True, 'attention.wav'],
'first_message_received': [True, 'message1.wav'],
'next_message_received_focused': [True, 'message2.wav'],
'next_message_received_unfocused': [True, 'message2.wav'],
'contact_connected': [False, 'connected.wav'],
'contact_disconnected': [False, 'disconnected.wav'],
'message_sent': [False, 'sent.wav'],
'muc_message_highlight': [True, 'gc_message1.wav', _('Sound to play when a group chat message contains one of the words in \'muc_highlight_words\' or your nickname is mentioned.')],
'muc_message_received': [True, 'gc_message2.wav', _('Sound to play when any group chat message arrives.')],
}
proxies_default = {
_('Tor'): ['socks5', 'localhost', 9050],
}
def foreach(self, cb, data=None):
for opt in self.__options[1]:
cb(data, opt, None, self.__options[1][opt])
for opt in self.__options_per_key:
cb(data, opt, None, None)
dict_ = self.__options_per_key[opt][1]
for opt2 in dict_.keys():
cb(data, opt2, [opt], None)
for opt3 in dict_[opt2]:
cb(data, opt3, [opt, opt2], dict_[opt2][opt3])
def get_children(self, node=None):
"""
Tree-like interface
"""
if node is None:
for child, option in self.__options[1].items():
yield (child, ), option
for grandparent in self.__options_per_key:
yield (grandparent, ), None
elif len(node) == 1:
grandparent, = node
for parent in self.__options_per_key[grandparent][1]:
yield (grandparent, parent), None
elif len(node) == 2:
grandparent, parent = node
children = self.__options_per_key[grandparent][1][parent]
for child, option in children.items():
yield (grandparent, parent, child), option
else:
raise ValueError('Invalid node')
def is_valid_int(self, val):
try:
ival = int(val)
except Exception:
return None
return ival
def is_valid_bool(self, val):
if val == 'True':
return True
if val == 'False':
return False
ival = self.is_valid_int(val)
if ival:
return True
if ival is None:
return None
return False
def is_valid_string(self, val):
return val
def is_valid(self, type_, val):
if not type_:
return None
if type_[0] == 'boolean':
return self.is_valid_bool(val)
if type_[0] == 'integer':
return self.is_valid_int(val)
if type_[0] == 'string':
return self.is_valid_string(val)
if re.match(type_[1], val):
return val
return None
def set(self, optname, value):
if optname not in self.__options[1]:
return
value = self.is_valid(self.__options[0][optname][Option.TYPE], value)
if value is None:
return
self.__options[1][optname] = value
self._timeout_save()
def get(self, optname=None):
if not optname:
return list(self.__options[1].keys())
if optname not in self.__options[1]:
return None
return self.__options[1][optname]
def get_default(self, optname):
if optname not in self.__options[0]:
return None
return self.__options[0][optname][Option.VAL]
def get_type(self, optname):
if optname not in self.__options[0]:
return None
return self.__options[0][optname][Option.TYPE][0]
def get_desc(self, optname):
if optname not in self.__options[0]:
return None
if len(self.__options[0][optname]) > Option.DESC:
return self.__options[0][optname][Option.DESC]
def get_restart(self, optname):
if optname not in self.__options[0]:
return None
if len(self.__options[0][optname]) > Option.RESTART:
return self.__options[0][optname][Option.RESTART]
def add_per(self, typename, name): # per_group_of_option
if typename not in self.__options_per_key:
return
opt = self.__options_per_key[typename]
if name in opt[1]:
# we already have added group name before
return 'you already have added %s before' % name
opt[1][name] = {}
for o in opt[0]:
opt[1][name][o] = opt[0][o][Option.VAL]
self._timeout_save()
def del_per(self, typename, name, subname=None): # per_group_of_option
if typename not in self.__options_per_key:
return
opt = self.__options_per_key[typename]
if subname is None:
del opt[1][name]
# if subname is specified, delete the item in the group.
elif subname in opt[1][name]:
del opt[1][name][subname]
self._timeout_save()
def del_all_per(self, typename, subname):
# Deletes all settings per typename
# Example: Delete `account_label` for all accounts
if typename not in self.__options_per_key:
raise ValueError('typename %s does not exist' % typename)
opt = self.__options_per_key[typename]
for name in opt[1]:
try:
del opt[1][name][subname]
except KeyError:
pass
self._timeout_save()
def set_per(self, optname, key, subname, value): # per_group_of_option
if optname not in self.__options_per_key:
return
if not key:
return
dict_ = self.__options_per_key[optname][1]
if key not in dict_:
self.add_per(optname, key)
obj = dict_[key]
if subname not in obj:
return
typ = self.__options_per_key[optname][0][subname][Option.TYPE]
value = self.is_valid(typ, value)
if value is None:
return
obj[subname] = value
self._timeout_save()
def get_per(self, optname, key=None, subname=None, default=None): # per_group_of_option
if optname not in self.__options_per_key:
return None
dict_ = self.__options_per_key[optname][1]
if not key:
return list(dict_.keys())
if key not in dict_:
if default is not None:
return default
if subname in self.__options_per_key[optname][0]:
return self.__options_per_key[optname][0][subname][1]
return None
obj = dict_[key]
if not subname:
return obj
if subname not in obj:
return None
return obj[subname]
def get_all(self):
return copy.deepcopy(self.__options[1])
def get_all_per(self, optname):
return copy.deepcopy(self.__options_per_key[optname][1])
def get_default_per(self, optname, subname):
if optname not in self.__options_per_key:
return None
dict_ = self.__options_per_key[optname][0]
if subname not in dict_:
return None
return dict_[subname][Option.VAL]
def get_type_per(self, optname, subname):
if optname not in self.__options_per_key:
return None
dict_ = self.__options_per_key[optname][0]
if subname not in dict_:
return None
return dict_[subname][Option.TYPE][0]
def get_desc_per(self, optname, subname=None):
if optname not in self.__options_per_key:
return None
dict_ = self.__options_per_key[optname][0]
if subname not in dict_:
return None
obj = dict_[subname]
if len(obj) > Option.DESC:
return obj[Option.DESC]
return None
def get_restart_per(self, optname, key=None, subname=None):
if optname not in self.__options_per_key:
return False
dict_ = self.__options_per_key[optname][0]
if not key:
return False
if key not in dict_:
return False
obj = dict_[key]
if not subname:
return False
if subname not in obj:
return False
if len(obj[subname]) > Option.RESTART:
return obj[subname][Option.RESTART]
return False
def get_options(self, optname, return_type=str):
options = self.get(optname).split(',')
options = [return_type(option.strip()) for option in options]
return options
def _init_options(self):
for opt in self.__options[0]:
self.__options[1][opt] = self.__options[0][opt][Option.VAL]
if gajim.IS_PORTABLE:
self.__options[1]['use_keyring'] = False
def _really_save(self):
from gajim.common import app
if app.interface:
app.interface.save_config()
self.save_timeout_id = None
return False
def _timeout_save(self):
if self.save_timeout_id:
return
self.save_timeout_id = GLib.timeout_add(1000, self._really_save)
def __init__(self):
#init default values
self._init_options()
self.save_timeout_id = None
for event in self.soundevents_default:
default = self.soundevents_default[event]
self.add_per('soundevents', event)
self.set_per('soundevents', event, 'enabled', default[0])
self.set_per('soundevents', event, 'path', default[1])

234
gajim/common/configpaths.py Normal file
View file

@ -0,0 +1,234 @@
# Copyright (C) 2006 Jean-Marie Traissard <jim AT lapin.org>
# Junglecow J <junglecow AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Brendan Taylor <whateley AT gmail.com>
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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/>.
from typing import Dict # pylint: disable=unused-import
from typing import List
from typing import Generator
from typing import Optional # pylint: disable=unused-import
from typing import Tuple
import os
import sys
import tempfile
from pathlib import Path
from gi.repository import GLib
import gajim
from gajim.common.i18n import _
from gajim.common.const import PathType, PathLocation
from gajim.common.types import PathTuple
def get(key: str) -> Path:
return _paths[key]
def get_plugin_dirs() -> List[Path]:
if gajim.IS_FLATPAK:
return [Path(_paths['PLUGINS_BASE']),
Path('/app/plugins')]
return [Path(_paths['PLUGINS_BASE']),
Path(_paths['PLUGINS_USER'])]
def get_paths(type_: PathType) -> Generator[Path, None, None]:
for key, value in _paths.items():
path_type = value[2]
if type_ != path_type:
continue
yield _paths[key]
def override_path(*args, **kwargs):
_paths.add(*args, **kwargs)
def set_separation(active: bool) -> None:
_paths.profile_separation = active
def set_profile(profile: str) -> None:
_paths.profile = profile
def set_config_root(config_root: str) -> None:
_paths.custom_config_root = Path(config_root).resolve()
def init() -> None:
_paths.init()
def create_paths() -> None:
for path in get_paths(PathType.FOLDER):
if path.is_file():
print(_('%s is a file but it should be a directory') % path)
print(_('Gajim will now exit'))
sys.exit()
if not path.exists():
for parent_path in reversed(path.parents):
# Create all parent folders
# don't use mkdir(parent=True), as it ignores `mode`
# when creating the parents
if not parent_path.exists():
print(('creating %s directory') % parent_path)
parent_path.mkdir(mode=0o700)
print(('creating %s directory') % path)
path.mkdir(mode=0o700)
class ConfigPaths:
def __init__(self) -> None:
self._paths = {} # type: Dict[str, PathTuple]
self.profile = ''
self.profile_separation = False
self.custom_config_root = None # type: Optional[Path]
if os.name == 'nt':
if gajim.IS_PORTABLE:
application_path = Path(sys.executable).parent
self.config_root = self.cache_root = self.data_root = \
application_path.parent / 'UserData'
else:
# Documents and Settings\[User Name]\Application Data\Gajim
self.config_root = self.cache_root = self.data_root = \
Path(os.environ['appdata']) / 'Gajim'
else:
self.config_root = Path(GLib.get_user_config_dir()) / 'gajim'
self.cache_root = Path(GLib.get_user_cache_dir()) / 'gajim'
self.data_root = Path(GLib.get_user_data_dir()) / 'gajim'
if sys.version_info < (3, 9):
import pkg_resources
basedir = Path(pkg_resources.resource_filename("gajim", "."))
else:
import importlib.resources
basedir = importlib.resources.files('gajim')
source_paths = [
('DATA', basedir / 'data'),
('STYLE', basedir / 'data' / 'style'),
('EMOTICONS', basedir / 'data' / 'emoticons'),
('GUI', basedir / 'data' / 'gui'),
('ICONS', basedir / 'data' / 'icons'),
('HOME', Path.home()),
('PLUGINS_BASE', basedir / 'data' / 'plugins'),
]
for path in source_paths:
self.add(*path)
def __getitem__(self, key: str) -> Path:
location, path, _ = self._paths[key]
if location == PathLocation.CONFIG:
return self.config_root / path
if location == PathLocation.CACHE:
return self.cache_root / path
if location == PathLocation.DATA:
return self.data_root / path
return path
def items(self) -> Generator[Tuple[str, PathTuple], None, None]:
for key, value in self._paths.items():
yield (key, value)
def _prepare(self, path: Path, unique: bool) -> Path:
if os.name == 'nt':
path = Path(str(path).capitalize())
if self.profile:
if unique or self.profile_separation:
return Path(f'{path}.{self.profile}')
return path
def add(self,
name: str,
path: Path,
location: PathLocation = None,
path_type: PathType = None,
unique: bool = False) -> None:
if location is not None:
path = self._prepare(path, unique)
self._paths[name] = (location, path, path_type)
def init(self):
if self.custom_config_root:
self.config_root = self.custom_config_root
self.cache_root = self.data_root = self.custom_config_root
user_dir_paths = [
('TMP', Path(tempfile.gettempdir())),
('MY_CONFIG', Path(), PathLocation.CONFIG, PathType.FOLDER),
('MY_CACHE', Path(), PathLocation.CACHE, PathType.FOLDER),
('MY_DATA', Path(), PathLocation.DATA, PathType.FOLDER),
]
for path in user_dir_paths:
self.add(*path)
# These paths are unique per profile
unique_profile_paths = [
# Data paths
('SECRETS_FILE', 'secrets', PathLocation.DATA, PathType.FILE),
('MY_PEER_CERTS', 'certs', PathLocation.DATA, PathType.FOLDER),
('CERT_STORE', 'cert_store', PathLocation.DATA, PathType.FOLDER),
('DEBUG', 'debug', PathLocation.DATA, PathType.FOLDER),
('PLUGINS_DATA', 'plugins_data', PathLocation.DATA, PathType.FOLDER),
# Config paths
('SETTINGS', 'settings.sqlite', PathLocation.CONFIG, PathType.FILE),
('CONFIG_FILE', 'config', PathLocation.CONFIG, PathType.FILE),
('PLUGINS_CONFIG_DIR',
'pluginsconfig', PathLocation.CONFIG, PathType.FOLDER),
('MY_CERT', 'localcerts', PathLocation.CONFIG, PathType.FOLDER),
]
for path in unique_profile_paths:
self.add(*path, unique=True)
# These paths are only unique per profile if the commandline arg
# `separate` is passed
paths = [
# Data paths
('LOG_DB', 'logs.db', PathLocation.DATA, PathType.FILE),
('PLUGINS_DOWNLOAD', 'plugins_download', PathLocation.CACHE, PathType.FOLDER),
('PLUGINS_USER', 'plugins', PathLocation.DATA, PathType.FOLDER),
('MY_EMOTS',
'emoticons', PathLocation.DATA, PathType.FOLDER_OPTIONAL),
('MY_ICONSETS',
'iconsets', PathLocation.DATA, PathType.FOLDER_OPTIONAL),
# Cache paths
('CACHE_DB', 'cache.db', PathLocation.CACHE, PathType.FILE),
('AVATAR', 'avatars', PathLocation.CACHE, PathType.FOLDER),
('BOB', 'bob', PathLocation.CACHE, PathType.FOLDER),
# Config paths
('MY_THEME', 'theme', PathLocation.CONFIG, PathType.FOLDER),
]
for path in paths:
self.add(*path)
_paths = ConfigPaths()

201
gajim/common/connection.py Normal file
View file

@ -0,0 +1,201 @@
# Copyright (C) 2003-2005 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
# Stéphan Kochen <stephan AT kochen.nl>
# Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
# Stefan Bethge <stefan AT lanpartei.de>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org>
# Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# 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 time
import logging
from gajim.common import helpers
from gajim.common import app
from gajim.common import idle
from gajim.common import modules
from gajim.common.nec import NetworkEvent
from gajim.common.const import ClientState
log = logging.getLogger('gajim.c.connection')
SERVICE_START_TLS = 'xmpp-client'
SERVICE_DIRECT_TLS = 'xmpps-client'
class CommonConnection:
"""
Common connection class, can be derived for normal connection or zeroconf
connection
"""
def __init__(self, name):
self.name = name
self._modules = {}
self.connection = None # xmpppy ClientCommon instance
self.is_zeroconf = False
self.password = None
self.server_resource = helpers.get_resource(self.name)
self.priority = app.get_priority(name, 'offline')
self.time_to_reconnect = None
self._reconnect_timer_source = None
self._state = ClientState.DISCONNECTED
self._status = 'offline'
self._status_message = ''
# If handlers have been registered
self.handlers_registered = False
self.pep = {}
self.roster_supported = True
self._stun_servers = [] # STUN servers of our jabber server
# Tracks the calls of the connect_machine() method
self._connect_machine_calls = 0
self.get_config_values_or_default()
def _set_state(self, state):
log.info('State: %s', state)
self._state = state
@property
def state(self):
return self._state
@property
def status(self):
return self._status
@property
def status_message(self):
return self._status_message
def _register_new_handlers(self, con):
for handler in modules.get_handlers(self):
if len(handler) == 5:
name, func, typ, ns, priority = handler
con.RegisterHandler(name, func, typ, ns, priority=priority)
else:
con.RegisterHandler(*handler)
self.handlers_registered = True
def _unregister_new_handlers(self, con):
if not con:
return
for handler in modules.get_handlers(self):
if len(handler) > 4:
handler = handler[:4]
con.UnregisterHandler(*handler)
self.handlers_registered = False
def dispatch(self, event, data):
"""
Always passes account name as first param
"""
app.ged.raise_event(event, self.name, data)
def get_module(self, name):
return modules.get(self.name, name)
def reconnect(self):
"""
To be implemented by derived classes
"""
raise NotImplementedError
def quit(self, kill_core):
if kill_core and app.account_is_connected(self.name):
self.disconnect(reconnect=False)
def new_account(self, name, config, sync=False):
"""
To be implemented by derived classes
"""
raise NotImplementedError
def _on_new_account(self, con=None, con_type=None):
"""
To be implemented by derived classes
"""
raise NotImplementedError
def _event_dispatcher(self, realm, event, data):
if realm == '':
if event == 'STANZA RECEIVED':
app.nec.push_incoming_event(
NetworkEvent('stanza-received',
conn=self,
stanza_str=str(data)))
elif event == 'DATA SENT':
app.nec.push_incoming_event(
NetworkEvent('stanza-sent',
conn=self,
stanza_str=str(data)))
def change_status(self, show, msg, auto=False):
if not msg:
msg = ''
self._status = show
self._status_message = msg
if self._state.is_disconnected:
if show == 'offline':
return
self.server_resource = helpers.get_resource(self.name)
self.connect_and_init(show, msg)
return
if self._state.is_connecting or self._state.is_reconnect_scheduled:
if show == 'offline':
self.disconnect(reconnect=False)
elif self._state.is_reconnect_scheduled:
self.reconnect()
return
# We are connected
if show == 'offline':
presence = self.get_module('Presence').get_presence(
typ='unavailable',
status=msg,
caps=False)
self.connection.send(presence, now=True)
self.disconnect(reconnect=False)
return
idle_time = None
if auto:
if app.is_installed('IDLE') and app.settings.get('autoaway'):
idle_sec = idle.Monitor.get_idle_sec()
idle_time = time.strftime(
'%Y-%m-%dT%H:%M:%SZ',
time.gmtime(time.time() - idle_sec))
self._update_status(show, msg, idle_time=idle_time)

View file

@ -0,0 +1,181 @@
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
# Junglecow J <junglecow AT gmail.com>
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Jean-Marie Traissard <jim AT lapin.org>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 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 logging
import operator
import nbxmpp
from gajim.common import app
from gajim.common.connection_handlers_events import PresenceReceivedEvent
from gajim.common.connection_handlers_events import NotificationEvent
log = logging.getLogger('gajim.c.connection_handlers')
# basic connection handlers used here and in zeroconf
class ConnectionHandlersBase:
def __init__(self):
# keep track of sessions this connection has with other JIDs
self.sessions = {}
def get_sessions(self, jid):
"""
Get all sessions for the given full jid
"""
if not app.interface.is_pm_contact(jid, self.name):
jid = app.get_jid_without_resource(jid)
try:
return list(self.sessions[jid].values())
except KeyError:
return []
def get_or_create_session(self, fjid, thread_id):
"""
Return an existing session between this connection and 'jid', returns a
new one if none exist
"""
pm = True
jid = fjid
if not app.interface.is_pm_contact(fjid, self.name):
pm = False
jid = app.get_jid_without_resource(fjid)
session = self.find_session(jid, thread_id)
if session:
return session
if pm:
return self.make_new_session(fjid, thread_id, type_='pm')
return self.make_new_session(fjid, thread_id)
def find_session(self, jid, thread_id):
try:
if not thread_id:
return self.find_null_session(jid)
return self.sessions[jid][thread_id]
except KeyError:
return None
def terminate_sessions(self):
self.sessions = {}
def delete_session(self, jid, thread_id):
if not jid in self.sessions:
jid = app.get_jid_without_resource(jid)
if not jid in self.sessions:
return
del self.sessions[jid][thread_id]
if not self.sessions[jid]:
del self.sessions[jid]
def find_null_session(self, jid):
"""
Find all of the sessions between us and a remote jid in which we haven't
received a thread_id yet and returns the session that we last sent a
message to
"""
sessions = list(self.sessions[jid].values())
# sessions that we haven't received a thread ID in
idless = [s for s in sessions if not s.received_thread_id]
# filter out everything except the default session type
chat_sessions = [s for s in idless if isinstance(s,
app.default_session_type)]
if chat_sessions:
# return the session that we last sent a message in
return sorted(chat_sessions,
key=operator.attrgetter('last_send'))[-1]
return None
def get_latest_session(self, jid):
"""
Get the session that we last sent a message to
"""
if jid not in self.sessions:
return None
sessions = self.sessions[jid].values()
if not sessions:
return None
return sorted(sessions, key=operator.attrgetter('last_send'))[-1]
def find_controlless_session(self, jid, resource=None):
"""
Find an active session that doesn't have a control attached
"""
try:
sessions = list(self.sessions[jid].values())
# filter out everything except the default session type
chat_sessions = [s for s in sessions if isinstance(s,
app.default_session_type)]
orphaned = [s for s in chat_sessions if not s.control]
if resource:
orphaned = [s for s in orphaned if s.resource == resource]
return orphaned[0]
except (KeyError, IndexError):
return None
def make_new_session(self, jid, thread_id=None, type_='chat', cls=None):
"""
Create and register a new session
thread_id=None to generate one.
type_ should be 'chat' or 'pm'.
"""
if not cls:
cls = app.default_session_type
sess = cls(self, nbxmpp.JID.from_string(jid), thread_id, type_)
# determine if this session is a pm session
# if not, discard the resource so that all sessions are stored bare
if type_ != 'pm':
jid = app.get_jid_without_resource(jid)
if not jid in self.sessions:
self.sessions[jid] = {}
self.sessions[jid][sess.thread_id] = sess
return sess
class ConnectionHandlers(ConnectionHandlersBase):
def __init__(self):
ConnectionHandlersBase.__init__(self)
app.nec.register_incoming_event(PresenceReceivedEvent)
app.nec.register_incoming_event(NotificationEvent)

View file

@ -0,0 +1,435 @@
# Copyright (C) 2010-2014 Yann Leboulanger <asterix AT lagaule.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/>.
# pylint: disable=no-init
# pylint: disable=attribute-defined-outside-init
import logging
from nbxmpp.namespaces import Namespace
from gajim.common import nec
from gajim.common import helpers
from gajim.common import app
from gajim.common import i18n
from gajim.common.i18n import _
from gajim.common.jingle_transport import JingleTransportSocks5
from gajim.common.file_props import FilesProp
log = logging.getLogger('gajim.c.connection_handlers_events')
class PresenceReceivedEvent(nec.NetworkIncomingEvent):
name = 'presence-received'
class OurShowEvent(nec.NetworkIncomingEvent):
name = 'our-show'
def init(self):
self.reconnect = False
class MessageSentEvent(nec.NetworkIncomingEvent):
name = 'message-sent'
class ConnectionLostEvent(nec.NetworkIncomingEvent):
name = 'connection-lost'
def generate(self):
app.nec.push_incoming_event(OurShowEvent(None, conn=self.conn,
show='offline'))
return True
class FileRequestReceivedEvent(nec.NetworkIncomingEvent):
name = 'file-request-received'
def init(self):
self.jingle_content = None
self.FT_content = None
def generate(self):
self.id_ = self.stanza.getID()
self.fjid = self.conn.get_module('Bytestream')._ft_get_from(self.stanza)
self.jid = app.get_jid_without_resource(self.fjid)
if not self.jingle_content:
return
secu = self.jingle_content.getTag('security')
self.FT_content.use_security = bool(secu)
if secu:
fingerprint = secu.getTag('fingerprint')
if fingerprint:
self.FT_content.x509_fingerprint = fingerprint.getData()
if not self.FT_content.transport:
self.FT_content.transport = JingleTransportSocks5()
self.FT_content.transport.set_our_jid(
self.FT_content.session.ourjid)
self.FT_content.transport.set_connection(
self.FT_content.session.connection)
sid = self.stanza.getTag('jingle').getAttr('sid')
self.file_props = FilesProp.getNewFileProp(self.conn.name, sid)
self.file_props.transport_sid = self.FT_content.transport.sid
self.FT_content.file_props = self.file_props
self.FT_content.transport.set_file_props(self.file_props)
self.file_props.streamhosts.extend(
self.FT_content.transport.remote_candidates)
for host in self.file_props.streamhosts:
host['initiator'] = self.FT_content.session.initiator
host['target'] = self.FT_content.session.responder
self.file_props.session_type = 'jingle'
self.file_props.stream_methods = Namespace.BYTESTREAM
desc = self.jingle_content.getTag('description')
if self.jingle_content.getAttr('creator') == 'initiator':
file_tag = desc.getTag('file')
self.file_props.sender = self.fjid
self.file_props.receiver = self.conn.get_own_jid()
else:
file_tag = desc.getTag('file')
h = file_tag.getTag('hash')
h = h.getData() if h else None
n = file_tag.getTag('name')
n = n.getData() if n else None
pjid = app.get_jid_without_resource(self.fjid)
file_info = self.conn.get_module('Jingle').get_file_info(
pjid, hash_=h, name=n, account=self.conn.name)
self.file_props.file_name = file_info['file-name']
self.file_props.sender = self.conn.get_own_jid()
self.file_props.receiver = self.fjid
self.file_props.type_ = 's'
for child in file_tag.getChildren():
name = child.getName()
val = child.getData()
if val is None:
continue
if name == 'name':
self.file_props.name = val
if name == 'size':
self.file_props.size = int(val)
if name == 'hash':
self.file_props.algo = child.getAttr('algo')
self.file_props.hash_ = val
if name == 'date':
self.file_props.date = val
self.file_props.request_id = self.id_
file_desc_tag = file_tag.getTag('desc')
if file_desc_tag is not None:
self.file_props.desc = file_desc_tag.getData()
self.file_props.transfered_size = []
return True
class NotificationEvent(nec.NetworkIncomingEvent):
name = 'notification'
base_network_events = ['decrypted-message-received',
'gc-message-received',
'presence-received']
def generate(self):
# what's needed to compute output
self.account = self.base_event.conn.name
self.conn = self.base_event.conn
self.jid = ''
self.control = None
self.control_focused = False
self.first_unread = False
# For output
self.do_sound = False
self.sound_file = ''
self.sound_event = '' # gajim sound played if not sound_file is set
self.show_popup = False
self.do_popup = False
self.popup_title = ''
self.popup_text = ''
self.popup_event_type = ''
self.popup_msg_type = ''
self.icon_name = None
self.transport_name = None
self.show = None
self.popup_timeout = -1
self.do_command = False
self.command = ''
self.show_in_notification_area = False
self.show_in_roster = False
self.detect_type()
if self.notif_type == 'msg':
self.handle_incoming_msg_event(self.base_event)
elif self.notif_type == 'gc-msg':
self.handle_incoming_gc_msg_event(self.base_event)
elif self.notif_type == 'pres':
self.handle_incoming_pres_event(self.base_event)
return True
def detect_type(self):
if self.base_event.name == 'decrypted-message-received':
self.notif_type = 'msg'
if self.base_event.name == 'gc-message-received':
self.notif_type = 'gc-msg'
if self.base_event.name == 'presence-received':
self.notif_type = 'pres'
def handle_incoming_msg_event(self, msg_obj):
# don't alert for carbon copied messages from ourselves
if msg_obj.properties.is_sent_carbon:
return
if not msg_obj.msgtxt:
return
self.jid = msg_obj.jid
if msg_obj.properties.is_muc_pm:
self.jid = msg_obj.fjid
self.control = app.interface.msg_win_mgr.search_control(
msg_obj.jid, self.account, msg_obj.resource)
if self.control is None:
event_type = msg_obj.properties.type.value
if msg_obj.properties.is_muc_pm:
event_type = 'pm'
if len(app.events.get_events(
self.account, msg_obj.jid, [event_type])) <= 1:
self.first_unread = True
else:
self.control_focused = self.control.has_focus()
if msg_obj.properties.is_muc_pm:
nick = msg_obj.resource
else:
nick = app.get_name_from_jid(self.conn.name, self.jid)
if self.first_unread:
self.sound_event = 'first_message_received'
elif self.control_focused:
self.sound_event = 'next_message_received_focused'
else:
self.sound_event = 'next_message_received_unfocused'
if app.settings.get('notification_preview_message'):
self.popup_text = msg_obj.msgtxt
if self.popup_text and (self.popup_text.startswith('/me ') or \
self.popup_text.startswith('/me\n')):
self.popup_text = '* ' + nick + self.popup_text[3:]
else:
# We don't want message preview, do_preview = False
self.popup_text = ''
if msg_obj.properties.is_muc_pm:
self.popup_msg_type = 'pm'
self.popup_event_type = _('New Private Message')
else: # chat message
self.popup_msg_type = 'chat'
self.popup_event_type = _('New Message')
num_unread = len(app.events.get_events(self.conn.name, self.jid,
['printed_' + self.popup_msg_type, self.popup_msg_type]))
self.popup_title = i18n.ngettext(
'New message from %(nickname)s',
'%(n_msgs)i unread messages from %(nickname)s',
num_unread) % {'nickname': nick, 'n_msgs': num_unread}
if app.settings.get('show_notifications'):
if self.first_unread or not self.control_focused:
if app.settings.get('autopopupaway'):
# always show notification
self.do_popup = True
if app.connections[self.conn.name].status in ('online', 'chat'):
# we're online or chat
self.do_popup = True
if msg_obj.properties.attention and not app.settings.get(
'ignore_incoming_attention'):
self.popup_timeout = 0
self.do_popup = True
else:
self.popup_timeout = app.settings.get('notification_timeout')
sound = app.settings.get_soundevent_settings('attention_received')
if msg_obj.properties.attention and not app.settings.get(
'ignore_incoming_attention') and sound['enabled']:
self.sound_event = 'attention_received'
self.do_sound = True
elif self.first_unread and helpers.allow_sound_notification(
self.conn.name, 'first_message_received'):
self.do_sound = True
elif not self.first_unread and self.control_focused and \
helpers.allow_sound_notification(self.conn.name,
'next_message_received_focused'):
self.do_sound = True
elif not self.first_unread and not self.control_focused and \
helpers.allow_sound_notification(self.conn.name,
'next_message_received_unfocused'):
self.do_sound = True
def handle_incoming_gc_msg_event(self, msg_obj):
if not msg_obj.gc_control:
# we got a message from a room we're not in? ignore it
return
self.jid = msg_obj.jid
sound = msg_obj.gc_control.highlighting_for_message(
msg_obj.msgtxt, msg_obj.properties.timestamp)[1]
nick = msg_obj.properties.muc_nickname
if nick == msg_obj.gc_control.nick:
# A message from ourself
return
self.do_sound = True
if sound == 'received':
self.sound_event = 'muc_message_received'
elif sound == 'highlight':
self.sound_event = 'muc_message_highlight'
else:
self.do_sound = False
self.control = app.interface.msg_win_mgr.search_control(
msg_obj.jid, self.account)
if self.control is not None:
self.control_focused = self.control.has_focus()
if app.settings.get('show_notifications'):
contact = app.contacts.get_groupchat_contact(self.account,
self.jid)
notify_for_muc = sound == 'highlight' or contact.can_notify()
if not notify_for_muc:
self.do_popup = False
elif self.control_focused:
self.do_popup = False
elif app.settings.get('autopopupaway'):
# always show notification
self.do_popup = True
elif app.connections[self.conn.name].status in ('online', 'chat'):
# we're online or chat
self.do_popup = True
self.popup_msg_type = 'gc_msg'
self.popup_event_type = _('New Group Chat Message')
if app.settings.get('notification_preview_message'):
self.popup_text = msg_obj.msgtxt
if self.popup_text and (self.popup_text.startswith('/me ') or
self.popup_text.startswith('/me\n')):
self.popup_text = '* ' + nick + self.popup_text[3:]
type_events = ['printed_marked_gc_msg', 'printed_gc_msg']
count = len(app.events.get_events(self.account, self.jid, type_events))
contact = app.contacts.get_contact(self.account, self.jid)
self.popup_title = i18n.ngettext(
'New message from %(nickname)s',
'%(n_msgs)i unread messages in %(groupchat_name)s',
count) % {'nickname': nick,
'n_msgs': count,
'groupchat_name': contact.get_shown_name()}
def handle_incoming_pres_event(self, pres_obj):
if app.jid_is_transport(pres_obj.jid):
return True
account = pres_obj.conn.name
self.jid = pres_obj.jid
resource = pres_obj.resource or ''
# It isn't an agent
for c in pres_obj.contact_list:
if c.resource == resource:
# we look for other connected resources
continue
if c.show not in ('offline', 'error'):
return True
# no other resource is connected, let's look in metacontacts
family = app.contacts.get_metacontacts_family(account, self.jid)
for info in family:
acct_ = info['account']
jid_ = info['jid']
c_ = app.contacts.get_contact_with_highest_priority(acct_, jid_)
if not c_:
continue
if c_.jid == self.jid:
continue
if c_.show not in ('offline', 'error'):
return True
if pres_obj.old_show < 2 and pres_obj.new_show > 1:
event = 'contact_connected'
server = app.get_server_from_jid(self.jid)
account_server = account + '/' + server
block_transport = False
if account_server in app.block_signed_in_notifications and \
app.block_signed_in_notifications[account_server]:
block_transport = True
sound = app.settings.get_soundevent_settings('contact_connected')
if sound['enabled'] and not app.block_signed_in_notifications[account] and\
not block_transport and helpers.allow_sound_notification(account,
'contact_connected'):
self.sound_event = event
self.do_sound = True
elif pres_obj.old_show > 1 and pres_obj.new_show < 2:
event = 'contact_disconnected'
sound = app.settings.get_soundevent_settings('contact_disconnected')
if sound['enabled'] and helpers.allow_sound_notification(account, event):
self.sound_event = event
self.do_sound = True
# Status change (not connected/disconnected or error (<1))
elif pres_obj.new_show > 1:
event = 'status_change'
else:
return True
if app.jid_is_transport(self.jid):
self.transport_name = app.get_transport_name_from_jid(self.jid)
self.show = pres_obj.show
self.popup_timeout = app.settings.get('notification_timeout')
nick = i18n.direction_mark + app.get_name_from_jid(account, self.jid)
if event == 'status_change':
self.popup_title = _('%(nick)s Changed Status') % \
{'nick': nick}
self.popup_text = _('%(nick)s is now %(status)s') % \
{'nick': nick, 'status': helpers.get_uf_show(pres_obj.show)}
if pres_obj.status:
self.popup_text = self.popup_text + " : " + pres_obj.status
self.popup_event_type = _('Contact Changed Status')
class InformationEvent(nec.NetworkIncomingEvent):
name = 'information'
def init(self):
self.args = None
self.kwargs = {}
self.dialog_name = None
self.popup = True
def generate(self):
if self.args is None:
self.args = ()
else:
self.args = (self.args,)
return True

1115
gajim/common/const.py Normal file

File diff suppressed because it is too large Load diff

991
gajim/common/contacts.py Normal file
View file

@ -0,0 +1,991 @@
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Lukas Petrovicky <lukas AT petrovicky.net>
# Tomasz Melcer <liori AT exroot.org>
# Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# 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/>.
from functools import partial
try:
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.account import Account
from gajim import common
from gajim.common.const import Chatstate
except ImportError as e:
if __name__ != "__main__":
raise ImportError(str(e))
class ContactSettings:
def __init__(self, account, jid):
self.get = partial(app.settings.get_contact_setting, account, jid)
self.set = partial(app.settings.set_contact_setting, account, jid)
class GroupChatSettings:
def __init__(self, account, jid):
self.get = partial(app.settings.get_group_chat_setting, account, jid)
self.set = partial(app.settings.set_group_chat_setting, account, jid)
class XMPPEntity:
"""
Base representation of entities in XMPP
"""
def __init__(self, jid, account, resource):
self.jid = jid
self.resource = resource
self.account = account
class CommonContact(XMPPEntity):
def __init__(self, jid, account, resource, show, presence, status, name,
chatstate):
XMPPEntity.__init__(self, jid, account, resource)
self._show = show
self._presence = presence
self.status = status
self.name = name
# this is contact's chatstate
self._chatstate = chatstate
self._is_pm_contact = False
@property
def show(self):
return self._show
@show.setter
def show(self, value):
self._show = value
@property
def presence(self):
return self._presence
@presence.setter
def presence(self, value):
self._presence = value
@property
def is_available(self):
return self._presence.is_available
@property
def chatstate_enum(self):
return self._chatstate
@property
def chatstate(self):
if self._chatstate is None:
return
return str(self._chatstate)
@chatstate.setter
def chatstate(self, value):
if value is None:
self._chatstate = value
else:
self._chatstate = Chatstate[value.upper()]
@property
def is_gc_contact(self):
return isinstance(self, GC_Contact)
@property
def is_pm_contact(self):
return self._is_pm_contact
@property
def is_groupchat(self):
return False
def get_full_jid(self):
raise NotImplementedError
def get_shown_name(self):
raise NotImplementedError
def supports(self, requested_feature):
"""
Return True if the contact has advertised to support the feature
identified by the given namespace. False otherwise.
"""
if self.show == 'offline':
# Unfortunately, if all resources are offline, the contact
# includes the last resource that was online. Check for its
# show, so we can be sure it's existent. Otherwise, we still
# return caps for a contact that has no resources left.
return False
disco_info = app.storage.cache.get_last_disco_info(self.get_full_jid())
if disco_info is None:
return False
return disco_info.supports(requested_feature)
@property
def uses_phone(self):
disco_info = app.storage.cache.get_last_disco_info(self.get_full_jid())
if disco_info is None:
return False
return disco_info.has_category('phone')
class Contact(CommonContact):
"""
Information concerning a contact
"""
def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0,
chatstate=None, idle_time=None, avatar_sha=None, groupchat=False,
is_pm_contact=False):
if not isinstance(jid, str):
print('no str')
if groups is None:
groups = []
CommonContact.__init__(self, jid, account, resource, show,
None, status, name, chatstate)
self.contact_name = '' # nick chosen by contact
self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
self.avatar_sha = avatar_sha
self._is_groupchat = groupchat
self._is_pm_contact = is_pm_contact
if groupchat:
self.settings = GroupChatSettings(account.name, jid)
else:
self.settings = ContactSettings(account.name, jid)
self.sub = sub
self.ask = ask
self.priority = priority
self.idle_time = idle_time
self.pep = {}
def connect_signal(self, setting, func):
app.settings.connect_signal(
setting, func, self.account.name, self.jid)
def get_full_jid(self):
if self.resource:
return self.jid + '/' + self.resource
return self.jid
def get_shown_name(self):
if self._is_groupchat:
return self._get_groupchat_name()
if self.name:
return self.name
if self.contact_name:
return self.contact_name
return self.jid.split('@')[0]
def _get_groupchat_name(self):
from gajim.common.helpers import get_groupchat_name
con = app.connections[self.account.name]
return get_groupchat_name(con, self.jid)
def get_shown_groups(self):
if self.is_observer():
return [_('Observers')]
if self.is_groupchat:
return [_('Group chats')]
if self.is_transport():
return [_('Transports')]
if not self.groups:
return [_('General')]
return self.groups
def is_hidden_from_roster(self):
"""
If contact should not be visible in roster
"""
# XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
if self.is_transport():
return False
if self.sub in ('both', 'to'):
return False
if self.sub in ('none', 'from') and self.ask == 'subscribe':
return False
if self.sub in ('none', 'from') and (self.name or self.groups):
return False
if _('Not in contact list') in self.groups:
return False
return True
def is_observer(self):
# XEP-0162: http://www.xmpp.org/extensions/xep-0162.html
is_observer = False
if self.sub == 'from' and not self.is_transport()\
and self.is_hidden_from_roster():
is_observer = True
return is_observer
@property
def is_groupchat(self):
return self._is_groupchat
@property
def is_connected(self):
try:
return app.gc_connected[self.account.name][self.jid]
except Exception:
return False
def is_transport(self):
# if not '@' or '@' starts the jid then contact is transport
return self.jid.find('@') <= 0
def can_notify(self):
if not self.is_groupchat:
raise ValueError
all_ = app.settings.get('notify_on_all_muc_messages')
room = self.settings.get('notify_on_all_messages')
return all_ or room
class GC_Contact(CommonContact):
"""
Information concerning each groupchat contact
"""
def __init__(self, room_jid, account, name='', show='', presence=None,
status='', role='', affiliation='', jid='', resource='',
chatstate=None, avatar_sha=None):
CommonContact.__init__(self, jid, account, resource, show,
presence, status, name, chatstate)
self.room_jid = room_jid
self.role = role
self.affiliation = affiliation
self.avatar_sha = avatar_sha
self.settings = ContactSettings(account.name, jid)
def get_full_jid(self):
return self.room_jid + '/' + self.name
def get_shown_name(self):
return self.name
def get_avatar(self, *args, **kwargs):
return common.app.interface.get_avatar(self, *args, **kwargs)
def as_contact(self):
"""
Create a Contact instance from this GC_Contact instance
"""
return Contact(jid=self.get_full_jid(), account=self.account,
name=self.name, groups=[], show=self.show, status=self.status,
sub='none', avatar_sha=self.avatar_sha,
is_pm_contact=True)
class LegacyContactsAPI:
"""
This is a GOD class for accessing contact and groupchat information.
The API has several flaws:
* it mixes concerns because it deals with contacts, groupchats,
groupchat contacts and metacontacts
* some methods like get_contact() may return None. This leads to
a lot of duplication all over Gajim because it is not sure
if we receive a proper contact or just None.
It is a long way to cleanup this API. Therefore just stick with it
and use it as before. We will try to figure out a migration path.
"""
def __init__(self):
self._metacontact_manager = MetacontactManager(self)
self._accounts = {}
def add_account(self, account_name):
self._accounts[account_name] = Account(account_name, Contacts(),
GC_Contacts())
self._metacontact_manager.add_account(account_name)
def get_accounts(self, zeroconf=True):
accounts = list(self._accounts.keys())
if not zeroconf:
if 'Local' in accounts:
accounts.remove('Local')
return accounts
def remove_account(self, account):
del self._accounts[account]
self._metacontact_manager.remove_account(account)
def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0,
chatstate=None, idle_time=None,
avatar_sha=None, groupchat=False):
if groups is None:
groups = []
# Use Account object if available
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,
chatstate=chatstate, idle_time=idle_time, avatar_sha=avatar_sha,
groupchat=groupchat)
def create_self_contact(self, jid, account, resource, show, status, priority,
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,
resource=resource)
self_contact.pep = conn.pep
return self_contact
def create_not_in_roster_contact(self, jid, account, resource='', name='',
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 contact list')], show='not in roster',
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,
chatstate=contact.chatstate_enum,
idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
def add_contact(self, account, contact):
if account not in self._accounts:
self.add_account(account)
return self._accounts[account].contacts.add_contact(contact)
def remove_contact(self, account, contact):
if account not in self._accounts:
return
return self._accounts[account].contacts.remove_contact(contact)
def remove_jid(self, account, jid, remove_meta=True):
self._accounts[account].contacts.remove_jid(jid)
if remove_meta:
self._metacontact_manager.remove_metacontact(account, jid)
def get_groupchat_contact(self, account, jid):
return self._accounts[account].contacts.get_groupchat_contact(jid)
def get_contacts(self, account, jid):
return self._accounts[account].contacts.get_contacts(jid)
def get_contact(self, account, jid, resource=None):
return self._accounts[account].contacts.get_contact(jid, resource=resource)
def get_contact_strict(self, account, jid, resource):
return self._accounts[account].contacts.get_contact_strict(jid, resource)
def get_avatar(self, account, *args, **kwargs):
return self._accounts[account].contacts.get_avatar(*args, **kwargs)
def get_avatar_sha(self, account, jid):
return self._accounts[account].contacts.get_avatar_sha(jid)
def set_avatar(self, account, jid, sha):
self._accounts[account].contacts.set_avatar(jid, sha)
def iter_contacts(self, account):
for contact in self._accounts[account].contacts.iter_contacts():
yield contact
def get_contact_from_full_jid(self, account, fjid):
return self._accounts[account].contacts.get_contact_from_full_jid(fjid)
def get_first_contact_from_jid(self, account, jid):
return self._accounts[account].contacts.get_first_contact_from_jid(jid)
def get_contacts_from_group(self, account, group):
return self._accounts[account].contacts.get_contacts_from_group(group)
def get_contacts_jid_list(self, account):
return self._accounts[account].contacts.get_contacts_jid_list()
def get_jid_list(self, account):
return self._accounts[account].contacts.get_jid_list()
def change_contact_jid(self, old_jid, new_jid, account):
return self._accounts[account].change_contact_jid(old_jid, new_jid)
def get_highest_prio_contact_from_contacts(self, contacts):
if not contacts:
return None
prim_contact = contacts[0]
for contact in contacts[1:]:
if int(contact.priority) > int(prim_contact.priority):
prim_contact = contact
return prim_contact
def get_contact_with_highest_priority(self, account, jid):
contacts = self.get_contacts(account, jid)
if not contacts and '/' in jid:
# jid may be a fake jid, try it
room, nick = jid.split('/', 1)
contact = self.get_gc_contact(account, room, nick)
return contact
return self.get_highest_prio_contact_from_contacts(contacts)
def get_nb_online_total_contacts(self, accounts=None, groups=None):
"""
Return the number of online contacts and the total number of contacts
"""
if not accounts:
accounts = self.get_accounts()
if groups is None:
groups = []
nbr_online = 0
nbr_total = 0
for account in accounts:
our_jid = common.app.get_jid_from_account(account)
for jid in self.get_jid_list(account):
if jid == our_jid:
continue
if (common.app.jid_is_transport(jid) and
_('Transports') not in groups):
# do not count transports
continue
if self.has_brother(account, jid, accounts) and not \
self.is_big_brother(account, jid, accounts):
# count metacontacts only once
continue
contact = self._accounts[account].contacts._contacts[jid][0]
if _('Not in contact list') in contact.groups:
continue
in_groups = False
if groups == []:
in_groups = True
else:
for group in groups:
if group in contact.get_shown_groups():
in_groups = True
break
if in_groups:
if contact.show not in ('offline', 'error'):
nbr_online += 1
nbr_total += 1
return nbr_online, nbr_total
def __getattr__(self, attr_name):
# Only called if self has no attr_name
if hasattr(self._metacontact_manager, attr_name):
return getattr(self._metacontact_manager, attr_name)
raise AttributeError(attr_name)
def create_gc_contact(self, room_jid, account, name='', show='',
presence=None, status='', role='',
affiliation='', jid='', resource='', avatar_sha=None):
account = self._accounts.get(account, account) # Use Account object if available
return GC_Contact(room_jid, account, name, show, presence, status,
role, affiliation, jid, resource, avatar_sha=avatar_sha)
def add_gc_contact(self, account, gc_contact):
return self._accounts[account].gc_contacts.add_gc_contact(gc_contact)
def remove_gc_contact(self, account, gc_contact):
return self._accounts[account].gc_contacts.remove_gc_contact(gc_contact)
def remove_room(self, account, room_jid):
return self._accounts[account].gc_contacts.remove_room(room_jid)
def get_gc_list(self, account):
return self._accounts[account].gc_contacts.get_gc_list()
def get_nick_list(self, account, room_jid):
return self._accounts[account].gc_contacts.get_nick_list(room_jid)
def get_gc_contact_list(self, account, room_jid):
return self._accounts[account].gc_contacts.get_gc_contact_list(room_jid)
def get_gc_contact(self, account, room_jid, nick):
return self._accounts[account].gc_contacts.get_gc_contact(room_jid, nick)
def is_gc_contact(self, account, jid):
return self._accounts[account].gc_contacts.is_gc_contact(jid)
def get_nb_role_total_gc_contacts(self, account, room_jid, role):
return self._accounts[account].gc_contacts.get_nb_role_total_gc_contacts(room_jid, role)
def set_gc_avatar(self, account, room_jid, nick, sha):
contact = self.get_gc_contact(account, room_jid, nick)
if contact is None:
return
contact.avatar_sha = sha
def get_combined_chatstate(self, account, jid):
return self._accounts[account].contacts.get_combined_chatstate(jid)
class Contacts():
"""
This is a breakout of the contact related behavior of the old
Contacts class (which is not called LegacyContactsAPI)
"""
def __init__(self):
# list of contacts {jid1: [C1, C2]}, } one Contact per resource
self._contacts = {}
def add_contact(self, contact):
if contact.jid not in self._contacts or contact.is_groupchat:
self._contacts[contact.jid] = [contact]
return
contacts = self._contacts[contact.jid]
# We had only one that was offline, remove it
if len(contacts) == 1 and contacts[0].show == 'offline':
# Do not use self.remove_contact: it deletes
# self._contacts[account][contact.jid]
contacts.remove(contacts[0])
# If same JID with same resource already exists, use the new one
for c in contacts:
if c.resource == contact.resource:
self.remove_contact(c)
break
contacts.append(contact)
def remove_contact(self, contact):
if contact.jid not in self._contacts:
return
if contact in self._contacts[contact.jid]:
self._contacts[contact.jid].remove(contact)
if not self._contacts[contact.jid]:
del self._contacts[contact.jid]
def remove_jid(self, jid):
"""
Remove all contacts for a given jid
"""
if jid in self._contacts:
del self._contacts[jid]
def get_contacts(self, jid):
"""
Return the list of contact instances for this jid
"""
return list(self._contacts.get(jid, []))
def get_contact(self, jid, resource=None):
### WARNING ###
# This function returns a *RANDOM* resource if resource = None!
# Do *NOT* use if you need to get the contact to which you
# send a message for example, as a bare JID in XMPP means
# highest available resource, which this function ignores!
"""
Return the contact instance for the given resource if it's given else the
first contact is no resource is given or None if there is not
"""
if jid in self._contacts:
if not resource:
return self._contacts[jid][0]
for c in self._contacts[jid]:
if c.resource == resource:
return c
return self._contacts[jid][0]
def get_contact_strict(self, jid, resource):
"""
Return the contact instance for the given resource or None
"""
if jid in self._contacts:
for c in self._contacts[jid]:
if c.resource == resource:
return c
def get_groupchat_contact(self, jid):
if jid in self._contacts:
contacts = self._contacts[jid]
if contacts[0].is_groupchat:
return contacts[0]
def get_avatar(self, jid, size, scale, show=None):
if jid not in self._contacts:
return None
for resource in self._contacts[jid]:
avatar = common.app.interface.get_avatar(
resource, size, scale, show)
if avatar is None:
self.set_avatar(jid, None)
return avatar
def get_avatar_sha(self, jid):
if jid not in self._contacts:
return None
for resource in self._contacts[jid]:
if resource.avatar_sha is not None:
return resource.avatar_sha
return None
def set_avatar(self, jid, sha):
if jid not in self._contacts:
return
for resource in self._contacts[jid]:
resource.avatar_sha = sha
def iter_contacts(self):
for jid in list(self._contacts.keys()):
for contact in self._contacts[jid][:]:
yield contact
def get_jid_list(self):
return list(self._contacts.keys())
def get_contacts_jid_list(self):
return [jid for jid, contact in self._contacts.items() if not
contact[0].is_groupchat]
def get_contact_from_full_jid(self, fjid):
"""
Get Contact object for specific resource of given jid
"""
barejid, resource = common.app.get_room_and_nick_from_fjid(fjid)
return self.get_contact_strict(barejid, resource)
def get_first_contact_from_jid(self, jid):
if jid in self._contacts:
return self._contacts[jid][0]
def get_contacts_from_group(self, group):
"""
Return all contacts in the given group
"""
group_contacts = []
for jid in self._contacts:
contacts = self.get_contacts(jid)
if group in contacts[0].groups:
group_contacts += contacts
return group_contacts
def change_contact_jid(self, old_jid, new_jid):
if old_jid not in self._contacts:
return
self._contacts[new_jid] = []
for _contact in self._contacts[old_jid]:
_contact.jid = new_jid
self._contacts[new_jid].append(_contact)
del self._contacts[old_jid]
def get_combined_chatstate(self, jid):
if jid not in self._contacts:
return
contacts = self._contacts[jid]
states = []
for contact in contacts:
if contact.chatstate_enum is None:
continue
states.append(contact.chatstate_enum)
return str(min(states)) if states else None
class GC_Contacts():
def __init__(self):
# list of contacts that are in gc {room_jid: {nick: C}}}
self._rooms = {}
def add_gc_contact(self, gc_contact):
if gc_contact.room_jid not in self._rooms:
self._rooms[gc_contact.room_jid] = {gc_contact.name: gc_contact}
else:
self._rooms[gc_contact.room_jid][gc_contact.name] = gc_contact
def remove_gc_contact(self, gc_contact):
if gc_contact.room_jid not in self._rooms:
return
if gc_contact.name not in self._rooms[gc_contact.room_jid]:
return
del self._rooms[gc_contact.room_jid][gc_contact.name]
# It was the last nick in room ?
if not self._rooms[gc_contact.room_jid]:
del self._rooms[gc_contact.room_jid]
def remove_room(self, room_jid):
if room_jid in self._rooms:
del self._rooms[room_jid]
def get_gc_list(self):
return self._rooms.keys()
def get_nick_list(self, room_jid):
gc_list = self.get_gc_list()
if not room_jid in gc_list:
return []
return list(self._rooms[room_jid].keys())
def get_gc_contact_list(self, room_jid):
try:
return list(self._rooms[room_jid].values())
except Exception:
return []
def get_gc_contact(self, room_jid, nick):
try:
return self._rooms[room_jid][nick]
except KeyError:
return None
def is_gc_contact(self, jid):
"""
>>> gc = GC_Contacts()
>>> gc._rooms = {'gajim@conference.gajim.org' : {'test' : True}}
>>> gc.is_gc_contact('gajim@conference.gajim.org/test')
True
>>> gc.is_gc_contact('test@jabbim.com')
False
"""
jid = jid.split('/')
if len(jid) != 2:
return False
gcc = self.get_gc_contact(jid[0], jid[1])
return gcc is not None
def get_nb_role_total_gc_contacts(self, room_jid, role):
"""
Return the number of group chat contacts for the given role and the total
number of group chat contacts
"""
if room_jid not in self._rooms:
return 0, 0
nb_role = nb_total = 0
for nick in self._rooms[room_jid]:
if self._rooms[room_jid][nick].role == role:
nb_role += 1
nb_total += 1
return nb_role, nb_total
class MetacontactManager():
def __init__(self, contacts):
self._metacontacts_tags = {}
self._contacts = contacts
def add_account(self, account):
if account not in self._metacontacts_tags:
self._metacontacts_tags[account] = {}
def remove_account(self, account):
del self._metacontacts_tags[account]
def define_metacontacts(self, account, tags_list):
self._metacontacts_tags[account] = tags_list
def _get_new_metacontacts_tag(self, jid):
if not jid in self._metacontacts_tags:
return jid
#FIXME: can this append ?
assert False
def iter_metacontacts_families(self, account):
for tag in self._metacontacts_tags[account]:
family = self._get_metacontacts_family_from_tag(account, tag)
yield family
def _get_metacontacts_tag(self, account, jid):
"""
Return the tag of a jid
"""
if not account in self._metacontacts_tags:
return None
for tag in self._metacontacts_tags[account]:
for data in self._metacontacts_tags[account][tag]:
if data['jid'] == jid:
return tag
return None
def add_metacontact(self, brother_account, brother_jid, account, jid, order=None):
tag = self._get_metacontacts_tag(brother_account, brother_jid)
if not tag:
tag = self._get_new_metacontacts_tag(brother_jid)
self._metacontacts_tags[brother_account][tag] = [{'jid': brother_jid,
'tag': tag}]
if brother_account != account:
con = common.app.connections[brother_account]
con.get_module('MetaContacts').store_metacontacts(
self._metacontacts_tags[brother_account])
# be sure jid has no other tag
old_tag = self._get_metacontacts_tag(account, jid)
while old_tag:
self.remove_metacontact(account, jid)
old_tag = self._get_metacontacts_tag(account, jid)
if tag not in self._metacontacts_tags[account]:
self._metacontacts_tags[account][tag] = [{'jid': jid, 'tag': tag}]
else:
if order:
self._metacontacts_tags[account][tag].append({'jid': jid,
'tag': tag, 'order': order})
else:
self._metacontacts_tags[account][tag].append({'jid': jid,
'tag': tag})
con = common.app.connections[account]
con.get_module('MetaContacts').store_metacontacts(
self._metacontacts_tags[account])
def remove_metacontact(self, account, jid):
if account not in self._metacontacts_tags:
return
found = None
for tag in self._metacontacts_tags[account]:
for data in self._metacontacts_tags[account][tag]:
if data['jid'] == jid:
found = data
break
if found:
self._metacontacts_tags[account][tag].remove(found)
con = common.app.connections[account]
con.get_module('MetaContacts').store_metacontacts(
self._metacontacts_tags[account])
break
def has_brother(self, account, jid, accounts):
tag = self._get_metacontacts_tag(account, jid)
if not tag:
return False
meta_jids = self._get_metacontacts_jids(tag, accounts)
return len(meta_jids) > 1 or len(meta_jids[account]) > 1
def is_big_brother(self, account, jid, accounts):
family = self.get_metacontacts_family(account, jid)
if family:
nearby_family = [data for data in family
if account in accounts]
bb_data = self._get_metacontacts_big_brother(nearby_family)
if bb_data['jid'] == jid and bb_data['account'] == account:
return True
return False
def _get_metacontacts_jids(self, tag, accounts):
"""
Return all jid for the given tag in the form {acct: [jid1, jid2],.}
"""
answers = {}
for account in self._metacontacts_tags:
if tag in self._metacontacts_tags[account]:
if account not in accounts:
continue
answers[account] = []
for data in self._metacontacts_tags[account][tag]:
answers[account].append(data['jid'])
return answers
def get_metacontacts_family(self, account, jid):
"""
Return the family of the given jid, including jid in the form:
[{'account': acct, 'jid': jid, 'order': order}, ] 'order' is optional
"""
tag = self._get_metacontacts_tag(account, jid)
return self._get_metacontacts_family_from_tag(account, tag)
def _get_metacontacts_family_from_tag(self, account, tag):
if not tag:
return []
answers = []
if tag in self._metacontacts_tags[account]:
for data in self._metacontacts_tags[account][tag]:
data['account'] = account
answers.append(data)
return answers
def _metacontact_key(self, data):
"""
Data is {'jid': jid, 'account': account, 'order': order} order is
optional
"""
show_list = ['not in roster', 'error', 'offline', 'dnd',
'xa', 'away', 'chat', 'online', 'requested', 'message']
jid = data['jid']
account = data['account']
# contact can be null when a jid listed in the metacontact data
# is not in our roster
contact = self._contacts.get_contact_with_highest_priority(
account, jid)
show = show_list.index(contact.show) if contact else 0
priority = contact.priority if contact else 0
has_order = 'order' in data
order = data.get('order', 0)
transport = common.app.get_transport_name_from_jid(jid)
server = common.app.get_server_from_jid(jid)
myserver = app.settings.get_account_setting(account, 'hostname')
return (bool(contact), show > 2, has_order, order, bool(transport),
show, priority, server == myserver, jid, account)
def get_nearby_family_and_big_brother(self, family, account):
"""
Return the nearby family and its Big Brother
Nearby family is the part of the family that is grouped with the
metacontact. A metacontact may be over different accounts. If accounts
are not merged then the given family is split account wise.
(nearby_family, big_brother_jid, big_brother_account)
"""
if app.settings.get('mergeaccounts'):
# group all together
nearby_family = family
else:
# we want one nearby_family per account
nearby_family = [data for data in family if account == data['account']]
if not nearby_family:
return (None, None, None)
big_brother_data = self._get_metacontacts_big_brother(nearby_family)
big_brother_jid = big_brother_data['jid']
big_brother_account = big_brother_data['account']
return (nearby_family, big_brother_jid, big_brother_account)
def _get_metacontacts_big_brother(self, family):
"""
Which of the family will be the big brother under which all others will be
?
"""
return max(family, key=self._metacontact_key)
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

View file

@ -0,0 +1,112 @@
# Copyright (C) 2009-2014 Yann Leboulanger <asterix AT lagaule.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 logging
from datetime import datetime
from gi.repository import GLib
from nbxmpp.structs import LocationData
from gajim.common import app
if app.is_installed('GEOCLUE'):
from gi.repository import Geoclue # pylint: disable=ungrouped-imports,no-name-in-module
log = logging.getLogger('gajim.c.dbus.location')
class LocationListener:
_instance = None
@classmethod
def get(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
self._data = {}
self.location_info = {}
self.simple = None
# Note: do not remove third parameter `param`
# because notify signal expects three parameters
def _on_location_update(self, simple, _param=None):
location = simple.get_location()
timestamp = location.get_property("timestamp")[0]
lat = location.get_property("latitude")
lon = location.get_property("longitude")
alt = location.get_property("altitude")
# in XEP-0080 it's horizontal accuracy
acc = location.get_property("accuracy")
# update data with info we just received
self._data = {'lat': lat, 'lon': lon, 'alt': alt, 'accuracy': acc}
self._data['timestamp'] = self._timestamp_to_utc(timestamp)
self._send_location()
def _on_simple_ready(self, _obj, result):
try:
self.simple = Geoclue.Simple.new_finish(result)
except GLib.Error as error:
log.warning("Could not enable geolocation: %s", error.message)
else:
self.simple.connect('notify::location', self._on_location_update)
self._on_location_update(self.simple)
def get_data(self):
Geoclue.Simple.new("org.gajim.Gajim",
Geoclue.AccuracyLevel.EXACT,
None,
self._on_simple_ready)
def start(self):
self.location_info = {}
self.get_data()
def _send_location(self):
accounts = app.connections.keys()
for acct in accounts:
if not app.account_is_available(acct):
continue
if not app.settings.get_account_setting(acct, 'publish_location'):
continue
if self.location_info == self._data:
continue
if 'timestamp' in self.location_info and 'timestamp' in self._data:
last_data = self.location_info.copy()
del last_data['timestamp']
new_data = self._data.copy()
del new_data['timestamp']
if last_data == new_data:
continue
app.connections[acct].get_module('UserLocation').set_location(
LocationData(**self._data))
self.location_info = self._data.copy()
@staticmethod
def _timestamp_to_utc(timestamp):
time = datetime.utcfromtimestamp(timestamp)
return time.strftime('%Y-%m-%dT%H:%MZ')
def enable():
if not app.is_installed('GEOCLUE'):
log.warning('GeoClue not installed')
return
listener = LocationListener.get()
listener.start()

142
gajim/common/dbus/logind.py Normal file
View file

@ -0,0 +1,142 @@
# Copyright (C) 2014 Kamil Paral <kamil.paral AT gmail.com>
#
# 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/>.
'''
Watch for system sleep using systemd-logind.
Documentation: http://www.freedesktop.org/wiki/Software/systemd/inhibit
'''
import os
import logging
from gi.repository import Gio
from gi.repository import GLib
from gajim.common import app
from gajim.common.i18n import _
log = logging.getLogger('gajim.c.dbus.logind')
class LogindListener:
_instance = None
@classmethod
def get(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
# file descriptor object of the inhibitor
self._inhibit_fd = None
Gio.bus_watch_name(
Gio.BusType.SYSTEM,
'org.freedesktop.login1',
Gio.BusNameWatcherFlags.NONE,
self._on_appear_logind,
self._on_vanish_logind)
def _on_prepare_for_sleep(self, connection, _sender_name, _object_path,
interface_name, signal_name, parameters,
*_user_data):
'''Signal handler for PrepareForSleep event'''
log.debug('Received signal %s.%s%s',
interface_name, signal_name, parameters)
before = parameters[0] # Signal is either before or after sleep occurs
if before:
warn = self._inhibit_fd is None
log.log(
logging.WARNING if warn else logging.INFO,
'Preparing for sleep by disconnecting from network%s',
', without holding a sleep inhibitor' if warn else '')
for name, conn in app.connections.items():
if app.account_is_connected(name):
st = conn.status_message
conn.change_status('offline',
_('Machine is going to sleep'))
# TODO: Make this nicer
conn._status_message = st # pylint: disable=protected-access
conn.time_to_reconnect = 5
self._disinhibit_sleep()
else:
try:
self._inhibit_sleep(connection)
except GLib.Error as error:
log.warning('Inhibit failed: %s', error)
for conn in app.connections.values():
if conn.state.is_disconnected and conn.time_to_reconnect:
conn.reconnect()
def _inhibit_sleep(self, connection):
'''Obtain a sleep delay inhibitor from logind'''
if self._inhibit_fd is not None:
# Something is wrong, we have an inhibitor fd, and we are asking for
# yet another one.
log.warning('Trying to obtain a sleep inhibitor '
'while already holding one.')
ret, ret_fdlist = connection.call_with_unix_fd_list_sync(
'org.freedesktop.login1',
'/org/freedesktop/login1',
'org.freedesktop.login1.Manager',
'Inhibit',
GLib.Variant('(ssss)', (
'sleep', 'org.gajim.Gajim', _('Disconnect from the network'),
'delay' # Inhibitor will delay but not block sleep
)),
GLib.VariantType.new('(h)'),
Gio.DBusCallFlags.NONE, -1, None, None)
log.info('Inhibit sleep')
self._inhibit_fd = ret_fdlist.get(ret.unpack()[0])
def _disinhibit_sleep(self):
'''Relinquish our sleep delay inhibitor'''
if self._inhibit_fd is not None:
os.close(self._inhibit_fd)
self._inhibit_fd = None
log.info('Disinhibit sleep')
def _on_appear_logind(self, connection, name, name_owner, *_user_data):
'''Use signal and locks provided by org.freedesktop.login1'''
log.info('Name %s appeared, owned by %s', name, name_owner)
connection.signal_subscribe(
'org.freedesktop.login1',
'org.freedesktop.login1.Manager',
'PrepareForSleep',
'/org/freedesktop/login1',
None,
Gio.DBusSignalFlags.NONE,
self._on_prepare_for_sleep,
None)
self._inhibit_sleep(connection)
def _on_vanish_logind(self, _connection, name, *_user_data):
'''Release remaining resources related to org.freedesktop.login1'''
log.info('Name %s vanished', name)
self._disinhibit_sleep()
def enable():
return
# LogindListener.get()

View file

@ -0,0 +1,208 @@
# Copyright (C) 2006 Gustavo Carneiro <gjcarneiro AT gmail.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2008 Jean-Marie Traissard <jim AT lapin.org>
# Jonathan Schleifer <js-gajim AT webkeks.org>
# Stephan Erb <steve-e AT h3c.de>
#
# 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 logging
from gi.repository import Gio
from gi.repository import GLib
from nbxmpp.structs import TuneData
from gajim.common import app
from gajim.common.nec import NetworkEvent
log = logging.getLogger('gajim.c.dbus.music_track')
MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.'
class MusicTrackListener:
_instance = None
@classmethod
def get(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
self.players = {}
self.connection = None
self._current_tune = None
def _emit(self, info):
self._current_tune = info
app.nec.push_incoming_event(
NetworkEvent('music-track-changed', info=info))
@property
def current_tune(self):
return self._current_tune
def start(self):
proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus',
None)
self.connection = proxy.get_connection()
self.connection.signal_subscribe(
'org.freedesktop.DBus',
'org.freedesktop.DBus',
'NameOwnerChanged',
'/org/freedesktop/DBus',
None,
Gio.DBusSignalFlags.NONE,
self._signal_name_owner_changed)
try:
result = proxy.call_sync(
'ListNames',
None,
Gio.DBusCallFlags.NONE,
-1,
None)
except GLib.Error as error:
log.debug("Could not list names: %s", error.message)
return
for name in result[0]:
if name.startswith(MPRIS_PLAYER_PREFIX):
self._add_player(name)
for name in list(self.players):
self._get_playing_track(name)
def stop(self):
for name in list(self.players):
if name.startswith(MPRIS_PLAYER_PREFIX):
self._remove_player(name)
def _signal_name_owner_changed(self,
_connection,
_sender_name,
_object_path,
_interface_name,
_signal_name,
parameters,
*_user_data):
name, old_owner, new_owner = parameters
if name.startswith(MPRIS_PLAYER_PREFIX):
if new_owner and not old_owner:
self._add_player(name)
else:
self._remove_player(name)
def _add_player(self, name):
'''Set up a listener for music player signals'''
log.info('%s appeared', name)
if name in self.players:
return
self.players[name] = self.connection.signal_subscribe(
name,
'org.freedesktop.DBus.Properties',
'PropertiesChanged',
'/org/mpris/MediaPlayer2',
None,
Gio.DBusSignalFlags.NONE,
self._signal_received,
name)
def _remove_player(self, name):
log.info('%s vanished', name)
if name in self.players:
self.connection.signal_unsubscribe(
self.players[name])
self.players.pop(name)
self._emit(None)
def _signal_received(self,
_connection,
_sender_name,
_object_path,
interface_name,
_signal_name,
parameters,
*user_data):
'''Signal handler for PropertiesChanged event'''
log.info('Signal received: %s - %s', interface_name, parameters)
self._get_playing_track(user_data[0])
@staticmethod
def _get_music_info(properties):
meta = properties.get('Metadata')
if meta is None or not meta:
return None
status = properties.get('PlaybackStatus')
if status is None or status == 'Paused':
return None
title = meta.get('xesam:title')
album = meta.get('xesam:album')
# xesam:artist is always a list of strings if not None
artist = meta.get('xesam:artist')
if artist is not None:
artist = ', '.join(artist)
return TuneData(artist=artist, title=title, source=album)
def _get_playing_track(self, name):
'''Return a TuneData for the currently playing
song, or None if no song is playing'''
proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
name,
'/org/mpris/MediaPlayer2',
'org.freedesktop.DBus.Properties',
None)
def proxy_call_finished(proxy, res):
try:
result = proxy.call_finish(res)
except GLib.Error as error:
log.debug("Could not enable music listener: %s", error.message)
return
info = self._get_music_info(result[0])
if info is not None:
self._emit(info)
proxy.call("GetAll",
GLib.Variant('(s)', ('org.mpris.MediaPlayer2.Player',)),
Gio.DBusCallFlags.NONE,
-1,
None,
proxy_call_finished)
def enable():
listener = MusicTrackListener.get()
listener.start()

229
gajim/common/dh.py Normal file
View file

@ -0,0 +1,229 @@
# Copyright (C) 2007-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
#
# 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/>.
"""
This module defines a number of constants; specifically, large primes suitable
for use with the Diffie-Hellman key exchange
These constants have been obtained from RFC2409 and RFC3526.
"""
import string
generators = [
None, # one to get the right offset
2,
2,
None,
None,
2,
None,
None,
None,
None,
None,
None,
None,
None,
2, # group 14
2,
2,
2,
2
]
_HEX_PRIMES = [
None,
# group 1
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A63A3620 FFFFFFFF FFFFFFFF''',
# group 2
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE65381
FFFFFFFF FFFFFFFF''',
# XXX how do I obtain these?
None,
None,
# group 5
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA237327 FFFFFFFF FFFFFFFF''',
None,
None,
None,
None,
None,
None,
None,
None,
# group 14
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AACAA68 FFFFFFFF FFFFFFFF''',
# group 15
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF''',
# group 16
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA
2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6
287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED
1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199
FFFFFFFF FFFFFFFF''',
# group 17
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08
8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B
302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9
A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6
49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8
FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C
180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718
3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D
04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D
B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226
1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC
E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26
99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB
04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2
233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127
D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492
36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406
AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918
DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151
2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03
F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F
BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA
CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B
B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632
387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E
6DCC4024 FFFFFFFF FFFFFFFF''',
# group 18
'''FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1
29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD
EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245
E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED
EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D
C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F
83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D
670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B
E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9
DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510
15728E5A 8AAAC42D AD33170D 04507A33 A85521AB DF1CBA64
ECFB8504 58DBEF0A 8AEA7157 5D060C7D B3970F85 A6E1E4C7
ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226 1AD2EE6B
F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C
BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31
43DB5BFC E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7
88719A10 BDBA5B26 99C32718 6AF4E23C 1A946834 B6150BDA
2583E9CA 2AD44CE8 DBBBC2DB 04DE8EF9 2E8EFC14 1FBECAA6
287C5947 4E6BC05D 99B2964F A090C3A2 233BA186 515BE7ED
1F612970 CEE2D7AF B81BDD76 2170481C D0069127 D5B05AA9
93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492
36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD
F8FF9406 AD9E530E E5DB382F 413001AE B06A53ED 9027D831
179727B0 865A8918 DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B
DB7F1447 E6CC254B 33205151 2BD7AF42 6FB8F401 378CD2BF
5983CA01 C64B92EC F032EA15 D1721D03 F482D7CE 6E74FEF6
D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F BEC7E8F3
23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA
CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328
06A1D58B B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C
DA56C9EC 2EF29632 387FE8D7 6E3C0468 043E8F66 3F4860EE
12BF2D5B 0B7474D6 E694F91E 6DBE1159 74A3926F 12FEE5E4
38777CB6 A932DF8C D8BEC4D0 73B931BA 3BC832B6 8D9DD300
741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C 5AE4F568
3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9
22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B
4BCBC886 2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A
062B3CF5 B3A278A6 6D2A13F8 3F44F82D DF310EE0 74AB6A36
4597E899 A0255DC1 64F31CC5 0846851D F9AB4819 5DED7EA1
B1D510BD 7EE74D73 FAF36BC3 1ECFA268 359046F4 EB879F92
4009438B 481C6CD7 889A002E D5EE382B C9190DA6 FC026E47
9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71
60C980DD 98EDD3DF FFFFFFFF FFFFFFFF'''
]
_ALL_ASCII = ''.join(map(chr, range(256)))
def hex_to_decimal(stripee):
if not stripee:
return None
return int(stripee.translate(_ALL_ASCII).translate(
str.maketrans("", "", string.whitespace)), 16)
primes = list(map(hex_to_decimal, _HEX_PRIMES))

464
gajim/common/events.py Normal file
View file

@ -0,0 +1,464 @@
# Copyright (C) 2006 Jean-Marie Traissard <jim AT lapin.org>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# 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 time
from gajim.common import app
class Event:
"""
Information concerning each event
"""
def __init__(self, time_=None, show_in_roster=False, show_in_systray=True):
"""
type_ in chat, normal, file-request, file-error, file-completed,
file-request-error, file-send-error, file-stopped, gc_msg, pm,
printed_chat, printed_gc_msg, printed_marked_gc_msg, printed_pm,
gc-invitation, subscription_request, unsubscribedm jingle-incoming
parameters is (per type_):
chat, normal, pm: [message, subject, kind, time, encrypted, resource,
msg_log_id]
where kind in error, incoming
file-*: file_props
gc_msg: None
printed_chat: [message, subject, control, msg_log_id]
printed_*: None
messages that are already printed in chat, but not read
gc-invitation: [room_jid, reason, password, jid_from]
subscription_request: [text, nick]
unsubscribed: contact
jingle-incoming: (fulljid, sessionid, content_types)
"""
if time_:
self.time_ = time_
else:
self.time_ = time.time()
self.show_in_roster = show_in_roster
self.show_in_systray = show_in_systray
# Set when adding the event
self.jid = None
self.account = None
class ChatEvent(Event):
type_ = 'chat'
def __init__(self, message, subject, kind, time_, resource,
msg_log_id, correct_id=None, message_id=None, session=None,
displaymarking=None, sent_forwarded=False, show_in_roster=False,
show_in_systray=True, additional_data=None):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.message = message
self.subject = subject
self.kind = kind
self.time = time_
self.resource = resource
self.msg_log_id = msg_log_id
self.message_id = message_id
self.correct_id = correct_id
self.session = session
self.displaymarking = displaymarking
self.sent_forwarded = sent_forwarded
if additional_data is None:
from gajim.common.helpers import AdditionalDataDict
additional_data = AdditionalDataDict()
self.additional_data = additional_data
class PmEvent(ChatEvent):
type_ = 'pm'
class PrintedChatEvent(Event):
type_ = 'printed_chat'
def __init__(self, message, subject, control, msg_log_id, time_=None,
message_id=None, stanza_id=None, show_in_roster=False,
show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.message = message
self.subject = subject
self.control = control
self.msg_log_id = msg_log_id
self.message_id = message_id
self.stanza_id = stanza_id
class PrintedGcMsgEvent(PrintedChatEvent):
type_ = 'printed_gc_msg'
class PrintedMarkedGcMsgEvent(PrintedChatEvent):
type_ = 'printed_marked_gc_msg'
class PrintedPmEvent(PrintedChatEvent):
type_ = 'printed_pm'
class SubscriptionRequestEvent(Event):
type_ = 'subscription_request'
def __init__(self, text, nick, time_=None, show_in_roster=False,
show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.text = text
self.nick = nick
class UnsubscribedEvent(Event):
type_ = 'unsubscribed'
def __init__(self, contact, time_=None, show_in_roster=False,
show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.contact = contact
class GcInvitationtEvent(Event):
type_ = 'gc-invitation'
def __init__(self, event):
Event.__init__(self, None, show_in_roster=False, show_in_systray=True)
for key, value in vars(event).items():
setattr(self, key, value)
def get_inviter_name(self):
if self.from_.bare_match(self.muc):
return self.from_.resource
contact = app.contacts.get_first_contact_from_jid(
self.account, self.from_.bare)
if contact is None:
return str(self.from_)
return contact.get_shown_name()
class FileRequestEvent(Event):
type_ = 'file-request'
def __init__(self, file_props, time_=None, show_in_roster=False, show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.file_props = file_props
class FileSendErrorEvent(FileRequestEvent):
type_ = 'file-send-error'
class FileErrorEvent(FileRequestEvent):
type_ = 'file-error'
class FileRequestErrorEvent(FileRequestEvent):
type_ = 'file-request-error'
class FileCompletedEvent(FileRequestEvent):
type_ = 'file-completed'
class FileStoppedEvent(FileRequestEvent):
type_ = 'file-stopped'
class FileHashErrorEvent(FileRequestEvent):
type_ = 'file-hash-error'
class JingleIncomingEvent(Event):
type_ = 'jingle-incoming'
def __init__(self, peerjid, sid, content_types, time_=None, show_in_roster=False, show_in_systray=True):
Event.__init__(self, time_, show_in_roster=show_in_roster,
show_in_systray=show_in_systray)
self.peerjid = peerjid
self.sid = sid
self.content_types = content_types
class Events:
"""
Information concerning all events
"""
def __init__(self):
self._events = {} # list of events {acct: {jid1: [E1, E2]}, }
self._event_added_listeners = []
self._event_removed_listeners = []
def event_added_subscribe(self, listener):
"""
Add a listener when an event is added to the queue
"""
if not listener in self._event_added_listeners:
self._event_added_listeners.append(listener)
def event_added_unsubscribe(self, listener):
"""
Remove a listener when an event is added to the queue
"""
if listener in self._event_added_listeners:
self._event_added_listeners.remove(listener)
def event_removed_subscribe(self, listener):
"""
Add a listener when an event is removed from the queue
"""
if not listener in self._event_removed_listeners:
self._event_removed_listeners.append(listener)
def event_removed_unsubscribe(self, listener):
"""
Remove a listener when an event is removed from the queue
"""
if listener in self._event_removed_listeners:
self._event_removed_listeners.remove(listener)
def fire_event_added(self, event):
for listener in self._event_added_listeners:
listener(event)
def fire_event_removed(self, event_list):
for listener in self._event_removed_listeners:
listener(event_list)
def add_account(self, account):
self._events[account] = {}
def get_accounts(self):
return self._events.keys()
def remove_account(self, account):
del self._events[account]
def add_event(self, account, jid, event):
# No such account before ?
if account not in self._events:
self._events[account] = {jid: [event]}
# no such jid before ?
elif jid not in self._events[account]:
self._events[account][jid] = [event]
else:
self._events[account][jid].append(event)
event.jid = jid
event.account = account
self.fire_event_added(event)
def remove_events(self, account, jid, event=None, types=None):
"""
If event is not specified, remove all events from this jid, optionally
only from given type return True if no such event found
"""
if types is None:
types = []
if account not in self._events:
return True
if jid not in self._events[account]:
return True
if event: # remove only one event
if event in self._events[account][jid]:
if len(self._events[account][jid]) == 1:
del self._events[account][jid]
else:
self._events[account][jid].remove(event)
self.fire_event_removed([event])
return
return True
if types:
new_list = [] # list of events to keep
removed_list = [] # list of removed events
for ev in self._events[account][jid]:
if ev.type_ not in types:
new_list.append(ev)
else:
removed_list.append(ev)
if len(new_list) == len(self._events[account][jid]):
return True
if new_list:
self._events[account][jid] = new_list
else:
del self._events[account][jid]
self.fire_event_removed(removed_list)
return
# No event nor type given, remove them all
removed_list = self._events[account][jid]
del self._events[account][jid]
self.fire_event_removed(removed_list)
def change_jid(self, account, old_jid, new_jid):
if account not in self._events:
return
if old_jid not in self._events[account]:
return
if new_jid in self._events[account]:
self._events[account][new_jid] += self._events[account][old_jid]
else:
self._events[account][new_jid] = self._events[account][old_jid]
del self._events[account][old_jid]
def get_nb_events(self, types=None, account=None):
if types is None:
types = []
return self._get_nb_events(types=types, account=account)
def get_events(self, account, jid=None, types=None):
"""
Return all events from the given account of the form {jid1: [], jid2:
[]}. If jid is given, returns all events from the given jid in a list: []
optionally only from given type
"""
if types is None:
types = []
if account not in self._events:
return []
if not jid:
events_list = {} # list of events
for jid_ in self._events[account]:
events = []
for ev in self._events[account][jid_]:
if not types or ev.type_ in types:
events.append(ev)
if events:
events_list[jid_] = events
return events_list
if jid not in self._events[account]:
return []
events_list = [] # list of events
for ev in self._events[account][jid]:
if not types or ev.type_ in types:
events_list.append(ev)
return events_list
def get_all_events(self, types=None):
accounts = self._events.keys()
events = []
for account in accounts:
for jid in self._events[account]:
for event in self._events[account][jid]:
if types is None or event.type_ in types:
events.append(event)
return events
def get_first_event(self, account=None, jid=None, type_=None):
"""
Return the first event of type type_ if given
"""
if not account:
return self._get_first_event_with_attribute(self._events)
events_list = self.get_events(account, jid, type_)
# be sure it's bigger than latest event
first_event_time = time.time() + 1
first_event = None
for event in events_list:
if event.time_ < first_event_time:
first_event_time = event.time_
first_event = event
return first_event
def _get_nb_events(self, account=None, jid=None, attribute=None, types=None):
"""
Return the number of pending events
"""
if types is None:
types = []
nb = 0
if account:
accounts = [account]
else:
accounts = self._events.keys()
for acct in accounts:
if acct not in self._events:
continue
if jid:
jids = [jid]
else:
jids = self._events[acct].keys()
for j in jids:
if j not in self._events[acct]:
continue
for event in self._events[acct][j]:
if types and event.type_ not in types:
continue
if not attribute or \
attribute == 'systray' and event.show_in_systray or \
attribute == 'roster' and event.show_in_roster:
nb += 1
return nb
def _get_some_events(self, attribute):
"""
Attribute in systray, roster
"""
events = {}
for account in self._events:
events[account] = {}
for jid in self._events[account]:
events[account][jid] = []
for event in self._events[account][jid]:
if attribute == 'systray' and event.show_in_systray or \
attribute == 'roster' and event.show_in_roster:
events[account][jid].append(event)
if not events[account][jid]:
del events[account][jid]
if not events[account]:
del events[account]
return events
def _get_first_event_with_attribute(self, events):
"""
Get the first event
events is in the form {account1: {jid1: [ev1, ev2], },. }
"""
# be sure it's bigger than latest event
first_event_time = time.time() + 1
first_account = None
first_jid = None
first_event = None
for account in events:
for jid in events[account]:
for event in events[account][jid]:
if event.time_ < first_event_time:
first_event_time = event.time_
first_account = account
first_jid = jid
first_event = event
return first_account, first_jid, first_event
def get_nb_systray_events(self, types=None):
"""
Return the number of events displayed in roster
"""
if types is None:
types = []
return self._get_nb_events(attribute='systray', types=types)
def get_systray_events(self):
"""
Return all events that must be displayed in systray:
{account1: {jid1: [ev1, ev2], },. }
"""
return self._get_some_events('systray')
def get_first_systray_event(self):
events = self.get_systray_events()
return self._get_first_event_with_attribute(events)
def get_nb_roster_events(self, account=None, jid=None, types=None):
"""
Return the number of events displayed in roster
"""
if types is None:
types = []
return self._get_nb_events(attribute='roster', account=account,
jid=jid, types=types)
def get_roster_events(self):
"""
Return all events that must be displayed in roster:
{account1: {jid1: [ev1, ev2], },. }
"""
return self._get_some_events('roster')

157
gajim/common/exceptions.py Normal file
View file

@ -0,0 +1,157 @@
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2005-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2006 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2007 Brendan Taylor <whateley AT gmail.com>
#
# 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/>.
from gajim.common.i18n import _
class PysqliteOperationalError(Exception):
"""
Sqlite2 raised pysqlite2.dbapi2.OperationalError
"""
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text
class DatabaseMalformed(Exception):
"""
The database can't be read
"""
def __init__(self, path=''):
Exception.__init__(self)
self.path = path
def __str__(self):
return _('The database file (%s) cannot be read. '
'Try to repair it (see '
'https://dev.gajim.org/gajim/gajim/wikis/help/DatabaseBackup)'
' or remove it (all history will be lost).') % self.path
class ServiceNotAvailable(Exception):
"""
This exception is raised when we cannot use Gajim remotely'
"""
def __init__(self):
Exception.__init__(self)
def __str__(self):
return _('Service not available: Gajim is not running, or remote_control is False')
class DbusNotSupported(Exception):
"""
D-Bus is not installed or python bindings are missing
"""
def __init__(self):
Exception.__init__(self)
def __str__(self):
return _('D-Bus is not present on this machine or python module is missing')
class SessionBusNotPresent(Exception):
"""
This exception indicates that there is no session daemon
"""
def __init__(self):
Exception.__init__(self)
def __str__(self):
return _('Session bus is not available.\nTry reading %(url)s') % \
{'url': 'https://dev.gajim.org/gajim/gajim/wikis/help/GajimDBus'}
class SystemBusNotPresent(Exception):
"""
This exception indicates that there is no session daemon
"""
def __init__(self):
Exception.__init__(self)
def __str__(self):
return _('System bus is not available.\nTry reading %(url)s') % \
{'url': 'https://dev.gajim.org/gajim/gajim/wikis/help/GajimDBus'}
class NegotiationError(Exception):
"""
A session negotiation failed
"""
class Cancelled(Exception):
"""
The user cancelled an operation
"""
class LatexError(Exception):
"""
LaTeX processing failed for some reason
"""
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text
class GajimGeneralException(Exception):
"""
This exception is our general exception
"""
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text
class PluginsystemError(Exception):
"""
Error in the pluginsystem
"""
def __init__(self, text=''):
Exception.__init__(self)
self.text = text
def __str__(self):
return self.text
class StanzaMalformed(Exception):
"""
Malfromed Stanza
"""
def __init__(self, message, stanza=''):
Exception.__init__(self, message, stanza)
self._msg = '{}\n{}'.format(message, stanza)
def __str__(self):
return self._msg
class SendMessageError(Exception):
pass
class FileError(Exception):
pass

173
gajim/common/file_props.py Normal file
View file

@ -0,0 +1,173 @@
"""
This module is in charge of taking care of all the information related to
individual files. Files are identified by the account name and its sid.
>>> print(FilesProp.getFileProp('jabberid', '10'))
None
>>> fp = FilesProp()
Traceback (most recent call last):
...
Exception: this class should not be instatiated
>>> print(FilesProp.getAllFileProp())
[]
>>> fp = FilesProp.getNewFileProp('jabberid', '10')
>>> fp2 = FilesProp.getFileProp('jabberid', '10')
>>> fp == fp2
True
"""
from typing import Any # pylint: disable=unused-import
from typing import ClassVar # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from typing import Tuple # pylint: disable=unused-import
class FilesProp:
_files_props = {} # type: ClassVar[Dict[Tuple[str, str], Any]]
def __init__(self):
raise Exception('this class should not be instantiated')
@classmethod
def getNewFileProp(cls, account, sid):
fp = FileProp(account, sid)
cls.setFileProp(fp, account, sid)
return fp
@classmethod
def getFileProp(cls, account, sid):
if (account, sid) in cls._files_props.keys():
return cls._files_props[account, sid]
@classmethod
def getFilePropByAccount(cls, account):
# Returns a list of file_props in one account
file_props = []
for account_, sid in cls._files_props:
if account_ == account:
file_props.append(cls._files_props[account, sid])
return file_props
@classmethod
def getFilePropByType(cls, type_, sid):
# This method should be deleted. Getting fileprop by type and sid is not
# unique enough. More than one fileprop might have the same type and sid
files_prop = cls.getAllFileProp()
for fp in files_prop:
if fp.type_ == type_ and fp.sid == sid:
return fp
@classmethod
def getFilePropBySid(cls, sid):
# This method should be deleted. It is kept to make things compatible
# This method should be replaced and instead get the file_props by
# account and sid
files_prop = cls.getAllFileProp()
for fp in files_prop:
if fp.sid == sid:
return fp
@classmethod
def getFilePropByTransportSid(cls, account, sid):
files_prop = cls.getAllFileProp()
for fp in files_prop:
if fp.account == account and fp.transport_sid == sid:
return fp
@classmethod
def getAllFileProp(cls):
return list(cls._files_props.values())
@classmethod
def setFileProp(cls, fp, account, sid):
cls._files_props[account, sid] = fp
@classmethod
def deleteFileProp(cls, file_prop):
files_props = cls._files_props
a = s = None
for key in files_props:
account, sid = key
fp = files_props[account, sid]
if fp is file_prop:
a = account
s = sid
if a is not None and s is not None:
del files_props[a, s]
class FileProp:
def __init__(self, account, sid):
# Do not instantiate this class directly. Call FilesProp.getNeFileProp
# instead
self.streamhosts = []
self.transfered_size = []
self.started = False
self.completed = False
self.paused = False
self.stalled = False
self.connected = False
self.stopped = False
self.is_a_proxy = False
self.proxyhost = None
self.proxy_sender = None
self.proxy_receiver = None
self.streamhost_used = None
# method callback called in case of transfer failure
self.failure_cb = None
# method callback called when disconnecting
self.disconnect_cb = None
self.continue_cb = None
self.sha_str = None
# transfer type: 's' for sending and 'r' for receiving
self.type_ = None
self.error = None
# Elapsed time of the file transfer
self.elapsed_time = 0
self.last_time = None
self.received_len = None
# full file path
self.file_name = None
self.name = None
self.date = None
self.desc = None
self.offset = None
self.sender = None
self.receiver = None
self.tt_account = None
self.size = None
self._sid = sid
self.transport_sid = None
self.account = account
self.mime_type = None
self.algo = None
self.direction = None
self.syn_id = None
self.seq = None
self.hash_ = None
self.fd = None
self.startexmpp = None
# Type of the session, if it is 'jingle' or 'si'
self.session_type = None
self.request_id = None
self.proxyhosts = None
self.dstaddr = None
def getsid(self):
# Getter of the property sid
return self._sid
def setsid(self, value):
# The sid value will change
# we need to change the in _files_props key as well
del FilesProp._files_props[self.account, self._sid]
self._sid = value
FilesProp._files_props[self.account, self._sid] = self
sid = property(getsid, setsid)
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -0,0 +1,110 @@
# 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/>.
from typing import Dict
from gajim.common.helpers import Observable
from gajim.common.const import FTState
class FileTransfer(Observable):
_state_descriptions = {} # type: Dict[FTState, str]
def __init__(self, account):
Observable.__init__(self)
self._account = account
self._seen = 0
self.size = 0
self._state = None
self._error_text = ''
self._error_domain = None
@property
def account(self):
return self._account
@property
def state(self):
return self._state
@property
def seen(self):
return self._seen
@property
def is_complete(self):
if self.size == 0:
return False
return self._seen >= self.size
@property
def filename(self):
raise NotImplementedError
@property
def error_text(self):
return self._error_text
@property
def error_domain(self):
return self._error_domain
def get_state_description(self):
return self._state_descriptions.get(self._state, '')
def set_preparing(self):
self._state = FTState.PREPARING
self.notify('state-changed', FTState.PREPARING)
def set_encrypting(self):
self._state = FTState.ENCRYPTING
self.notify('state-changed', FTState.ENCRYPTING)
def set_decrypting(self):
self._state = FTState.DECRYPTING
self.notify('state-changed', FTState.DECRYPTING)
def set_started(self):
self._state = FTState.STARTED
self.notify('state-changed', FTState.STARTED)
def set_error(self, domain, text=''):
self._error_text = text
self._error_domain = domain
self._state = FTState.ERROR
self.notify('state-changed', FTState.ERROR)
self.disconnect_signals()
def set_cancelled(self):
self._state = FTState.CANCELLED
self.notify('state-changed', FTState.CANCELLED)
self.disconnect_signals()
def set_in_progress(self):
self._state = FTState.IN_PROGRESS
self.notify('state-changed', FTState.IN_PROGRESS)
def set_finished(self):
self._state = FTState.FINISHED
self.notify('state-changed', FTState.FINISHED)
self.disconnect_signals()
def update_progress(self):
self.notify('progress')
def cancel(self):
self.notify('cancel')

110
gajim/common/ged.py Normal file
View file

@ -0,0 +1,110 @@
# 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/>.
'''
Global Events Dispatcher module.
:author: Mateusz Biliński <mateusz@bilinski.it>
:since: 8th August 2008
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
:copyright: Copyright (2011) Yann Leboulanger <asterix@lagaule.org>
:license: GPL
'''
import logging
import traceback
import inspect
from nbxmpp import NodeProcessed
log = logging.getLogger('gajim.c.ged')
PRECORE = 10
CORE = 20
POSTCORE = 30
PREGUI = 40
PREGUI1 = 50
GUI1 = 60
POSTGUI1 = 70
PREGUI2 = 80
GUI2 = 90
POSTGUI2 = 100
POSTGUI = 110
OUT_PREGUI = 10
OUT_PREGUI1 = 20
OUT_GUI1 = 30
OUT_POSTGUI1 = 40
OUT_PREGUI2 = 50
OUT_GUI2 = 60
OUT_POSTGUI2 = 70
OUT_POSTGUI = 80
OUT_PRECORE = 90
OUT_CORE = 100
OUT_POSTCORE = 110
class GlobalEventsDispatcher:
def __init__(self):
self.handlers = {}
def register_event_handler(self, event_name, priority, handler):
if event_name in self.handlers:
handlers_list = self.handlers[event_name]
i = 0
for i, handler_tuple in enumerate(handlers_list):
if priority < handler_tuple[0]:
break
else:
# no event with smaller prio found, put it at the end
i += 1
handlers_list.insert(i, (priority, handler))
else:
self.handlers[event_name] = [(priority, handler)]
def remove_event_handler(self, event_name, priority, handler):
if event_name in self.handlers:
try:
self.handlers[event_name].remove((priority, handler))
except ValueError as error:
log.warning(
'''Function (%s) with priority "%s" never
registered as handler of event "%s". Couldn\'t remove.
Error: %s''', handler, priority, event_name, error)
def raise_event(self, event_name, *args, **kwargs):
log.debug('Raise event: %s', event_name)
if event_name in self.handlers:
node_processed = False
# Iterate over a copy of the handlers list, so while iterating
# the original handlers list can be modified
for _priority, handler in list(self.handlers[event_name]):
try:
if inspect.ismethod(handler):
log.debug('Call handler %s on %s',
handler.__name__,
handler.__self__)
else:
log.debug('Call handler %s', handler.__name__)
if handler(*args, **kwargs):
return True
except NodeProcessed:
node_processed = True
except Exception:
log.error('Error while running an event handler: %s',
handler)
traceback.print_exc()
if node_processed:
raise NodeProcessed

1442
gajim/common/helpers.py Normal file

File diff suppressed because it is too large Load diff

194
gajim/common/i18n.py Normal file
View file

@ -0,0 +1,194 @@
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2004 Vincent Hanquez <tab AT snarc.org>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2009 Benjamin Richter <br AT waldteufel-online.net>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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 sys
import locale
import gettext
import unicodedata
from pathlib import Path
from gi.repository import GLib
DOMAIN = 'gajim'
LANG = 'en'
direction_mark = '\u200E'
_translation = gettext.NullTranslations()
def get_locale_dirs():
if os.name == 'nt':
return
path = gettext.find(DOMAIN)
if path is not None:
# gettext can find the location itself
# so we dont need the localedir
return
if Path('/app/share/run-as-flatpak').exists():
# Check if we run as flatpak
return [Path('/app/share/')]
data_dirs = [GLib.get_user_data_dir()] + GLib.get_system_data_dirs()
return [Path(dir_) for dir_ in data_dirs]
def iter_locale_dirs():
locale_dirs = get_locale_dirs()
if locale_dirs is None:
yield None
return
# gettext fallback
locale_dirs.append(Path(sys.base_prefix) / 'share')
found_paths = []
for path in locale_dirs:
locale_dir = path / 'locale'
if locale_dir in found_paths:
continue
found_paths.append(locale_dir)
if locale_dir.is_dir():
yield str(locale_dir)
def get_default_lang():
if os.name == "nt":
import ctypes
windll = ctypes.windll.kernel32
return locale.windows_locale[windll.GetUserDefaultUILanguage()]
if sys.platform == "darwin":
from AppKit import NSLocale
# FIXME: This returns a two letter language code (en, de, fr)
# We need a way to get en_US, de_DE etc.
return NSLocale.currentLocale().languageCode()
return locale.getdefaultlocale()[0] or 'en'
def get_rfc5646_lang(lang=None):
if lang is None:
lang = LANG
return lang.replace('_', '-')
def get_short_lang_code(lang=None):
if lang is None:
lang = LANG
return lang[:2]
def initialize_direction_mark():
from gi.repository import Gtk
global direction_mark
if Gtk.Widget.get_default_direction() == Gtk.TextDirection.RTL:
direction_mark = '\u200F'
def paragraph_direction_mark(text):
"""
Determine paragraph writing direction according to
http://www.unicode.org/reports/tr9/#The_Paragraph_Level
Returns either Unicode LTR mark or RTL mark.
"""
for char in text:
bidi = unicodedata.bidirectional(char)
if bidi == 'L':
return '\u200E'
if bidi in ('AL', 'R'):
return '\u200F'
return '\u200E'
def Q_(text):
"""
Translate the given text, optionally qualified with a special
construction, which will help translators to disambiguate between
same terms, but in different contexts.
When translated text is returned - this rudimentary construction
will be stripped off, if it's present.
Here is the construction to use:
Q_("?vcard:Unknown")
Everything between ? and : - is the qualifier to convey the context
to the translators. Everything after : - is the text itself.
"""
text = _(text)
if text.startswith('?'):
text = text.split(':', 1)[1]
return text
def ngettext(s_sing, s_plural, n, replace_sing=None, replace_plural=None):
"""
Use as:
i18n.ngettext(
'leave room %s', 'leave rooms %s', len(rooms), 'a', 'a, b, c')
In other words this is a hack to ngettext() to support %s %d etc..
"""
text = _translation.ngettext(s_sing, s_plural, n)
if n == 1 and replace_sing is not None:
text = text % replace_sing
elif n > 1 and replace_plural is not None:
text = text % replace_plural
return text
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error as error:
print(error, file=sys.stderr)
try:
LANG = get_default_lang()
if os.name == 'nt':
# Set the env var on Windows because gettext.find() uses it to
# find the translation
# 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
traceback.print_exc()
# Search for the translation in all locale dirs
for dir_ in iter_locale_dirs():
try:
_translation = gettext.translation(DOMAIN, dir_)
_ = _translation.gettext
if hasattr(locale, 'bindtextdomain'):
locale.bindtextdomain(DOMAIN, dir_) # type: ignore
except OSError:
continue
else:
break
else:
print('No translations found', file=sys.stderr)
print('Dirs searched: %s' % get_locale_dirs(), file=sys.stderr)
_ = _translation.gettext

362
gajim/common/idle.py Normal file
View file

@ -0,0 +1,362 @@
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2008 Mateusz Biliński <mateusz AT bilinski.it>
# Copyright (C) 2008 Thorsten P. 'dGhvcnN0ZW5wIEFUIHltYWlsIGNvbQ==\n'.decode("base64")
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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 sys
import time
import ctypes
import ctypes.util
import logging
from gi.repository import Gio
from gi.repository import GLib
from gi.repository import GObject
from gajim.common import app
from gajim.common.const import Display
from gajim.common.const import IdleState
log = logging.getLogger('gajim.c.idle')
class DBusFreedesktopIdleMonitor:
def __init__(self):
self.last_idle_time = 0
self._extended_away = False
log.debug('Connecting to D-Bus')
self.dbus_proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
'org.freedesktop.ScreenSaver',
'/org/freedesktop/ScreenSaver',
'org.freedesktop.ScreenSaver',
None
)
log.debug('D-Bus connected')
# Only the following call will trigger exceptions if the D-Bus
# interface/method/... does not exist. Using the failing method
# for class init to allow other idle monitors to be used on failure.
self._get_idle_sec_fail()
log.debug('D-Bus call test successful')
def _get_idle_sec_fail(self):
(idle_time,) = self.dbus_proxy.call_sync(
'GetSessionIdleTime',
None,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
None
)
return idle_time//1000
def get_idle_sec(self):
try:
self.last_idle_time = self._get_idle_sec_fail()
except GLib.Error as error:
log.warning(
'org.freedesktop.ScreenSaver.GetSessionIdleTime() failed: %s',
error)
return self.last_idle_time
def set_extended_away(self, state):
self._extended_away = state
def is_extended_away(self):
return self._extended_away
class DBusGnomeIdleMonitor:
def __init__(self):
self.last_idle_time = 0
self._extended_away = False
log.debug('Connecting to D-Bus')
self.dbus_gnome_proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusProxyFlags.NONE,
None,
'org.gnome.Mutter.IdleMonitor',
'/org/gnome/Mutter/IdleMonitor/Core',
'org.gnome.Mutter.IdleMonitor',
None
)
log.debug('D-Bus connected')
# Only the following call will trigger exceptions if the D-Bus
# interface/method/... does not exist. Using the failing method
# for class init to allow other idle monitors to be used on failure.
self._get_idle_sec_fail()
log.debug('D-Bus call test successful')
def _get_idle_sec_fail(self):
(idle_time,) = self.dbus_gnome_proxy.call_sync(
'GetIdletime',
None,
Gio.DBusCallFlags.NO_AUTO_START,
-1,
None
)
return int(idle_time / 1000)
def get_idle_sec(self):
try:
self.last_idle_time = self._get_idle_sec_fail()
except GLib.Error as error:
log.warning(
'org.gnome.Mutter.IdleMonitor.GetIdletime() failed: %s',
error)
return self.last_idle_time
def set_extended_away(self, state):
self._extended_away = state
def is_extended_away(self):
return self._extended_away
class XssIdleMonitor:
def __init__(self):
self._extended_away = False
class XScreenSaverInfo(ctypes.Structure):
_fields_ = [
('window', ctypes.c_ulong),
('state', ctypes.c_int),
('kind', ctypes.c_int),
('til_or_since', ctypes.c_ulong),
('idle', ctypes.c_ulong),
('eventMask', ctypes.c_ulong)
]
XScreenSaverInfo_p = ctypes.POINTER(XScreenSaverInfo)
display_p = ctypes.c_void_p
xid = ctypes.c_ulong
c_int_p = ctypes.POINTER(ctypes.c_int)
libX11path = ctypes.util.find_library('X11')
if libX11path is None:
raise OSError('libX11 could not be found.')
libX11 = ctypes.cdll.LoadLibrary(libX11path)
libX11.XOpenDisplay.restype = display_p
libX11.XOpenDisplay.argtypes = (ctypes.c_char_p,)
libX11.XDefaultRootWindow.restype = xid
libX11.XDefaultRootWindow.argtypes = (display_p,)
libXsspath = ctypes.util.find_library('Xss')
if libXsspath is None:
raise OSError('libXss could not be found.')
self.libXss = ctypes.cdll.LoadLibrary(libXsspath)
self.libXss.XScreenSaverQueryExtension.argtypes = display_p, c_int_p, c_int_p
self.libXss.XScreenSaverAllocInfo.restype = XScreenSaverInfo_p
self.libXss.XScreenSaverQueryInfo.argtypes = (
display_p, xid, XScreenSaverInfo_p)
self.dpy_p = libX11.XOpenDisplay(None)
if self.dpy_p is None:
raise OSError('Could not open X Display.')
_event_basep = ctypes.c_int()
_error_basep = ctypes.c_int()
extension = self.libXss.XScreenSaverQueryExtension(
self.dpy_p, ctypes.byref(_event_basep), ctypes.byref(_error_basep))
if extension == 0:
raise OSError('XScreenSaver Extension not available on display.')
self.xss_info_p = self.libXss.XScreenSaverAllocInfo()
if self.xss_info_p is None:
raise OSError('XScreenSaverAllocInfo: Out of Memory.')
self.rootwindow = libX11.XDefaultRootWindow(self.dpy_p)
def get_idle_sec(self):
info = self.libXss.XScreenSaverQueryInfo(
self.dpy_p, self.rootwindow, self.xss_info_p)
if info == 0:
return info
return int(self.xss_info_p.contents.idle / 1000)
def set_extended_away(self, state):
self._extended_away = state
def is_extended_away(self):
return False
class WindowsIdleMonitor:
def __init__(self):
self.OpenInputDesktop = ctypes.windll.user32.OpenInputDesktop
self.CloseDesktop = ctypes.windll.user32.CloseDesktop
self.SystemParametersInfo = ctypes.windll.user32.SystemParametersInfoW
self.GetTickCount = ctypes.windll.kernel32.GetTickCount
self.GetLastInputInfo = ctypes.windll.user32.GetLastInputInfo
self._locked_time = None
class LASTINPUTINFO(ctypes.Structure):
_fields_ = [('cbSize', ctypes.c_uint), ('dwTime', ctypes.c_uint)]
self.lastInputInfo = LASTINPUTINFO()
self.lastInputInfo.cbSize = ctypes.sizeof(self.lastInputInfo)
def get_idle_sec(self):
self.GetLastInputInfo(ctypes.byref(self.lastInputInfo))
return float(self.GetTickCount() - self.lastInputInfo.dwTime) / 1000
def is_extended_away(self):
# Check if Screen Saver is running
# 0x72 is SPI_GETSCREENSAVERRUNNING
saver_runing = ctypes.c_int(0)
info = self.SystemParametersInfo(
0x72, 0, ctypes.byref(saver_runing), 0)
if info and saver_runing.value:
return True
# Check if Screen is locked
# Also a UAC prompt counts as locked
# So just return True if we are more than 10 seconds locked
desk = self.OpenInputDesktop(0, False, 0)
unlocked = bool(desk)
self.CloseDesktop(desk)
if unlocked:
self._locked_time = None
return False
if self._locked_time is None:
self._locked_time = time.time()
return False
threshold = time.time() - 10
if threshold > self._locked_time:
return True
class IdleMonitor(GObject.GObject):
__gsignals__ = {
'state-changed': (
GObject.SignalFlags.RUN_LAST | GObject.SignalFlags.ACTION,
None, # return value
() # arguments
)}
def __init__(self):
GObject.GObject.__init__(self)
self.set_interval()
self._state = IdleState.AWAKE
self._idle_monitor = self._get_idle_monitor()
if self.is_available():
GLib.timeout_add_seconds(1, self._poll)
def set_interval(self, away_interval=60, xa_interval=120):
log.info('Set interval: away: %s, xa: %s',
away_interval, xa_interval)
self._away_interval = away_interval
self._xa_interval = xa_interval
def set_extended_away(self, state):
self._idle_monitor.set_extended_away(state)
def is_available(self):
return self._idle_monitor is not None
@property
def state(self):
if not self.is_available():
return IdleState.UNKNOWN
return self._state
def is_xa(self):
return self.state == IdleState.XA
def is_away(self):
return self.state == IdleState.AWAY
def is_awake(self):
return self.state == IdleState.AWAKE
def is_unknown(self):
return self.state == IdleState.UNKNOWN
@staticmethod
def _get_idle_monitor():
if sys.platform == 'win32':
return WindowsIdleMonitor()
try:
return DBusFreedesktopIdleMonitor()
except GLib.Error as error:
log.info('Idle time via D-Bus not available: %s', error)
try:
return DBusGnomeIdleMonitor()
except GLib.Error as error:
log.info('Idle time via D-Bus (GNOME) not available: %s', error)
if app.is_display(Display.WAYLAND):
return
try:
return XssIdleMonitor()
except OSError as error:
log.info('Idle time via XScreenSaverInfo not available: %s', error)
def get_idle_sec(self):
return self._idle_monitor.get_idle_sec()
def _poll(self):
"""
Check to see if we should change state
"""
if self._idle_monitor.is_extended_away():
log.info('Extended Away: Screensaver or Locked Screen')
self._set_state(IdleState.XA)
return True
idle_time = self.get_idle_sec()
# xa is stronger than away so check for xa first
if idle_time > self._xa_interval:
self._set_state(IdleState.XA)
elif idle_time > self._away_interval:
self._set_state(IdleState.AWAY)
else:
self._set_state(IdleState.AWAKE)
return True
def _set_state(self, state):
if self._state == state:
return
self._state = state
log.info('State changed: %s', state)
self.emit('state-changed')
Monitor = IdleMonitor()

View file

@ -0,0 +1,245 @@
# 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/>.
"""
Handles Jingle contents (XEP 0166)
"""
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import configpaths
from gajim.common.jingle_xtls import SELF_SIGNED_CERTIFICATE
from gajim.common.jingle_xtls import load_cert_file
contents = {} # type: Dict[str, Any]
def get_jingle_content(node):
namespace = node.getNamespace()
if namespace in contents:
return contents[namespace](node)
class JingleContentSetupException(Exception):
"""
Exception that should be raised when a content fails to setup.
"""
class JingleContent:
"""
An abstraction of content in Jingle sessions
"""
def __init__(self, session, transport, senders):
self.session = session
self.transport = transport
# will be filled by JingleSession.add_content()
# don't uncomment these lines, we will catch more buggy code then
# (a JingleContent not added to session shouldn't send anything)
self.creator = None
self.name = None
self.accepted = False
self.sent = False
self.negotiated = False
self.media = None
self.senders = senders
if self.senders is None:
self.senders = 'both'
self.allow_sending = True # Used for stream direction, attribute 'senders'
# These were found by the Politie
self.file_props = None
self.use_security = None
self.callbacks = {
# these are called when *we* get stanzas
'content-accept': [self.__on_transport_info,
self.__on_content_accept],
'content-add': [self.__on_transport_info],
'content-modify': [],
'content-reject': [],
'content-remove': [],
'description-info': [],
'security-info': [],
'session-accept': [self.__on_transport_info,
self.__on_content_accept],
'session-info': [],
'session-initiate': [self.__on_transport_info],
'session-terminate': [],
'transport-info': [self.__on_transport_info],
'transport-replace': [self.__on_transport_replace],
'transport-accept': [self.__on_transport_replace],
'transport-reject': [],
'iq-result': [],
'iq-error': [],
# these are called when *we* sent these stanzas
'content-accept-sent': [self.__fill_jingle_stanza,
self.__on_content_accept],
'content-add-sent': [self.__fill_jingle_stanza],
'session-initiate-sent': [self.__fill_jingle_stanza],
'session-accept-sent': [self.__fill_jingle_stanza,
self.__on_content_accept],
'session-terminate-sent': [],
}
def is_ready(self):
return self.accepted and not self.sent
def __on_content_accept(self, stanza, content, error, action):
self.on_negotiated()
def on_negotiated(self):
if self.accepted:
self.negotiated = True
self.session.content_negotiated(self.media)
def add_remote_candidates(self, candidates):
"""
Add a list of candidates to the list of remote candidates
"""
self.transport.remote_candidates = candidates
def on_stanza(self, stanza, content, error, action):
"""
Called when something related to our content was sent by peer
"""
if action in self.callbacks:
for callback in self.callbacks[action]:
callback(stanza, content, error, action)
def __on_transport_replace(self, stanza, content, error, action):
content.addChild(node=self.transport.make_transport())
def __on_transport_info(self, stanza, content, error, action):
"""
Got a new transport candidate
"""
candidates = self.transport.parse_transport_stanza(
content.getTag('transport'))
if candidates:
self.add_remote_candidates(candidates)
def __content(self, payload=None):
"""
Build a XML content-wrapper for our data
"""
if payload is None:
payload = []
return nbxmpp.Node('content',
attrs={'name': self.name,
'creator': self.creator,
'senders': self.senders},
payload=payload)
def send_candidate(self, candidate):
"""
Send a transport candidate for a previously defined transport.
"""
content = self.__content()
content.addChild(node=self.transport.make_transport([candidate]))
self.session.send_transport_info(content)
def send_error_candidate(self):
"""
Sends a candidate-error when we can't connect to a candidate.
"""
content = self.__content()
tp = self.transport.make_transport(add_candidates=False)
tp.addChild(name='candidate-error')
content.addChild(node=tp)
self.session.send_transport_info(content)
def send_description_info(self):
content = self.__content()
self._fill_content(content)
self.session.send_description_info(content)
def __fill_jingle_stanza(self, stanza, content, error, action):
"""
Add our things to session-initiate stanza
"""
self._fill_content(content)
self.sent = True
content.addChild(node=self.transport.make_transport())
def _fill_content(self, content):
description_node = nbxmpp.simplexml.Node(
tag=Namespace.JINGLE_FILE_TRANSFER_5 + ' description')
file_tag = description_node.setTag('file')
if self.file_props.name:
node = nbxmpp.simplexml.Node(tag='name')
node.addData(self.file_props.name)
file_tag.addChild(node=node)
if self.file_props.date:
node = nbxmpp.simplexml.Node(tag='date')
node.addData(self.file_props.date)
file_tag.addChild(node=node)
if self.file_props.size:
node = nbxmpp.simplexml.Node(tag='size')
node.addData(self.file_props.size)
file_tag.addChild(node=node)
if self.file_props.type_ == 'r':
if self.file_props.hash_:
file_tag.addChild('hash', attrs={'algo': self.file_props.algo},
namespace=Namespace.HASHES_2,
payload=self.file_props.hash_)
else:
# if the file is less than 10 mb, then it is small
# lets calculate it right away
if self.file_props.size < 10000000 and not self.file_props.hash_:
hash_data = self._compute_hash()
if hash_data:
file_tag.addChild(node=hash_data)
pjid = app.get_jid_without_resource(self.session.peerjid)
file_info = {'name' : self.file_props.name,
'file-name' : self.file_props.file_name,
'hash' : self.file_props.hash_,
'size' : self.file_props.size,
'date' : self.file_props.date,
'peerjid' : pjid
}
self.session.connection.get_module('Jingle').set_file_info(file_info)
desc = file_tag.setTag('desc')
if self.file_props.desc:
desc.setData(self.file_props.desc)
if self.use_security:
security = nbxmpp.simplexml.Node(
tag=Namespace.JINGLE_XTLS + ' security')
certpath = configpaths.get('MY_CERT') / (SELF_SIGNED_CERTIFICATE
+ '.cert')
cert = load_cert_file(certpath)
if cert:
digest_algo = (cert.get_signature_algorithm()
.decode('utf-8').split('With')[0])
security.addChild('fingerprint').addData(cert.digest(
digest_algo).decode('utf-8'))
for m in ('x509', ): # supported authentication methods
method = nbxmpp.simplexml.Node(tag='method')
method.setAttr('name', m)
security.addChild(node=method)
content.addChild(node=security)
content.addChild(node=description_node)
def destroy(self):
self.callbacks = None
del self.session.contents[(self.creator, self.name)]

409
gajim/common/jingle_ft.py Normal file
View file

@ -0,0 +1,409 @@
# 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/>.
"""
Handles Jingle File Transfer (XEP 0234)
"""
import hashlib
import logging
import threading
from enum import IntEnum, unique
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import configpaths
from gajim.common import jingle_xtls
from gajim.common.jingle_content import contents, JingleContent
from gajim.common.jingle_transport import JingleTransportSocks5, TransportType
from gajim.common import helpers
from gajim.common.connection_handlers_events import FileRequestReceivedEvent
from gajim.common.jingle_ftstates import (
StateInitialized, StateCandSent, StateCandReceived, StateTransfering,
StateCandSentAndRecv, StateTransportReplace)
log = logging.getLogger('gajim.c.jingle_ft')
@unique
class State(IntEnum):
NOT_STARTED = 0
INITIALIZED = 1
# We send the candidates and we are waiting for a reply
CAND_SENT = 2
# We received the candidates and we are waiting to reply
CAND_RECEIVED = 3
# We have sent and received the candidates
# This also includes any candidate-error received or sent
CAND_SENT_AND_RECEIVED = 4
TRANSPORT_REPLACE = 5
# We are transferring the file
TRANSFERRING = 6
class JingleFileTransfer(JingleContent):
def __init__(self, session, transport=None, file_props=None,
use_security=False, senders=None):
JingleContent.__init__(self, session, transport, senders)
log.info("transport value: %s", transport)
# events we might be interested in
self.callbacks['session-initiate'] += [self.__on_session_initiate]
self.callbacks['session-initiate-sent'] += [
self.__on_session_initiate_sent]
self.callbacks['content-add'] += [self.__on_session_initiate]
self.callbacks['session-accept'] += [self.__on_session_accept]
self.callbacks['session-terminate'] += [self.__on_session_terminate]
self.callbacks['session-info'] += [self.__on_session_info]
self.callbacks['transport-accept'] += [self.__on_transport_accept]
self.callbacks['transport-replace'] += [self.__on_transport_replace]
self.callbacks['session-accept-sent'] += [self.__transport_setup]
# fallback transport method
self.callbacks['transport-reject'] += [self.__on_transport_reject]
self.callbacks['transport-info'] += [self.__on_transport_info]
self.callbacks['iq-result'] += [self.__on_iq_result]
self.use_security = use_security
self.x509_fingerprint = None
self.file_props = file_props
self.weinitiate = self.session.weinitiate
self.werequest = self.session.werequest
if self.file_props is not None:
if self.session.werequest:
self.file_props.sender = self.session.peerjid
self.file_props.receiver = self.session.ourjid
else:
self.file_props.sender = self.session.ourjid
self.file_props.receiver = self.session.peerjid
self.file_props.session_type = 'jingle'
self.file_props.sid = session.sid
self.file_props.transfered_size = []
self.file_props.transport_sid = self.transport.sid
log.info("FT request: %s", file_props)
if transport is None:
self.transport = JingleTransportSocks5()
self.transport.set_connection(session.connection)
self.transport.set_file_props(self.file_props)
self.transport.set_our_jid(session.ourjid)
log.info('ourjid: %s', session.ourjid)
self.session = session
self.media = 'file'
self.nominated_cand = {}
if app.contacts.is_gc_contact(session.connection.name,
session.peerjid):
roomjid = session.peerjid.split('/')[0]
dstaddr = hashlib.sha1(('%s%s%s' % (self.file_props.sid,
session.ourjid, roomjid))
.encode('utf-8')).hexdigest()
self.file_props.dstaddr = dstaddr
self.state = State.NOT_STARTED
self.states = {
State.INITIALIZED : StateInitialized(self),
State.CAND_SENT : StateCandSent(self),
State.CAND_RECEIVED : StateCandReceived(self),
State.TRANSFERRING : StateTransfering(self),
State.TRANSPORT_REPLACE : StateTransportReplace(self),
State.CAND_SENT_AND_RECEIVED : StateCandSentAndRecv(self)
}
cert_name = (configpaths.get('MY_CERT') /
jingle_xtls.SELF_SIGNED_CERTIFICATE)
if not (cert_name.with_suffix('.cert').exists()
and cert_name.with_suffix('.pkey').exists()):
jingle_xtls.make_certs(cert_name, 'gajim')
def __state_changed(self, nextstate, args=None):
# Executes the next state action and sets the next state
current_state = self.state
st = self.states[nextstate]
st.action(args)
# state can have been changed during the action. Don't go back.
if self.state == current_state:
self.state = nextstate
def __on_session_initiate(self, stanza, content, error, action):
log.debug("Jingle FT request received")
app.nec.push_incoming_event(FileRequestReceivedEvent(None,
conn=self.session.connection,
stanza=stanza,
jingle_content=content,
FT_content=self))
if self.session.request:
# accept the request
self.session.approve_content(self.media, self.name)
self.session.accept_session()
def __on_session_initiate_sent(self, stanza, content, error, action):
pass
def __send_hash(self):
# Send hash in a session info
checksum = nbxmpp.Node(tag='checksum',
payload=[nbxmpp.Node(tag='file',
payload=[self._compute_hash()])])
checksum.setNamespace(Namespace.JINGLE_FILE_TRANSFER_5)
self.session.__session_info(checksum)
pjid = app.get_jid_without_resource(self.session.peerjid)
file_info = {'name' : self.file_props.name,
'file-name' : self.file_props.file_name,
'hash' : self.file_props.hash_,
'size' : self.file_props.size,
'date' : self.file_props.date,
'peerjid' : pjid
}
self.session.connection.get_module('Jingle').set_file_info(file_info)
def _compute_hash(self):
# Calculates the hash and returns a xep-300 hash stanza
if self.file_props.algo is None:
return
try:
file_ = open(self.file_props.file_name, 'rb')
except IOError:
# can't open file
return
h = nbxmpp.Hashes2()
hash_ = h.calculateHash(self.file_props.algo, file_)
file_.close()
# DEBUG
#hash_ = '1294809248109223'
if not hash_:
# Hash algorithm not supported
return
self.file_props.hash_ = hash_
h.addHash(hash_, self.file_props.algo)
return h
def on_cert_received(self):
self.session.approve_session()
self.session.approve_content('file', name=self.name)
def __on_session_accept(self, stanza, content, error, action):
log.info("__on_session_accept")
con = self.session.connection
security = content.getTag('security')
if not security: # responder can not verify our fingerprint
self.use_security = False
else:
fingerprint = security.getTag('fingerprint')
if fingerprint:
fingerprint = fingerprint.getData()
self.x509_fingerprint = fingerprint
if not jingle_xtls.check_cert(app.get_jid_without_resource(
self.session.responder), fingerprint):
id_ = jingle_xtls.send_cert_request(con,
self.session.responder)
jingle_xtls.key_exchange_pend(id_,
self.continue_session_accept,
[stanza])
raise nbxmpp.NodeProcessed
self.continue_session_accept(stanza)
def continue_session_accept(self, stanza):
if self.state == State.TRANSPORT_REPLACE:
# If we are requesting we don't have the file
if self.session.werequest:
raise nbxmpp.NodeProcessed
# We send the file
self.__state_changed(State.TRANSFERRING)
raise nbxmpp.NodeProcessed
self.file_props.streamhosts = self.transport.remote_candidates
# Calculate file hash in a new thread
# if we haven't sent the hash already.
if self.file_props.hash_ is None and self.file_props.algo and \
not self.werequest:
self.hash_thread = threading.Thread(target=self.__send_hash)
self.hash_thread.start()
for host in self.file_props.streamhosts:
host['initiator'] = self.session.initiator
host['target'] = self.session.responder
host['sid'] = self.file_props.sid
fingerprint = None
if self.use_security:
fingerprint = 'client'
if self.transport.type_ == TransportType.SOCKS5:
sid = self.file_props.transport_sid
app.socks5queue.connect_to_hosts(self.session.connection.name,
sid,
self.on_connect,
self._on_connect_error,
fingerprint=fingerprint,
receiving=False)
raise nbxmpp.NodeProcessed
self.__state_changed(State.TRANSFERRING)
raise nbxmpp.NodeProcessed
def __on_session_terminate(self, stanza, content, error, action):
log.info("__on_session_terminate")
def __on_session_info(self, stanza, content, error, action):
pass
def __on_transport_accept(self, stanza, content, error, action):
log.info("__on_transport_accept")
def __on_transport_replace(self, stanza, content, error, action):
log.info("__on_transport_replace")
def __on_transport_reject(self, stanza, content, error, action):
log.info("__on_transport_reject")
def __on_transport_info(self, stanza, content, error, action):
log.info("__on_transport_info")
cand_error = content.getTag('transport').getTag('candidate-error')
cand_used = content.getTag('transport').getTag('candidate-used')
if (cand_error or cand_used) and \
self.state >= State.CAND_SENT_AND_RECEIVED:
raise nbxmpp.NodeProcessed
if cand_error:
if not app.socks5queue.listener.connections:
app.socks5queue.listener.disconnect()
self.nominated_cand['peer-cand'] = False
if self.state == State.CAND_SENT:
if not self.nominated_cand['our-cand'] and \
not self.nominated_cand['peer-cand']:
if not self.weinitiate:
return
self.__state_changed(State.TRANSPORT_REPLACE)
else:
response = stanza.buildReply('result')
response.delChild(response.getQuery())
self.session.connection.connection.send(response)
self.__state_changed(State.TRANSFERRING)
raise nbxmpp.NodeProcessed
else:
args = {'candError' : True}
self.__state_changed(State.CAND_RECEIVED, args)
return
if cand_used:
streamhost_cid = cand_used.getAttr('cid')
streamhost_used = None
for cand in self.transport.candidates:
if cand['candidate_id'] == streamhost_cid:
streamhost_used = cand
break
if streamhost_used is None or streamhost_used['type'] == 'proxy':
if app.socks5queue.listener and \
not app.socks5queue.listener.connections:
app.socks5queue.listener.disconnect()
if content.getTag('transport').getTag('activated'):
self.state = State.TRANSFERRING
app.socks5queue.send_file(self.file_props,
self.session.connection.name, 'client')
return
args = {'content': content,
'sendCand': False}
if self.state == State.CAND_SENT:
self.__state_changed(State.CAND_SENT_AND_RECEIVED, args)
self.__state_changed(State.TRANSFERRING)
raise nbxmpp.NodeProcessed
self.__state_changed(State.CAND_RECEIVED, args)
def __on_iq_result(self, stanza, content, error, action):
log.info("__on_iq_result")
if self.state in (State.NOT_STARTED, State.CAND_RECEIVED):
self.__state_changed(State.INITIALIZED)
elif self.state == State.CAND_SENT_AND_RECEIVED:
if not self.nominated_cand['our-cand'] and \
not self.nominated_cand['peer-cand']:
if not self.weinitiate:
return
self.__state_changed(State.TRANSPORT_REPLACE)
return
# initiate transfer
self.__state_changed(State.TRANSFERRING)
def __transport_setup(self, stanza=None, content=None, error=None,
action=None):
# Sets up a few transport specific things for the file transfer
if self.transport.type_ == TransportType.IBB:
# No action required, just set the state to transferring
self.state = State.TRANSFERRING
else:
self._listen_host()
def on_connect(self, streamhost):
"""
send candidate-used stanza
"""
log.info('send_candidate_used')
if streamhost is None:
return
args = {'streamhost' : streamhost,
'sendCand' : True}
self.nominated_cand['our-cand'] = streamhost
self.__send_candidate(args)
def _on_connect_error(self, sid):
log.info('connect error, sid=%s', sid)
args = {'candError' : True,
'sendCand' : True}
self.__send_candidate(args)
def __send_candidate(self, args):
if self.state == State.CAND_RECEIVED:
self.__state_changed(State.CAND_SENT_AND_RECEIVED, args)
else:
self.__state_changed(State.CAND_SENT, args)
def _store_socks5_sid(self, sid, hash_id):
# callback from socsk5queue.start_listener
self.file_props.hash_ = hash_id
def _listen_host(self):
receiver = self.file_props.receiver
sender = self.file_props.sender
sha_str = helpers.get_auth_sha(self.file_props.sid, sender,
receiver)
self.file_props.sha_str = sha_str
port = app.settings.get('file_transfers_port')
fingerprint = None
if self.use_security:
fingerprint = 'server'
listener = app.socks5queue.start_listener(port, sha_str,
self._store_socks5_sid,
self.file_props,
fingerprint=fingerprint,
typ='sender' if self.weinitiate else 'receiver')
if not listener:
# send error message, notify the user
return
def is_our_candidate_used(self):
'''
If this method returns true then the candidate we nominated will be
used, if false, the candidate nominated by peer will be used
'''
if not self.nominated_cand['peer-cand']:
return True
if not self.nominated_cand['our-cand']:
return False
peer_pr = int(self.nominated_cand['peer-cand']['priority'])
our_pr = int(self.nominated_cand['our-cand']['priority'])
if peer_pr != our_pr:
return our_pr > peer_pr
return self.weinitiate
def start_ibb_transfer(self):
if self.file_props.type_ == 's':
self.__state_changed(State.TRANSFERRING)
def get_content(desc):
return JingleFileTransfer
contents[Namespace.JINGLE_FILE_TRANSFER_5] = get_content

View file

@ -0,0 +1,228 @@
# 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 logging
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.jingle_transport import TransportType
from gajim.common.socks5 import Socks5ReceiverClient
from gajim.common.socks5 import Socks5SenderClient
log = logging.getLogger('gajim.c.jingle_ftstates')
class JingleFileTransferStates:
'''
This class implements the state machine design pattern
'''
def __init__(self, jingleft):
self.jft = jingleft
def action(self, args=None):
'''
This method MUST be overridden by a subclass
'''
raise NotImplementedError('This is an abstract method!')
class StateInitialized(JingleFileTransferStates):
'''
This state initializes the file transfer
'''
def action(self, args=None):
if self.jft.weinitiate:
# update connection's fileprops
self.jft._listen_host()
# Listen on configured port for file transfer
else:
fingerprint = None
if self.jft.use_security:
fingerprint = 'client'
# Connect to the candidate host, on success call on_connect method
app.socks5queue.connect_to_hosts(self.jft.session.connection.name,
self.jft.file_props.transport_sid, self.jft.on_connect,
self.jft._on_connect_error, fingerprint=fingerprint)
class StateCandSent(JingleFileTransferStates):
'''
This state sends our nominated candidate
'''
def _send_candidate(self, args):
if 'candError' in args:
self.jft.nominated_cand['our-cand'] = False
self.jft.send_error_candidate()
return
# Send candidate used
streamhost = args['streamhost']
self.jft.nominated_cand['our-cand'] = streamhost
content = nbxmpp.Node('content')
content.setAttr('creator', 'initiator')
content.setAttr('name', self.jft.name)
transport = nbxmpp.Node('transport')
transport.setNamespace(Namespace.JINGLE_BYTESTREAM)
transport.setAttr('sid', self.jft.transport.sid)
candidateused = nbxmpp.Node('candidate-used')
candidateused.setAttr('cid', streamhost['candidate_id'])
transport.addChild(node=candidateused)
content.addChild(node=transport)
self.jft.session.send_transport_info(content)
def action(self, args=None):
self._send_candidate(args)
class StateCandReceived(JingleFileTransferStates):
'''
This state happens when we receive a candidate.
It takes the arguments: canError if we receive a candidate-error
'''
def _recv_candidate(self, args):
if 'candError' in args:
return
content = args['content']
streamhost_cid = content.getTag('transport').getTag('candidate-used').\
getAttr('cid')
streamhost_used = None
for cand in self.jft.transport.candidates:
if cand['candidate_id'] == streamhost_cid:
streamhost_used = cand
break
if streamhost_used is None:
log.info("unknown streamhost")
return
# We save the candidate nominated by peer
self.jft.nominated_cand['peer-cand'] = streamhost_used
def action(self, args=None):
self._recv_candidate(args)
class StateCandSentAndRecv(StateCandSent, StateCandReceived):
'''
This state happens when we have received and sent the candidates.
It takes the boolean argument: sendCand in order to decide whether
we should execute the action of when we receive or send a candidate.
'''
def action(self, args=None):
if args['sendCand']:
self._send_candidate(args)
else:
self._recv_candidate(args)
class StateTransportReplace(JingleFileTransferStates):
'''
This state initiates transport replace
'''
def action(self, args=None):
self.jft.session.transport_replace()
class StateTransfering(JingleFileTransferStates):
'''
This state will start the transfer depending on the type of transport
we have.
'''
def _start_ibb_transfer(self, con):
self.jft.file_props.transport_sid = self.jft.transport.sid
fp = open(self.jft.file_props.file_name, 'rb')
con.get_module('IBB').send_open(self.jft.session.peerjid,
self.jft.file_props.sid,
fp)
def _start_sock5_transfer(self):
# It tells whether we start the transfer as client or server
mode = None
if self.jft.is_our_candidate_used():
mode = 'client'
streamhost_used = self.jft.nominated_cand['our-cand']
app.socks5queue.remove_server(self.jft.file_props.transport_sid)
else:
mode = 'server'
streamhost_used = self.jft.nominated_cand['peer-cand']
app.socks5queue.remove_client(self.jft.file_props.transport_sid)
app.socks5queue.remove_other_servers(streamhost_used['host'])
if streamhost_used['type'] == 'proxy':
self.jft.file_props.is_a_proxy = True
if self.jft.file_props.type_ == 's' and self.jft.weinitiate:
self.jft.file_props.proxy_sender = streamhost_used['initiator']
self.jft.file_props.proxy_receiver = streamhost_used['target']
else:
self.jft.file_props.proxy_sender = streamhost_used['target']
self.jft.file_props.proxy_receiver = streamhost_used[
'initiator']
if self.jft.file_props.type_ == 's':
s = app.socks5queue.senders
for sender in s:
if s[sender].host == streamhost_used['host'] and \
s[sender].connected:
return
elif self.jft.file_props.type_ == 'r':
r = app.socks5queue.readers
for reader in r:
if r[reader].host == streamhost_used['host'] and \
r[reader].connected:
return
else:
raise TypeError
self.jft.file_props.streamhost_used = True
streamhost_used['sid'] = self.jft.file_props.transport_sid
self.jft.file_props.streamhosts = []
self.jft.file_props.streamhosts.append(streamhost_used)
self.jft.file_props.proxyhosts = []
self.jft.file_props.proxyhosts.append(streamhost_used)
if self.jft.file_props.type_ == 's':
app.socks5queue.idx += 1
idx = app.socks5queue.idx
sockobj = Socks5SenderClient(app.idlequeue, idx,
app.socks5queue, _sock=None,
host=str(streamhost_used['host']),
port=int(streamhost_used['port']),
fingerprint=None, connected=False,
file_props=self.jft.file_props)
else:
sockobj = Socks5ReceiverClient(app.idlequeue, streamhost_used,
transport_sid=self.jft.file_props.transport_sid,
file_props=self.jft.file_props, fingerprint=None)
sockobj.proxy = True
sockobj.streamhost = streamhost_used
app.socks5queue.add_sockobj(self.jft.session.connection.name,
sockobj)
streamhost_used['idx'] = sockobj.queue_idx
# If we offered the nominated candidate used, we activate
# the proxy
if not self.jft.is_our_candidate_used():
app.socks5queue.on_success[self.jft.file_props.transport_sid]\
= self.jft.transport._on_proxy_auth_ok
# TODO: add on failure
else:
app.socks5queue.send_file(self.jft.file_props,
self.jft.session.connection.name, mode)
def action(self, args=None):
if self.jft.transport.type_ == TransportType.IBB:
self._start_ibb_transfer(self.jft.session.connection)
elif self.jft.transport.type_ == TransportType.SOCKS5:
self._start_sock5_transfer()

534
gajim/common/jingle_rtp.py Normal file
View file

@ -0,0 +1,534 @@
# 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/>.
"""
Handles Jingle RTP sessions (XEP 0167)
"""
import os
import logging
import socket
from collections import deque
from datetime import datetime
import nbxmpp
from nbxmpp.namespaces import Namespace
from gi.repository import GLib
try:
from gi.repository import Farstream
from gi.repository import Gst
except Exception:
pass
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.jingle_transport import JingleTransportICEUDP
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
log = logging.getLogger('gajim.c.jingle_rtp')
class JingleRTPContent(JingleContent):
def __init__(self, session, media, transport=None):
if transport is None:
transport = JingleTransportICEUDP(None)
JingleContent.__init__(self, session, transport, None)
self.media = media
self._dtmf_running = False
self.farstream_media = {
'audio': Farstream.MediaType.AUDIO,
'video': Farstream.MediaType.VIDEO}[media]
self.pipeline = None
self.src_bin = None
self.stream_failed_once = False
self.candidates_ready = False # True when local candidates are prepared
# TODO
self.conference = None
self.funnel = None
self.p2psession = None
self.p2pstream = None
self.callbacks['session-initiate'] += [self.__on_remote_codecs]
self.callbacks['content-add'] += [self.__on_remote_codecs]
self.callbacks['description-info'] += [self.__on_remote_codecs]
self.callbacks['content-accept'] += [self.__on_remote_codecs]
self.callbacks['session-accept'] += [self.__on_remote_codecs]
self.callbacks['session-terminate'] += [self.__stop]
self.callbacks['session-terminate-sent'] += [self.__stop]
def setup_stream(self, on_src_pad_added):
# pipeline and bus
self.pipeline = Gst.Pipeline()
bus = self.pipeline.get_bus()
bus.add_signal_watch()
bus.connect('message', self._on_gst_message)
# conference
self.conference = Gst.ElementFactory.make('fsrtpconference', None)
self.pipeline.add(self.conference)
self.funnel = None
self.p2psession = self.conference.new_session(self.farstream_media)
participant = self.conference.new_participant()
# FIXME: Consider a workaround, here...
# pidgin and telepathy-gabble don't follow the XEP, and it won't work
# due to bad controlling-mode
params = {'controlling-mode': self.session.weinitiate, 'debug': False}
if app.settings.get('use_stun_server'):
stun_server = app.settings.get('stun_server')
if not stun_server and self.session.connection._stun_servers:
stun_server = self.session.connection._stun_servers[0]['host']
if stun_server:
try:
ip = socket.getaddrinfo(stun_server, 0, socket.AF_UNSPEC,
socket.SOCK_STREAM)[0][4][0]
except socket.gaierror as e:
log.warning('Lookup of stun ip failed: %s', str(e))
else:
params['stun-ip'] = ip
self.p2pstream = self.p2psession.new_stream(participant,
Farstream.StreamDirection.BOTH)
self.p2pstream.connect('src-pad-added', on_src_pad_added)
self.p2pstream.set_transmitter_ht('nice', params)
def is_ready(self):
return JingleContent.is_ready(self) and self.candidates_ready
def make_bin_from_config(self, config_key, pipeline, text):
pipeline = pipeline % app.settings.get(config_key)
log.debug('Pipeline: %s', str(pipeline))
try:
gst_bin = Gst.parse_bin_from_description(pipeline, True)
return gst_bin
except GLib.GError as err:
app.nec.push_incoming_event(
InformationEvent(
None,
conn=self.session.connection,
level='error',
pri_txt=_('%s configuration error') % text,
sec_txt=_('Couldnt set up %(text)s. Check your '
'configuration.\nPipeline:\n%(pipeline)s\n'
'Error:\n%(error)s') % {
'text': text,
'pipeline': pipeline,
'error': str(err)}))
raise JingleContentSetupException
def add_remote_candidates(self, candidates):
JingleContent.add_remote_candidates(self, candidates)
# FIXME: connectivity should not be established yet
# Instead, it should be established after session-accept!
if self.sent:
self.p2pstream.add_remote_candidates(candidates)
def batch_dtmf(self, events):
"""
Send several DTMF tones
"""
if self._dtmf_running:
raise Exception("There is a DTMF batch already running")
events = deque(events)
self._dtmf_running = True
self.start_dtmf(events.popleft())
GLib.timeout_add(500, self._next_dtmf, events)
def _next_dtmf(self, events):
self.stop_dtmf()
if events:
self.start_dtmf(events.popleft())
GLib.timeout_add(500, self._next_dtmf, events)
else:
self._dtmf_running = False
def start_dtmf(self, event):
if event in ('*', '#'):
event = {'*': Farstream.DTMFEvent.STAR,
'#': Farstream.DTMFEvent.POUND}[event]
else:
event = int(event)
self.p2psession.start_telephony_event(event, 2)
def stop_dtmf(self):
self.p2psession.stop_telephony_event()
def _fill_content(self, content):
content.addChild(Namespace.JINGLE_RTP + ' description',
attrs={'media': self.media},
payload=list(self.iter_codecs()))
def _setup_funnel(self):
self.funnel = Gst.ElementFactory.make('funnel', None)
self.pipeline.add(self.funnel)
self.funnel.link(self.sink)
self.sink.set_state(Gst.State.PLAYING)
self.funnel.set_state(Gst.State.PLAYING)
def _on_src_pad_added(self, _stream, pad, codec):
log.info('Used codec: %s', codec.to_string())
if not self.funnel:
self._setup_funnel()
pad.link(self.funnel.get_request_pad('sink_%u'))
def _on_gst_message(self, _bus, message):
if message.type == Gst.MessageType.ELEMENT:
name = message.get_structure().get_name()
message_string = message.get_structure().to_string()
log.debug('gst element message: %s', message_string)
if name == 'farstream-new-active-candidate-pair':
pass
elif name == 'farstream-recv-codecs-changed':
pass
elif name == 'farstream-codecs-changed':
if self.sent and self.p2psession.props.codecs_without_config:
self.send_description_info()
if self.transport.remote_candidates:
# those lines MUST be done after we get info on our
# codecs
self.p2pstream.add_remote_candidates(
self.transport.remote_candidates)
self.transport.remote_candidates = []
self.p2pstream.set_property('direction',
Farstream.StreamDirection.BOTH)
elif name == 'farstream-local-candidates-prepared':
self.candidates_ready = True
if self.is_ready():
self.session.on_session_state_changed(self)
elif name == 'farstream-new-local-candidate':
candidate = self.p2pstream.parse_new_local_candidate(message)[1]
self.transport.candidates.append(candidate)
if self.sent:
# FIXME: Is this case even possible?
self.send_candidate(candidate)
elif name == 'farstream-component-state-changed':
state = message.get_structure().get_value('state')
if state == Farstream.StreamState.FAILED:
reason = nbxmpp.Node('reason')
reason.setTag('failed-transport')
self.session.remove_content(self.creator, self.name, reason)
elif name == 'farstream-error':
log.error('Farstream error #%d!\nMessage: %s',
message.get_structure().get_value('error-no'),
message.get_structure().get_value('error-msg'))
elif message.type == Gst.MessageType.ERROR:
# TODO: Fix it to fallback to videotestsrc anytime an error occur,
# or raise an error, Jingle way
# or maybe one-sided stream?
gerror_msg = message.get_structure().get_value('gerror')
debug_msg = message.get_structure().get_value('debug')
log.error(gerror_msg)
log.error(debug_msg)
if not self.stream_failed_once:
app.nec.push_incoming_event(
InformationEvent(
None, dialog_name='gstreamer-error',
kwargs={'error': gerror_msg, 'debug': debug_msg}))
sink_pad = self.p2psession.get_property('sink-pad')
# Remove old source
self.src_bin.get_static_pad('src').unlink(sink_pad)
self.src_bin.set_state(Gst.State.NULL)
self.pipeline.remove(self.src_bin)
if not self.stream_failed_once:
# Add fallback source
self.src_bin = self.get_fallback_src()
self.pipeline.add(self.src_bin)
self.src_bin.get_static_pad('src').link(sink_pad)
self.stream_failed_once = True
else:
reason = nbxmpp.Node('reason')
reason.setTag('failed-application')
self.session.remove_content(self.creator, self.name, reason)
# Start playing again
self.pipeline.set_state(Gst.State.PLAYING)
@staticmethod
def get_fallback_src():
return Gst.ElementFactory.make('fakesrc', None)
def on_negotiated(self):
if self.accepted:
if self.p2psession.get_property('codecs'):
# those lines MUST be done after we get info on our codecs
if self.transport.remote_candidates:
self.p2pstream.add_remote_candidates(
self.transport.remote_candidates)
self.transport.remote_candidates = []
# TODO: Farstream.StreamDirection.BOTH only if senders='both'
# self.p2pstream.set_property('direction',
# Farstream.StreamDirection.BOTH)
JingleContent.on_negotiated(self)
def __on_remote_codecs(self, _stanza, content, _error, _action):
"""
Get peer codecs from what we get from peer
"""
codecs = []
for codec in content.getTag('description').iterTags('payload-type'):
if not codec['id'] or not codec['name'] or not codec['clockrate']:
# ignore invalid payload-types
continue
farstream_codec = Farstream.Codec.new(
int(codec['id']),
codec['name'],
self.farstream_media,
int(codec['clockrate']))
if 'channels' in codec:
farstream_codec.channels = int(codec['channels'])
else:
farstream_codec.channels = 1
for param in codec.iterTags('parameter'):
farstream_codec.add_optional_parameter(
param['name'], str(param['value']))
log.debug('Remote codec: %s (%s)',
codec['name'], codec['clockrate'])
codecs.append(farstream_codec)
if codecs:
try:
self.p2pstream.set_remote_codecs(codecs)
except GLib.Error:
raise FailedApplication
def iter_codecs(self):
codecs = self.p2psession.props.codecs_without_config
for codec in codecs:
attrs = {
'name': codec.encoding_name,
'id': codec.id,
}
if codec.channels > 0:
attrs['channels'] = codec.channels
if codec.clock_rate:
attrs['clockrate'] = codec.clock_rate
if codec.optional_params:
payload = [nbxmpp.Node('parameter',
{'name': p.name, 'value': p.value})
for p in codec.optional_params]
else:
payload = []
yield nbxmpp.Node('payload-type', attrs, payload)
def __stop(self, *things):
self.pipeline.set_state(Gst.State.NULL)
def __del__(self):
self.__stop()
def destroy(self):
JingleContent.destroy(self)
self.p2pstream.disconnect_by_func(self._on_src_pad_added)
self.pipeline.get_bus().disconnect_by_func(self._on_gst_message)
class JingleAudio(JingleRTPContent):
"""
Jingle VoIP sessions consist of audio content transported over an ICE UDP
protocol
"""
def __init__(self, session, transport=None):
JingleRTPContent.__init__(self, session, 'audio', transport)
self.setup_stream()
def set_mic_volume(self, vol):
"""
vol must be between 0 and 1
"""
self.mic_volume.set_property('volume', vol)
def set_out_volume(self, vol):
"""
vol must be between 0 and 1
"""
self.out_volume.set_property('volume', vol)
def setup_stream(self):
JingleRTPContent.setup_stream(self, self._on_src_pad_added)
# list of codecs that are explicitly allowed
allow_codecs = [
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'OPUS',
Farstream.MediaType.AUDIO, 48000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'SPEEX',
Farstream.MediaType.AUDIO, 32000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'G722',
Farstream.MediaType.AUDIO, 8000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'SPEEX',
Farstream.MediaType.AUDIO, 16000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'PCMA',
Farstream.MediaType.AUDIO, 8000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'PCMU',
Farstream.MediaType.AUDIO, 8000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'SPEEX',
Farstream.MediaType.AUDIO, 8000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'AMR',
Farstream.MediaType.AUDIO, 8000),
]
# disable all other codecs
disable_codecs = []
codecs_without_config = self.p2psession.props.codecs_without_config
allowed_encoding_names = [c.encoding_name for c in allow_codecs]
allowed_encoding_names.append('telephone-event')
for codec in codecs_without_config:
if codec.encoding_name not in allowed_encoding_names:
disable_codecs.append(Farstream.Codec.new(
Farstream.CODEC_ID_DISABLE,
codec.encoding_name,
Farstream.MediaType.AUDIO,
codec.clock_rate))
self.p2psession.set_codec_preferences(allow_codecs + disable_codecs)
# the local parts
# TODO: Add queues?
self.src_bin = self.make_bin_from_config(
'audio_input_device',
'%s ! audioconvert',
_('audio input'))
self.sink = self.make_bin_from_config(
'audio_output_device',
'audioconvert ! volume name=gajim_out_vol ! %s',
_('audio output'))
self.mic_volume = self.src_bin.get_by_name('gajim_vol')
self.out_volume = self.sink.get_by_name('gajim_out_vol')
# link gst elements
self.pipeline.add(self.sink)
self.pipeline.add(self.src_bin)
self.src_bin.get_static_pad('src').link(
self.p2psession.get_property('sink-pad'))
# The following is needed for farstream to process ICE requests:
self.pipeline.set_state(Gst.State.PLAYING)
class JingleVideo(JingleRTPContent):
def __init__(self, session, transport=None):
JingleRTPContent.__init__(self, session, 'video', transport)
self.sink = None
self.setup_stream()
def setup_stream(self):
# TODO: Everything is not working properly:
# sometimes, one window won't show up,
# sometimes it'll freeze...
JingleRTPContent.setup_stream(self, self._on_src_pad_added)
bus = self.pipeline.get_bus()
bus.enable_sync_message_emission()
# list of codecs that are explicitly allowed
# for now only VP8/H264 (available in gst-plugins-good)
allow_codecs = [
#Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'VP9',
# Farstream.MediaType.VIDEO, 90000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'VP8',
Farstream.MediaType.VIDEO, 90000),
Farstream.Codec.new(Farstream.CODEC_ID_ANY, 'H264',
Farstream.MediaType.VIDEO, 90000),
]
# disable all other codecs
disable_codecs = []
codecs_without_config = self.p2psession.props.codecs_without_config
allowed_encoding_names = [c.encoding_name for c in allow_codecs]
for codec in codecs_without_config:
if codec.encoding_name not in allowed_encoding_names:
disable_codecs.append(Farstream.Codec.new(
Farstream.CODEC_ID_DISABLE,
codec.encoding_name,
Farstream.MediaType.VIDEO,
codec.clock_rate))
self.p2psession.set_codec_preferences(allow_codecs + disable_codecs)
def do_setup(self, self_display_sink, other_sink):
if app.settings.get('video_see_self'):
tee = ('! tee name=split ! queue name=self-display-queue split. ! '
'queue name=network-queue')
else:
tee = ''
self.sink = other_sink
self.pipeline.add(self.sink)
self.src_bin = self.make_bin_from_config(
'video_input_device',
'%%s %s' % tee,
_('video input'))
self.pipeline.add(self.src_bin)
if app.settings.get('video_see_self'):
self.pipeline.add(self_display_sink)
self_display_queue = self.src_bin.get_by_name('self-display-queue')
self_display_queue.get_static_pad('src').link_maybe_ghosting(
self_display_sink.get_static_pad('sink'))
self.src_bin.get_static_pad('src').link(
self.p2psession.get_property('sink-pad'))
# The following is needed for farstream to process ICE requests:
self.pipeline.set_state(Gst.State.PLAYING)
if log.getEffectiveLevel() == logging.DEBUG:
# Use 'export GST_DEBUG_DUMP_DOT_DIR=/tmp/' before starting Gajim
timestamp = datetime.now().strftime('%m-%d-%Y-%H-%M-%S')
name = f'video-graph-{timestamp}'
debug_dir = os.environ.get('GST_DEBUG_DUMP_DOT_DIR')
name_dot = f'{debug_dir}/{name}.dot'
name_png = f'{debug_dir}/{name}.png'
Gst.debug_bin_to_dot_file(
self.pipeline, Gst.DebugGraphDetails.ALL, name)
if debug_dir:
try:
os.system(f'dot -Tpng {name_dot} > {name_png}')
except Exception:
log.debug('Could not save pipeline graph. Make sure '
'graphviz is installed.')
def get_fallback_src(self):
# TODO: Use avatar?
pipeline = ('videotestsrc is-live=true ! video/x-raw,framerate=10/1 ! '
'videoconvert')
return Gst.parse_bin_from_description(pipeline, True)
def get_content(desc):
if desc['media'] == 'audio':
return JingleAudio
if desc['media'] == 'video':
return JingleVideo
contents[Namespace.JINGLE_RTP] = get_content

View file

@ -0,0 +1,849 @@
# 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/>.
"""
Handles Jingle sessions (XEP 0166)
"""
#TODO:
# * 'senders' attribute of 'content' element
# * security preconditions
# * actions:
# - content-modify
# - session-info
# - security-info
# - transport-accept, transport-reject
# - Tie-breaking
# * timeout
import logging
from enum import Enum, unique
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common.jingle_transport import get_jingle_transport
from gajim.common.jingle_transport import JingleTransportIBB
from gajim.common.jingle_content import get_jingle_content
from gajim.common.jingle_content import JingleContentSetupException
from gajim.common.jingle_ft import State
from gajim.common.connection_handlers_events import FilesProp
from gajim.common.nec import NetworkEvent
log = logging.getLogger("app.c.jingle_session")
# FIXME: Move it to JingleSession.States?
@unique
class JingleStates(Enum):
"""
States in which jingle session may exist
"""
ENDED = 0
PENDING = 1
ACTIVE = 2
class OutOfOrder(Exception):
"""
Exception that should be raised when an action is received when in the wrong
state
"""
class TieBreak(Exception):
"""
Exception that should be raised in case of a tie, when we overrule the other
action
"""
class FailedApplication(Exception):
"""
Exception that should be raised in case responder supports none of the
payload-types offered by the initiator
"""
class JingleSession:
"""
This represents one jingle session, that is, one or more content types
negotiated between an initiator and a responder.
"""
def __init__(self, con, weinitiate, jid, iq_id=None, sid=None,
werequest=False):
"""
con -- connection object,
weinitiate -- boolean, are we the initiator?
jid - jid of the other entity
"""
self.contents = {} # negotiated contents
self.connection = con # connection to use
# our full jid
self.ourjid = str(self.connection.get_own_jid())
self.peerjid = jid # jid we connect to
# jid we use as the initiator
self.initiator = self.ourjid if weinitiate else self.peerjid
# jid we use as the responder
self.responder = self.peerjid if weinitiate else self.ourjid
# are we an initiator?
self.weinitiate = weinitiate
# Are we requesting or offering a file?
self.werequest = werequest
self.request = False
# what state is session in? (one from JingleStates)
self.state = JingleStates.ENDED
if not sid:
sid = generate_id()
self.sid = sid # sessionid
# iq stanza id, used to determine which sessions to summon callback
# later on when iq-result stanza arrives
if iq_id is not None:
self.iq_ids = [iq_id]
else:
self.iq_ids = []
self.accepted = True # is this session accepted by user
# Tells whether this session is a file transfer or not
self.session_type_ft = False
# callbacks to call on proper contents
# use .prepend() to add new callbacks, especially when you're going
# to send error instead of ack
self.callbacks = {
'content-accept': [self.__ack, self.__on_content_accept,
self.__broadcast],
'content-add': [self.__ack,
self.__on_content_add, self.__broadcast
], #TODO
'content-modify': [self.__ack], #TODO
'content-reject': [self.__ack, self.__on_content_remove],
'content-remove': [self.__ack, self.__on_content_remove],
'description-info': [self.__ack, self.__broadcast], #TODO
'security-info': [self.__ack], #TODO
'session-accept': [self.__ack, self.__on_session_accept,
self.__on_content_accept,
self.__broadcast],
'session-info': [self.__ack, self.__broadcast,
self.__on_session_info],
'session-initiate': [self.__ack, self.__on_session_initiate,
self.__broadcast],
'session-terminate': [self.__ack, self.__on_session_terminate,
self.__broadcast_all],
'transport-info': [self.__ack, self.__broadcast],
'transport-replace': [self.__ack, self.__broadcast,
self.__on_transport_replace], #TODO
'transport-accept': [self.__ack, self.__on_session_accept,
self.__on_content_accept,
self.__broadcast],
'transport-reject': [self.__ack], #TODO
'iq-result': [self.__broadcast],
'iq-error': [self.__on_error],
}
def collect_iq_id(self, iq_id):
if iq_id is not None:
self.iq_ids.append(iq_id)
def approve_session(self):
"""
Called when user accepts session in UI (when we aren't the initiator)
"""
self.accept_session()
def decline_session(self):
"""
Called when user declines session in UI (when we aren't the initiator)
"""
reason = nbxmpp.Node('reason')
reason.addChild('decline')
self._session_terminate(reason)
def cancel_session(self):
"""
Called when user declines session in UI (when we aren't the initiator)
"""
reason = nbxmpp.Node('reason')
reason.addChild('cancel')
self._session_terminate(reason)
def approve_content(self, media, name=None):
content = self.get_content(media, name)
if content:
content.accepted = True
self.on_session_state_changed(content)
def reject_content(self, media, name=None):
content = self.get_content(media, name)
if content:
if self.state == JingleStates.ACTIVE:
self.__content_reject(content)
content.destroy()
self.on_session_state_changed()
def end_session(self):
"""
Called when user stops or cancel session in UI
"""
reason = nbxmpp.Node('reason')
if self.state == JingleStates.ACTIVE:
reason.addChild('success')
else:
reason.addChild('cancel')
self._session_terminate(reason)
def get_content(self, media=None, name=None):
if media is None:
return
for content in self.contents.values():
if content.media == media:
if name is None or content.name == name:
return content
def add_content(self, name, content, creator='we'):
"""
Add new content to session. If the session is active, this will send
proper stanza to update session
Creator must be one of ('we', 'peer', 'initiator', 'responder')
"""
assert creator in ('we', 'peer', 'initiator', 'responder')
if (creator == 'we' and self.weinitiate) or (creator == 'peer' and \
not self.weinitiate):
creator = 'initiator'
elif (creator == 'peer' and self.weinitiate) or (creator == 'we' and \
not self.weinitiate):
creator = 'responder'
content.creator = creator
content.name = name
self.contents[(creator, name)] = content
if (creator == 'initiator') == self.weinitiate:
# The content is from us, accept it
content.accepted = True
def remove_content(self, creator, name, reason=None):
"""
Remove the content `name` created by `creator`
by sending content-remove, or by sending session-terminate if
there is no content left.
"""
if (creator, name) in self.contents:
content = self.contents[(creator, name)]
self.__content_remove(content, reason)
self.contents[(creator, name)].destroy()
if not self.contents:
self.end_session()
def modify_content(self, creator, name, transport=None):
'''
Currently used for transport replacement
'''
content = self.contents[(creator, name)]
file_props = content.transport.file_props
file_props.transport_sid = transport.sid
transport.set_file_props(file_props)
content.transport = transport
# The content will have to be resend now that it is modified
content.sent = False
content.accepted = True
def on_session_state_changed(self, content=None):
if self.state == JingleStates.ENDED:
# Session not yet started, only one action possible: session-initiate
if self.is_ready() and self.weinitiate:
self.__session_initiate()
elif self.state == JingleStates.PENDING:
# We can either send a session-accept or a content-add
if self.is_ready() and not self.weinitiate:
self.__session_accept()
elif content and (content.creator == 'initiator') == self.weinitiate:
self.__content_add(content)
elif content and self.weinitiate:
self.__content_accept(content)
elif self.state == JingleStates.ACTIVE:
# We can either send a content-add or a content-accept. However, if
# we are sending a file we can only use session_initiate.
if not content:
return
we_created_content = (content.creator == 'initiator') \
== self.weinitiate
if we_created_content and content.media == 'file':
self.__session_initiate()
if we_created_content:
# We initiated this content. It's a pending content-add.
self.__content_add(content)
else:
# The other side created this content, we accept it.
self.__content_accept(content)
def is_ready(self):
"""
Return True when all codecs and candidates are ready (for all contents)
"""
return (all((content.is_ready() for content in self.contents.values()))
and self.accepted)
def accept_session(self):
"""
Mark the session as accepted
"""
self.accepted = True
self.on_session_state_changed()
def start_session(self):
"""
Mark the session as ready to be started
"""
self.accepted = True
self.on_session_state_changed()
def send_session_info(self):
pass
def send_content_accept(self, content):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('content-accept')
jingle.addChild(node=content)
self.connection.connection.send(stanza)
def send_transport_info(self, content):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('transport-info')
jingle.addChild(node=content)
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
def send_description_info(self, content):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('description-info')
jingle.addChild(node=content)
self.connection.connection.send(stanza)
def on_stanza(self, stanza):
"""
A callback for ConnectionJingle. It gets stanza, then tries to send it to
all internally registered callbacks. First one to raise
nbxmpp.NodeProcessed breaks function
"""
jingle = stanza.getTag('jingle')
error = stanza.getTag('error')
if error:
# it's an iq-error stanza
action = 'iq-error'
elif jingle:
# it's a jingle action
action = jingle.getAttr('action')
if action not in self.callbacks:
self.__send_error(stanza, 'bad-request')
return
# FIXME: If we aren't initiated and it's not a session-initiate...
if action not in ['session-initiate', 'session-terminate'] \
and self.state == JingleStates.ENDED:
self.__send_error(stanza, 'item-not-found', 'unknown-session')
return
else:
# it's an iq-result (ack) stanza
action = 'iq-result'
callables = self.callbacks[action]
try:
for call in callables:
call(stanza=stanza, jingle=jingle, error=error, action=action)
except nbxmpp.NodeProcessed:
pass
except TieBreak:
self.__send_error(stanza, 'conflict', 'tiebreak')
except OutOfOrder:
# FIXME
self.__send_error(stanza, 'unexpected-request', 'out-of-order')
except FailedApplication:
reason = nbxmpp.Node('reason')
reason.addChild('failed-application')
self._session_terminate(reason)
def __ack(self, stanza, jingle, error, action):
"""
Default callback for action stanzas -- simple ack and stop processing
"""
response = stanza.buildReply('result')
response.delChild(response.getQuery())
self.connection.connection.send(response)
def __on_error(self, stanza, jingle, error, action):
# FIXME
text = error.getTagData('text')
error_name = None
for child in error.getChildren():
if child.getNamespace() == Namespace.JINGLE_ERRORS:
error_name = child.getName()
break
if child.getNamespace() == Namespace.STANZAS:
error_name = child.getName()
self.__dispatch_error(error_name, text, error.getAttr('type'))
def transport_replace(self):
transport = JingleTransportIBB()
# For debug only, delete this and replace for a function
# that will identify contents by its sid
for creator, name in self.contents:
self.modify_content(creator, name, transport)
cont = self.contents[(creator, name)]
cont.transport = transport
stanza, jingle = self.__make_jingle('transport-replace')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'transport-replace')
self.connection.connection.send(stanza)
self.state = JingleStates.PENDING
def __on_transport_replace(self, stanza, jingle, error, action):
for content in jingle.iterTags('content'):
creator = content['creator']
name = content['name']
if (creator, name) in self.contents:
transport_ns = content.getTag('transport').getNamespace()
if transport_ns == Namespace.JINGLE_ICE_UDP:
# FIXME: We don't manage anything else than ICE-UDP now...
# What was the previous transport?!?
# Anyway, content's transport is not modifiable yet
pass
elif transport_ns == Namespace.JINGLE_IBB:
transport = JingleTransportIBB(node=content.getTag(
'transport'))
self.modify_content(creator, name, transport)
self.state = JingleStates.PENDING
self.contents[(creator, name)].state = State.TRANSPORT_REPLACE
self.__ack(stanza, jingle, error, action)
self.__transport_accept(transport)
else:
stanza, jingle = self.__make_jingle('transport-reject')
content = jingle.setTag('content', attrs={'creator': creator,
'name': name})
content.setTag('transport', namespace=transport_ns)
self.connection.connection.send(stanza)
raise nbxmpp.NodeProcessed
else:
# FIXME: This resource is unknown to us, what should we do?
# For now, reject the transport
stanza, jingle = self.__make_jingle('transport-reject')
content = jingle.setTag('content', attrs={'creator': creator,
'name': name})
content.setTag('transport', namespace=transport_ns)
self.connection.connection.send(stanza)
raise nbxmpp.NodeProcessed
def __on_session_info(self, stanza, jingle, error, action):
# TODO: active, (un)hold, (un)mute
payload = jingle.getPayload()
if payload[0].getName() == 'ringing':
# ignore ringing
raise nbxmpp.NodeProcessed
if self.state != JingleStates.ACTIVE:
raise OutOfOrder
for child in payload:
if child.getName() == 'checksum':
hash_ = child.getTag('file').getTag(name='hash',
namespace=Namespace.HASHES_2)
if hash_ is None:
continue
algo = hash_.getAttr('algo')
if algo in nbxmpp.Hashes2.supported:
file_props = FilesProp.getFileProp(self.connection.name,
self.sid)
file_props.algo = algo
file_props.hash_ = hash_.getData()
raise nbxmpp.NodeProcessed
self.__send_error(stanza, 'feature-not-implemented', 'unsupported-info',
type_='modify')
raise nbxmpp.NodeProcessed
def __on_content_remove(self, stanza, jingle, error, action):
for content in jingle.iterTags('content'):
creator = content['creator']
name = content['name']
if (creator, name) in self.contents:
content = self.contents[(creator, name)]
# TODO: this will fail if content is not an RTP content
self._raise_event('jingle-disconnected-received',
media=content.media,
reason='removed')
content.destroy()
if not self.contents:
reason = nbxmpp.Node('reason')
reason.setTag('success')
self._session_terminate(reason)
def __on_session_accept(self, stanza, jingle, error, action):
# FIXME
if self.state != JingleStates.PENDING:
raise OutOfOrder
self.state = JingleStates.ACTIVE
@staticmethod
def __on_content_accept(stanza, jingle, error, action):
"""
Called when we get content-accept stanza or equivalent one (like
session-accept)
"""
# check which contents are accepted
# for content in jingle.iterTags('content'):
# creator = content['creator']
# name = content['name']
return
def __on_content_add(self, stanza, jingle, error, action):
if self.state == JingleStates.ENDED:
raise OutOfOrder
parse_result = self.__parse_contents(jingle)
contents = parse_result[0]
# rejected_contents = parse_result[1]
# for name, creator in rejected_contents:
# content = JingleContent()
# self.add_content(name, content, creator)
# self.__content_reject(content)
# self.contents[(content.creator, content.name)].destroy()
self._raise_event('jingle-request-received', contents=contents)
def __on_session_initiate(self, stanza, jingle, error, action):
"""
We got a jingle session request from other entity, therefore we are the
receiver... Unpack the data, inform the user
"""
if self.state != JingleStates.ENDED:
raise OutOfOrder
self.initiator = jingle['initiator']
self.responder = self.ourjid
self.peerjid = self.initiator
self.accepted = False # user did not accept this session yet
# TODO: If the initiator is unknown to the receiver (e.g., via presence
# subscription) and the receiver has a policy of not communicating via
# Jingle with unknown entities, it SHOULD return a <service-unavailable/>
# error.
# Lets check what kind of jingle session does the peer want
contents, _contents_rejected, reason_txt = self.__parse_contents(jingle)
# If there's no content we understand...
if not contents:
# TODO: http://xmpp.org/extensions/xep-0166.html#session-terminate
reason = nbxmpp.Node('reason')
reason.setTag(reason_txt)
self.__ack(stanza, jingle, error, action)
self._session_terminate(reason)
raise nbxmpp.NodeProcessed
# If we are not receiving a file
# Check if there's already a session with this user:
if contents[0].media != 'file':
for session in self.connection.get_module('Jingle').get_jingle_sessions(self.peerjid):
if session is not self:
reason = nbxmpp.Node('reason')
alternative_session = reason.setTag('alternative-session')
alternative_session.setTagData('sid', session.sid)
self.__ack(stanza, jingle, error, action)
self._session_terminate(reason)
raise nbxmpp.NodeProcessed
else:
# Stop if we don't have the requested file or the peer is not
# allowed to request the file
request = contents[0].senders == 'responder'
if request:
self.request = True
hash_tag = request.getTag('file').getTag('hash')
hash_data = hash_tag.getData() if hash_tag else None
n = request.getTag('file').getTag('name')
n = n.getData() if n else None
pjid = app.get_jid_without_resource(self.peerjid)
file_info = self.connection.get_module('Jingle').get_file_info(
pjid, hash_data, n, self.connection.name)
if not file_info:
log.warning('The peer %s is requesting a ' \
'file that we dont have or ' \
'it is not allowed to request', pjid)
self.decline_session()
raise nbxmpp.NodeProcessed
self.state = JingleStates.PENDING
# Send event about starting a session
self._raise_event('jingle-request-received', contents=contents)
def __broadcast(self, stanza, jingle, error, action):
"""
Broadcast the stanza contents to proper content handlers
"""
#if jingle is None: # it is a iq-result stanza
# for cn in self.contents.values():
# cn.on_stanza(stanza, None, error, action)
# return
# special case: iq-result stanza does not come with a jingle element
if action == 'iq-result':
for cn in self.contents.values():
cn.on_stanza(stanza, None, error, action)
return
for content in jingle.iterTags('content'):
name = content['name']
creator = content['creator']
if (creator, name) not in self.contents:
text = 'Content %s (created by %s) does not exist' % (name, creator)
self.__send_error(stanza, 'bad-request', text=text, type_='modify')
raise nbxmpp.NodeProcessed
cn = self.contents[(creator, name)]
cn.on_stanza(stanza, content, error, action)
def __on_session_terminate(self, stanza, jingle, error, action):
self.connection.get_module('Jingle').delete_jingle_session(self.sid)
reason, text = self.__reason_from_stanza(jingle)
if reason not in ('success', 'cancel', 'decline'):
self.__dispatch_error(reason, text)
if text:
text = '%s (%s)' % (reason, text)
else:
# TODO
text = reason
if reason == 'decline':
self._raise_event('jingle-disconnected-received',
media=None,
reason=text)
if reason == 'success':
self._raise_event('jingle-disconnected-received',
media=None,
reason=text)
if reason == 'cancel' and self.session_type_ft:
self._raise_event('jingle-ft-cancelled-received',
media=None,
reason=text)
def __broadcast_all(self, stanza, jingle, error, action):
"""
Broadcast the stanza to all content handlers
"""
for content in self.contents.values():
content.on_stanza(stanza, None, error, action)
def __parse_contents(self, jingle):
# TODO: Needs some reworking
contents = []
contents_rejected = []
reasons = set()
for element in jingle.iterTags('content'):
transport = get_jingle_transport(element.getTag('transport'))
if transport:
transport.ourjid = self.ourjid
content_type = get_jingle_content(element.getTag('description'))
if content_type:
try:
if transport:
content = content_type(self, transport=transport)
self.add_content(element['name'],
content, 'peer')
contents.append(content)
else:
reasons.add('unsupported-transports')
contents_rejected.append((element['name'], 'peer'))
except JingleContentSetupException:
reasons.add('failed-application')
else:
contents_rejected.append((element['name'], 'peer'))
reasons.add('unsupported-applications')
failure_reason = None
# Store the first reason of failure
for reason in ('failed-application', 'unsupported-transports',
'unsupported-applications'):
if reason in reasons:
failure_reason = reason
break
return (contents, contents_rejected, failure_reason)
def __dispatch_error(self, error=None, text=None, type_=None):
if text:
text = '%s (%s)' % (error, text)
if type_ != 'modify':
self._raise_event('jingle-error-received', reason=text or error)
@staticmethod
def __reason_from_stanza(stanza):
# TODO: Move to GUI?
reason = 'success'
reasons = [
'success', 'busy', 'cancel', 'connectivity-error', 'decline',
'expired', 'failed-application', 'failed-transport',
'general-error', 'gone', 'incompatible-parameters', 'media-error',
'security-error', 'timeout', 'unsupported-applications',
'unsupported-transports'
]
tag = stanza.getTag('reason')
text = ''
if tag:
text = tag.getTagData('text')
for r in reasons:
if tag.getTag(r):
reason = r
break
return (reason, text)
def __make_jingle(self, action, reason=None):
stanza = nbxmpp.Iq(typ='set', to=nbxmpp.JID.from_string(self.peerjid),
frm=self.ourjid)
attrs = {
'action': action,
'sid': self.sid,
'initiator' : self.initiator
}
jingle = stanza.addChild('jingle', attrs=attrs,
namespace=Namespace.JINGLE)
if reason is not None:
jingle.addChild(node=reason)
return stanza, jingle
def __send_error(self, stanza, error, jingle_error=None, text=None, type_=None):
err_stanza = nbxmpp.Error(stanza, '%s %s' % (Namespace.STANZAS, error))
err = err_stanza.getTag('error')
if type_:
err.setAttr('type', type_)
if jingle_error:
err.setTag(jingle_error, namespace=Namespace.JINGLE_ERRORS)
if text:
err.setTagData('text', text)
self.connection.connection.send(err_stanza)
self.__dispatch_error(jingle_error or error, text, type_)
@staticmethod
def __append_content(jingle, content):
"""
Append <content/> element to <jingle/> element
"""
jingle.addChild('content',
attrs={'name': content.name,
'creator': content.creator,
'senders': content.senders})
def __append_contents(self, jingle):
"""
Append all <content/> elements to <jingle/>
"""
# TODO: integrate with __appendContent?
# TODO: parameters 'name', 'content'?
for content in self.contents.values():
if content.is_ready():
self.__append_content(jingle, content)
def __session_initiate(self):
assert self.state == JingleStates.ENDED
stanza, jingle = self.__make_jingle('session-initiate')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'session-initiate-sent')
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.PENDING
def __session_accept(self):
assert self.state == JingleStates.PENDING
stanza, jingle = self.__make_jingle('session-accept')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'session-accept-sent')
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.ACTIVE
def __session_info(self, payload=None):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('session-info')
if payload:
jingle.addChild(node=payload)
self.connection.connection.send(stanza)
def _JingleFileTransfer__session_info(self, payload):
# For some strange reason when I call
# self.session.__session_info(payload) from the jingleFileTransfer object
# within a thread, this method gets called instead. Even though, it
# isn't being called explicitly.
self.__session_info(payload)
def _session_terminate(self, reason=None):
stanza, jingle = self.__make_jingle('session-terminate', reason=reason)
self.__broadcast_all(stanza, jingle, None, 'session-terminate-sent')
if self.connection.connection and self.connection.state.is_available:
self.connection.connection.send(stanza)
# TODO: Move to GUI?
reason, text = self.__reason_from_stanza(jingle)
if reason not in ('success', 'cancel', 'decline'):
self.__dispatch_error(reason, text)
if text:
text = '%s (%s)' % (reason, text)
else:
text = reason
self.connection.get_module('Jingle').delete_jingle_session(self.sid)
self._raise_event('jingle-disconnected-received',
media=None,
reason=text)
def __content_add(self, content):
# TODO: test
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('content-add')
self.__append_content(jingle, content)
self.__broadcast(stanza, jingle, None, 'content-add-sent')
id_ = self.connection.connection.send(stanza)
self.collect_iq_id(id_)
def __content_accept(self, content):
# TODO: test
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('content-accept')
self.__append_content(jingle, content)
self.__broadcast(stanza, jingle, None, 'content-accept-sent')
id_ = self.connection.connection.send(stanza)
self.collect_iq_id(id_)
def __content_reject(self, content):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('content-reject')
self.__append_content(jingle, content)
self.connection.connection.send(stanza)
# TODO: this will fail if content is not an RTP content
self._raise_event('jingle-disconnected-received',
media=content.media,
reason='rejected')
def __content_modify(self):
assert self.state != JingleStates.ENDED
def __content_remove(self, content, reason=None):
assert self.state != JingleStates.ENDED
if self.connection.connection and self.connection.state.is_available:
stanza, jingle = self.__make_jingle('content-remove', reason=reason)
self.__append_content(jingle, content)
self.connection.connection.send(stanza)
# TODO: this will fail if content is not an RTP content
self._raise_event('jingle-disconnected-received',
media=content.media,
reason='removed')
def content_negotiated(self, media):
self._raise_event('jingle-connected-received', media=media)
def __transport_accept(self, transport):
assert self.state != JingleStates.ENDED
stanza, jingle = self.__make_jingle('transport-accept')
self.__append_contents(jingle)
self.__broadcast(stanza, jingle, None, 'transport-accept')
self.connection.connection.send(stanza)
self.collect_iq_id(stanza.getID())
self.state = JingleStates.ACTIVE
def _raise_event(self, name, **kwargs):
jid, resource = app.get_room_and_nick_from_fjid(self.peerjid)
app.nec.push_incoming_event(
NetworkEvent(name,
conn=self.connection,
fjid=self.peerjid,
jid=jid,
sid=self.sid,
resource=resource,
jingle_session=self,
**kwargs))

View file

@ -0,0 +1,502 @@
# 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/>.
"""
Handles Jingle Transports (currently only ICE-UDP)
"""
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
import logging
import socket
from enum import IntEnum, unique
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.util import generate_id
from gajim.common import app
log = logging.getLogger('gajim.c.jingle_transport')
transports = {} # type: Dict[str, Any]
def get_jingle_transport(node):
namespace = node.getNamespace()
if namespace in transports:
return transports[namespace](node)
@unique
class TransportType(IntEnum):
"""
Possible types of a JingleTransport
"""
ICEUDP = 1
SOCKS5 = 2
IBB = 3
class JingleTransport:
"""
An abstraction of a transport in Jingle sessions
"""
__slots__ = ['type_', 'candidates', 'remote_candidates', 'connection',
'file_props', 'ourjid', 'sid']
def __init__(self, type_):
self.type_ = type_
self.candidates = []
self.remote_candidates = []
self.connection = None
self.file_props = None
self.ourjid = None
self.sid = None
def _iter_candidates(self):
for candidate in self.candidates:
yield self.make_candidate(candidate)
def make_candidate(self, candidate):
"""
Build a candidate stanza for the given candidate
"""
def make_transport(self, candidates=None):
"""
Build a transport stanza with the given candidates (or self.candidates if
candidates is None)
"""
if not candidates:
candidates = list(self._iter_candidates())
else:
candidates = (self.make_candidate(candidate) for candidate in candidates)
transport = nbxmpp.Node('transport', payload=candidates)
return transport
def parse_transport_stanza(self, transport):
"""
Return the list of transport candidates from a transport stanza
"""
return []
def set_connection(self, conn):
self.connection = conn
if not self.sid:
self.sid = generate_id()
def set_file_props(self, file_props):
self.file_props = file_props
def set_our_jid(self, jid):
self.ourjid = jid
def set_sid(self, sid):
self.sid = sid
class JingleTransportSocks5(JingleTransport):
"""
Socks5 transport in jingle scenario
Note: Don't forget to call set_file_props after initialization
"""
def __init__(self, node=None):
JingleTransport.__init__(self, TransportType.SOCKS5)
self.connection = None
self.remote_candidates = []
self.sid = None
if node and node.getAttr('sid'):
self.sid = node.getAttr('sid')
def make_candidate(self, candidate):
log.info('candidate dict, %s', candidate)
attrs = {
'cid': candidate['candidate_id'],
'host': candidate['host'],
'jid': candidate['jid'],
'port': candidate['port'],
'priority': candidate['priority'],
'type': candidate['type']
}
return nbxmpp.Node('candidate', attrs=attrs)
def make_transport(self, candidates=None, add_candidates=True):
if add_candidates:
self._add_local_ips_as_candidates()
self._add_additional_candidates()
self._add_proxy_candidates()
transport = JingleTransport.make_transport(self, candidates)
else:
transport = nbxmpp.Node('transport')
transport.setNamespace(Namespace.JINGLE_BYTESTREAM)
transport.setAttr('sid', self.sid)
if self.file_props.dstaddr:
transport.setAttr('dstaddr', self.file_props.dstaddr)
return transport
def parse_transport_stanza(self, transport):
candidates = []
for candidate in transport.iterTags('candidate'):
typ = 'direct' # default value
if candidate.has_attr('type'):
typ = candidate['type']
cand = {
'state': 0,
'target': self.ourjid,
'host': candidate['host'],
'port': int(candidate['port']),
'candidate_id': candidate['cid'],
'type': typ,
'priority': candidate['priority']
}
candidates.append(cand)
# we need this when we construct file_props on session-initiation
if candidates:
self.remote_candidates = candidates
return candidates
def _add_candidates(self, candidates):
for cand in candidates:
in_remote = False
for cand2 in self.remote_candidates:
if cand['host'] == cand2['host'] and \
cand['port'] == cand2['port']:
in_remote = True
break
if not in_remote:
self.candidates.append(cand)
def _add_local_ips_as_candidates(self):
if not app.settings.get_account_setting(self.connection.name,
'ft_send_local_ips'):
return
if not self.connection:
return
port = int(app.settings.get('file_transfers_port'))
#type preference of connection type. XEP-0260 section 2.2
type_preference = 126
priority = (2**16) * type_preference
hosts = set()
local_ip_cand = []
my_ip = self.connection.local_address
if my_ip is None:
log.warning('No local address available')
else:
candidate = {
'host': my_ip,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority
}
hosts.add(my_ip)
local_ip_cand.append(candidate)
try:
for addrinfo in socket.getaddrinfo(socket.gethostname(), None):
addr = addrinfo[4][0]
if not addr in hosts and not addr.startswith('127.') and \
addr != '::1':
candidate = {
'host': addr,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
hosts.add(addr)
local_ip_cand.append(candidate)
except socket.gaierror:
pass # ignore address-related errors for getaddrinfo
try:
from netifaces import interfaces, ifaddresses, AF_INET, AF_INET6
for ifaceName in interfaces():
addresses = ifaddresses(ifaceName)
if AF_INET in addresses:
for address in addresses[AF_INET]:
addr = address['addr']
if addr in hosts or addr.startswith('127.'):
continue
candidate = {
'host': addr,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
hosts.add(addr)
local_ip_cand.append(candidate)
if AF_INET6 in addresses:
for address in addresses[AF_INET6]:
addr = address['addr']
if addr in hosts or addr.startswith('::1') or \
addr.count(':') != 7:
continue
candidate = {
'host': addr,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
hosts.add(addr)
local_ip_cand.append(candidate)
except ImportError:
pass
self._add_candidates(local_ip_cand)
def _add_additional_candidates(self):
if not self.connection:
return
type_preference = 126
priority = (2**16) * type_preference
additional_ip_cand = []
port = int(app.settings.get('file_transfers_port'))
ft_add_hosts = app.settings.get('ft_add_hosts_to_send')
if ft_add_hosts:
hosts = [e.strip() for e in ft_add_hosts.split(',')]
for host in hosts:
candidate = {
'host': host,
'candidate_id': generate_id(),
'port': port,
'type': 'direct',
'jid': self.ourjid,
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
additional_ip_cand.append(candidate)
self._add_candidates(additional_ip_cand)
def _add_proxy_candidates(self):
if not self.connection:
return
type_preference = 10
priority = (2**16) * type_preference
proxy_cand = []
socks5conn = self.connection
proxyhosts = socks5conn.get_module('Bytestream')._get_file_transfer_proxies_from_config(self.file_props)
if proxyhosts:
self.file_props.proxyhosts = proxyhosts
for proxyhost in proxyhosts:
candidate = {
'host': proxyhost['host'],
'candidate_id': generate_id(),
'port': int(proxyhost['port']),
'type': 'proxy',
'jid': proxyhost['jid'],
'priority': priority,
'initiator': self.file_props.sender,
'target': self.file_props.receiver
}
proxy_cand.append(candidate)
self._add_candidates(proxy_cand)
def get_content(self):
sesn = self.connection.get_module('Jingle').get_jingle_session(
self.ourjid, self.file_props.sid)
for content in sesn.contents.values():
if content.transport == self:
return content
def _on_proxy_auth_ok(self, proxy):
log.info('proxy auth ok for %s', str(proxy))
# send activate request to proxy, send activated confirmation to peer
if not self.connection:
return
sesn = self.connection.get_module('Jingle').get_jingle_session(
self.ourjid, self.file_props.sid)
if sesn is None:
return
iq = nbxmpp.Iq(to=proxy['jid'], frm=self.ourjid, typ='set')
auth_id = "au_" + proxy['sid']
iq.setID(auth_id)
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
query.setAttr('sid', proxy['sid'])
activate = query.setTag('activate')
activate.setData(sesn.peerjid)
iq.setID(auth_id)
self.connection.connection.send(iq)
content = nbxmpp.Node('content')
content.setAttr('creator', 'initiator')
content_object = self.get_content()
content.setAttr('name', content_object.name)
transport = nbxmpp.Node('transport')
transport.setNamespace(Namespace.JINGLE_BYTESTREAM)
transport.setAttr('sid', proxy['sid'])
activated = nbxmpp.Node('activated')
cid = None
if 'cid' in proxy:
cid = proxy['cid']
else:
for host in self.candidates:
if host['host'] == proxy['host'] and host['jid'] == proxy['jid'] \
and host['port'] == proxy['port']:
cid = host['candidate_id']
break
if cid is None:
raise Exception('cid is missing')
activated.setAttr('cid', cid)
transport.addChild(node=activated)
content.addChild(node=transport)
sesn.send_transport_info(content)
class JingleTransportIBB(JingleTransport):
def __init__(self, node=None, block_sz=None):
JingleTransport.__init__(self, TransportType.IBB)
if block_sz:
self.block_sz = block_sz
else:
self.block_sz = '4096'
self.connection = None
self.sid = None
if node and node.getAttr('sid'):
self.sid = node.getAttr('sid')
def make_transport(self):
transport = nbxmpp.Node('transport')
transport.setNamespace(Namespace.JINGLE_IBB)
transport.setAttr('block-size', self.block_sz)
transport.setAttr('sid', self.sid)
return transport
try:
from gi.repository import Farstream
except ImportError:
pass
class JingleTransportICEUDP(JingleTransport):
def __init__(self, node):
JingleTransport.__init__(self, TransportType.ICEUDP)
def make_candidate(self, candidate):
types = {
Farstream.CandidateType.HOST: 'host',
Farstream.CandidateType.SRFLX: 'srflx',
Farstream.CandidateType.PRFLX: 'prflx',
Farstream.CandidateType.RELAY: 'relay',
Farstream.CandidateType.MULTICAST: 'multicast'
}
attrs = {
'component': candidate.component_id,
'foundation': '1', # hack
'generation': '0',
'ip': candidate.ip,
'network': '0',
'port': candidate.port,
'priority': int(candidate.priority), # hack
'id': app.get_an_id()
}
if candidate.type in types:
attrs['type'] = types[candidate.type]
if candidate.proto == Farstream.NetworkProtocol.UDP:
attrs['protocol'] = 'udp'
else:
# we actually don't handle properly different tcp options in jingle
attrs['protocol'] = 'tcp'
return nbxmpp.Node('candidate', attrs=attrs)
def make_transport(self, candidates=None):
transport = JingleTransport.make_transport(self, candidates)
transport.setNamespace(Namespace.JINGLE_ICE_UDP)
if self.candidates and self.candidates[0].username and \
self.candidates[0].password:
transport.setAttr('ufrag', self.candidates[0].username)
transport.setAttr('pwd', self.candidates[0].password)
return transport
def parse_transport_stanza(self, transport):
candidates = []
for candidate in transport.iterTags('candidate'):
foundation = str(candidate['foundation'])
component_id = int(candidate['component'])
ip = str(candidate['ip'])
port = int(candidate['port'])
base_ip = None
base_port = 0
if candidate['protocol'] == 'udp':
proto = Farstream.NetworkProtocol.UDP
else:
# we actually don't handle properly different tcp options in
# jingle
proto = Farstream.NetworkProtocol.TCP
priority = int(candidate['priority'])
types = {
'host': Farstream.CandidateType.HOST,
'srflx': Farstream.CandidateType.SRFLX,
'prflx': Farstream.CandidateType.PRFLX,
'relay': Farstream.CandidateType.RELAY,
'multicast': Farstream.CandidateType.MULTICAST
}
if 'type' in candidate and candidate['type'] in types:
type_ = types[candidate['type']]
else:
log.warning('Unknown type %s', candidate['type'])
type_ = Farstream.CandidateType.HOST
username = str(transport['ufrag'])
password = str(transport['pwd'])
ttl = 0
cand = Farstream.Candidate.new_full(foundation, component_id, ip,
port, base_ip, base_port,
proto, priority, type_,
username, password, ttl)
candidates.append(cand)
self.remote_candidates.extend(candidates)
return candidates
transports[Namespace.JINGLE_ICE_UDP] = JingleTransportICEUDP
transports[Namespace.JINGLE_BYTESTREAM] = JingleTransportSocks5
transports[Namespace.JINGLE_IBB] = JingleTransportIBB

282
gajim/common/jingle_xtls.py Normal file
View file

@ -0,0 +1,282 @@
# 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 logging
from pathlib import Path
from OpenSSL import SSL, crypto
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common import configpaths
log = logging.getLogger('gajim.c.jingle_xtls')
# key-exchange id -> [callback, args], accept that session once key-exchange completes
pending_contents = {}
def key_exchange_pend(id_, cb, args):
# args is a list
pending_contents[id_] = [cb, args]
def approve_pending_content(id_):
cb = pending_contents[id_][0]
args = pending_contents[id_][1]
cb(*args)
TYPE_RSA = crypto.TYPE_RSA
TYPE_DSA = crypto.TYPE_DSA
SELF_SIGNED_CERTIFICATE = 'localcert'
DH_PARAMS = 'dh_params.pem'
DEFAULT_DH_PARAMS = 'dh4096.pem'
def default_callback(connection, certificate, error_num, depth, return_code):
log.info("certificate: %s", certificate)
return return_code
def load_cert_file(cert_path, cert_store=None):
"""
This is almost identical to the one in nbxmpp.tls_nb
"""
if not cert_path.is_file():
return None
try:
f = open(cert_path)
except IOError as e:
log.warning('Unable to open certificate file %s: %s', cert_path,
str(e))
return None
lines = f.readlines()
i = 0
begin = -1
for line in lines:
if 'BEGIN CERTIFICATE' in line:
begin = i
elif 'END CERTIFICATE' in line and begin > -1:
cert = ''.join(lines[begin:i+2])
try:
x509cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
if cert_store:
cert_store.add_cert(x509cert)
f.close()
return x509cert
except crypto.Error as exception_obj:
log.warning('Unable to load a certificate from file %s: %s',
cert_path, exception_obj.args[0][0][2])
except Exception:
log.warning('Unknown error while loading certificate from file '
'%s', cert_path)
begin = -1
i += 1
f.close()
def get_context(fingerprint, verify_cb=None, remote_jid=None):
"""
constructs and returns the context objects
"""
ctx = SSL.Context(SSL.SSLv23_METHOD)
flags = (SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 | SSL.OP_SINGLE_DH_USE \
| SSL.OP_NO_TICKET)
ctx.set_options(flags)
ctx.set_cipher_list(b'HIGH:!aNULL:!3DES')
if fingerprint == 'server': # for testing purposes only
ctx.set_verify(SSL.VERIFY_NONE|SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
verify_cb or default_callback)
elif fingerprint == 'client':
ctx.set_verify(SSL.VERIFY_PEER, verify_cb or default_callback)
cert_name = configpaths.get('MY_CERT') / SELF_SIGNED_CERTIFICATE
ctx.use_privatekey_file(str(cert_name.with_suffix('.pkey')).encode('utf-8'))
ctx.use_certificate_file(str(cert_name.with_suffix('.cert')).encode('utf-8'))
# Try to load Diffie-Hellman parameters.
# First try user DH parameters, if this fails load the default DH parameters
dh_params_name = configpaths.get('MY_CERT') / DH_PARAMS
try:
with open(dh_params_name, "r"):
ctx.load_tmp_dh(dh_params_name.encode('utf-8'))
except FileNotFoundError as err:
default_dh_params_name = (configpaths.get('DATA') / 'other' /
DEFAULT_DH_PARAMS)
try:
with open(default_dh_params_name, "r"):
ctx.load_tmp_dh(str(default_dh_params_name).encode('utf-8'))
except FileNotFoundError as err:
log.error('Unable to load default DH parameter file: %s, %s',
default_dh_params_name, err)
raise
if remote_jid:
store = ctx.get_cert_store()
path = configpaths.get('MY_PEER_CERTS').expanduser() / (remote_jid
+ '.cert')
if path.exists():
load_cert_file(path, cert_store=store)
log.debug('certificate file %s loaded fingerprint %s',
path, fingerprint)
return ctx
def read_cert(certpath):
certificate = ''
with open(certpath, 'r') as certfile:
for line in certfile.readlines():
if not line.startswith('-'):
certificate += line
return certificate
def send_cert(con, jid_from, sid):
certpath = configpaths.get('MY_CERT') / (SELF_SIGNED_CERTIFICATE
+ '.cert')
certificate = read_cert(certpath)
iq = nbxmpp.Iq('result', to=jid_from)
iq.setAttr('id', sid)
pubkey = iq.setTag('pubkeys')
pubkey.setNamespace(Namespace.PUBKEY_PUBKEY)
keyinfo = pubkey.setTag('keyinfo')
name = keyinfo.setTag('name')
name.setData('CertificateHash')
cert = keyinfo.setTag('x509cert')
cert.setData(certificate)
con.send(iq)
def handle_new_cert(con, obj, jid_from):
jid = app.get_jid_without_resource(jid_from)
certpath = configpaths.get('MY_PEER_CERTS').expanduser() / (jid + '.cert')
id_ = obj.getAttr('id')
x509cert = obj.getTag('pubkeys').getTag('keyinfo').getTag('x509cert')
cert = x509cert.getData()
f = open(certpath, 'w')
f.write('-----BEGIN CERTIFICATE-----\n')
f.write(cert)
f.write('-----END CERTIFICATE-----\n')
f.close()
approve_pending_content(id_)
def check_cert(jid, fingerprint):
certpath = configpaths.get('MY_PEER_CERTS').expanduser() / (jid + '.cert')
if certpath.exists():
cert = load_cert_file(certpath)
if cert:
digest_algo = cert.get_signature_algorithm().decode('utf-8')\
.split('With')[0]
if cert.digest(digest_algo) == fingerprint:
return True
return False
def send_cert_request(con, to_jid):
iq = nbxmpp.Iq('get', to=to_jid)
id_ = generate_id()
iq.setAttr('id', id_)
pubkey = iq.setTag('pubkeys')
pubkey.setNamespace(Namespace.PUBKEY_PUBKEY)
con.connection.send(iq)
return str(id_)
# the following code is partly due to pyopenssl examples
def createKeyPair(type_, bits):
"""
Create a public/private key pair.
Arguments: type_ - Key type, must be one of TYPE_RSA and TYPE_DSA
bits - Number of bits to use in the key
Returns: The public/private key pair in a PKey object
"""
pkey = crypto.PKey()
pkey.generate_key(type_, bits)
return pkey
def createCertRequest(pkey, digest="sha256", **name):
"""
Create a certificate request.
Arguments: pkey - The key to associate with the request
digest - Digestion method to use for signing, default is sha256
**name - The name of the subject of the request, possible
arguments are:
C - Country name
ST - State or province name
L - Locality name
O - Organization name
OU - Organizational unit name
CN - Common name
emailAddress - E-mail address
Returns: The certificate request in an X509Req object
"""
req = crypto.X509Req()
subj = req.get_subject()
for (key, value) in name.items():
setattr(subj, key, value)
req.set_pubkey(pkey)
req.sign(pkey, digest)
return req
def createCertificate(req, issuerCert, issuerKey, serial, notBefore, notAfter, digest="sha256"):
"""
Generate a certificate given a certificate request.
Arguments: req - Certificate request to use
issuerCert - The certificate of the issuer
issuerKey - The private key of the issuer
serial - Serial number for the certificate
notBefore - Timestamp (relative to now) when the certificate
starts being valid
notAfter - Timestamp (relative to now) when the certificate
stops being valid
digest - Digest method to use for signing, default is sha256
Returns: The signed certificate in an X509 object
"""
cert = crypto.X509()
cert.set_serial_number(serial)
cert.gmtime_adj_notBefore(notBefore)
cert.gmtime_adj_notAfter(notAfter)
cert.set_issuer(issuerCert.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(issuerKey, digest)
return cert
def make_certs(filepath, CN):
"""
make self signed certificates
filepath : absolute path of certificate file, will be appended the '.pkey'
and '.cert' extensions
CN : common name
"""
key = createKeyPair(TYPE_RSA, 4096)
req = createCertRequest(key, CN=CN)
cert = createCertificate(req, req, key, 0, 0, 60*60*24*365*5) # five years
with open(filepath.with_suffix('.pkey'), 'wb') as f:
filepath.with_suffix('.pkey').chmod(0o600)
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
with open(filepath.with_suffix('.cert'), 'wb') as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
if __name__ == '__main__':
make_certs(Path('selfcert'), 'gajim')

View file

@ -0,0 +1,231 @@
# Copyright (C) 2009 Bruno Tarquini <btarquini AT gmail.com>
#
# 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 logging
import os
import sys
import time
from datetime import datetime
from gajim.common import app
from gajim.common import configpaths
from gajim.common.i18n import _
def parseLogLevel(arg):
"""
Either numeric value or level name from logging module
"""
if arg.isdigit():
return int(arg)
if arg.isupper() and hasattr(logging, arg):
return getattr(logging, arg)
print(_('%s is not a valid loglevel') % repr(arg), file=sys.stderr)
return 0
def parseLogTarget(arg):
"""
[gajim.]c.x.y -> gajim.c.x.y
.other_logger -> other_logger
<None> -> gajim
"""
arg = arg.lower()
if not arg:
return 'gajim'
if arg.startswith('.'):
return arg[1:]
if arg.startswith('gajim'):
return arg
return 'gajim.' + arg
def parseAndSetLogLevels(arg):
"""
[=]LOGLEVEL -> gajim=LOGLEVEL
gajim=LOGLEVEL -> gajim=LOGLEVEL
.other=10 -> other=10
.=10 -> <nothing>
c.x.y=c.z=20 -> gajim.c.x.y=20
gajim.c.z=20
gajim=10,c.x=20 -> gajim=10
gajim.c.x=20
"""
for directive in arg.split(','):
directive = directive.strip()
if not directive:
continue
if '=' not in directive:
directive = '=' + directive
targets, level = directive.rsplit('=', 1)
level = parseLogLevel(level.strip())
for target in targets.split('='):
target = parseLogTarget(target.strip())
if target:
logging.getLogger(target).setLevel(level)
print("Logger %s level set to %d" % (target, level),
file=sys.stderr)
class colors:
# pylint: disable=C0326
NONE = chr(27) + "[0m"
BLACk = chr(27) + "[30m"
RED = chr(27) + "[31m"
GREEN = chr(27) + "[32m"
BROWN = chr(27) + "[33m"
BLUE = chr(27) + "[34m"
MAGENTA = chr(27) + "[35m"
CYAN = chr(27) + "[36m"
LIGHT_GRAY = chr(27) + "[37m"
DARK_GRAY = chr(27) + "[30;1m"
BRIGHT_RED = chr(27) + "[31;1m"
BRIGHT_GREEN = chr(27) + "[32;1m"
YELLOW = chr(27) + "[33;1m"
BRIGHT_BLUE = chr(27) + "[34;1m"
PURPLE = chr(27) + "[35;1m"
BRIGHT_CYAN = chr(27) + "[36;1m"
WHITE = chr(27) + "[37;1m"
def colorize(text, color):
return color + text + colors.NONE
class FancyFormatter(logging.Formatter):
"""
An eye-candy formatter with colors
"""
colors_mapping = {
'DEBUG': colors.BLUE,
'INFO': colors.GREEN,
'WARNING': colors.BROWN,
'ERROR': colors.RED,
'CRITICAL': colors.BRIGHT_RED,
}
def __init__(self, fmt, datefmt=None, use_color=False):
logging.Formatter.__init__(self, fmt, datefmt)
self.use_color = use_color
def formatTime(self, record, datefmt=None):
f = logging.Formatter.formatTime(self, record, datefmt)
if self.use_color:
f = colorize(f, colors.DARK_GRAY)
return f
def format(self, record):
level = record.levelname
record.levelname = '(%s)' % level[0]
if self.use_color:
c = FancyFormatter.colors_mapping.get(level, '')
record.levelname = colorize(record.levelname, c)
record.name = '%-25s' % colorize(record.name, colors.CYAN)
else:
record.name = '%-25s|' % record.name
return logging.Formatter.format(self, record)
def init():
"""
Iinitialize the logging system
"""
if app.get_debug_mode():
_cleanup_debug_logs()
_redirect_output()
use_color = False
if os.name != 'nt':
use_color = sys.stderr.isatty()
consoleloghandler = logging.StreamHandler()
consoleloghandler.setFormatter(
FancyFormatter(
'%(asctime)s %(levelname)s %(name)-35s %(message)s',
'%x %H:%M:%S',
use_color
)
)
root_log = logging.getLogger('gajim')
root_log.setLevel(logging.WARNING)
root_log.addHandler(consoleloghandler)
root_log.propagate = False
root_log = logging.getLogger('nbxmpp')
root_log.setLevel(logging.WARNING)
root_log.addHandler(consoleloghandler)
root_log.propagate = False
root_log = logging.getLogger('gnupg')
root_log.setLevel(logging.WARNING)
root_log.addHandler(consoleloghandler)
root_log.propagate = False
# GAJIM_DEBUG is set only on Windows when using Gajim-Debug.exe
# Gajim-Debug.exe shows a command line prompt and we want to redirect
# log output to it
if app.get_debug_mode() or os.environ.get('GAJIM_DEBUG', False):
set_verbose()
def set_loglevels(loglevels_string):
parseAndSetLogLevels(loglevels_string)
def set_verbose():
parseAndSetLogLevels('gajim=DEBUG')
parseAndSetLogLevels('.nbxmpp=INFO')
def set_quiet():
parseAndSetLogLevels('gajim=CRITICAL')
parseAndSetLogLevels('.nbxmpp=CRITICAL')
def _redirect_output():
debug_folder = configpaths.get('DEBUG')
date = datetime.today().strftime('%d%m%Y-%H%M%S')
filename = '%s-debug.log' % date
fd = open(debug_folder / filename, 'a')
sys.stderr = sys.stdout = fd
def _cleanup_debug_logs():
debug_folder = configpaths.get('DEBUG')
debug_files = list(debug_folder.glob('*-debug.log*'))
now = time.time()
for file in debug_files:
# Delete everything older than 3 days
if file.stat().st_ctime < now - 259200:
file.unlink()
# tests
if __name__ == '__main__':
init()
set_loglevels('gajim.c=DEBUG,INFO')
log = logging.getLogger('gajim')
log.debug('debug')
log.info('info')
log.warning('warn')
log.error('error')
log.critical('critical')
log = logging.getLogger('gajim.c.x.dispatcher')
log.debug('debug')
log.info('info')
log.warning('warn')
log.error('error')
log.critical('critical')

View file

@ -0,0 +1,174 @@
# 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/>.
from typing import Any
from typing import Dict # pylint: disable=unused-import
from typing import List
from typing import Tuple
import sys
import logging
from importlib import import_module
from unittest.mock import MagicMock
from gajim.common.types import ConnectionT
log = logging.getLogger('gajim.c.m')
ZEROCONF_MODULES = ['iq',
'adhoc_commands',
'receipts',
'discovery',
'chatstates']
MODULES = [
'adhoc_commands',
'annotations',
'bits_of_binary',
'blocking',
'bookmarks',
'caps',
'carbons',
'chat_markers',
'chatstates',
'delimiter',
'discovery',
'entity_time',
'gateway',
'httpupload',
'http_auth',
'iq',
'last_activity',
'mam',
'message',
'metacontacts',
'muc',
'pep',
'ping',
'presence',
'pubsub',
'receipts',
'register',
'roster',
'roster_item_exchange',
'search',
'security_labels',
'software_version',
'user_activity',
'user_avatar',
'user_location',
'user_mood',
'user_nickname',
'user_tune',
'vcard4',
'vcard_avatars',
'vcard_temp',
'announce',
'ibb',
'jingle',
'bytestream',
]
_imported_modules = [] # type: List[tuple]
_modules = {} # type: Dict[str, Dict[str, Any]]
_store_publish_modules = [
'UserMood',
'UserActivity',
'UserLocation',
'UserTune',
] # type: List[str]
class ModuleMock:
def __init__(self, name: str) -> None:
self._name = name
# HTTPUpload, ..
self.available = False
# Blocking
self.blocked = [] # type: List[Any]
# Delimiter
self.delimiter = '::'
# Bookmarks
self.bookmarks = {} # type: Dict[Any, Any]
# Various Modules
self.supported = False
def __getattr__(self, key: str) -> MagicMock:
return MagicMock()
def register_modules(con: ConnectionT, *args: Any, **kwargs: Any) -> None:
if con in _modules:
return
_modules[con.name] = {}
for module_name in MODULES:
if con.name == 'Local':
if module_name not in ZEROCONF_MODULES:
continue
instance, name = _load_module(module_name, con, *args, **kwargs)
_modules[con.name][name] = instance
def register_single_module(con: ConnectionT, instance: Any, name: str) -> None:
if con.name not in _modules:
raise ValueError('Unknown account name: %s' % con.name)
_modules[con.name][name] = instance
def unregister_modules(con: ConnectionT) -> None:
for instance in _modules[con.name].values():
if hasattr(instance, 'cleanup'):
instance.cleanup()
del _modules[con.name]
def unregister_single_module(con: ConnectionT, name: str) -> None:
if con.name not in _modules:
return
if name not in _modules[con.name]:
return
del _modules[con.name][name]
def send_stored_publish(account: str) -> None:
for name in _store_publish_modules:
_modules[account][name].send_stored_publish()
def get(account: str, name: str) -> Any:
try:
return _modules[account][name]
except KeyError:
return ModuleMock(name)
def _load_module(name: str, con: ConnectionT, *args: Any, **kwargs: Any) -> Any:
if name not in MODULES:
raise ValueError('Module %s does not exist' % name)
module = sys.modules.get(name)
if module is None:
module = import_module('.%s' % name, package='gajim.common.modules')
return module.get_instance(con, *args, **kwargs) # type: ignore
def get_handlers(con: ConnectionT) -> List[Tuple[Any, ...]]:
handlers = [] # type: List[Tuple[Any, ...]]
for module in _modules[con.name].values():
handlers += module.handlers
return handlers

View file

@ -0,0 +1,434 @@
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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 nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules import dataforms
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import _
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.modules.base import BaseModule
class AdHocCommand:
commandnode = 'command'
commandname = 'The Command'
commandfeatures = (Namespace.DATA,)
@staticmethod
def is_visible_for(_samejid):
"""
This returns True if that command should be visible and invocable for
others
samejid - True when command is invoked by an entity with the same bare
jid.
"""
return True
def __init__(self, conn, jid, sessionid):
self.connection = conn
self.jid = jid
self.sessionid = sessionid
def build_response(self, request, status='executing', defaultaction=None,
actions=None):
assert status in ('executing', 'completed', 'canceled')
response = request.buildReply('result')
cmd = response.getTag('command', namespace=Namespace.COMMANDS)
cmd.setAttr('sessionid', self.sessionid)
cmd.setAttr('node', self.commandnode)
cmd.setAttr('status', status)
if defaultaction is not None or actions is not None:
if defaultaction is not None:
assert defaultaction in ('cancel', 'execute', 'prev', 'next',
'complete')
attrs = {'action': defaultaction}
else:
attrs = {}
cmd.addChild('actions', attrs, actions)
return response, cmd
def bad_request(self, stanza):
self.connection.connection.send(
nbxmpp.Error(stanza, Namespace.STANZAS + ' bad-request'))
def cancel(self, request):
response = self.build_response(request, status='canceled')[0]
self.connection.connection.send(response)
return False # finish the session
class ChangeStatusCommand(AdHocCommand):
commandnode = 'change-status'
commandname = _('Change status information')
def __init__(self, conn, jid, sessionid):
AdHocCommand.__init__(self, conn, jid, sessionid)
self._callback = self.first_step
@staticmethod
def is_visible_for(samejid):
"""
Change status is visible only if the entity has the same bare jid
"""
return samejid
def execute(self, request):
return self._callback(request)
def first_step(self, request):
# first query...
response, cmd = self.build_response(request,
defaultaction='execute',
actions=['execute'])
cmd.addChild(
node=dataforms.SimpleDataForm(
title=_('Change status'),
instructions=_('Set the presence type and description'),
fields=[
dataforms.create_field(
'list-single',
var='presence-type',
label='Type of presence:',
options=[
('chat', _('Free for chat')),
('online', _('Online')),
('away', _('Away')),
('xa', _('Extended away')),
('dnd', _('Do not disturb')),
('offline', _('Offline - disconnect'))],
value='online',
required=True),
dataforms.create_field(
'text-multi',
var='presence-desc',
label=_('Presence description:'))
]
)
)
self.connection.connection.send(response)
# for next invocation
self._callback = self.second_step
return True # keep the session
def second_step(self, request):
# check if the data is correct
try:
form = dataforms.SimpleDataForm(
extend=request.getTag('command').getTag('x'))
except Exception:
self.bad_request(request)
return False
try:
presencetype = form['presence-type'].value
if presencetype not in ('chat', 'online', 'away',
'xa', 'dnd', 'offline'):
self.bad_request(request)
return False
except Exception:
# KeyError if there's no presence-type field in form or
# AttributeError if that field is of wrong type
self.bad_request(request)
return False
try:
presencedesc = form['presence-desc'].value
except Exception: # same exceptions as in last comment
presencedesc = ''
response, cmd = self.build_response(request, status='completed')
cmd.addChild('note', {}, _('The status has been changed.'))
# if going offline, we need to push response so it won't go into
# queue and disappear
self.connection.connection.send(response,
now=presencetype == 'offline')
# send new status
app.interface.roster.send_status(
self.connection.name, presencetype, presencedesc)
return False # finish the session
class AdHocCommands(BaseModule):
_nbxmpp_extends = 'AdHoc'
_nbxmpp_methods = [
'request_command_list',
'execute_command',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._execute_command_received,
typ='set',
ns=Namespace.COMMANDS),
]
# a list of all commands exposed: node -> command class
self._commands = {}
if app.settings.get('remote_commands'):
for cmdobj in (ChangeStatusCommand,):
self._commands[cmdobj.commandnode] = cmdobj
# a list of sessions; keys are tuples (jid, sessionid, node)
self._sessions = {}
def get_own_bare_jid(self):
return self._con.get_own_jid().bare
def is_same_jid(self, jid):
"""
Test if the bare jid given is the same as our bare jid
"""
return nbxmpp.JID.from_string(jid).bare == self.get_own_bare_jid()
def command_list_query(self, stanza):
iq = stanza.buildReply('result')
jid = helpers.get_full_jid_from_iq(stanza)
query = iq.getTag('query')
# buildReply don't copy the node attribute. Re-add it
query.setAttr('node', Namespace.COMMANDS)
for node, cmd in self._commands.items():
if cmd.is_visible_for(self.is_same_jid(jid)):
query.addChild('item', {
# TODO: find the jid
'jid': str(self._con.get_own_jid()),
'node': node,
'name': cmd.commandname})
self._con.connection.send(iq)
def command_info_query(self, stanza):
"""
Send disco#info result for query for command (XEP-0050, example 6.).
Return True if the result was sent, False if not
"""
try:
jid = helpers.get_full_jid_from_iq(stanza)
except helpers.InvalidFormat:
self._log.warning('Invalid JID: %s, ignoring it', stanza.getFrom())
return False
node = stanza.getTagAttr('query', 'node')
if node not in self._commands:
return False
cmd = self._commands[node]
if cmd.is_visible_for(self.is_same_jid(jid)):
iq = stanza.buildReply('result')
query = iq.getTag('query')
query.addChild('identity',
attrs={'type': 'command-node',
'category': 'automation',
'name': cmd.commandname})
query.addChild('feature', attrs={'var': Namespace.COMMANDS})
for feature in cmd.commandfeatures:
query.addChild('feature', attrs={'var': feature})
self._con.connection.send(iq)
return True
return False
def command_items_query(self, stanza):
"""
Send disco#items result for query for command.
Return True if the result was sent, False if not.
"""
jid = helpers.get_full_jid_from_iq(stanza)
node = stanza.getTagAttr('query', 'node')
if node not in self._commands:
return False
cmd = self._commands[node]
if cmd.is_visible_for(self.is_same_jid(jid)):
iq = stanza.buildReply('result')
self._con.connection.send(iq)
return True
return False
def _execute_command_received(self, _con, stanza, _properties):
jid = helpers.get_full_jid_from_iq(stanza)
cmd = stanza.getTag('command')
if cmd is None:
self._log.error('Malformed stanza (no command node) %s', stanza)
raise nbxmpp.NodeProcessed
node = cmd.getAttr('node')
if node is None:
self._log.error('Malformed stanza (no node attr) %s', stanza)
raise nbxmpp.NodeProcessed
sessionid = cmd.getAttr('sessionid')
if sessionid is None:
# we start a new command session
# only if we are visible for the jid and command exist
if node not in self._commands.keys():
self._con.connection.send(
nbxmpp.Error(
stanza, Namespace.STANZAS + ' item-not-found'))
self._log.warning('Comand %s does not exist: %s', node, jid)
raise nbxmpp.NodeProcessed
newcmd = self._commands[node]
if not newcmd.is_visible_for(self.is_same_jid(jid)):
self._log.warning('Command not visible for jid: %s', jid)
raise nbxmpp.NodeProcessed
# generate new sessionid
sessionid = generate_id()
# create new instance and run it
obj = newcmd(conn=self, jid=jid, sessionid=sessionid)
rc = obj.execute(stanza)
if rc:
self._sessions[(jid, sessionid, node)] = obj
self._log.info('Comand %s executed: %s', node, jid)
raise nbxmpp.NodeProcessed
# 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]
raise nbxmpp.NodeProcessed
def send_command(self, jid, node, session_id,
form, action='execute'):
"""
Send the command with data form. Wait for reply
"""
self._log.info('Send Command: %s %s %s %s',
jid, node, session_id, action)
stanza = nbxmpp.Iq(typ='set', to=jid)
cmdnode = stanza.addChild('command',
namespace=Namespace.COMMANDS,
attrs={'node': node,
'action': action})
if session_id:
cmdnode.setAttr('sessionid', session_id)
if form:
cmdnode.addChild(node=form.get_purged())
self._con.connection.SendAndCallForResponse(
stanza, self._action_response_received)
def _action_response_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Error: %s', stanza.getError())
app.nec.push_incoming_event(
AdHocCommandError(None, conn=self._con,
error=stanza.getError()))
return
self._log.info('Received action response')
command = stanza.getTag('command')
app.nec.push_incoming_event(
AdHocCommandActionResponse(
None, conn=self._con, command=command))
def send_cancel(self, jid, node, session_id):
"""
Send the command with action='cancel'
"""
self._log.info('Cancel: %s %s %s', jid, node, session_id)
stanza = nbxmpp.Iq(typ='set', to=jid)
stanza.addChild('command', namespace=Namespace.COMMANDS,
attrs={
'node': node,
'sessionid': session_id,
'action': 'cancel'
})
self._con.connection.SendAndCallForResponse(
stanza, self._cancel_result_received)
def _cancel_result_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.warning('Error: %s', stanza.getError())
else:
self._log.info('Cancel successful')
class AdHocCommandError(NetworkIncomingEvent):
name = 'adhoc-command-error'
class AdHocCommandActionResponse(NetworkIncomingEvent):
name = 'adhoc-command-action-response'
def get_instance(*args, **kwargs):
return AdHocCommands(*args, **kwargs), 'AdHocCommands'

View file

@ -0,0 +1,66 @@
# 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/>.
# XEP-0145: Annotations
from typing import Any
from typing import Dict # pylint: disable=unused-import
from typing import Tuple
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.structs import AnnotationNote
from gajim.common.types import ConnectionT
from gajim.common.modules.base import BaseModule
class Annotations(BaseModule):
_nbxmpp_extends = 'Annotations'
_nbxmpp_methods = [
'request_annotations',
'set_annotations',
]
def __init__(self, con: ConnectionT) -> None:
BaseModule.__init__(self, con)
self._annotations = {} # type: Dict[str, AnnotationNote]
def request_annotations(self) -> None:
self._nbxmpp('Annotations').request_annotations(
callback=self._annotations_received)
def _annotations_received(self, task: Any) -> None:
try:
annotations = task.finish()
except (StanzaError, MalformedStanzaError) as error:
self._log.warning(error)
self._annotations = {}
return
for note in annotations:
self._annotations[note.jid] = note
def get_note(self, jid: str) -> AnnotationNote:
return self._annotations.get(jid)
def set_note(self, note: AnnotationNote) -> None:
self._annotations[note.jid] = note
self.set_annotations(self._annotations.values())
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Annotations, str]:
return Annotations(*args, **kwargs), 'Annotations'

View file

@ -0,0 +1,37 @@
# 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/>.
# Server MOTD and Announce
import nbxmpp
from gajim.common.modules.base import BaseModule
class Announce(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
def delete_motd(self):
server = self._con.get_own_jid().domain
jid = '%s/announce/motd/delete' % server
self.set_announce(jid)
def set_announce(self, jid, subject=None, body=None):
message = nbxmpp.Message(to=jid, body=body, subject=subject)
self._nbxmpp().send(message)
def get_instance(*args, **kwargs):
return Announce(*args, **kwargs), 'Announce'

View file

@ -0,0 +1,97 @@
# 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/>.
from typing import Any # pylint: disable=unused-import
from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
import logging
from functools import partial
from unittest.mock import Mock
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.nec import EventHelper
from gajim.common.modules.util import LogAdapter
class BaseModule(EventHelper):
_nbxmpp_extends = ''
_nbxmpp_methods = [] # type: List[str]
def __init__(self, con, *args, plugin=False, **kwargs):
EventHelper.__init__(self)
self._con = con
self._account = con.name
self._log = self._set_logger(plugin)
self._nbxmpp_callbacks = {} # type: Dict[str, Any]
self._stored_publish = None # type: Callable
self.handlers = [] # type: List[str]
def _set_logger(self, plugin):
logger_name = 'gajim.c.m.%s'
if plugin:
logger_name = 'gajim.p.%s'
logger_name = logger_name % self.__class__.__name__.lower()
logger = logging.getLogger(logger_name)
return LogAdapter(logger, {'account': self._account})
def __getattr__(self, key):
if key not in self._nbxmpp_methods:
raise AttributeError(
"attribute '%s' is neither part of object '%s' "
" nor declared in '_nbxmpp_methods'" % (
key, self.__class__.__name__))
if not app.account_is_connected(self._account):
self._log.warning('Account not connected, cant use %s', key)
return None
module = self._con.connection.get_module(self._nbxmpp_extends)
callback = self._nbxmpp_callbacks.get(key)
if callback is None:
return getattr(module, key)
return partial(getattr(module, key), callback=callback)
def _nbxmpp(self, module_name=None):
if not app.account_is_connected(self._account):
self._log.warning('Account not connected, cant use nbxmpp method')
return Mock()
if module_name is None:
return self._con.connection
return self._con.connection.get_module(module_name)
def _register_callback(self, method, callback):
self._nbxmpp_callbacks[method] = callback
def _register_pubsub_handler(self, callback):
handler = StanzaHandler(name='message',
callback=callback,
ns=Namespace.PUBSUB_EVENT,
priority=49)
self.handlers.append(handler)
def send_stored_publish(self):
if self._stored_publish is None:
return
self._log.info('Send stored publish')
self._stored_publish() # pylint: disable=not-callable
def cleanup(self):
self.unregister_events()

View file

@ -0,0 +1,204 @@
# Copyright (C) 2018 Emmanuel Gil Peyrot <linkmauve AT linkmauve.fr>
#
# 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 logging
import hashlib
from base64 import b64decode
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common import configpaths
from gajim.common.modules.base import BaseModule
log = logging.getLogger('gajim.c.m.bob')
class BitsOfBinary(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._answer_bob_request,
typ='get',
ns=Namespace.BOB),
]
# Used to track which cids are in-flight.
self.awaiting_cids = {}
def _answer_bob_request(self, _con, stanza, _properties):
self._log.info('Request from %s for BoB data', stanza.getFrom())
iq = stanza.buildReply('error')
err = nbxmpp.ErrorNode(nbxmpp.ERR_ITEM_NOT_FOUND)
iq.addChild(node=err)
self._log.info('Sending item-not-found')
self._con.connection.send(iq)
raise nbxmpp.NodeProcessed
def _on_bob_received(self, _nbxmpp_client, result, cid):
"""
Called when we receive BoB data
"""
if cid not in self.awaiting_cids:
return
if result.getType() == 'result':
data = result.getTags('data', namespace=Namespace.BOB)
if data.getAttr('cid') == cid:
for func in self.awaiting_cids[cid]:
cb = func[0]
args = func[1]
pos = func[2]
bob_data = data.getData()
def recurs(node, cid, data):
if node.getData() == 'cid:' + cid:
node.setData(data)
else:
for child in node.getChildren():
recurs(child, cid, data)
recurs(args[pos], cid, bob_data)
cb(*args)
del self.awaiting_cids[cid]
return
# An error occurred, call callback without modifying data.
for func in self.awaiting_cids[cid]:
cb = func[0]
args = func[1]
cb(*args)
del self.awaiting_cids[cid]
def get_bob_data(self, cid, to, callback, args, position):
"""
Request for BoB (XEP-0231) and when data will arrive, call callback
with given args, after having replaced cid by it's data in
args[position]
"""
if cid in self.awaiting_cids:
self.awaiting_cids[cid].appends((callback, args, position))
else:
self.awaiting_cids[cid] = [(callback, args, position)]
iq = nbxmpp.Iq(to=to, typ='get')
iq.addChild(name='data', attrs={'cid': cid}, namespace=Namespace.BOB)
self._con.connection.SendAndCallForResponse(
iq, self._on_bob_received, {'cid': cid})
def parse_bob_data(stanza):
data_node = stanza.getTag('data', namespace=Namespace.BOB)
if data_node is None:
return None
cid = data_node.getAttr('cid')
type_ = data_node.getAttr('type')
max_age = data_node.getAttr('max-age')
if max_age is not None:
try:
max_age = int(max_age)
except Exception:
log.exception(stanza)
return None
if cid is None or type_ is None:
log.warning('Invalid data node (no cid or type attr): %s', stanza)
return None
try:
algo_hash = cid.split('@')[0]
algo, hash_ = algo_hash.split('+')
except Exception:
log.exception('Invalid cid: %s', stanza)
return None
bob_data = data_node.getData()
if not bob_data:
log.warning('No data found: %s', stanza)
return None
filepath = configpaths.get('BOB') / algo_hash
if algo_hash in app.bob_cache or filepath.exists():
log.info('BoB data already cached')
return None
try:
bob_data = b64decode(bob_data)
except Exception:
log.warning('Unable to decode data')
log.exception(stanza)
return None
if len(bob_data) > 10000:
log.warning('%s: data > 10000 bytes', stanza.getFrom())
return None
try:
sha = hashlib.new(algo)
except ValueError as error:
log.warning(stanza)
log.warning(error)
return None
sha.update(bob_data)
if sha.hexdigest() != hash_:
log.warning('Invalid hash: %s', stanza)
return None
if max_age == 0:
app.bob_cache[algo_hash] = bob_data
else:
try:
with open(str(filepath), 'w+b') as file:
file.write(bob_data)
except Exception:
log.warning('Unable to save data')
log.exception(stanza)
return None
log.info('BoB data stored: %s', algo_hash)
return filepath
def store_bob_data(bob_data):
if bob_data is None:
return None
algo_hash = '%s+%s' % (bob_data.algo, bob_data.hash_)
filepath = configpaths.get('BOB') / algo_hash
if algo_hash in app.bob_cache or filepath.exists():
log.info('BoB data already cached')
return None
if bob_data.max_age == 0:
app.bob_cache[algo_hash] = bob_data.data
else:
try:
with open(str(filepath), 'w+b') as file:
file.write(bob_data.data)
except Exception:
log.exception('Unable to save data')
return None
log.info('BoB data stored: %s', algo_hash)
return filepath
def get_instance(*args, **kwargs):
return BitsOfBinary(*args, **kwargs), 'BitsOfBinary'

View file

@ -0,0 +1,139 @@
# 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/>.
# XEP-0191: Blocking Command
import nbxmpp
from nbxmpp.protocol import JID
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.util import raise_if_error
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import as_task
class Blocking(BaseModule):
_nbxmpp_extends = 'Blocking'
_nbxmpp_methods = [
'block',
'unblock',
'request_blocking_list',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.blocked = []
self.handlers = [
StanzaHandler(name='iq',
callback=self._blocking_push_received,
typ='set',
ns=Namespace.BLOCKING),
]
self.supported = False
def pass_disco(self, info):
if Namespace.BLOCKING not in info.features:
return
self.supported = True
app.nec.push_incoming_event(
NetworkEvent('feature-discovered',
account=self._account,
feature=Namespace.BLOCKING))
self._log.info('Discovered blocking: %s', info.jid)
@as_task
def get_blocking_list(self):
_task = yield
blocking_list = yield self._nbxmpp('Blocking').request_blocking_list()
raise_if_error(blocking_list)
self.blocked = list(blocking_list)
app.nec.push_incoming_event(NetworkEvent('blocking',
conn=self._con,
changed=self.blocked))
yield blocking_list
@as_task
def update_blocking_list(self, block, unblock):
_task = yield
if block:
result = yield self.block(block)
raise_if_error(result)
if unblock:
result = yield self.unblock(unblock)
raise_if_error(result)
yield True
def _blocking_push_received(self, _con, _stanza, properties):
if not properties.is_blocking:
return
changed_list = []
if properties.blocking.unblock_all:
self.blocked = []
for jid in self.blocked:
self._presence_probe(jid)
self._log.info('Unblock all Push')
for jid in properties.blocking.unblock:
changed_list.append(jid)
if jid not in self.blocked:
continue
self.blocked.remove(jid)
self._presence_probe(jid)
self._log.info('Unblock Push: %s', jid)
for jid in properties.blocking.block:
if jid in self.blocked:
continue
changed_list.append(jid)
self.blocked.append(jid)
self._set_contact_offline(str(jid))
self._log.info('Block Push: %s', jid)
app.nec.push_incoming_event(NetworkEvent('blocking',
conn=self._con,
changed=changed_list))
raise nbxmpp.NodeProcessed
def _set_contact_offline(self, jid: str) -> None:
contact_list = app.contacts.get_contacts(self._account, jid)
for contact in contact_list:
contact.show = 'offline'
def _presence_probe(self, jid: JID) -> None:
self._log.info('Presence probe: %s', jid)
# Send a presence Probe to get the current Status
probe = nbxmpp.Presence(jid, 'probe', frm=self._con.get_own_jid())
self._nbxmpp().send(probe)
def get_instance(*args, **kwargs):
return Blocking(*args, **kwargs), 'Blocking'

View file

@ -0,0 +1,353 @@
# 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/>.
# XEP-0048: Bookmarks
from typing import Any
from typing import List
from typing import Dict
from typing import Set
from typing import Tuple
from typing import Union
from typing import Optional
import functools
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from nbxmpp.structs import BookmarkData
from gi.repository import GLib
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
NODE_MAX_NS = 'http://jabber.org/protocol/pubsub#config-node-max'
class Bookmarks(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._bookmark_event_received)
self._register_pubsub_handler(self._bookmark_1_event_received)
self._conversion = False
self._compat = False
self._compat_pep = False
self._node_max = False
self._bookmarks = {}
self._join_timeouts = []
self._request_in_progress = True
@property
def conversion(self) -> bool:
return self._conversion
@property
def compat(self) -> bool:
return self._compat
@property
def compat_pep(self) -> bool:
return self._compat_pep
@property
def bookmarks(self) -> List[BookmarkData]:
return self._bookmarks.values()
@property
def pep_bookmarks_used(self) -> bool:
return self._bookmark_module() == 'PEPBookmarks'
@property
def nativ_bookmarks_used(self) -> bool:
return self._bookmark_module() == 'NativeBookmarks'
@event_node(Namespace.BOOKMARKS)
def _bookmark_event_received(self, _con, _stanza, properties):
if properties.pubsub_event.retracted:
return
if not properties.is_self_message:
self._log.warning('%s has an open access bookmarks node',
properties.jid)
return
if not self.pep_bookmarks_used:
return
if self._request_in_progress:
self._log.info('Ignore update, pubsub request in progress')
return
bookmarks = self._convert_to_dict(properties.pubsub_event.data)
old_bookmarks = self._bookmarks.copy()
self._bookmarks = bookmarks
self._act_on_changed_bookmarks(old_bookmarks)
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
@event_node(Namespace.BOOKMARKS_1)
def _bookmark_1_event_received(self, _con, _stanza, properties):
if not properties.is_self_message:
self._log.warning('%s has an open access bookmarks node',
properties.jid)
return
if not self.nativ_bookmarks_used:
return
if self._request_in_progress:
self._log.info('Ignore update, pubsub request in progress')
return
old_bookmarks = self._bookmarks.copy()
if properties.pubsub_event.deleted or properties.pubsub_event.purged:
self._log.info('Bookmark node deleted/purged')
self._bookmarks = {}
elif properties.pubsub_event.retracted:
jid = properties.pubsub_event.id
self._log.info('Retract: %s', jid)
bookmark = self._bookmarks.get(jid)
if bookmark is not None:
self._bookmarks.pop(bookmark, None)
else:
new_bookmark = properties.pubsub_event.data
self._bookmarks[new_bookmark.jid] = properties.pubsub_event.data
self._act_on_changed_bookmarks(old_bookmarks)
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
def pass_disco(self, info):
self._node_max = NODE_MAX_NS in info.features
self._compat_pep = Namespace.BOOKMARKS_COMPAT_PEP in info.features
self._compat = Namespace.BOOKMARKS_COMPAT in info.features
self._conversion = Namespace.BOOKMARK_CONVERSION in info.features
@functools.lru_cache(maxsize=1)
def _bookmark_module(self):
if not self._con.get_module('PubSub').publish_options:
return 'PrivateBookmarks'
if app.settings.get('dev_force_bookmark_2'):
return 'NativeBookmarks'
if self._compat_pep and self._node_max:
return 'NativeBookmarks'
if self._conversion:
return 'PEPBookmarks'
return 'PrivateBookmarks'
def _act_on_changed_bookmarks(
self, old_bookmarks: Dict[str, BookmarkData]) -> None:
new_bookmarks = self._convert_to_set(self._bookmarks)
old_bookmarks = self._convert_to_set(old_bookmarks)
changed = new_bookmarks - old_bookmarks
if not changed:
return
join = [jid for jid, autojoin in changed if autojoin]
bookmarks = []
for jid in join:
self._log.info('Schedule autojoin in 10s for: %s', jid)
bookmarks.append(self._bookmarks.get(jid))
# If another client creates a MUC, the MUC is locked until the
# configuration is finished. Give the user some time to finish
# the configuration.
timeout_id = GLib.timeout_add_seconds(
10, self._join_with_timeout, bookmarks)
self._join_timeouts.append(timeout_id)
# TODO: leave mucs
# leave = [jid for jid, autojoin in changed if not autojoin]
@staticmethod
def _convert_to_set(
bookmarks: Dict[str, BookmarkData]) -> Set[Tuple[str, bool]]:
set_ = set()
for jid, bookmark in bookmarks.items():
set_.add((jid, bookmark.autojoin))
return set_
@staticmethod
def _convert_to_dict(bookmarks: List) -> Dict[str, BookmarkData]:
_dict = {} # type: Dict[str, BookmarkData]
if bookmarks is None:
return _dict
for bookmark in bookmarks:
_dict[bookmark.jid] = bookmark
return _dict
def get_bookmark(self, jid: Union[str, JID]) -> BookmarkData:
return self._bookmarks.get(jid)
def request_bookmarks(self) -> None:
if not app.account_is_available(self._account):
return
self._request_in_progress = True
self._nbxmpp(self._bookmark_module()).request_bookmarks(
callback=self._bookmarks_received)
def _bookmarks_received(self, task: Any) -> None:
try:
bookmarks = task.finish()
except Exception as error:
self._log.warning(error)
bookmarks = None
self._request_in_progress = False
self._bookmarks = self._convert_to_dict(bookmarks)
self.auto_join_bookmarks()
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
def store_difference(self, bookmarks: List) -> None:
if self.nativ_bookmarks_used:
retract, add_or_modify = self._determine_changed_bookmarks(
bookmarks, self._bookmarks)
for bookmark in retract:
self.remove(bookmark.jid)
if add_or_modify:
self.store_bookmarks(add_or_modify)
self._bookmarks = self._convert_to_dict(bookmarks)
else:
self._bookmarks = self._convert_to_dict(bookmarks)
self.store_bookmarks()
def store_bookmarks(self, bookmarks: list = None) -> None:
if not app.account_is_available(self._account):
return
if bookmarks is None or not self.nativ_bookmarks_used:
bookmarks = self._bookmarks.values()
self._nbxmpp(self._bookmark_module()).store_bookmarks(bookmarks)
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
def _join_with_timeout(self, bookmarks: List[Any]) -> None:
self._join_timeouts.pop(0)
self.auto_join_bookmarks(bookmarks)
def auto_join_bookmarks(self,
bookmarks: Optional[List[Any]] = None) -> None:
if bookmarks is None:
bookmarks = self._bookmarks.values()
for bookmark in bookmarks:
if bookmark.autojoin:
# Only join non-opened groupchats. Opened one are already
# auto-joined on re-connection
if bookmark.jid not in app.gc_connected[self._account]:
# we are not already connected
self._log.info('Autojoin Bookmark: %s', bookmark.jid)
minimize = app.settings.get_group_chat_setting(
self._account,
bookmark.jid,
'minimize_on_autojoin')
app.interface.join_groupchat(self._account,
str(bookmark.jid),
minimized=minimize)
def modify(self, jid: str, **kwargs: Dict[str, str]) -> None:
bookmark = self._bookmarks.get(jid)
if bookmark is None:
return
new_bookmark = bookmark._replace(**kwargs)
if new_bookmark == bookmark:
# No change happened
return
self._log.info('Modify bookmark: %s %s', jid, kwargs)
self._bookmarks[jid] = new_bookmark
self.store_bookmarks([new_bookmark])
def add_or_modify(self, jid: str, **kwargs: Dict[str, str]) -> None:
bookmark = self._bookmarks.get(jid)
if bookmark is not None:
self.modify(jid, **kwargs)
return
new_bookmark = BookmarkData(jid=jid, **kwargs)
self._bookmarks[jid] = new_bookmark
self._log.info('Add new bookmark: %s', new_bookmark)
self.store_bookmarks([new_bookmark])
def remove(self, jid: JID, publish: bool = True) -> None:
removed = self._bookmarks.pop(jid, False)
if not removed:
return
if publish:
if self.nativ_bookmarks_used:
self._nbxmpp('NativeBookmarks').retract_bookmark(str(jid))
else:
self.store_bookmarks()
@staticmethod
def _determine_changed_bookmarks(
new_bookmarks: List[BookmarkData],
old_bookmarks: Dict[str, BookmarkData]) -> Tuple[
List[BookmarkData], List[BookmarkData]]:
new_jids = [bookmark.jid for bookmark in new_bookmarks]
new_bookmarks = set(new_bookmarks)
old_bookmarks = set(old_bookmarks.values())
retract = []
add_or_modify = []
changed_bookmarks = new_bookmarks.symmetric_difference(old_bookmarks)
for bookmark in changed_bookmarks:
if bookmark.jid not in new_jids:
retract.append(bookmark)
if bookmark in new_bookmarks:
add_or_modify.append(bookmark)
return retract, add_or_modify
def get_name_from_bookmark(self, jid: str) -> str:
bookmark = self._bookmarks.get(jid)
if bookmark is None:
return ''
return bookmark.name
def is_bookmark(self, jid: str) -> bool:
return jid in self._bookmarks
def _remove_timeouts(self):
for _id in self._join_timeouts:
GLib.source_remove(_id)
def cleanup(self):
self._remove_timeouts()
def get_instance(*args, **kwargs):
return Bookmarks(*args, **kwargs), 'Bookmarks'

View file

@ -0,0 +1,719 @@
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
# Junglecow J <junglecow AT gmail.com>
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
# Travis Shirk <travis AT pobox.com>
# Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
# Jean-Marie Traissard <jim AT lapin.org>
# Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 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 socket
import logging
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gi.repository import GLib
from gajim.common import app
from gajim.common import helpers
from gajim.common import jingle_xtls
from gajim.common.file_props import FilesProp
from gajim.common.socks5 import Socks5SenderClient
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
log = logging.getLogger('gajim.c.m.bytestream')
def is_transfer_paused(file_props):
if file_props.stopped:
return False
if file_props.completed:
return False
if file_props.disconnect_cb:
return False
return file_props.paused
def is_transfer_active(file_props):
if file_props.stopped:
return False
if file_props.completed:
return False
if not file_props.started:
return False
if file_props.paused:
return True
return not file_props.paused
def is_transfer_stopped(file_props):
if not file_props:
return True
if file_props.error:
return True
if file_props.completed:
return True
if not file_props.stopped:
return False
return True
class Bytestream(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
typ='result',
ns=Namespace.BYTESTREAM,
callback=self._on_bytestream_result),
StanzaHandler(name='iq',
typ='error',
ns=Namespace.BYTESTREAM,
callback=self._on_bytestream_error),
StanzaHandler(name='iq',
typ='set',
ns=Namespace.BYTESTREAM,
callback=self._on_bytestream_set),
StanzaHandler(name='iq',
typ='result',
callback=self._on_result),
]
self.no_gupnp_reply_id = None
self.ok_id = None
self.fail_id = None
def pass_disco(self, info):
if Namespace.BYTESTREAM not in info.features:
return
if app.settings.get_account_setting(self._account, 'use_ft_proxies'):
log.info('Discovered proxy: %s', info.jid)
our_fjid = self._con.get_own_jid()
testit = app.settings.get_account_setting(
self._account, 'test_ft_proxies_on_startup')
app.proxy65_manager.resolve(
info.jid, self._con.connection, str(our_fjid),
default=self._account, testit=testit)
raise nbxmpp.NodeProcessed
def _ft_get_receiver_jid(self, file_props):
if self._account == 'Local':
return file_props.receiver.jid
return file_props.receiver.jid + '/' + file_props.receiver.resource
def _ft_get_from(self, iq_obj):
if self._account == 'Local':
return iq_obj.getFrom()
return helpers.get_full_jid_from_iq(iq_obj)
def _ft_get_streamhost_jid_attr(self, streamhost):
if self._account == 'Local':
return streamhost.getAttr('jid')
return helpers.parse_jid(streamhost.getAttr('jid'))
def send_file_approval(self, file_props):
"""
Send iq, confirming that we want to download the file
"""
# user response to ConfirmationDialog may come after we've disconnected
if not app.account_is_available(self._account):
return
# file transfer initiated by a jingle session
log.info("send_file_approval: jingle session accept")
session = self._con.get_module('Jingle').get_jingle_session(
file_props.sender, file_props.sid)
if not session:
return
content = None
for content_ in session.contents.values():
if content_.transport.sid == file_props.transport_sid:
content = content_
break
if not content:
return
if not session.accepted:
content = session.get_content('file', content.name)
if content.use_security:
fingerprint = content.x509_fingerprint
if not jingle_xtls.check_cert(
app.get_jid_without_resource(file_props.sender),
fingerprint):
id_ = jingle_xtls.send_cert_request(
self._con, file_props.sender)
jingle_xtls.key_exchange_pend(id_,
content.on_cert_received, [])
return
session.approve_session()
session.approve_content('file', content.name)
def send_file_rejection(self, file_props):
"""
Inform sender that we refuse to download the file
typ is used when code = '400', in this case typ can be 'stream' for
invalid stream or 'profile' for invalid profile
"""
# user response to ConfirmationDialog may come after we've disconnected
if not app.account_is_available(self._account):
return
for session in self._con.get_module('Jingle').get_jingle_sessions(
None, file_props.sid):
session.cancel_session()
def send_success_connect_reply(self, streamhost):
"""
Send reply to the initiator of FT that we made a connection
"""
if not app.account_is_available(self._account):
return
if streamhost is None:
return
iq = nbxmpp.Iq(to=streamhost['initiator'],
typ='result',
frm=streamhost['target'])
iq.setAttr('id', streamhost['id'])
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
stream_tag = query.setTag('streamhost-used')
stream_tag.setAttr('jid', streamhost['jid'])
self._con.connection.send(iq)
def stop_all_active_file_transfers(self, contact):
"""
Stop all active transfer to or from the given contact
"""
for file_props in FilesProp.getAllFileProp():
if is_transfer_stopped(file_props):
continue
receiver_jid = file_props.receiver
if contact.get_full_jid() == receiver_jid:
file_props.error = -5
self.remove_transfer(file_props)
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=app.get_jid_without_resource(contact.jid),
file_props=file_props,
error_msg=''))
sender_jid = file_props.sender
if contact.get_full_jid() == sender_jid:
file_props.error = -3
self.remove_transfer(file_props)
def remove_all_transfers(self):
"""
Stop and remove all active connections from the socks5 pool
"""
for file_props in FilesProp.getAllFileProp():
self.remove_transfer(file_props)
def remove_transfer(self, file_props):
if file_props is None:
return
self.disconnect_transfer(file_props)
@staticmethod
def disconnect_transfer(file_props):
if file_props is None:
return
if file_props.hash_:
app.socks5queue.remove_sender(file_props.hash_)
if file_props.streamhosts:
for host in file_props.streamhosts:
if 'idx' in host and host['idx'] > 0:
app.socks5queue.remove_receiver(host['idx'])
app.socks5queue.remove_sender(host['idx'])
def _send_socks5_info(self, file_props):
"""
Send iq for the present streamhosts and proxies
"""
if not app.account_is_available(self._account):
return
receiver = file_props.receiver
sender = file_props.sender
sha_str = helpers.get_auth_sha(file_props.sid, sender, receiver)
file_props.sha_str = sha_str
port = app.settings.get('file_transfers_port')
listener = app.socks5queue.start_listener(
port,
sha_str,
self._result_socks5_sid, file_props)
if not listener:
file_props.error = -5
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=app.get_jid_without_resource(receiver),
file_props=file_props,
error_msg=''))
self._connect_error(file_props.sid,
error='not-acceptable',
error_type='modify')
else:
iq = nbxmpp.Iq(to=receiver, typ='set')
file_props.request_id = 'id_' + file_props.sid
iq.setID(file_props.request_id)
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
query.setAttr('sid', file_props.sid)
self._add_addiditional_streamhosts_to_query(query, file_props)
self._add_local_ips_as_streamhosts_to_query(query, file_props)
self._add_proxy_streamhosts_to_query(query, file_props)
self._add_upnp_igd_as_streamhost_to_query(query, file_props, iq)
# Upnp-igd is asynchronous, so it will send the iq itself
@staticmethod
def _add_streamhosts_to_query(query, sender, port, hosts):
for host in hosts:
streamhost = nbxmpp.Node(tag='streamhost')
query.addChild(node=streamhost)
streamhost.setAttr('port', str(port))
streamhost.setAttr('host', host)
streamhost.setAttr('jid', sender)
def _add_local_ips_as_streamhosts_to_query(self, query, file_props):
if not app.settings.get_account_setting(self._account,
'ft_send_local_ips'):
return
my_ip = self._con.local_address
if my_ip is None:
log.warning('No local address available')
return
try:
# The ip we're connected to server with
my_ips = [my_ip]
# all IPs from local DNS
for addr in socket.getaddrinfo(socket.gethostname(), None):
if (not addr[4][0] in my_ips and
not addr[4][0].startswith('127') and
not addr[4][0] == '::1'):
my_ips.append(addr[4][0])
sender = file_props.sender
port = app.settings.get('file_transfers_port')
self._add_streamhosts_to_query(query, sender, port, my_ips)
except socket.gaierror:
from gajim.common.connection_handlers_events import InformationEvent
app.nec.push_incoming_event(
InformationEvent(None, dialog_name='wrong-host'))
def _add_addiditional_streamhosts_to_query(self, query, file_props):
sender = file_props.sender
port = app.settings.get('file_transfers_port')
ft_add_hosts_to_send = app.settings.get('ft_add_hosts_to_send')
add_hosts = []
if ft_add_hosts_to_send:
add_hosts = [e.strip() for e in ft_add_hosts_to_send.split(',')]
else:
add_hosts = []
self._add_streamhosts_to_query(query, sender, port, add_hosts)
def _add_upnp_igd_as_streamhost_to_query(self, query, file_props, iq):
my_ip = self._con.local_address
if my_ip is None or not app.is_installed('UPNP'):
log.warning('No local address available')
self._con.connection.send(iq)
return
# check if we are connected with an IPv4 address
try:
socket.inet_aton(my_ip)
except socket.error:
self._con.connection.send(iq)
return
def ip_is_local(ip):
if '.' not in ip:
# it's an IPv6
return True
ip_s = ip.split('.')
ip_l = int(ip_s[0])<<24 | int(ip_s[1])<<16 | int(ip_s[2])<<8 | \
int(ip_s[3])
# 10/8
if ip_l & (255<<24) == 10<<24:
return True
# 172.16/12
if ip_l & (255<<24 | 240<<16) == (172<<24 | 16<<16):
return True
# 192.168
if ip_l & (255<<24 | 255<<16) == (192<<24 | 168<<16):
return True
return False
if not ip_is_local(my_ip):
self.connection.send(iq)
return
self.no_gupnp_reply_id = 0
def cleanup_gupnp():
if self.no_gupnp_reply_id:
GLib.source_remove(self.no_gupnp_reply_id)
self.no_gupnp_reply_id = 0
app.gupnp_igd.disconnect(self.ok_id)
app.gupnp_igd.disconnect(self.fail_id)
def success(_gupnp, _proto, ext_ip, _re, ext_port,
local_ip, local_port, _desc):
log.debug('Got GUPnP-IGD answer: external: %s:%s, internal: %s:%s',
ext_ip, ext_port, local_ip, local_port)
if local_port != app.settings.get('file_transfers_port'):
sender = file_props.sender
receiver = file_props.receiver
sha_str = helpers.get_auth_sha(file_props.sid,
sender,
receiver)
listener = app.socks5queue.start_listener(
local_port,
sha_str,
self._result_socks5_sid,
file_props.sid)
if listener:
self._add_streamhosts_to_query(query,
sender,
ext_port,
[ext_ip])
else:
self._add_streamhosts_to_query(query,
file_props.sender,
ext_port,
[ext_ip])
self._con.connection.send(iq)
cleanup_gupnp()
def fail(_gupnp, error, _proto, _ext_ip, _local_ip, _local_port, _desc):
log.debug('Got GUPnP-IGD error: %s', error)
self._con.connection.send(iq)
cleanup_gupnp()
def no_upnp_reply():
log.debug('Got not GUPnP-IGD answer')
# stop trying to use it
app.disable_dependency('UPNP')
self.no_gupnp_reply_id = 0
self._con.connection.send(iq)
cleanup_gupnp()
return False
self.ok_id = app.gupnp_igd.connect('mapped-external-port', success)
self.fail_id = app.gupnp_igd.connect('error-mapping-port', fail)
port = app.settings.get('file_transfers_port')
self.no_gupnp_reply_id = GLib.timeout_add_seconds(10, no_upnp_reply)
app.gupnp_igd.add_port('TCP',
0,
my_ip,
port,
3600,
'Gajim file transfer')
def _add_proxy_streamhosts_to_query(self, query, file_props):
proxyhosts = self._get_file_transfer_proxies_from_config(file_props)
if proxyhosts:
file_props.proxy_receiver = file_props.receiver
file_props.proxy_sender = file_props.sender
file_props.proxyhosts = proxyhosts
for proxyhost in proxyhosts:
self._add_streamhosts_to_query(query,
proxyhost['jid'],
proxyhost['port'],
[proxyhost['host']])
def _get_file_transfer_proxies_from_config(self, file_props):
configured_proxies = app.settings.get_account_setting(
self._account, 'file_transfer_proxies')
shall_use_proxies = app.settings.get_account_setting(
self._account, 'use_ft_proxies')
if shall_use_proxies:
proxyhost_dicts = []
proxies = []
if configured_proxies:
proxies = [item.strip() for item in
configured_proxies.split(',')]
default_proxy = app.proxy65_manager.get_default_for_name(
self._account)
if default_proxy:
# add/move default proxy at top of the others
if default_proxy in proxies:
proxies.remove(default_proxy)
proxies.insert(0, default_proxy)
for proxy in proxies:
(host, _port, jid) = app.proxy65_manager.get_proxy(
proxy, self._account)
if not host:
continue
host_dict = {
'state': 0,
'target': file_props.receiver,
'id': file_props.sid,
'sid': file_props.sid,
'initiator': proxy,
'host': host,
'port': str(_port),
'jid': jid
}
proxyhost_dicts.append(host_dict)
return proxyhost_dicts
return []
@staticmethod
def _result_socks5_sid(sid, hash_id):
"""
Store the result of SHA message from auth
"""
file_props = FilesProp.getFilePropBySid(sid)
file_props.hash_ = hash_id
def _connect_error(self, sid, error, error_type, msg=None):
"""
Called when there is an error establishing BS connection, or when
connection is rejected
"""
if not app.account_is_available(self._account):
return
file_props = FilesProp.getFileProp(self._account, sid)
if file_props is None:
log.error('can not send iq error on failed transfer')
return
if file_props.type_ == 's':
to = file_props.receiver
else:
to = file_props.sender
iq = nbxmpp.Iq(to=to, typ='error')
iq.setAttr('id', file_props.request_id)
err = iq.setTag('error')
err.setAttr('type', error_type)
err.setTag(error, namespace=Namespace.STANZAS)
self._con.connection.send(iq)
if msg:
self.disconnect_transfer(file_props)
file_props.error = -3
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=app.get_jid_without_resource(to),
file_props=file_props,
error_msg=msg))
def _proxy_auth_ok(self, proxy):
"""
Called after authentication to proxy server
"""
if not app.account_is_available(self._account):
return
file_props = FilesProp.getFileProp(self._account, proxy['sid'])
iq = nbxmpp.Iq(to=proxy['initiator'], typ='set')
auth_id = "au_" + proxy['sid']
iq.setID(auth_id)
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
query.setAttr('sid', proxy['sid'])
activate = query.setTag('activate')
activate.setData(file_props.proxy_receiver)
iq.setID(auth_id)
self._con.connection.send(iq)
def _on_bytestream_error(self, _con, iq_obj, _properties):
id_ = iq_obj.getAttr('id')
frm = helpers.get_full_jid_from_iq(iq_obj)
query = iq_obj.getTag('query')
app.proxy65_manager.error_cb(frm, query)
jid = helpers.get_jid_from_iq(iq_obj)
id_ = id_[3:]
file_props = FilesProp.getFilePropBySid(id_)
if not file_props:
return
file_props.error = -4
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=app.get_jid_without_resource(jid),
file_props=file_props,
error_msg=''))
raise nbxmpp.NodeProcessed
def _on_bytestream_set(self, con, iq_obj, _properties):
target = iq_obj.getAttr('to')
id_ = iq_obj.getAttr('id')
query = iq_obj.getTag('query')
sid = query.getAttr('sid')
file_props = FilesProp.getFileProp(self._account, sid)
streamhosts = []
for item in query.getChildren():
if item.getName() == 'streamhost':
host_dict = {
'state': 0,
'target': target,
'id': id_,
'sid': sid,
'initiator': self._ft_get_from(iq_obj)
}
for attr in item.getAttrs():
host_dict[attr] = item.getAttr(attr)
if 'host' not in host_dict:
continue
if 'jid' not in host_dict:
continue
if 'port' not in host_dict:
continue
streamhosts.append(host_dict)
file_props = FilesProp.getFilePropBySid(sid)
if file_props is not None:
if file_props.type_ == 's': # FIXME: remove fast xmlns
# only psi do this
if file_props.streamhosts:
file_props.streamhosts.extend(streamhosts)
else:
file_props.streamhosts = streamhosts
app.socks5queue.connect_to_hosts(
self._account,
sid,
self.send_success_connect_reply,
None)
raise nbxmpp.NodeProcessed
else:
log.warning('Gajim got streamhosts for unknown transfer. '
'Ignoring it.')
raise nbxmpp.NodeProcessed
file_props.streamhosts = streamhosts
def _connection_error(sid):
self._connect_error(sid,
'item-not-found',
'cancel',
msg='Could not connect to given hosts')
if file_props.type_ == 'r':
app.socks5queue.connect_to_hosts(
self._account,
sid,
self.send_success_connect_reply,
_connection_error)
raise nbxmpp.NodeProcessed
def _on_result(self, _con, iq_obj, _properties):
# if we want to respect xep-0065 we have to check for proxy
# activation result in any result iq
real_id = iq_obj.getAttr('id')
if real_id is None:
log.warning('Invalid IQ without id attribute:\n%s', iq_obj)
raise nbxmpp.NodeProcessed
if real_id is None or not real_id.startswith('au_'):
return
frm = self._ft_get_from(iq_obj)
id_ = real_id[3:]
file_props = FilesProp.getFilePropByTransportSid(self._account, id_)
if file_props.streamhost_used:
for host in file_props.proxyhosts:
if host['initiator'] == frm and 'idx' in host:
app.socks5queue.activate_proxy(host['idx'])
raise nbxmpp.NodeProcessed
def _on_bytestream_result(self, con, iq_obj, _properties):
frm = self._ft_get_from(iq_obj)
real_id = iq_obj.getAttr('id')
query = iq_obj.getTag('query')
app.proxy65_manager.resolve_result(frm, query)
try:
streamhost = query.getTag('streamhost-used')
except Exception: # this bytestream result is not what we need
pass
id_ = real_id[3:]
file_props = FilesProp.getFileProp(self._account, id_)
if file_props is None:
raise nbxmpp.NodeProcessed
if streamhost is None:
# proxy approves the activate query
if real_id.startswith('au_'):
if file_props.streamhost_used is False:
raise nbxmpp.NodeProcessed
if not file_props.proxyhosts:
raise nbxmpp.NodeProcessed
for host in file_props.proxyhosts:
if host['initiator'] == frm and \
query.getAttr('sid') == file_props.sid:
app.socks5queue.activate_proxy(host['idx'])
break
raise nbxmpp.NodeProcessed
jid = self._ft_get_streamhost_jid_attr(streamhost)
if file_props.streamhost_used is True:
raise nbxmpp.NodeProcessed
if real_id.startswith('au_'):
if file_props.stopped:
self.remove_transfer(file_props)
else:
app.socks5queue.send_file(file_props, self._account, 'server')
raise nbxmpp.NodeProcessed
proxy = None
if file_props.proxyhosts:
for proxyhost in file_props.proxyhosts:
if proxyhost['jid'] == jid:
proxy = proxyhost
if file_props.stopped:
self.remove_transfer(file_props)
raise nbxmpp.NodeProcessed
if proxy is not None:
file_props.streamhost_used = True
file_props.streamhosts.append(proxy)
file_props.is_a_proxy = True
idx = app.socks5queue.idx
sender = Socks5SenderClient(app.idlequeue,
idx,
app.socks5queue,
_sock=None,
host=str(proxy['host']),
port=int(proxy['port']),
fingerprint=None,
connected=False,
file_props=file_props)
sender.streamhost = proxy
app.socks5queue.add_sockobj(self._account, sender)
proxy['idx'] = sender.queue_idx
app.socks5queue.on_success[file_props.sid] = self._proxy_auth_ok
raise nbxmpp.NodeProcessed
if file_props.stopped:
self.remove_transfer(file_props)
else:
app.socks5queue.send_file(file_props, self._account, 'server')
raise nbxmpp.NodeProcessed
def get_instance(*args, **kwargs):
return Bytestream(*args, **kwargs), 'Bytestream'

View file

@ -0,0 +1,232 @@
# Copyright (C) 2009 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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/>.
# XEP-0115: Entity Capabilities
import weakref
from collections import defaultdict
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import DiscoIdentity
from nbxmpp.util import compute_caps_hash
from nbxmpp.errors import StanzaError
from gajim.common import app
from gajim.common.const import COMMON_FEATURES
from gajim.common.const import Entity
from gajim.common.helpers import get_optional_features
from gajim.common.nec import NetworkEvent
from gajim.common.task_manager import Task
from gajim.common.modules.base import BaseModule
class Caps(BaseModule):
_nbxmpp_extends = 'EntityCaps'
_nbxmpp_methods = [
'caps',
'set_caps'
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._entity_caps,
ns=Namespace.CAPS,
priority=51),
]
self._identities = [
DiscoIdentity(category='client', type='pc', name='Gajim')
]
self._queued_tasks_by_hash = defaultdict(set)
self._queued_tasks_by_jid = {}
def _queue_task(self, task):
old_task = self._get_task(task.entity.jid)
if old_task is not None:
self._remove_task(old_task)
self._log.info('Queue query for hash %s', task.entity.hash)
self._queued_tasks_by_hash[task.entity.hash].add(task)
self._queued_tasks_by_jid[task.entity.jid] = task
app.task_manager.add_task(task)
def _get_task(self, jid):
return self._queued_tasks_by_jid.get(jid)
def _get_similar_tasks(self, task):
return self._queued_tasks_by_hash.pop(task.entity.hash)
def _remove_task(self, task):
task.set_obsolete()
del self._queued_tasks_by_jid[task.entity.jid]
self._queued_tasks_by_hash[task.entity.hash].discard(task)
def _remove_all_tasks(self):
for task in self._queued_tasks_by_jid.values():
task.set_obsolete()
self._queued_tasks_by_jid.clear()
self._queued_tasks_by_hash.clear()
def _entity_caps(self, _con, _stanza, properties):
if properties.type.is_error or properties.type.is_unavailable:
return
if properties.is_self_presence:
return
if properties.entity_caps is None:
return
task = EntityCapsTask(self._account, properties, self._execute_task)
self._log.info('Received %s', task.entity)
disco_info = app.storage.cache.get_caps_entry(task.entity.method,
task.entity.hash)
if disco_info is None:
self._queue_task(task)
return
jid = str(properties.jid)
app.storage.cache.set_last_disco_info(jid, disco_info, cache_only=True)
app.nec.push_incoming_event(
NetworkEvent('caps-update',
account=self._account,
fjid=jid,
jid=properties.jid.bare))
def _execute_task(self, task):
self._log.info('Request %s from %s', task.entity.hash, task.entity.jid)
self._con.get_module('Discovery').disco_info(
task.entity.jid,
node=f'{task.entity.node}#{task.entity.hash}',
callback=self._on_disco_info,
user_data=task.entity.jid)
def _on_disco_info(self, nbxmpp_task):
jid = nbxmpp_task.get_user_data()
task = self._get_task(jid)
if task is None:
self._log.info('Task not found for %s', jid)
return
self._remove_task(task)
try:
disco_info = nbxmpp_task.finish()
except StanzaError as error:
self._log.warning(error)
return
self._log.info('Disco Info received: %s', disco_info.jid)
try:
compute_caps_hash(disco_info)
except Exception as error:
self._log.warning('Disco info malformed: %s %s',
disco_info.jid, error)
return
app.storage.cache.add_caps_entry(
str(disco_info.jid),
task.entity.method,
disco_info.get_caps_hash(),
disco_info)
self._log.info('Finished query for %s', task.entity.hash)
tasks = self._get_similar_tasks(task)
for task in tasks:
self._remove_task(task)
self._log.info('Update %s', task.entity.jid)
app.nec.push_incoming_event(
NetworkEvent('caps-update',
account=self._account,
fjid=str(task.entity.jid),
jid=task.entity.jid.bare))
def update_caps(self):
if not app.account_is_connected(self._account):
return
optional_features = get_optional_features(self._account)
self.set_caps(self._identities,
COMMON_FEATURES + optional_features,
'https://gajim.org')
if not app.account_is_available(self._account):
return
app.connections[self._account].change_status(
app.connections[self._account].status,
app.connections[self._account].status_message)
def cleanup(self):
self._remove_all_tasks()
BaseModule.cleanup(self)
class EntityCapsTask(Task):
def __init__(self, account, properties, callback):
Task.__init__(self)
self._account = account
self._callback = weakref.WeakMethod(callback)
self.entity = Entity(jid=properties.jid,
node=properties.entity_caps.node,
hash=properties.entity_caps.ver,
method=properties.entity_caps.hash)
self._from_muc = properties.from_muc
def execute(self):
callback = self._callback()
if callback is not None:
callback(self)
def preconditions_met(self):
try:
client = app.get_client(self._account)
except Exception:
return False
if self._from_muc:
muc = client.get_module('MUC').get_manager().get(
self.entity.jid.bare)
if muc is None or not muc.state.is_joined:
self.set_obsolete()
return False
return client.state.is_available
def __repr__(self):
return f'Entity Caps ({self.entity.jid} {self.entity.hash})'
def __hash__(self):
return hash(self.entity)
def get_instance(*args, **kwargs):
return Caps(*args, **kwargs), 'Caps'

View file

@ -0,0 +1,44 @@
# 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/>.
# XEP-0280: Message Carbons
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common.modules.base import BaseModule
class Carbons(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.supported = False
def pass_disco(self, info):
if Namespace.CARBONS not in info.features:
return
self.supported = True
self._log.info('Discovered carbons: %s', info.jid)
iq = nbxmpp.Iq('set')
iq.setTag('enable', namespace=Namespace.CARBONS)
self._log.info('Activate')
self._con.connection.send(iq)
def get_instance(*args, **kwargs):
return Carbons(*args, **kwargs), 'Carbons'

View file

@ -0,0 +1,123 @@
# 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/>.
# Chat Markers (XEP-0333)
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.structs import OutgoingMessage
class ChatMarkers(BaseModule):
_nbxmpp_extends = 'ChatMarkers'
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._process_chat_marker,
ns=Namespace.CHATMARKERS,
priority=47),
]
def _process_chat_marker(self, _con, _stanza, properties):
if not properties.is_marker or not properties.marker.is_displayed:
return
if properties.type.is_error:
return
if properties.type.is_groupchat:
manager = self._con.get_module('MUC').get_manager()
muc_data = manager.get(properties.muc_jid)
if muc_data is None:
return
if properties.muc_nickname != muc_data.nick:
return
self._raise_event('read-state-sync', properties)
return
if properties.is_carbon_message and properties.carbon.is_sent:
self._raise_event('read-state-sync', properties)
return
if properties.is_mam_message:
if properties.from_.bareMatch(self._con.get_own_jid()):
return
self._raise_event('displayed-received', properties)
def _raise_event(self, name, properties):
self._log.info('%s: %s %s',
name,
properties.jid,
properties.marker.id)
jid = properties.jid
if not properties.is_muc_pm and not properties.type.is_groupchat:
jid = properties.jid.bare
app.storage.archive.set_marker(
app.get_jid_from_account(self._account),
jid,
properties.marker.id,
'displayed')
app.nec.push_outgoing_event(
NetworkEvent(name,
account=self._account,
jid=jid,
properties=properties,
type=properties.type,
is_muc_pm=properties.is_muc_pm,
marker_id=properties.marker.id))
def _send_marker(self, contact, marker, id_, type_):
jid = contact.jid
if contact.is_pm_contact:
jid = app.get_jid_without_resource(contact.jid)
if type_ in ('gc', 'pm'):
if not app.settings.get_group_chat_setting(
self._account, jid, 'send_marker'):
return
else:
if not app.settings.get_contact_setting(
self._account, jid, 'send_marker'):
return
typ = 'groupchat' if type_ == 'gc' else 'chat'
message = OutgoingMessage(account=self._account,
contact=contact,
message=None,
type_=typ,
marker=(marker, id_),
play_sound=False)
self._con.send_message(message)
self._log.info('Send %s: %s', marker, contact.jid)
def send_displayed_marker(self, contact, id_, type_):
self._send_marker(contact, 'displayed', id_, str(type_))
def get_instance(*args, **kwargs):
return ChatMarkers(*args, **kwargs), 'ChatMarkers'

View file

@ -0,0 +1,358 @@
# 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/>.
# XEP-0085: Chat State Notifications
from typing import Any
from typing import Dict # pylint: disable=unused-import
from typing import List # pylint: disable=unused-import
from typing import Optional
from typing import Tuple
import time
from functools import wraps
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gi.repository import GLib
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.const import Chatstate as State
from gajim.common.structs import OutgoingMessage
from gajim.common.modules.base import BaseModule
from gajim.common.types import ContactT
from gajim.common.types import ConnectionT
INACTIVE_AFTER = 60
PAUSED_AFTER = 10
def ensure_enabled(func):
@wraps(func)
def func_wrapper(self, *args, **kwargs):
if not self.enabled:
return None
return func(self, *args, **kwargs)
return func_wrapper
class Chatstate(BaseModule):
def __init__(self, con: ConnectionT) -> None:
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._presence_received),
StanzaHandler(name='message',
callback=self._process_chatstate,
ns=Namespace.CHATSTATES,
priority=46),
]
# Our current chatstate with a specific contact
self._chatstates = {} # type: Dict[str, State]
self._last_keyboard_activity = {} # type: Dict[str, float]
self._last_mouse_activity = {} # type: Dict[str, float]
self._timeout_id = None
self._delay_timeout_ids = {} # type: Dict[str, str]
self._blocked = [] # type: List[str]
self._enabled = False
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, value):
if self._enabled == value:
return
self._log.info('Chatstate module %s',
'enabled' if value else 'disabled')
self._enabled = value
if value:
self._timeout_id = GLib.timeout_add_seconds(
2, self._check_last_interaction)
else:
self.cleanup()
self._chatstates = {}
self._last_keyboard_activity = {}
self._last_mouse_activity = {}
self._blocked = []
@ensure_enabled
def _presence_received(self,
_con: ConnectionT,
stanza: nbxmpp.Presence,
_properties: Any) -> None:
if stanza.getType() not in ('unavailable', 'error'):
return
full_jid = stanza.getFrom()
if full_jid is None or self._con.get_own_jid().bare_match(full_jid):
# Presence from ourself
return
contact = app.contacts.get_gc_contact(
self._account, full_jid.bare, full_jid.resource)
if contact is None:
contact = app.contacts.get_contact_from_full_jid(
self._account, str(full_jid))
if contact is None:
return
if contact.chatstate is None:
return
if contact.is_gc_contact:
jid = contact.get_full_jid()
else:
jid = contact.jid
contact.chatstate = None
self._chatstates.pop(jid, None)
self._last_mouse_activity.pop(jid, None)
self._last_keyboard_activity.pop(jid, None)
self._log.info('Reset chatstate for %s', jid)
app.nec.push_outgoing_event(
NetworkEvent('chatstate-received',
account=self._account,
contact=contact))
def _process_chatstate(self, _con, _stanza, properties):
if not properties.has_chatstate:
return
if (properties.is_self_message or
properties.type.is_groupchat or
properties.is_mam_message or
properties.is_carbon_message and properties.carbon.is_sent):
return
if properties.is_muc_pm:
contact = app.contacts.get_gc_contact(
self._account,
properties.jid.bare,
properties.jid.resource)
else:
contact = app.contacts.get_contact_from_full_jid(
self._account, str(properties.jid))
if contact is None:
return
contact.chatstate = properties.chatstate
self._log.info('Recv: %-10s - %s', properties.chatstate, properties.jid)
app.nec.push_outgoing_event(
NetworkEvent('chatstate-received',
account=self._account,
contact=contact))
@ensure_enabled
def _check_last_interaction(self) -> GLib.SOURCE_CONTINUE:
now = time.time()
for jid in list(self._last_mouse_activity.keys()):
time_ = self._last_mouse_activity[jid]
current_state = self._chatstates.get(jid)
if current_state is None:
self._last_mouse_activity.pop(jid, None)
self._last_keyboard_activity.pop(jid, None)
continue
if current_state in (State.GONE, State.INACTIVE):
continue
new_chatstate = None
if now - time_ > INACTIVE_AFTER:
new_chatstate = State.INACTIVE
elif current_state == State.COMPOSING:
key_time = self._last_keyboard_activity[jid]
if now - key_time > PAUSED_AFTER:
new_chatstate = State.PAUSED
if new_chatstate is not None:
if self._chatstates.get(jid) != new_chatstate:
contact = app.contacts.get_contact(self._account, jid)
if contact is None:
room, nick = app.get_room_and_nick_from_fjid(jid)
contact = app.contacts.get_gc_contact(
self._account, room, nick)
if contact is not None:
contact = contact.as_contact()
else:
# Contact not found, maybe we left the group chat
# or the contact was removed from the roster
self._log.info(
'Contact %s not found, reset chatstate', jid)
self._chatstates.pop(jid, None)
self._last_mouse_activity.pop(jid, None)
self._last_keyboard_activity.pop(jid, None)
continue
self.set_chatstate(contact, new_chatstate)
return GLib.SOURCE_CONTINUE
@ensure_enabled
def set_active(self, contact: ContactT) -> None:
if contact.settings.get('send_chatstate') == 'disabled':
return
self._last_mouse_activity[contact.jid] = time.time()
self._chatstates[contact.jid] = State.ACTIVE
def get_active_chatstate(self, contact: ContactT) -> Optional[str]:
# determines if we add 'active' on outgoing messages
if contact.settings.get('send_chatstate') == 'disabled':
return None
if not contact.is_groupchat:
# Dont send chatstates to ourself
if self._con.get_own_jid().bare_match(contact.jid):
return None
if not contact.supports(Namespace.CHATSTATES):
return None
self.set_active(contact)
return 'active'
@ensure_enabled
def block_chatstates(self, contact: ContactT, block: bool) -> None:
# Block sending chatstates to a contact
# Used for example if we cycle through the MUC nick list, which
# produces a lot of buffer 'changed' signals from the input textview.
# This would lead to sending ACTIVE -> COMPOSING -> ACTIVE ...
if block:
self._blocked.append(contact.jid)
else:
self._blocked.remove(contact.jid)
@ensure_enabled
def set_chatstate_delayed(self, contact: ContactT, state: State) -> None:
# Used when we go from Composing -> Active after deleting all text
# from the Textview. We delay the Active state because maybe the
# User starts writing again.
self.remove_delay_timeout(contact)
self._delay_timeout_ids[contact.jid] = GLib.timeout_add_seconds(
2, self.set_chatstate, contact, state)
@ensure_enabled
def set_chatstate(self, contact: ContactT, state: State) -> None:
# Dont send chatstates to ourself
if self._con.get_own_jid().bare_match(contact.jid):
return
if contact.jid in self._blocked:
return
self.remove_delay_timeout(contact)
current_state = self._chatstates.get(contact.jid)
setting = contact.settings.get('send_chatstate')
if setting == 'disabled':
# Send a last 'active' state after user disabled chatstates
if current_state is not None:
self._log.info('Disabled for %s', contact.jid)
self._log.info('Send last state: %-10s - %s',
State.ACTIVE, contact.jid)
self._send_chatstate(contact, str(State.ACTIVE))
self._chatstates.pop(contact.jid, None)
self._last_mouse_activity.pop(contact.jid, None)
self._last_keyboard_activity.pop(contact.jid, None)
return
if not contact.is_groupchat:
# Dont leak presence to contacts
# which are not allowed to see our status
if not contact.is_pm_contact:
if contact and contact.sub in ('to', 'none'):
self._log.info('Contact not subscribed: %s', contact.jid)
return
if contact.show == 'offline':
self._log.info('Contact offline: %s', contact.jid)
return
if not contact.supports(Namespace.CHATSTATES):
self._log.info('Chatstates not supported: %s', contact.jid)
return
if state in (State.ACTIVE, State.COMPOSING):
self._last_mouse_activity[contact.jid] = time.time()
if setting == 'composing_only':
if state in (State.INACTIVE, State.GONE):
state = State.ACTIVE
if current_state == state:
return
self._log.info('Send: %-10s - %s', state, contact.jid)
self._send_chatstate(contact, str(state))
self._chatstates[contact.jid] = state
def _send_chatstate(self, contact, chatstate):
type_ = 'groupchat' if contact.is_groupchat else 'chat'
message = OutgoingMessage(account=self._account,
contact=contact,
message=None,
type_=type_,
chatstate=chatstate,
play_sound=False)
self._con.send_message(message)
@ensure_enabled
def set_mouse_activity(self, contact: ContactT, was_paused: bool) -> None:
if contact.settings.get('send_chatstate') == 'disabled':
return
self._last_mouse_activity[contact.jid] = time.time()
if self._chatstates.get(contact.jid) == State.INACTIVE:
if was_paused:
self.set_chatstate(contact, State.PAUSED)
else:
self.set_chatstate(contact, State.ACTIVE)
@ensure_enabled
def set_keyboard_activity(self, contact: ContactT) -> None:
self._last_keyboard_activity[contact.jid] = time.time()
def remove_delay_timeout(self, contact):
timeout = self._delay_timeout_ids.get(contact.jid)
if timeout is not None:
GLib.source_remove(timeout)
del self._delay_timeout_ids[contact.jid]
def remove_all_delay_timeouts(self):
for timeout in self._delay_timeout_ids.values():
GLib.source_remove(timeout)
self._delay_timeout_ids = {}
def cleanup(self):
self.remove_all_delay_timeouts()
if self._timeout_id is not None:
GLib.source_remove(self._timeout_id)
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Chatstate, str]:
return Chatstate(*args, **kwargs), 'Chatstate'

View file

@ -0,0 +1,56 @@
# 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/>.
# XEP-0083: Nested Roster Groups
from nbxmpp.errors import is_error
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import as_task
class Delimiter(BaseModule):
_nbxmpp_extends = 'Delimiter'
_nbxmpp_methods = [
'request_delimiter',
'set_delimiter'
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.available = False
self.delimiter = '::'
@as_task
def get_roster_delimiter(self):
_task = yield
delimiter = yield self.request_delimiter()
if is_error(delimiter) or delimiter is None:
result = yield self.set_delimiter(self.delimiter)
if is_error(result):
self._con.connect_machine()
return
delimiter = self.delimiter
self.delimiter = delimiter
self.available = True
self._con.connect_machine()
def get_instance(*args, **kwargs):
return Delimiter(*args, **kwargs), 'Delimiter'

View file

@ -0,0 +1,265 @@
# 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/>.
# XEP-0030: Service Discovery
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from nbxmpp.errors import is_error
from gajim.common import app
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.nec import NetworkEvent
from gajim.common.modules.util import as_task
from gajim.common.modules.base import BaseModule
class Discovery(BaseModule):
_nbxmpp_extends = 'Discovery'
_nbxmpp_methods = [
'disco_info',
'disco_items',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._answer_disco_info,
typ='get',
ns=Namespace.DISCO_INFO),
StanzaHandler(name='iq',
callback=self._answer_disco_items,
typ='get',
ns=Namespace.DISCO_ITEMS),
]
self._account_info = None
self._server_info = None
@property
def account_info(self):
return self._account_info
@property
def server_info(self):
return self._server_info
def discover_server_items(self):
server = self._con.get_own_jid().domain
self.disco_items(server, callback=self._server_items_received)
def _server_items_received(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.warning('Server disco failed')
self._log.error(error)
return
self._log.info('Server items received')
self._log.debug(result)
for item in result.items:
if item.node is not None:
# Only disco components
continue
self.disco_info(item.jid, callback=self._server_items_info_received)
def _server_items_info_received(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.warning('Server item disco info failed')
self._log.warning(error)
return
self._log.info('Server item info received: %s', result.jid)
self._parse_transports(result)
try:
self._con.get_module('MUC').pass_disco(result)
self._con.get_module('HTTPUpload').pass_disco(result)
self._con.get_module('Bytestream').pass_disco(result)
except nbxmpp.NodeProcessed:
pass
app.nec.push_incoming_event(
NetworkIncomingEvent('server-disco-received'))
def discover_account_info(self):
own_jid = self._con.get_own_jid().bare
self.disco_info(own_jid, callback=self._account_info_received)
def _account_info_received(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.warning('Account disco info failed')
self._log.warning(error)
return
self._log.info('Account info received: %s', result.jid)
self._account_info = result
self._con.get_module('MAM').pass_disco(result)
self._con.get_module('PEP').pass_disco(result)
self._con.get_module('PubSub').pass_disco(result)
self._con.get_module('Bookmarks').pass_disco(result)
self._con.get_module('VCardAvatars').pass_disco(result)
self._con.get_module('Caps').update_caps()
def discover_server_info(self):
# Calling this method starts the connect_maschine()
server = self._con.get_own_jid().domain
self.disco_info(server, callback=self._server_info_received)
def _server_info_received(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.error('Server disco info failed')
self._log.error(error)
return
self._log.info('Server info received: %s', result.jid)
self._server_info = result
self._con.get_module('SecLabels').pass_disco(result)
self._con.get_module('Blocking').pass_disco(result)
self._con.get_module('VCardTemp').pass_disco(result)
self._con.get_module('Carbons').pass_disco(result)
self._con.get_module('HTTPUpload').pass_disco(result)
self._con.get_module('Register').pass_disco(result)
self._con.connect_machine(restart=True)
def _parse_transports(self, info):
for identity in info.identities:
if identity.category not in ('gateway', 'headline'):
continue
self._log.info('Found transport: %s %s %s',
info.jid, identity.category, identity.type)
jid = str(info.jid)
if jid not in app.transport_type:
app.transport_type[jid] = identity.type
if identity.type in self._con.available_transports:
self._con.available_transports[identity.type].append(jid)
else:
self._con.available_transports[identity.type] = [jid]
def _answer_disco_items(self, _con, stanza, _properties):
from_ = stanza.getFrom()
self._log.info('Answer disco items to %s', from_)
if self._con.get_module('AdHocCommands').command_items_query(stanza):
raise nbxmpp.NodeProcessed
node = stanza.getTagAttr('query', 'node')
if node is None:
result = stanza.buildReply('result')
self._con.connection.send(result)
raise nbxmpp.NodeProcessed
if node == Namespace.COMMANDS:
self._con.get_module('AdHocCommands').command_list_query(stanza)
raise nbxmpp.NodeProcessed
def _answer_disco_info(self, _con, stanza, _properties):
from_ = stanza.getFrom()
self._log.info('Answer disco info %s', from_)
if str(from_).startswith('echo.'):
# Service that echos all stanzas, ignore it
raise nbxmpp.NodeProcessed
if self._con.get_module('AdHocCommands').command_info_query(stanza):
raise nbxmpp.NodeProcessed
@as_task
def disco_muc(self,
jid,
request_vcard=False,
allow_redirect=False):
_task = yield
self._log.info('Request MUC info for %s', jid)
result = yield self._nbxmpp('MUC').request_info(
jid,
request_vcard=request_vcard,
allow_redirect=allow_redirect)
if is_error(result):
raise result
if result.redirected:
self._log.info('MUC info received after redirect: %s -> %s',
jid, result.info.jid)
else:
self._log.info('MUC info received: %s', result.info.jid)
app.storage.cache.set_last_disco_info(result.info.jid, result.info)
if result.vcard is not None:
avatar, avatar_sha = result.vcard.get_avatar()
if avatar is not None:
if not app.interface.avatar_exists(avatar_sha):
app.interface.save_avatar(avatar)
app.storage.cache.set_muc_avatar_sha(result.info.jid,
avatar_sha)
app.interface.avatar_storage.invalidate_cache(result.info.jid)
self._con.get_module('VCardAvatars').muc_disco_info_update(result.info)
app.nec.push_incoming_event(NetworkEvent(
'muc-disco-update',
account=self._account,
room_jid=result.info.jid))
yield result
@as_task
def disco_contact(self, contact):
_task = yield
fjid = contact.get_full_jid()
result = yield self.disco_info(fjid)
if is_error(result):
raise result
self._log.info('Disco Info received: %s', fjid)
app.storage.cache.set_last_disco_info(result.jid,
result,
cache_only=True)
app.nec.push_incoming_event(
NetworkEvent('caps-update',
account=self._account,
fjid=fjid,
jid=contact.jid))
def get_instance(*args, **kwargs):
return Discovery(*args, **kwargs), 'Discovery'

View file

@ -0,0 +1,116 @@
# 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/>.
# XEP-0202: Entity Time
import time
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.date_and_time import parse_datetime
from nbxmpp.modules.date_and_time import create_tzinfo
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class EntityTime(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._answer_request,
typ='get',
ns=Namespace.TIME_REVISED),
]
def request_entity_time(self, jid, resource):
if not app.account_is_available(self._account):
return
if resource:
jid += '/' + resource
iq = nbxmpp.Iq(to=jid, typ='get')
iq.addChild('time', namespace=Namespace.TIME_REVISED)
self._log.info('Requested: %s', jid)
self._con.connection.SendAndCallForResponse(iq, self._result_received)
def _result_received(self, _nbxmpp_client, stanza):
time_info = None
if not nbxmpp.isResultNode(stanza):
self._log.info('Error: %s', stanza.getError())
else:
time_info = self._extract_info(stanza)
self._log.info('Received: %s %s', stanza.getFrom(), time_info)
app.nec.push_incoming_event(NetworkEvent('time-result-received',
conn=self._con,
jid=stanza.getFrom(),
time_info=time_info))
def _extract_info(self, stanza):
time_ = stanza.getTag('time')
if not time_:
self._log.warning('No time node: %s', stanza)
return None
tzo = time_.getTag('tzo').getData()
if not tzo:
self._log.warning('Wrong tzo node: %s', stanza)
return None
remote_tz = create_tzinfo(tz_string=tzo)
if remote_tz is None:
self._log.warning('Wrong tzo node: %s', stanza)
return None
utc_time = time_.getTag('utc').getData()
date_time = parse_datetime(utc_time, check_utc=True)
if date_time is None:
self._log.warning('Wrong timezone definition: %s %s',
utc_time, stanza.getFrom())
return None
date_time = date_time.astimezone(remote_tz)
return date_time.strftime('%c %Z')
def _answer_request(self, _con, stanza, _properties):
self._log.info('%s asked for the time', stanza.getFrom())
if app.settings.get_account_setting(self._account, 'send_time_info'):
iq = stanza.buildReply('result')
time_ = iq.setTag('time', namespace=Namespace.TIME_REVISED)
formated_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
time_.setTagData('utc', formated_time)
isdst = time.localtime().tm_isdst
zone = -(time.timezone, time.altzone)[isdst] / 60.0
tzo = (zone / 60, abs(zone % 60))
time_.setTagData('tzo', '%+03d:%02d' % (tzo))
self._log.info('Answer: %s %s', formated_time, '%+03d:%02d' % (tzo))
else:
iq = stanza.buildReply('error')
err = nbxmpp.ErrorNode(nbxmpp.ERR_SERVICE_UNAVAILABLE)
iq.addChild(node=err)
self._log.info('Send service-unavailable')
self._con.connection.send(iq)
raise nbxmpp.NodeProcessed
def get_instance(*args, **kwargs):
return EntityTime(*args, **kwargs), 'EntityTime'

View file

@ -0,0 +1,101 @@
# 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/>.
# XEP-0100: Gateway Interaction
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class Gateway(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
def unsubscribe(self, agent):
if not app.account_is_available(self._account):
return
iq = nbxmpp.Iq('set', Namespace.REGISTER, to=agent)
iq.setQuery().setTag('remove')
self._con.connection.SendAndCallForResponse(
iq, self._on_unsubscribe_result)
self._con.get_module('Roster').del_item(agent)
def _on_unsubscribe_result(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Error: %s', stanza.getError())
return
agent = stanza.getFrom().bare
jid_list = []
for jid in app.contacts.get_jid_list(self._account):
if jid.endswith('@' + agent):
jid_list.append(jid)
self._log.info('Removing contact %s due to'
' unregistered transport %s', jid, agent)
self._con.get_module('Presence').unsubscribe(jid)
# Transport contacts can't have 2 resources
if jid in app.to_be_removed[self._account]:
# This way we'll really remove it
app.to_be_removed[self._account].remove(jid)
app.nec.push_incoming_event(
NetworkEvent('agent-removed',
conn=self._con,
agent=agent,
jid_list=jid_list))
def request_gateway_prompt(self, jid, prompt=None):
typ_ = 'get'
if prompt:
typ_ = 'set'
iq = nbxmpp.Iq(typ=typ_, to=jid)
query = iq.addChild(name='query', namespace=Namespace.GATEWAY)
if prompt:
query.setTagData('prompt', prompt)
self._con.connection.SendAndCallForResponse(iq, self._on_prompt_result)
def _on_prompt_result(self, _nbxmpp_client, stanza):
jid = str(stanza.getFrom())
fjid = stanza.getFrom().bare
resource = stanza.getFrom().resource
query = stanza.getTag('query')
if query is not None:
desc = query.getTagData('desc')
prompt = query.getTagData('prompt')
prompt_jid = query.getTagData('jid')
else:
desc = None
prompt = None
prompt_jid = None
app.nec.push_incoming_event(
NetworkEvent('gateway-prompt-received',
conn=self._con,
fjid=fjid,
jid=jid,
resource=resource,
desc=desc,
prompt=prompt,
prompt_jid=prompt_jid,
stanza=stanza))
def get_instance(*args, **kwargs):
return Gateway(*args, **kwargs), 'Gateway'

View file

@ -0,0 +1,78 @@
# 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/>.
# XEP-0070: Verifying HTTP Requests via XMPP
import nbxmpp
from nbxmpp.structs import StanzaHandler
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class HTTPAuth(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._http_auth,
ns=Namespace.HTTP_AUTH,
priority=45),
StanzaHandler(name='iq',
callback=self._http_auth,
typ='get',
ns=Namespace.HTTP_AUTH,
priority=45)
]
def _http_auth(self, _con, stanza, properties):
if not properties.is_http_auth:
return
self._log.info('Auth request received')
auto_answer = app.settings.get_account_setting(self._account,
'http_auth')
if auto_answer in ('yes', 'no'):
self.build_http_auth_answer(stanza, auto_answer)
raise nbxmpp.NodeProcessed
app.nec.push_incoming_event(
NetworkEvent('http-auth-received',
conn=self._con,
iq_id=properties.http_auth.id,
method=properties.http_auth.method,
url=properties.http_auth.url,
msg=properties.http_auth.body,
stanza=stanza))
raise nbxmpp.NodeProcessed
def build_http_auth_answer(self, stanza, answer):
if answer == 'yes':
self._log.info('Auth request approved')
confirm = stanza.getTag('confirm')
reply = stanza.buildReply('result')
if stanza.getName() == 'message':
reply.addChild(node=confirm)
self._con.connection.send(reply)
elif answer == 'no':
self._log.info('Auth request denied')
err = nbxmpp.Error(stanza, nbxmpp.protocol.ERR_NOT_AUTHORIZED)
self._con.connection.send(err)
def get_instance(*args, **kwargs):
return HTTPAuth(*args, **kwargs), 'HTTPAuth'

View file

@ -0,0 +1,404 @@
# 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/>.
# XEP-0363: HTTP File Upload
import os
import io
from urllib.parse import urlparse
import mimetypes
from nbxmpp.namespaces import Namespace
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import HTTPUploadStanzaError
from nbxmpp.util import convert_tls_error_flags
from gi.repository import GLib
from gi.repository import Soup
from gajim.common import app
from gajim.common.i18n import _
from gajim.common.helpers import get_tls_error_phrase
from gajim.common.helpers import get_user_proxy
from gajim.common.const import FTState
from gajim.common.filetransfer import FileTransfer
from gajim.common.modules.base import BaseModule
from gajim.common.exceptions import FileError
class HTTPUpload(BaseModule):
_nbxmpp_extends = 'HTTPUpload'
def __init__(self, con):
BaseModule.__init__(self, con)
self.available = False
self.component = None
self.httpupload_namespace = None
self.max_file_size = None # maximum file size in bytes
self._proxy_resolver = None
self._queued_messages = {}
self._session = Soup.Session()
self._session.props.ssl_strict = False
self._session.props.user_agent = 'Gajim %s' % app.version
def _set_proxy_if_available(self):
proxy = get_user_proxy(self._account)
if proxy is None:
self._proxy_resolver = None
self._session.props.proxy_resolver = None
else:
self._proxy_resolver = proxy.get_resolver()
self._session.props.proxy_resolver = self._proxy_resolver
def pass_disco(self, info):
if not info.has_httpupload:
return
self.available = True
self.httpupload_namespace = Namespace.HTTPUPLOAD_0
self.component = info.jid
self.max_file_size = info.httpupload_max_file_size
self._log.info('Discovered component: %s', info.jid)
if self.max_file_size is None:
self._log.warning('Component does not provide maximum file size')
else:
size = GLib.format_size_full(self.max_file_size,
GLib.FormatSizeFlags.IEC_UNITS)
self._log.info('Component has a maximum file size of: %s', size)
for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account):
ctrl.update_actions()
def make_transfer(self, path, encryption, contact, groupchat=False):
if not path or not os.path.exists(path):
raise FileError(_('Could not access file'))
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:
raise FileError(msg)
mime = mimetypes.MimeTypes().guess_type(path)[0]
if not mime:
mime = 'application/octet-stream' # fallback mime type
self._log.info("Detected MIME type of file: %s", mime)
return HTTPFileTransfer(self._account,
path,
contact,
mime,
encryption,
groupchat)
def cancel_transfer(self, transfer):
transfer.set_cancelled()
message = self._queued_messages.get(id(transfer))
if message is None:
return
self._session.cancel_message(message, Soup.Status.CANCELLED)
def start_transfer(self, transfer):
if transfer.encryption is not None and not transfer.is_encrypted:
transfer.set_encrypting()
plugin = app.plugin_manager.encryption_plugins[transfer.encryption]
if hasattr(plugin, 'encrypt_file'):
plugin.encrypt_file(transfer,
self._account,
self.start_transfer)
else:
transfer.set_error('encryption-not-available')
return
transfer.set_preparing()
self._log.info('Sending request for slot')
self._nbxmpp('HTTPUpload').request_slot(
jid=self.component,
filename=transfer.filename,
size=transfer.size,
content_type=transfer.mime,
callback=self._received_slot,
user_data=transfer)
def _received_slot(self, task):
transfer = task.get_user_data()
try:
result = task.finish()
except (StanzaError,
HTTPUploadStanzaError,
MalformedStanzaError) as error:
if error.app_condition == 'file-too-large':
size_text = GLib.format_size_full(
error.get_max_file_size(),
GLib.FormatSizeFlags.IEC_UNITS)
error_text = _('File is too large, '
'maximum allowed file size is: %s' % size_text)
transfer.set_error('file-too-large', error_text)
else:
transfer.set_error('misc', str(error))
return
transfer.process_result(result)
if (urlparse(transfer.put_uri).scheme != 'https' or
urlparse(transfer.get_uri).scheme != 'https'):
transfer.set_error('unsecure')
return
self._log.info('Uploading file to %s', transfer.put_uri)
self._log.info('Please download from %s', transfer.get_uri)
self._upload_file(transfer)
def _upload_file(self, transfer):
transfer.set_started()
message = Soup.Message.new('PUT', transfer.put_uri)
message.connect('starting', self._check_certificate, transfer)
# Set CAN_REBUILD so chunks get discarded after they have been
# written to the network
message.set_flags(Soup.MessageFlags.CAN_REBUILD |
Soup.MessageFlags.NO_REDIRECT)
message.props.request_body.set_accumulate(False)
message.props.request_headers.set_content_type(transfer.mime, None)
message.props.request_headers.set_content_length(transfer.size)
for name, value in transfer.headers.items():
message.props.request_headers.append(name, value)
message.connect('wrote-headers', self._on_wrote_headers, transfer)
message.connect('wrote-chunk', self._on_wrote_chunk, transfer)
self._queued_messages[id(transfer)] = message
self._set_proxy_if_available()
self._session.queue_message(message, self._on_finish, transfer)
def _check_certificate(self, message, transfer):
https_used, tls_certificate, tls_errors = message.get_https_status()
if not https_used:
self._log.warning('HTTPS was not used for upload')
transfer.set_error('unsecure')
self._session.cancel_message(message, Soup.Status.CANCELLED)
return
tls_errors = convert_tls_error_flags(tls_errors)
if app.cert_store.verify(tls_certificate, tls_errors):
return
for error in tls_errors:
phrase = get_tls_error_phrase(error)
self._log.warning('TLS verification failed: %s', phrase)
transfer.set_error('tls-verification-failed', phrase)
self._session.cancel_message(message, Soup.Status.CANCELLED)
def _on_finish(self, _session, message, transfer):
self._queued_messages.pop(id(transfer), None)
if message.props.status_code == Soup.Status.CANCELLED:
self._log.info('Upload cancelled')
return
if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED):
self._log.info('Upload completed successfully')
transfer.set_finished()
else:
phrase = Soup.Status.get_phrase(message.props.status_code)
self._log.error('Got unexpected http upload response code: %s',
phrase)
transfer.set_error('http-response', phrase)
def _on_wrote_chunk(self, message, transfer):
transfer.update_progress()
if transfer.is_complete:
message.props.request_body.complete()
return
bytes_ = transfer.get_chunk()
self._session.pause_message(message)
GLib.idle_add(self._append, message, bytes_)
def _append(self, message, bytes_):
if message.props.status_code == Soup.Status.CANCELLED:
return
self._session.unpause_message(message)
message.props.request_body.append(bytes_)
@staticmethod
def _on_wrote_headers(message, transfer):
message.props.request_body.append(transfer.get_chunk())
class HTTPFileTransfer(FileTransfer):
_state_descriptions = {
FTState.ENCRYPTING: _('Encrypting file…'),
FTState.PREPARING: _('Requesting HTTP File Upload Slot…'),
FTState.STARTED: _('Uploading via HTTP File Upload…'),
}
_errors = {
'unsecure': _('The server returned an insecure transport (HTTP).'),
'encryption-not-available': _('There is no encryption method available '
'for the chosen encryption.')
}
def __init__(self,
account,
path,
contact,
mime,
encryption,
groupchat):
FileTransfer.__init__(self, account)
self._path = path
self._encryption = encryption
self._groupchat = groupchat
self._contact = contact
self._mime = mime
self.size = os.stat(path).st_size
self.put_uri = None
self.get_uri = None
self._uri_transform_func = None
self._stream = None
self._data = None
self._headers = {}
self._is_encrypted = False
@property
def mime(self):
return self._mime
@property
def contact(self):
return self._contact
@property
def is_groupchat(self):
return self._groupchat
@property
def encryption(self):
return self._encryption
@property
def headers(self):
return self._headers
@property
def path(self):
return self._path
@property
def is_encrypted(self):
return self._is_encrypted
def get_transformed_uri(self):
if self._uri_transform_func is not None:
return self._uri_transform_func(self.get_uri)
return self.get_uri
def set_uri_transform_func(self, func):
self._uri_transform_func = func
@property
def filename(self):
return os.path.basename(self._path)
def set_error(self, domain, text=''):
if not text:
text = self._errors[domain]
self._close()
super().set_error(domain, text)
def set_finished(self):
self._close()
super().set_finished()
def set_encrypted_data(self, data):
self._data = data
self._is_encrypted = True
def _close(self):
if self._stream is not None:
self._stream.close()
def get_chunk(self):
if self._stream is None:
if self._encryption is None:
self._stream = open(self._path, 'rb')
else:
self._stream = io.BytesIO(self._data)
data = self._stream.read(16384)
if not data:
self._close()
return None
self._seen += len(data)
if self.is_complete:
self._close()
return data
def get_data(self):
with open(self._path, 'rb') as file:
data = file.read()
return data
def process_result(self, result):
self.put_uri = result.put_uri
self.get_uri = result.get_uri
self._headers = result.headers
def get_instance(*args, **kwargs):
return HTTPUpload(*args, **kwargs), 'HTTPUpload'

239
gajim/common/modules/ibb.py Normal file
View file

@ -0,0 +1,239 @@
# 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/>.
# XEP-0047: In-Band Bytestreams
import time
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import NodeProcessed
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from gajim.common import app
from gajim.common.helpers import to_user_string
from gajim.common.modules.base import BaseModule
from gajim.common.file_props import FilesProp
class IBB(BaseModule):
_nbxmpp_extends = 'IBB'
_nbxmpp_methods = [
'send_open',
'send_close',
'send_data',
'send_reply',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._ibb_received,
ns=Namespace.IBB),
]
def _ibb_received(self, _con, stanza, properties):
if not properties.is_ibb:
return
if properties.ibb.type == 'data':
self._log.info('Data received, sid: %s, seq: %s',
properties.ibb.sid, properties.ibb.seq)
file_props = FilesProp.getFilePropByTransportSid(self._account,
properties.ibb.sid)
if not file_props:
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
raise NodeProcessed
if file_props.connected:
self._on_data_received(stanza, file_props, properties)
self.send_reply(stanza)
elif properties.ibb.type == 'open':
self._log.info('Open received, sid: %s, blocksize: %s',
properties.ibb.sid, properties.ibb.block_size)
file_props = FilesProp.getFilePropByTransportSid(self._account,
properties.ibb.sid)
if not file_props:
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
raise NodeProcessed
file_props.block_size = properties.ibb.block_size
file_props.direction = '<'
file_props.seq = 0
file_props.received_len = 0
file_props.last_time = time.time()
file_props.error = 0
file_props.paused = False
file_props.connected = True
file_props.completed = False
file_props.disconnect_cb = None
file_props.continue_cb = None
file_props.syn_id = stanza.getID()
file_props.fp = open(file_props.file_name, 'wb')
self.send_reply(stanza)
elif properties.ibb.type == 'close':
self._log.info('Close received, sid: %s', properties.ibb.sid)
file_props = FilesProp.getFilePropByTransportSid(self._account,
properties.ibb.sid)
if not file_props:
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
raise NodeProcessed
self.send_reply(stanza)
file_props.fp.close()
file_props.completed = file_props.received_len >= file_props.size
if not file_props.completed:
file_props.error = -1
app.socks5queue.complete_transfer_cb(self._account, file_props)
raise NodeProcessed
def _on_data_received(self, stanza, file_props, properties):
ibb = properties.ibb
if ibb.seq != file_props.seq:
self.send_reply(stanza, nbxmpp.ERR_UNEXPECTED_REQUEST)
self.send_close(file_props)
raise NodeProcessed
self._log.debug('Data received: sid: %s, %s+%s bytes',
ibb.sid, file_props.fp.tell(), len(ibb.data))
file_props.seq += 1
file_props.started = True
file_props.fp.write(ibb.data)
current_time = time.time()
file_props.elapsed_time += current_time - file_props.last_time
file_props.last_time = current_time
file_props.received_len += len(ibb.data)
app.socks5queue.progress_transfer_cb(self._account, file_props)
if file_props.received_len >= file_props.size:
file_props.completed = True
def send_open(self, to, sid, fp):
self._log.info('Send open to %s, sid: %s', to, sid)
file_props = FilesProp.getFilePropBySid(sid)
file_props.direction = '>'
file_props.block_size = 4096
file_props.fp = fp
file_props.seq = -1
file_props.error = 0
file_props.paused = False
file_props.received_len = 0
file_props.last_time = time.time()
file_props.connected = True
file_props.completed = False
file_props.disconnect_cb = None
file_props.continue_cb = None
self._nbxmpp('IBB').send_open(to,
file_props.transport_sid,
4096,
callback=self._on_open_result,
user_data=file_props)
return file_props
def _on_open_result(self, task):
try:
task.finish()
except StanzaError as error:
app.socks5queue.error_cb('Error', to_user_string(error))
self._log.warning(error)
return
file_props = task.get_user_data()
self.send_data(file_props)
def send_close(self, file_props):
file_props.connected = False
file_props.fp.close()
file_props.stopped = True
to = file_props.receiver
if file_props.direction == '<':
to = file_props.sender
self._log.info('Send close to %s, sid: %s',
to, file_props.transport_sid)
self._nbxmpp('IBB').send_close(to, file_props.transport_sid,
callback=self._on_close_result)
if file_props.completed:
app.socks5queue.complete_transfer_cb(self._account, file_props)
else:
if file_props.type_ == 's':
peerjid = file_props.receiver
else:
peerjid = file_props.sender
session = self._con.get_module('Jingle').get_jingle_session(
peerjid, file_props.sid, 'file')
# According to the xep, the initiator also cancels
# the jingle session if there are no more files to send using IBB
if session.weinitiate:
session.cancel_session()
def _on_close_result(self, task):
try:
task.finish()
except StanzaError as error:
app.socks5queue.error_cb('Error', to_user_string(error))
self._log.warning(error)
return
def send_data(self, file_props):
if file_props.completed:
self.send_close(file_props)
return
chunk = file_props.fp.read(file_props.block_size)
if chunk:
file_props.seq += 1
file_props.started = True
if file_props.seq == 65536:
file_props.seq = 0
self._log.info('Send data to %s, sid: %s',
file_props.receiver, file_props.transport_sid)
self._nbxmpp('IBB').send_data(file_props.receiver,
file_props.transport_sid,
file_props.seq,
chunk,
callback=self._on_data_result,
user_data=file_props)
current_time = time.time()
file_props.elapsed_time += current_time - file_props.last_time
file_props.last_time = current_time
file_props.received_len += len(chunk)
if file_props.size == file_props.received_len:
file_props.completed = True
app.socks5queue.progress_transfer_cb(self._account, file_props)
def _on_data_result(self, task):
try:
task.finish()
except StanzaError as error:
app.socks5queue.error_cb('Error', to_user_string(error))
self._log.warning(error)
return
file_props = task.get_user_data()
self.send_data(file_props)
def get_instance(*args, **kwargs):
return IBB(*args, **kwargs), 'IBB'

View file

@ -0,0 +1,88 @@
# 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/>.
# Iq handler
import nbxmpp
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.helpers import to_user_string
from gajim.common.nec import NetworkEvent
from gajim.common.file_props import FilesProp
from gajim.common.modules.base import BaseModule
class Iq(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._iq_error_received,
typ='error',
priority=51),
]
def _iq_error_received(self, _con, _stanza, properties):
self._log.info('Error: %s', properties.error)
if properties.error.condition in ('jid-malformed',
'forbidden',
'not-acceptable'):
sid = self._get_sid(properties.id)
file_props = FilesProp.getFileProp(self._account, sid)
if file_props:
if properties.error.condition == 'jid-malformed':
file_props.error = -3
else:
file_props.error = -4
app.nec.push_incoming_event(
NetworkEvent('file-request-error',
conn=self._con,
jid=properties.jid.bare,
file_props=file_props,
error_msg=to_user_string(properties.error)))
self._con.get_module('Bytestream').disconnect_transfer(
file_props)
raise nbxmpp.NodeProcessed
if properties.error.condition == 'item-not-found':
sid = self._get_sid(properties.id)
file_props = FilesProp.getFileProp(self._account, sid)
if file_props:
app.nec.push_incoming_event(
NetworkEvent('file-send-error',
account=self._account,
jid=str(properties.jid),
file_props=file_props))
self._con.get_module('Bytestream').disconnect_transfer(
file_props)
raise nbxmpp.NodeProcessed
app.nec.push_incoming_event(
NetworkEvent('iq-error-received',
account=self._account,
properties=properties))
raise nbxmpp.NodeProcessed
@staticmethod
def _get_sid(id_):
sid = id_
if len(id_) > 3 and id_[2] == '_':
sid = id_[3:]
return sid
def get_instance(*args, **kwargs):
return Iq(*args, **kwargs), 'Iq'

View file

@ -0,0 +1,311 @@
# 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/>.
"""
Handles the jingle signalling protocol
"""
#TODO:
# * things in XEP 0176, including:
# - http://xmpp.org/extensions/xep-0176.html#protocol-restarts
# - http://xmpp.org/extensions/xep-0176.html#fallback
# * XEP 0177 (raw udp)
# * UI:
# - make state and codec information available to the user
# - video integration
# * config:
# - codecs
import logging
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import helpers
from gajim.common import app
from gajim.common import jingle_xtls
from gajim.common.modules.base import BaseModule
from gajim.common.jingle_session import JingleSession
from gajim.common.jingle_session import JingleStates
from gajim.common.jingle_ft import JingleFileTransfer
from gajim.common.jingle_transport import JingleTransportSocks5
from gajim.common.jingle_transport import JingleTransportIBB
from gajim.common.jingle_rtp import JingleAudio, JingleVideo
logger = logging.getLogger('gajim.c.m.jingle')
class Jingle(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
typ='result',
callback=self._on_jingle_iq),
StanzaHandler(name='iq',
typ='error',
callback=self._on_jingle_iq),
StanzaHandler(name='iq',
typ='set',
ns=Namespace.JINGLE,
callback=self._on_jingle_iq),
StanzaHandler(name='iq',
typ='get',
ns=Namespace.PUBKEY_PUBKEY,
callback=self._on_pubkey_request),
StanzaHandler(name='iq',
typ='result',
ns=Namespace.PUBKEY_PUBKEY,
callback=self._pubkey_result_received),
]
# dictionary: sessionid => JingleSession object
self._sessions = {}
# dictionary: (jid, iq stanza id) => JingleSession object,
# one time callbacks
self.__iq_responses = {}
self.files = []
def delete_jingle_session(self, sid):
"""
Remove a jingle session from a jingle stanza dispatcher
"""
if sid in self._sessions:
#FIXME: Move this elsewhere?
for content in list(self._sessions[sid].contents.values()):
content.destroy()
self._sessions[sid].callbacks = []
del self._sessions[sid]
def _on_pubkey_request(self, con, stanza, _properties):
jid_from = helpers.get_full_jid_from_iq(stanza)
self._log.info('Pubkey request from %s', jid_from)
sid = stanza.getAttr('id')
jingle_xtls.send_cert(con, jid_from, sid)
raise nbxmpp.NodeProcessed
def _pubkey_result_received(self, con, stanza, _properties):
jid_from = helpers.get_full_jid_from_iq(stanza)
self._log.info('Pubkey result from %s', jid_from)
jingle_xtls.handle_new_cert(con, stanza, jid_from)
def _on_jingle_iq(self, _con, stanza, _properties):
"""
The jingle stanza dispatcher
Route jingle stanza to proper JingleSession object, or create one if it
is a new session.
TODO: Also check if the stanza isn't an error stanza, if so route it
adequately.
"""
# get data
try:
jid = helpers.get_full_jid_from_iq(stanza)
except helpers.InvalidFormat:
logger.warning('Invalid JID: %s, ignoring it', stanza.getFrom())
return
id_ = stanza.getID()
if (jid, id_) in self.__iq_responses.keys():
self.__iq_responses[(jid, id_)].on_stanza(stanza)
del self.__iq_responses[(jid, id_)]
raise nbxmpp.NodeProcessed
jingle = stanza.getTag('jingle')
# a jingle element is not necessary in iq-result stanza
# don't check for that
if jingle:
sid = jingle.getAttr('sid')
else:
sid = None
for sesn in self._sessions.values():
if id_ in sesn.iq_ids:
sesn.on_stanza(stanza)
return
# do we need to create a new jingle object
if sid not in self._sessions:
#TODO: tie-breaking and other things...
newjingle = JingleSession(self._con, weinitiate=False, jid=jid,
iq_id=id_, sid=sid)
self._sessions[sid] = newjingle
# we already have such session in dispatcher...
self._sessions[sid].collect_iq_id(id_)
self._sessions[sid].on_stanza(stanza)
# Delete invalid/unneeded sessions
if sid in self._sessions and \
self._sessions[sid].state == JingleStates.ENDED:
self.delete_jingle_session(sid)
raise nbxmpp.NodeProcessed
def start_audio(self, jid):
if self.get_jingle_session(jid, media='audio'):
return self.get_jingle_session(jid, media='audio').sid
jingle = self.get_jingle_session(jid, media='video')
if jingle:
jingle.add_content('voice', JingleAudio(jingle))
else:
jingle = JingleSession(self._con, weinitiate=True, jid=jid)
self._sessions[jingle.sid] = jingle
jingle.add_content('voice', JingleAudio(jingle))
jingle.start_session()
return jingle.sid
def start_video(self, jid):
if self.get_jingle_session(jid, media='video'):
return self.get_jingle_session(jid, media='video').sid
jingle = self.get_jingle_session(jid, media='audio')
if jingle:
video = JingleVideo(jingle)
jingle.add_content('video', video)
else:
jingle = JingleSession(self._con, weinitiate=True, jid=jid)
self._sessions[jingle.sid] = jingle
video = JingleVideo(jingle)
jingle.add_content('video', video)
jingle.start_session()
return jingle.sid
def start_audio_video(self, jid):
if self.get_jingle_session(jid, media='video'):
return self.get_jingle_session(jid, media='video').sid
audio_session = self.get_jingle_session(jid, media='audio')
video_session = self.get_jingle_session(jid, media='video')
if audio_session and video_session:
return audio_session.sid
if audio_session:
video = JingleVideo(audio_session)
audio_session.add_content('video', video)
return audio_session.sid
if video_session:
audio = JingleAudio(video_session)
video_session.add_content('audio', audio)
return video_session.sid
jingle_session = JingleSession(self._con, weinitiate=True, jid=jid)
self._sessions[jingle_session.sid] = jingle_session
audio = JingleAudio(jingle_session)
video = JingleVideo(jingle_session)
jingle_session.add_content('audio', audio)
jingle_session.add_content('video', video)
jingle_session.start_session()
return jingle_session.sid
def start_file_transfer(self, jid, file_props, request=False):
logger.info("start file transfer with file: %s", file_props)
contact = app.contacts.get_contact_with_highest_priority(
self._account, app.get_jid_without_resource(jid))
if app.contacts.is_gc_contact(self._account, jid):
gcc = jid.split('/')
if len(gcc) == 2:
contact = app.contacts.get_gc_contact(self._account,
gcc[0],
gcc[1])
if contact is None:
return None
use_security = contact.supports(Namespace.JINGLE_XTLS)
jingle = JingleSession(self._con,
weinitiate=True,
jid=jid,
werequest=request)
# this is a file transfer
jingle.session_type_ft = True
self._sessions[jingle.sid] = jingle
file_props.sid = jingle.sid
if contact.supports(Namespace.JINGLE_BYTESTREAM):
transport = JingleTransportSocks5()
elif contact.supports(Namespace.JINGLE_IBB):
transport = JingleTransportIBB()
else:
transport = None
senders = 'initiator'
if request:
senders = 'responder'
transfer = JingleFileTransfer(jingle,
transport=transport,
file_props=file_props,
use_security=use_security,
senders=senders)
file_props.transport_sid = transport.sid
file_props.algo = self.__hash_support(contact)
jingle.add_content('file' + helpers.get_random_string(), transfer)
jingle.start_session()
return transfer.transport.sid
@staticmethod
def __hash_support(contact):
if contact.supports(Namespace.HASHES_2):
if contact.supports(Namespace.HASHES_BLAKE2B_512):
return 'blake2b-512'
if contact.supports(Namespace.HASHES_BLAKE2B_256):
return 'blake2b-256'
if contact.supports(Namespace.HASHES_SHA3_512):
return 'sha3-512'
if contact.supports(Namespace.HASHES_SHA3_256):
return 'sha3-256'
if contact.supports(Namespace.HASHES_SHA512):
return 'sha-512'
if contact.supports(Namespace.HASHES_SHA256):
return 'sha-256'
return None
def get_jingle_sessions(self, jid, sid=None, media=None):
if sid:
return [se for se in self._sessions.values() if se.sid == sid]
sessions = [se for se in self._sessions.values() if se.peerjid == jid]
if media:
if media not in ('audio', 'video', 'file'):
return []
return [se for se in sessions if se.get_content(media)]
return sessions
def set_file_info(self, file_):
# Saves information about the files we have transferred
# in case they need to be requested again.
self.files.append(file_)
def get_file_info(self, peerjid, hash_=None, name=None, _account=None):
if hash_:
for file in self.files: # DEBUG
#if f['hash'] == '1294809248109223':
if file['hash'] == hash_ and file['peerjid'] == peerjid:
return file
elif name:
for file in self.files:
if file['name'] == name and file['peerjid'] == peerjid:
return file
return None
def get_jingle_session(self, jid, sid=None, media=None):
if sid:
if sid in self._sessions:
return self._sessions[sid]
return None
if media:
if media not in ('audio', 'video', 'file'):
return None
for session in self._sessions.values():
if session.peerjid == jid and session.get_content(media):
return session
return None
def get_instance(*args, **kwargs):
return Jingle(*args, **kwargs), 'Jingle'

View file

@ -0,0 +1,59 @@
# 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/>.
# XEP-0012: Last Activity
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common import idle
from gajim.common.modules.base import BaseModule
class LastActivity(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
typ='get',
callback=self._answer_request,
ns=Namespace.LAST),
]
def _answer_request(self, _con, stanza, properties):
self._log.info('Request from %s', properties.jid)
allow_send = app.settings.get_account_setting(self._account,
'send_idle_time')
if app.is_installed('IDLE') and allow_send:
iq = stanza.buildReply('result')
query = iq.setQuery()
seconds = idle.Monitor.get_idle_sec()
query.attrs['seconds'] = seconds
self._log.info('Respond with seconds: %s', seconds)
else:
iq = stanza.buildReply('error')
err = nbxmpp.ErrorNode(nbxmpp.ERR_SERVICE_UNAVAILABLE)
iq.addChild(node=err)
self._con.connection.send(iq)
raise nbxmpp.NodeProcessed
def get_instance(*args, **kwargs):
return LastActivity(*args, **kwargs), 'LastActivity'

507
gajim/common/modules/mam.py Normal file
View file

@ -0,0 +1,507 @@
# 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/>.
# XEP-0313: Message Archive Management
import time
from datetime import datetime
from datetime import timedelta
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.util import generate_id
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import is_error
from nbxmpp.structs import StanzaHandler
from nbxmpp.modules.util import raise_if_error
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.const import ArchiveState
from gajim.common.const import KindConstant
from gajim.common.const import SyncThreshold
from gajim.common.helpers import AdditionalDataDict
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_correction
from gajim.common.modules.util import get_eme_message
from gajim.common.modules.util import as_task
from gajim.common.modules.base import BaseModule
class MAM(BaseModule):
_nbxmpp_extends = 'MAM'
_nbxmpp_methods = [
'request_preferences',
'set_preferences',
'make_query',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._set_message_archive_info,
priority=41),
StanzaHandler(name='message',
callback=self._mam_message_received,
priority=51),
]
self.available = False
self._mam_query_ids = {}
# Holds archive jids where catch up was successful
self._catch_up_finished = []
def pass_disco(self, info):
if Namespace.MAM_2 not in info.features:
return
self.available = True
self._log.info('Discovered MAM: %s', info.jid)
app.nec.push_incoming_event(
NetworkEvent('feature-discovered',
account=self._account,
feature=Namespace.MAM_2))
def reset_state(self):
self._mam_query_ids.clear()
self._catch_up_finished.clear()
def _remove_query_id(self, jid):
self._mam_query_ids.pop(jid, None)
def is_catch_up_finished(self, jid):
return jid in self._catch_up_finished
def _from_valid_archive(self, _stanza, properties):
if properties.type.is_groupchat:
expected_archive = properties.jid
else:
expected_archive = self._con.get_own_jid()
return properties.mam.archive.bare_match(expected_archive)
def _get_unique_id(self, properties):
if properties.type.is_groupchat:
return properties.mam.id, None
if properties.is_self_message:
return None, properties.id
if properties.is_muc_pm:
return properties.mam.id, properties.id
if self._con.get_own_jid().bare_match(properties.from_):
# message we sent
return properties.mam.id, properties.id
# A message we received
return properties.mam.id, None
def _set_message_archive_info(self, _con, _stanza, properties):
if (properties.is_mam_message or
properties.is_pubsub or
properties.is_muc_subject):
return
if properties.type.is_groupchat:
archive_jid = properties.jid.bare
timestamp = properties.timestamp
disco_info = app.storage.cache.get_last_disco_info(archive_jid)
if disco_info is None:
# This is the case on MUC creation
# After MUC configuration we receive a configuration change
# message before we had the chance to disco the new MUC
return
if disco_info.mam_namespace != Namespace.MAM_2:
return
else:
if not self.available:
return
archive_jid = self._con.get_own_jid().bare
timestamp = None
if properties.stanza_id is None:
return
if not archive_jid == properties.stanza_id.by:
return
if not self.is_catch_up_finished(archive_jid):
return
app.storage.archive.set_archive_infos(
archive_jid,
last_mam_id=properties.stanza_id.id,
last_muc_timestamp=timestamp)
def _mam_message_received(self, _con, stanza, properties):
if not properties.is_mam_message:
return
app.nec.push_incoming_event(
NetworkIncomingEvent('mam-message-received',
account=self._account,
stanza=stanza,
properties=properties))
if not self._from_valid_archive(stanza, properties):
self._log.warning('Message from invalid archive %s',
properties.mam.archive)
raise nbxmpp.NodeProcessed
self._log.info('Received message from archive: %s',
properties.mam.archive)
if not self._is_valid_request(properties):
self._log.warning('Invalid MAM Message: unknown query id %s',
properties.mam.query_id)
self._log.debug(stanza)
raise nbxmpp.NodeProcessed
is_groupchat = properties.type.is_groupchat
if is_groupchat:
kind = KindConstant.GC_MSG
else:
if properties.from_.bare_match(self._con.get_own_jid()):
kind = KindConstant.CHAT_MSG_SENT
else:
kind = KindConstant.CHAT_MSG_RECV
stanza_id, message_id = self._get_unique_id(properties)
# Search for duplicates
if app.storage.archive.find_stanza_id(self._account,
str(properties.mam.archive),
stanza_id,
message_id,
groupchat=is_groupchat):
self._log.info('Found duplicate with stanza-id: %s, '
'message-id: %s', stanza_id, message_id)
raise nbxmpp.NodeProcessed
additional_data = AdditionalDataDict()
if properties.has_user_delay:
# Record it as a user timestamp
additional_data.set_value(
'gajim', 'user_timestamp', properties.user_timestamp)
parse_oob(properties, additional_data)
msgtxt = properties.body
if properties.is_encrypted:
additional_data['encrypted'] = properties.encrypted.additional_data
else:
if properties.eme is not None:
msgtxt = get_eme_message(properties.eme)
if not msgtxt:
# For example Chatstates, Receipts, Chatmarkers
self._log.debug(stanza.getProperties())
return
with_ = properties.jid.bare
if properties.is_muc_pm:
# we store the message with the full JID
with_ = str(with_)
if properties.is_self_message:
# Self messages can only be deduped with origin-id
if message_id is None:
self._log.warning('Self message without origin-id found')
return
stanza_id = message_id
app.storage.archive.insert_into_logs(
self._account,
with_,
properties.mam.timestamp,
kind,
unread=False,
message=msgtxt,
contact_name=properties.muc_nickname,
additional_data=additional_data,
stanza_id=stanza_id,
message_id=properties.id)
app.nec.push_incoming_event(
NetworkEvent('mam-decrypted-message-received',
account=self._account,
additional_data=additional_data,
correct_id=parse_correction(properties),
archive_jid=properties.mam.archive,
msgtxt=properties.body,
properties=properties,
kind=kind,
)
)
def _is_valid_request(self, properties):
valid_id = self._mam_query_ids.get(properties.mam.archive, None)
return valid_id == properties.mam.query_id
def _get_query_id(self, jid):
query_id = generate_id()
self._mam_query_ids[jid] = query_id
return query_id
def _get_query_params(self):
own_jid = self._con.get_own_jid().bare
archive = app.storage.archive.get_archive_infos(own_jid)
mam_id = None
if archive is not None:
mam_id = archive.last_mam_id
start_date = None
if mam_id:
self._log.info('Request archive: %s, after mam-id %s',
own_jid, mam_id)
else:
# First Start, we request the last week
start_date = datetime.utcnow() - timedelta(days=7)
self._log.info('Request archive: %s, after date %s',
own_jid, start_date)
return mam_id, start_date
def _get_muc_query_params(self, jid, threshold):
archive = app.storage.archive.get_archive_infos(jid)
mam_id = None
start_date = None
if archive is None or archive.last_mam_id is None:
# First join
start_date = datetime.utcnow() - timedelta(days=1)
self._log.info('Request archive: %s, after date %s',
jid, start_date)
elif threshold == SyncThreshold.NO_THRESHOLD:
# Not our first join and no threshold set
mam_id = archive.last_mam_id
self._log.info('Request archive: %s, after mam-id %s',
jid, archive.last_mam_id)
else:
# Not our first join, check how much time elapsed since our
# last join and check against threshold
last_timestamp = archive.last_muc_timestamp
if last_timestamp is None:
self._log.info('No last muc timestamp found: %s', jid)
last_timestamp = 0
last = datetime.utcfromtimestamp(float(last_timestamp))
if datetime.utcnow() - last > timedelta(days=threshold):
# To much time has elapsed since last join, apply threshold
start_date = datetime.utcnow() - timedelta(days=threshold)
self._log.info('Too much time elapsed since last join, '
'request archive: %s, after date %s, '
'threshold: %s', jid, start_date, threshold)
else:
# Request from last mam-id
mam_id = archive.last_mam_id
self._log.info('Request archive: %s, after mam-id %s:',
jid, archive.last_mam_id)
return mam_id, start_date
@as_task
def request_archive_on_signin(self):
_task = yield
own_jid = self._con.get_own_jid().bare
if own_jid in self._mam_query_ids:
self._log.warning('request already running for %s', own_jid)
return
mam_id, start_date = self._get_query_params()
result = yield self._execute_query(own_jid, mam_id, start_date)
if is_error(result):
if result.condition != 'item-not-found':
self._log.warning(result)
return
app.storage.archive.reset_archive_infos(result.jid)
_, start_date = self._get_query_params()
result = yield self._execute_query(result.jid, None, start_date)
if is_error(result):
self._log.warning(result)
return
if result.rsm.last is not None:
# <last> is not provided if the requested page was empty
# so this means we did not get anything hence we only need
# to update the archive info if <last> is present
app.storage.archive.set_archive_infos(
result.jid,
last_mam_id=result.rsm.last,
last_muc_timestamp=time.time())
if start_date is not None:
# Record the earliest timestamp we request from
# the account archive. For the account archive we only
# set start_date at the very first request.
app.storage.archive.set_archive_infos(
result.jid,
oldest_mam_timestamp=start_date.timestamp())
@as_task
def request_archive_on_muc_join(self, jid):
_task = yield
threshold = app.settings.get_group_chat_setting(self._account,
jid,
'sync_threshold')
self._log.info('Threshold for %s: %s', jid, threshold)
if threshold == SyncThreshold.NO_SYNC:
return
mam_id, start_date = self._get_muc_query_params(jid, threshold)
result = yield self._execute_query(jid, mam_id, start_date)
if is_error(result):
if result.condition != 'item-not-found':
self._log.warning(result)
return
app.storage.archive.reset_archive_infos(result.jid)
_, start_date = self._get_muc_query_params(jid, threshold)
result = yield self._execute_query(result.jid, None, start_date)
if is_error(result):
self._log.warning(result)
return
if result.rsm.last is not None:
# <last> is not provided if the requested page was empty
# so this means we did not get anything hence we only need
# to update the archive info if <last> is present
app.storage.archive.set_archive_infos(
result.jid,
last_mam_id=result.rsm.last,
last_muc_timestamp=time.time())
@as_task
def _execute_query(self, jid, mam_id, start_date):
_task = yield
if jid in self._catch_up_finished:
self._catch_up_finished.remove(jid)
queryid = self._get_query_id(jid)
result = yield self.make_query(jid,
queryid,
after=mam_id,
start=start_date)
self._remove_query_id(result.jid)
raise_if_error(result)
while not result.complete:
app.storage.archive.set_archive_infos(result.jid,
last_mam_id=result.rsm.last)
queryid = self._get_query_id(result.jid)
result = yield self.make_query(result.jid,
queryid,
after=result.rsm.last,
start=start_date)
self._remove_query_id(result.jid)
raise_if_error(result)
self._catch_up_finished.append(result.jid)
self._log.info('Request finished: %s, last mam id: %s',
result.jid, result.rsm.last)
yield result
def request_archive_interval(self,
start_date,
end_date,
after=None,
queryid=None):
jid = self._con.get_own_jid().bare
if after is None:
self._log.info('Request interval: %s, from %s to %s',
jid, start_date, end_date)
else:
self._log.info('Request page: %s, after %s', jid, after)
if queryid is None:
queryid = self._get_query_id(jid)
self._mam_query_ids[jid] = queryid
self.make_query(jid,
queryid,
after=after,
start=start_date,
end=end_date,
callback=self._on_interval_result,
user_data=(queryid, start_date, end_date))
return queryid
def _on_interval_result(self, task):
queryid, start_date, end_date = task.get_user_data()
try:
result = task.finish()
except (StanzaError, MalformedStanzaError) as error:
self._remove_query_id(error.jid)
return
self._remove_query_id(result.jid)
if start_date:
timestamp = start_date.timestamp()
else:
timestamp = ArchiveState.ALL
if result.complete:
self._log.info('Request finished: %s, last mam id: %s',
result.jid, result.rsm.last)
app.storage.archive.set_archive_infos(
result.jid, oldest_mam_timestamp=timestamp)
app.nec.push_incoming_event(NetworkEvent(
'archiving-interval-finished',
account=self._account,
query_id=queryid))
else:
self.request_archive_interval(start_date,
end_date,
result.rsm.last,
queryid)
def get_instance(*args, **kwargs):
return MAM(*args, **kwargs), 'MAM'

View file

@ -0,0 +1,390 @@
# 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/>.
# Message handler
import time
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import should_log
from gajim.common.const import KindConstant
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import get_eme_message
from gajim.common.modules.misc import parse_correction
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_xhtml
class Message(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._check_if_unknown_contact,
priority=41),
StanzaHandler(name='message',
callback=self._message_received,
priority=50),
StanzaHandler(name='message',
typ='error',
callback=self._message_error_received,
priority=50),
]
# XEPs for which this message module should not be executed
self._message_namespaces = set([Namespace.ROSTERX,
Namespace.IBB])
def _check_if_unknown_contact(self, _con, stanza, properties):
if (properties.type.is_groupchat or
properties.is_muc_pm or
properties.is_self_message or
properties.is_mam_message):
return
if self._con.get_own_jid().domain == str(properties.jid):
# Server message
return
if not app.settings.get_account_setting(self._account,
'ignore_unknown_contacts'):
return
jid = properties.jid.bare
if self._con.get_module('Roster').get_item(jid) is None:
self._log.warning('Ignore message from unknown contact: %s', jid)
self._log.warning(stanza)
raise nbxmpp.NodeProcessed
def _message_received(self, _con, stanza, properties):
if (properties.is_mam_message or
properties.is_pubsub or
properties.type.is_error):
return
# Check if a child of the message contains any
# namespaces that we handle in other modules.
# nbxmpp executes less common handlers last
if self._message_namespaces & set(stanza.getProperties()):
return
self._log.info('Received from %s', stanza.getFrom())
app.nec.push_incoming_event(NetworkEvent(
'raw-message-received',
conn=self._con,
stanza=stanza,
account=self._account))
if properties.is_carbon_message and properties.carbon.is_sent:
# Ugly, we treat the from attr as the remote jid,
# to make that work with sent carbons we have to do this.
# TODO: Check where in Gajim and plugins we depend on that behavior
stanza.setFrom(stanza.getTo())
from_ = stanza.getFrom()
fjid = str(from_)
jid = from_.bare
resource = from_.resource
type_ = properties.type
stanza_id, message_id = self._get_unique_id(properties)
if properties.type.is_groupchat and properties.has_server_delay:
# Only for XEP-0045 MUC History
# Dont check for message text because the message could be
# encrypted.
if app.storage.archive.deduplicate_muc_message(
self._account,
properties.jid.bare,
properties.jid.resource,
properties.timestamp,
properties.id):
raise nbxmpp.NodeProcessed
if (properties.is_self_message or properties.is_muc_pm):
archive_jid = self._con.get_own_jid().bare
if app.storage.archive.find_stanza_id(
self._account,
archive_jid,
stanza_id,
message_id,
properties.type.is_groupchat):
return
msgtxt = properties.body
# TODO: remove all control UI stuff
gc_control = app.interface.msg_win_mgr.get_gc_control(
jid, self._account)
if not gc_control:
minimized = app.interface.minimized_controls[self._account]
gc_control = minimized.get(jid)
session = None
if not properties.type.is_groupchat:
if properties.is_muc_pm and properties.type.is_error:
session = self._con.find_session(fjid, properties.thread)
if not session:
session = self._con.get_latest_session(fjid)
if not session:
session = self._con.make_new_session(
fjid, properties.thread, type_='pm')
else:
session = self._con.get_or_create_session(
fjid, properties.thread)
if properties.thread and not session.received_thread_id:
session.received_thread_id = True
session.last_receive = time.time()
additional_data = AdditionalDataDict()
if properties.has_user_delay:
additional_data.set_value(
'gajim', 'user_timestamp', properties.user_timestamp)
parse_oob(properties, additional_data)
parse_xhtml(properties, additional_data)
app.nec.push_incoming_event(NetworkEvent('update-client-info',
account=self._account,
jid=jid,
resource=resource))
if properties.is_encrypted:
additional_data['encrypted'] = properties.encrypted.additional_data
else:
if properties.eme is not None:
msgtxt = get_eme_message(properties.eme)
displaymarking = None
if properties.has_security_label:
displaymarking = properties.security_label.displaymarking
event_attr = {
'conn': self._con,
'stanza': stanza,
'account': self._account,
'additional_data': additional_data,
'fjid': fjid,
'jid': jid,
'resource': resource,
'stanza_id': stanza_id,
'unique_id': stanza_id or message_id,
'correct_id': parse_correction(properties),
'msgtxt': msgtxt,
'session': session,
'delayed': properties.user_timestamp is not None,
'gc_control': gc_control,
'popup': False,
'msg_log_id': None,
'displaymarking': displaymarking,
'properties': properties,
}
if type_.is_groupchat:
if not msgtxt:
return
event_attr.update({
'room_jid': jid,
})
event = NetworkEvent('gc-message-received', **event_attr)
app.nec.push_incoming_event(event)
# TODO: Some plugins modify msgtxt in the GUI event
self._log_muc_message(event)
return
app.nec.push_incoming_event(
NetworkEvent('decrypted-message-received', **event_attr))
def _message_error_received(self, _con, _stanza, properties):
jid = properties.jid
if not properties.is_muc_pm:
jid = jid.new_as_bare()
self._log.info(properties.error)
app.storage.archive.set_message_error(
app.get_jid_from_account(self._account),
jid,
properties.id,
properties.error)
app.nec.push_incoming_event(
NetworkEvent('message-error',
account=self._account,
jid=jid,
room_jid=jid,
message_id=properties.id,
error=properties.error))
def _log_muc_message(self, event):
self._check_for_mam_compliance(event.room_jid, event.stanza_id)
if (should_log(self._account, event.jid) and
event.msgtxt and event.properties.muc_nickname):
# if not event.nick, it means message comes from room itself
# usually it hold description and can be send at each connection
# so don't store it in logs
app.storage.archive.insert_into_logs(
self._account,
event.jid,
event.properties.timestamp,
KindConstant.GC_MSG,
message=event.msgtxt,
contact_name=event.properties.muc_nickname,
additional_data=event.additional_data,
stanza_id=event.stanza_id,
message_id=event.properties.id)
def _check_for_mam_compliance(self, room_jid, stanza_id):
disco_info = app.storage.cache.get_last_disco_info(room_jid)
if stanza_id is None and disco_info.mam_namespace == Namespace.MAM_2:
self._log.warning('%s announces mam:2 without stanza-id', room_jid)
def _get_unique_id(self, properties):
if properties.is_self_message:
# Deduplicate self message with message-id
return None, properties.id
if properties.stanza_id is None:
return None, None
if properties.type.is_groupchat:
disco_info = app.storage.cache.get_last_disco_info(
properties.jid.bare)
if disco_info.mam_namespace != Namespace.MAM_2:
return None, None
archive = properties.jid
else:
if not self._con.get_module('MAM').available:
return None, None
archive = self._con.get_own_jid()
if archive.bare_match(properties.stanza_id.by):
return properties.stanza_id.id, None
# stanza-id not added by the archive, ignore it.
return None, None
def build_message_stanza(self, message):
own_jid = self._con.get_own_jid()
stanza = nbxmpp.Message(to=message.jid,
body=message.message,
typ=message.type_,
subject=message.subject,
xhtml=message.xhtml)
if message.correct_id:
stanza.setTag('replace', attrs={'id': message.correct_id},
namespace=Namespace.CORRECT)
# XEP-0359
message.message_id = generate_id()
stanza.setID(message.message_id)
stanza.setOriginID(message.message_id)
if message.label:
stanza.addChild(node=message.label.to_node())
# XEP-0172: user_nickname
if message.user_nick:
stanza.setTag('nick', namespace=Namespace.NICK).setData(
message.user_nick)
# XEP-0203
# TODO: Seems delayed is not set anywhere
if message.delayed:
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ',
time.gmtime(message.delayed))
stanza.addChild('delay',
namespace=Namespace.DELAY2,
attrs={'from': str(own_jid), 'stamp': timestamp})
# XEP-0224
if message.attention:
stanza.setTag('attention', namespace=Namespace.ATTENTION)
# XEP-0066
if message.oob_url is not None:
oob = stanza.addChild('x', namespace=Namespace.X_OOB)
oob.addChild('url').setData(message.oob_url)
# XEP-0184
if not own_jid.bare_match(message.jid):
if message.message and not message.is_groupchat:
stanza.setReceiptRequest()
# Mark Message as MUC PM
if message.contact.is_pm_contact:
stanza.setTag('x', namespace=Namespace.MUC_USER)
# XEP-0085
if message.chatstate is not None:
stanza.setTag(message.chatstate, namespace=Namespace.CHATSTATES)
if not message.message:
stanza.setTag('no-store',
namespace=Namespace.MSG_HINTS)
# XEP-0333
if message.message:
stanza.setMarkable()
if message.marker:
marker, id_ = message.marker
stanza.setMarker(marker, id_)
# Add other nodes
if message.nodes is not None:
for node in message.nodes:
stanza.addChild(node=node)
return stanza
def log_message(self, message):
if not message.is_loggable:
return
if not should_log(self._account, message.jid):
return
if message.message is None:
return
app.storage.archive.insert_into_logs(
self._account,
message.jid,
message.timestamp,
message.kind,
message=message.message,
subject=message.subject,
additional_data=message.additional_data,
message_id=message.message_id,
stanza_id=message.message_id)
def get_instance(*args, **kwargs):
return Message(*args, **kwargs), 'Message'

View file

@ -0,0 +1,107 @@
# 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/>.
# XEP-0209: Metacontacts
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import helpers
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class MetaContacts(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.available = False
def get_metacontacts(self):
if not app.settings.get('metacontacts_enabled'):
self._con.connect_machine()
return
self._log.info('Request')
node = nbxmpp.Node('storage', attrs={'xmlns': 'storage:metacontacts'})
iq = nbxmpp.Iq('get', Namespace.PRIVATE, payload=node)
self._con.connection.SendAndCallForResponse(
iq, self._metacontacts_received)
def _metacontacts_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Request error: %s', stanza.getError())
else:
self.available = True
meta_list = self._parse_metacontacts(stanza)
self._log.info('Received: %s', meta_list)
app.nec.push_incoming_event(NetworkEvent(
'metacontacts-received', conn=self._con, meta_list=meta_list))
self._con.connect_machine()
@staticmethod
def _parse_metacontacts(stanza):
meta_list = {}
query = stanza.getQuery()
storage = query.getTag('storage')
metas = storage.getTags('meta')
for meta in metas:
try:
jid = helpers.parse_jid(meta.getAttr('jid'))
except helpers.InvalidFormat:
continue
tag = meta.getAttr('tag')
data = {'jid': jid}
order = meta.getAttr('order')
try:
order = int(order)
except Exception:
order = 0
if order is not None:
data['order'] = order
if tag in meta_list:
meta_list[tag].append(data)
else:
meta_list[tag] = [data]
return meta_list
def store_metacontacts(self, tags_list):
if not app.account_is_available(self._account):
return
iq = nbxmpp.Iq('set', Namespace.PRIVATE)
meta = iq.getQuery().addChild('storage',
namespace='storage:metacontacts')
for tag in tags_list:
for data in tags_list[tag]:
jid = data['jid']
dict_ = {'jid': jid, 'tag': tag}
if 'order' in data:
dict_['order'] = data['order']
meta.addChild(name='meta', attrs=dict_)
self._log.info('Store: %s', tags_list)
self._con.connection.SendAndCallForResponse(
iq, self._store_response_received)
def _store_response_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.info('Store error: %s', stanza.getError())
def get_instance(*args, **kwargs):
return MetaContacts(*args, **kwargs), 'MetaContacts'

View file

@ -0,0 +1,51 @@
# 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/>.
# All XEPs that dont need their own module
import logging
from gajim.common.i18n import get_rfc5646_lang
log = logging.getLogger('gajim.c.m.misc')
# XEP-0066: Out of Band Data
def parse_oob(properties, additional_data):
if not properties.is_oob:
return
additional_data.set_value('gajim', 'oob_url', properties.oob.url)
if properties.oob.desc is not None:
additional_data.set_value('gajim', 'oob_desc',
properties.oob.desc)
# XEP-0308: Last Message Correction
def parse_correction(properties):
if not properties.is_correction:
return None
return properties.correction.id
# XEP-0071: XHTML-IM
def parse_xhtml(properties, additional_data):
if not properties.has_xhtml:
return
body = properties.xhtml.get_body(get_rfc5646_lang())
additional_data.set_value('gajim', 'xhtml', body)

857
gajim/common/modules/muc.py Normal file
View file

@ -0,0 +1,857 @@
# 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/>.
# XEP-0045: Multi-User Chat
# XEP-0249: Direct MUC Invitations
import logging
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.const import InviteType
from nbxmpp.const import PresenceType
from nbxmpp.const import StatusCode
from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from gi.repository import GLib
from gajim.common import app
from gajim.common import helpers
from gajim.common import ged
from gajim.common.const import KindConstant
from gajim.common.const import MUCJoinedState
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import get_default_muc_config
from gajim.common.helpers import to_user_string
from gajim.common.helpers import event_filter
from gajim.common.nec import NetworkEvent
from gajim.common.modules.bits_of_binary import store_bob_data
from gajim.common.modules.base import BaseModule
log = logging.getLogger('gajim.c.m.muc')
class MUC(BaseModule):
_nbxmpp_extends = 'MUC'
_nbxmpp_methods = [
'get_affiliation',
'set_role',
'set_affiliation',
'set_config',
'set_subject',
'cancel_config',
'send_captcha',
'cancel_captcha',
'decline',
'invite',
'request_config',
'request_voice',
'approve_voice_request',
'destroy',
'request_disco_info'
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._on_muc_user_presence,
ns=Namespace.MUC_USER,
priority=49),
StanzaHandler(name='presence',
callback=self._on_error_presence,
typ='error',
priority=49),
StanzaHandler(name='message',
callback=self._on_subject_change,
typ='groupchat',
priority=49),
StanzaHandler(name='message',
callback=self._on_config_change,
ns=Namespace.MUC_USER,
priority=49),
StanzaHandler(name='message',
callback=self._on_invite_or_decline,
typ='normal',
ns=Namespace.MUC_USER,
priority=49),
StanzaHandler(name='message',
callback=self._on_invite_or_decline,
ns=Namespace.CONFERENCE,
priority=49),
StanzaHandler(name='message',
callback=self._on_captcha_challenge,
ns=Namespace.CAPTCHA,
priority=49),
StanzaHandler(name='message',
callback=self._on_voice_request,
ns=Namespace.DATA,
priority=49)
]
self.register_events([
('account-disconnected', ged.CORE, self._on_account_disconnected),
])
self._manager = MUCManager(self._log)
self._rejoin_muc = set()
self._join_timeouts = {}
self._rejoin_timeouts = {}
self._muc_service_jid = None
@property
def supported(self):
return self._muc_service_jid is not None
@property
def service_jid(self):
return self._muc_service_jid
def get_manager(self):
return self._manager
def pass_disco(self, info):
for identity in info.identities:
if identity.category != 'conference':
continue
if identity.type != 'text':
continue
if Namespace.MUC in info.features:
self._log.info('Discovered MUC: %s', info.jid)
self._muc_service_jid = info.jid
raise nbxmpp.NodeProcessed
def join(self, muc_data):
if not app.account_is_available(self._account):
return
self._manager.add(muc_data)
disco_info = app.storage.cache.get_last_disco_info(muc_data.jid,
max_age=60)
if disco_info is None:
self._con.get_module('Discovery').disco_muc(
muc_data.jid,
callback=self._on_disco_result)
else:
self._join(muc_data)
def create(self, muc_data):
if not app.account_is_available(self._account):
return
self._manager.add(muc_data)
self._create(muc_data)
def _on_disco_result(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.info('Disco %s failed: %s', error.jid, error.get_text())
app.nec.push_incoming_event(
NetworkEvent('muc-join-failed',
account=self._account,
room_jid=error.jid.bare,
error=error))
return
muc_data = self._manager.get(result.info.jid)
if muc_data is None:
self._log.warning('MUC Data not found, join aborted')
return
self._join(muc_data)
def _join(self, muc_data):
presence = self._con.get_module('Presence').get_presence(
muc_data.occupant_jid,
show=self._con.status,
status=self._con.status_message)
muc_x = presence.setTag(Namespace.MUC + ' x')
muc_x.setTag('history', {'maxchars': '0'})
if muc_data.password is not None:
muc_x.setTagData('password', muc_data.password)
self._log.info('Join MUC: %s', muc_data.jid)
self._manager.set_state(muc_data.jid, MUCJoinedState.JOINING)
self._con.connection.send(presence)
def _rejoin(self, room_jid):
muc_data = self._manager.get(room_jid)
if muc_data.state == MUCJoinedState.NOT_JOINED:
self._log.info('Rejoin %s', room_jid)
self._join(muc_data)
return True
def _create(self, muc_data):
presence = self._con.get_module('Presence').get_presence(
muc_data.occupant_jid,
show=self._con.status,
status=self._con.status_message)
presence.setTag(Namespace.MUC + ' x')
self._log.info('Create MUC: %s', muc_data.jid)
self._manager.set_state(muc_data.jid, MUCJoinedState.CREATING)
self._con.connection.send(presence)
def leave(self, room_jid, reason=None):
self._log.info('Leave MUC: %s', room_jid)
self._remove_join_timeout(room_jid)
self._remove_rejoin_timeout(room_jid)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
muc_data = self._manager.get(room_jid)
self._con.get_module('Presence').send_presence(
muc_data.occupant_jid,
typ='unavailable',
status=reason,
caps=False)
# We leave a group chat, disable bookmark autojoin
self._con.get_module('Bookmarks').modify(room_jid, autojoin=False)
def configure_room(self, room_jid):
self._nbxmpp('MUC').request_config(room_jid,
callback=self._on_room_config)
def _on_room_config(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.info(error)
app.nec.push_incoming_event(NetworkEvent(
'muc-configuration-failed',
account=self._account,
room_jid=error.jid,
error=error))
return
self._log.info('Configure room: %s', result.jid)
muc_data = self._manager.get(result.jid)
self._apply_config(result.form, muc_data.config)
self.set_config(result.jid,
result.form,
callback=self._on_config_result)
@staticmethod
def _apply_config(form, config=None):
default_config = get_default_muc_config()
if config is not None:
default_config.update(config)
for var, value in default_config.items():
try:
field = form[var]
except KeyError:
pass
else:
field.value = value
def _on_config_result(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.info(error)
app.nec.push_incoming_event(NetworkEvent(
'muc-configuration-failed',
account=self._account,
room_jid=error.jid,
error=error))
return
self._con.get_module('Discovery').disco_muc(
result.jid, callback=self._on_disco_result_after_config)
# If this is an automatic room creation
try:
invites = app.automatic_rooms[self._account][result.jid]['invities']
except KeyError:
return
user_list = {}
for jid in invites:
user_list[jid] = {'affiliation': 'member'}
self.set_affiliation(result.jid, user_list)
for jid in invites:
self.invite(result.jid, jid)
def _on_disco_result_after_config(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.info('Disco %s failed: %s', error.jid, error.get_text())
return
jid = result.info.jid
muc_data = self._manager.get(jid)
self._room_join_complete(muc_data)
self._log.info('Configuration finished: %s', jid)
app.nec.push_incoming_event(NetworkEvent(
'muc-configuration-finished',
account=self._account,
room_jid=jid))
def update_presence(self):
mucs = self._manager.get_mucs_with_state([MUCJoinedState.JOINED,
MUCJoinedState.JOINING])
status, message, idle = self._con.get_presence_state()
for muc_data in mucs:
self._con.get_module('Presence').send_presence(
muc_data.occupant_jid,
show=status,
status=message,
idle_time=idle)
def change_nick(self, room_jid, new_nick):
status, message, _idle = self._con.get_presence_state()
self._con.get_module('Presence').send_presence(
'%s/%s' % (room_jid, new_nick),
show=status,
status=message)
def _on_error_presence(self, _con, _stanza, properties):
room_jid = properties.jid.bare
muc_data = self._manager.get(room_jid)
if muc_data is None:
return
if muc_data.state == MUCJoinedState.JOINING:
if properties.error.condition == 'conflict':
self._remove_rejoin_timeout(room_jid)
muc_data.nick += '_'
self._log.info('Nickname conflict: %s change to %s',
muc_data.jid, muc_data.nick)
self._join(muc_data)
elif properties.error.condition == 'not-authorized':
self._remove_rejoin_timeout(room_jid)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
self._raise_muc_event('muc-password-required', properties)
else:
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
if room_jid not in self._rejoin_muc:
app.nec.push_incoming_event(
NetworkEvent('muc-join-failed',
account=self._account,
room_jid=room_jid,
error=properties.error))
elif muc_data.state == MUCJoinedState.CREATING:
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
app.nec.push_incoming_event(
NetworkEvent('muc-creation-failed',
account=self._account,
room_jid=room_jid,
error=properties.error))
elif muc_data.state == MUCJoinedState.CAPTCHA_REQUEST:
app.nec.push_incoming_event(
NetworkEvent('muc-captcha-error',
account=self._account,
room_jid=room_jid,
error_text=to_user_string(properties.error)))
self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
elif muc_data.state == MUCJoinedState.CAPTCHA_FAILED:
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
else:
self._raise_muc_event('muc-presence-error', properties)
def _on_muc_user_presence(self, _con, stanza, properties):
if properties.type == PresenceType.ERROR:
return
room_jid = str(properties.muc_jid)
if room_jid not in self._manager:
self._log.warning('Presence from unknown MUC')
self._log.warning(stanza)
return
muc_data = self._manager.get(room_jid)
if properties.is_muc_destroyed:
for contact in app.contacts.get_gc_contact_list(
self._account, room_jid):
contact.presence = PresenceType.UNAVAILABLE
self._log.info('MUC destroyed: %s', room_jid)
self._remove_join_timeout(room_jid)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
self._raise_muc_event('muc-destroyed', properties)
return
contact = app.contacts.get_gc_contact(self._account,
room_jid,
properties.muc_nickname)
if properties.is_nickname_changed:
if properties.is_muc_self_presence:
muc_data.nick = properties.muc_user.nick
self._con.get_module('Bookmarks').modify(muc_data.jid,
nick=muc_data.nick)
app.contacts.remove_gc_contact(self._account, contact)
contact.name = properties.muc_user.nick
app.contacts.add_gc_contact(self._account, contact)
initiator = 'Server' if properties.is_nickname_modified else 'User'
self._log.info('%s nickname changed: %s to %s',
initiator,
properties.jid,
properties.muc_user.nick)
self._raise_muc_event('muc-nickname-changed', properties)
return
if contact is None and properties.type.is_available:
self._add_new_muc_contact(properties)
if properties.is_muc_self_presence:
self._log.info('Self presence: %s', properties.jid)
if muc_data.state == MUCJoinedState.JOINING:
if (properties.is_nickname_modified or
muc_data.nick != properties.muc_nickname):
muc_data.nick = properties.muc_nickname
self._log.info('Server modified nickname to: %s',
properties.muc_nickname)
elif muc_data.state == MUCJoinedState.CREATING:
if properties.is_new_room:
self.configure_room(room_jid)
self._start_join_timeout(room_jid)
self._raise_muc_event('muc-self-presence', properties)
else:
self._log.info('User joined: %s', properties.jid)
self._raise_muc_event('muc-user-joined', properties)
return
if properties.is_muc_self_presence and properties.is_kicked:
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
self._raise_muc_event('muc-self-kicked', properties)
status_codes = properties.muc_status_codes or []
if StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
self._start_rejoin_timeout(room_jid)
return
if properties.is_muc_self_presence and properties.type.is_unavailable:
# Its not a kick, so this is the reflection of our own
# unavailable presence, because we left the MUC
return
if properties.type.is_unavailable:
for _event in app.events.get_events(self._account,
jid=str(properties.jid),
types=['pm']):
contact.show = properties.show
contact.presence = properties.type
contact.status = properties.status
contact.affiliation = properties.affiliation
app.interface.handle_event(self._account,
str(properties.jid),
'pm')
# Handle only the first pm event, the rest will be
# handled by the opened ChatControl
break
if contact is None:
# If contact is None, its probably that a user left from a not
# insync MUC, can happen on older servers
self._log.warning('Unknown contact left groupchat: %s',
properties.jid)
else:
# We remove the contact from the MUC, but there could be
# a PrivateChatControl open, so we update the contacts presence
contact.presence = properties.type
app.contacts.remove_gc_contact(self._account, contact)
self._log.info('User %s left', properties.jid)
self._raise_muc_event('muc-user-left', properties)
return
if contact.affiliation != properties.affiliation:
contact.affiliation = properties.affiliation
self._log.info('Affiliation changed: %s %s',
properties.jid,
properties.affiliation)
self._raise_muc_event('muc-user-affiliation-changed', properties)
if contact.role != properties.role:
contact.role = properties.role
self._log.info('Role changed: %s %s',
properties.jid,
properties.role)
self._raise_muc_event('muc-user-role-changed', properties)
if (contact.status != properties.status or
contact.show != properties.show):
contact.status = properties.status
contact.show = properties.show
self._log.info('Show/Status changed: %s %s %s',
properties.jid,
properties.status,
properties.show)
self._raise_muc_event('muc-user-status-show-changed', properties)
def _start_rejoin_timeout(self, room_jid):
self._remove_rejoin_timeout(room_jid)
self._rejoin_muc.add(room_jid)
self._log.info('Start rejoin timeout for: %s', room_jid)
id_ = GLib.timeout_add_seconds(2, self._rejoin, room_jid)
self._rejoin_timeouts[room_jid] = id_
def _remove_rejoin_timeout(self, room_jid):
self._rejoin_muc.discard(room_jid)
id_ = self._rejoin_timeouts.get(room_jid)
if id_ is not None:
self._log.info('Remove rejoin timeout for: %s', room_jid)
GLib.source_remove(id_)
del self._rejoin_timeouts[room_jid]
def _start_join_timeout(self, room_jid):
self._remove_join_timeout(room_jid)
self._log.info('Start join timeout for: %s', room_jid)
id_ = GLib.timeout_add_seconds(
10, self._fake_subject_change, room_jid)
self._join_timeouts[room_jid] = id_
def _remove_join_timeout(self, room_jid):
id_ = self._join_timeouts.get(room_jid)
if id_ is not None:
self._log.info('Remove join timeout for: %s', room_jid)
GLib.source_remove(id_)
del self._join_timeouts[room_jid]
def _raise_muc_event(self, event_name, properties):
app.nec.push_incoming_event(
NetworkEvent(event_name,
account=self._account,
room_jid=properties.jid.bare,
properties=properties))
self._log_muc_event(event_name, properties)
def _log_muc_event(self, event_name, properties):
if event_name not in ['muc-user-joined',
'muc-user-left',
'muc-user-status-show-changed']:
return
if (not app.settings.get('log_contact_status_changes') or
not helpers.should_log(self._account, properties.jid)):
return
additional_data = AdditionalDataDict()
if properties.muc_user is not None:
if properties.muc_user.jid is not None:
additional_data.set_value(
'gajim', 'real_jid', str(properties.muc_user.jid))
# TODO: Refactor
if properties.type == PresenceType.UNAVAILABLE:
show = 'offline'
else:
show = properties.show.value
show = app.storage.archive.convert_show_values_to_db_api_values(show)
app.storage.archive.insert_into_logs(
self._account,
properties.jid.bare,
properties.timestamp,
KindConstant.GCSTATUS,
contact_name=properties.muc_nickname,
message=properties.status or None,
show=show,
additional_data=additional_data)
def _add_new_muc_contact(self, properties):
real_jid = None
if properties.muc_user.jid is not None:
real_jid = str(properties.muc_user.jid)
contact = app.contacts.create_gc_contact(
room_jid=properties.jid.bare,
account=self._account,
name=properties.muc_nickname,
show=properties.show,
status=properties.status,
presence=properties.type,
role=properties.role,
affiliation=properties.affiliation,
jid=real_jid,
avatar_sha=properties.avatar_sha)
app.contacts.add_gc_contact(self._account, contact)
def _on_subject_change(self, _con, _stanza, properties):
if not properties.is_muc_subject:
return
self._handle_subject_change(str(properties.muc_jid),
properties.subject,
properties.muc_nickname,
properties.user_timestamp)
raise nbxmpp.NodeProcessed
def _fake_subject_change(self, room_jid):
# This is for servers which dont send empty subjects as part of the
# event order on joining a MUC. For example jabber.ru
self._log.warning('Fake subject received for %s', room_jid)
del self._join_timeouts[room_jid]
self._handle_subject_change(room_jid, None, None, None)
def _handle_subject_change(self, room_jid, subject, nickname, timestamp):
contact = app.contacts.get_groupchat_contact(self._account, room_jid)
if contact is None:
return
contact.status = subject
app.nec.push_incoming_event(
NetworkEvent('muc-subject',
account=self._account,
room_jid=room_jid,
subject=subject,
nickname=nickname,
user_timestamp=timestamp,
is_fake=subject is None))
muc_data = self._manager.get(room_jid)
if muc_data.state == MUCJoinedState.JOINING:
self._room_join_complete(muc_data)
app.nec.push_incoming_event(
NetworkEvent('muc-joined',
account=self._account,
room_jid=muc_data.jid))
def _room_join_complete(self, muc_data):
self._remove_join_timeout(muc_data.jid)
self._manager.set_state(muc_data.jid, MUCJoinedState.JOINED)
self._remove_rejoin_timeout(muc_data.jid)
# We successfully joined a MUC, set add bookmark with autojoin
self._con.get_module('Bookmarks').add_or_modify(
muc_data.jid,
autojoin=True,
password=muc_data.password,
nick=muc_data.nick)
def _on_voice_request(self, _con, _stanza, properties):
if not properties.is_voice_request:
return
jid = str(properties.jid)
contact = app.contacts.get_groupchat_contact(self._account, jid)
if contact is None:
return
app.nec.push_incoming_event(
NetworkEvent('muc-voice-request',
account=self._account,
room_jid=str(properties.muc_jid),
voice_request=properties.voice_request))
raise nbxmpp.NodeProcessed
def _on_captcha_challenge(self, _con, _stanza, properties):
if not properties.is_captcha_challenge:
return
if properties.is_mam_message:
# Some servers store captcha challenges in MAM, dont process them
self._log.warning('Ignore captcha challenge received from MAM')
raise nbxmpp.NodeProcessed
muc_data = self._manager.get(properties.jid)
if muc_data is None:
return
if muc_data.state != MUCJoinedState.JOINING:
self._log.warning('Received captcha request but state != %s',
MUCJoinedState.JOINING)
return
contact = app.contacts.get_groupchat_contact(self._account,
str(properties.jid))
if contact is None:
return
self._log.info('Captcha challenge received from %s', properties.jid)
store_bob_data(properties.captcha.bob_data)
muc_data.captcha_id = properties.id
self._manager.set_state(properties.jid, MUCJoinedState.CAPTCHA_REQUEST)
self._remove_rejoin_timeout(properties.jid)
app.nec.push_incoming_event(
NetworkEvent('muc-captcha-challenge',
account=self._account,
room_jid=properties.jid.bare,
form=properties.captcha.form))
raise nbxmpp.NodeProcessed
def cancel_captcha(self, room_jid):
muc_data = self._manager.get(room_jid)
if muc_data is None:
return
if muc_data.captcha_id is None:
self._log.warning('No captcha message id available')
return
self._nbxmpp('MUC').cancel_captcha(room_jid, muc_data.captcha_id)
self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
def send_captcha(self, room_jid, form_node):
self._manager.set_state(room_jid, MUCJoinedState.JOINING)
self._nbxmpp('MUC').send_captcha(room_jid,
form_node,
callback=self._on_captcha_result)
def _on_captcha_result(self, task):
try:
task.finish()
except StanzaError as error:
muc_data = self._manager.get(error.jid)
if muc_data is None:
return
self._manager.set_state(error.jid, MUCJoinedState.CAPTCHA_FAILED)
app.nec.push_incoming_event(
NetworkEvent('muc-captcha-error',
account=self._account,
room_jid=str(error.jid),
error_text=to_user_string(error)))
def _on_config_change(self, _con, _stanza, properties):
if not properties.is_muc_config_change:
return
room_jid = str(properties.muc_jid)
self._log.info('Received config change: %s %s',
room_jid, properties.muc_status_codes)
app.nec.push_incoming_event(
NetworkEvent('muc-config-changed',
account=self._account,
room_jid=room_jid,
status_codes=properties.muc_status_codes))
raise nbxmpp.NodeProcessed
def _on_invite_or_decline(self, _con, _stanza, properties):
if properties.muc_decline is not None:
data = properties.muc_decline
if helpers.ignore_contact(self._account, data.from_):
raise nbxmpp.NodeProcessed
self._log.info('Invite declined from: %s, reason: %s',
data.from_, data.reason)
app.nec.push_incoming_event(
NetworkEvent('muc-decline',
account=self._account,
**data._asdict()))
raise nbxmpp.NodeProcessed
if properties.muc_invite is not None:
data = properties.muc_invite
if helpers.ignore_contact(self._account, data.from_):
raise nbxmpp.NodeProcessed
self._log.info('Invite from: %s, to: %s', data.from_, data.muc)
if app.in_groupchat(self._account, data.muc):
# We are already in groupchat. Ignore invitation
self._log.info('We are already in this room')
raise nbxmpp.NodeProcessed
self._con.get_module('Discovery').disco_muc(
data.muc,
request_vcard=True,
callback=self._on_disco_result_after_invite,
user_data=data)
raise nbxmpp.NodeProcessed
def _on_disco_result_after_invite(self, task):
try:
result = task.finish()
except StanzaError as error:
self._log.warning(error)
return
invite_data = task.get_user_data()
app.nec.push_incoming_event(
NetworkEvent('muc-invitation',
account=self._account,
info=result.info,
**invite_data._asdict()))
def invite(self, room, to, reason=None, continue_=False):
type_ = InviteType.MEDIATED
contact = app.contacts.get_contact_from_full_jid(self._account, to)
if contact and contact.supports(Namespace.CONFERENCE):
type_ = InviteType.DIRECT
password = app.gc_passwords.get(room, None)
self._log.info('Invite %s to %s', to, room)
return self._nbxmpp('MUC').invite(room, to, reason, password,
continue_, type_)
@event_filter(['account'])
def _on_account_disconnected(self, _event):
for room_jid in list(self._rejoin_timeouts.keys()):
self._remove_rejoin_timeout(room_jid)
for room_jid in list(self._join_timeouts.keys()):
self._remove_join_timeout(room_jid)
class MUCManager:
def __init__(self, logger):
self._log = logger
self._mucs = {}
def add(self, muc):
self._mucs[muc.jid] = muc
def remove(self, muc):
self._mucs.pop(muc.jid, None)
def get(self, room_jid):
return self._mucs.get(room_jid)
def set_state(self, room_jid, state):
muc = self._mucs.get(room_jid)
if muc is not None:
if muc.state == state:
return
self._log.info('Set MUC state: %s %s', room_jid, state)
muc.state = state
def get_joined_mucs(self):
mucs = self._mucs.values()
return [muc.jid for muc in mucs if muc.state == MUCJoinedState.JOINED]
def get_mucs_with_state(self, states):
return [muc for muc in self._mucs.values() if muc.state in states]
def reset_state(self):
for muc in self._mucs.values():
self.set_state(muc.jid, MUCJoinedState.NOT_JOINED)
def __contains__(self, room_jid):
return room_jid in self._mucs
def get_instance(*args, **kwargs):
return MUC(*args, **kwargs), 'MUC'

View file

@ -0,0 +1,39 @@
# 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/>.
# XEP-0163: Personal Eventing Protocol
from typing import Any
from typing import Tuple
from gajim.common.types import ConnectionT
from gajim.common.modules.base import BaseModule
class PEP(BaseModule):
def __init__(self, con: ConnectionT) -> None:
BaseModule.__init__(self, con)
self.supported = False
def pass_disco(self, info):
for identity in info.identities:
if identity.category == 'pubsub':
if identity.type == 'pep':
self._log.info('Discovered PEP support: %s', info.jid)
self.supported = True
def get_instance(*args: Any, **kwargs: Any) -> Tuple[PEP, str]:
return PEP(*args, **kwargs), 'PEP'

View file

@ -0,0 +1,82 @@
# 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/>.
# XEP-0199: XMPP Ping
from typing import Any
from typing import Tuple
from typing import Generator
import time
from nbxmpp.errors import is_error
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.types import ConnectionT
from gajim.common.types import ContactsT
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import as_task
class Ping(BaseModule):
_nbxmpp_extends = 'Ping'
_nbxmpp_methods = [
'ping',
]
def __init__(self, con: ConnectionT) -> None:
BaseModule.__init__(self, con)
self.handlers = []
@as_task
def send_ping(self, contact: ContactsT) -> Generator:
_task = yield
if not app.account_is_available(self._account):
return
jid = contact.get_full_jid()
self._log.info('Send ping to %s', jid)
app.nec.push_incoming_event(NetworkEvent('ping-sent',
account=self._account,
contact=contact))
ping_time = time.time()
response = yield self.ping(jid, timeout=10)
if is_error(response):
app.nec.push_incoming_event(NetworkEvent(
'ping-error',
account=self._account,
contact=contact,
error=str(response)))
return
diff = round(time.time() - ping_time, 2)
self._log.info('Received pong from %s after %s seconds',
response.jid, diff)
app.nec.push_incoming_event(NetworkEvent('ping-reply',
account=self._account,
contact=contact,
seconds=diff))
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Ping, str]:
return Ping(*args, **kwargs), 'Ping'

View file

@ -0,0 +1,394 @@
# 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/>.
# Presence handler
import time
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import PresenceType
from gajim.common import app
from gajim.common import idle
from gajim.common.i18n import _
from gajim.common.nec import NetworkEvent
from gajim.common.helpers import should_log
from gajim.common.const import KindConstant
from gajim.common.const import ShowConstant
from gajim.common.modules.base import BaseModule
class Presence(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._presence_received,
priority=50),
StanzaHandler(name='presence',
callback=self._subscribe_received,
typ='subscribe',
priority=49),
StanzaHandler(name='presence',
callback=self._subscribed_received,
typ='subscribed',
priority=49),
StanzaHandler(name='presence',
callback=self._unsubscribe_received,
typ='unsubscribe',
priority=49),
StanzaHandler(name='presence',
callback=self._unsubscribed_received,
typ='unsubscribed',
priority=49),
]
# keep the jids we auto added (transports contacts) to not send the
# SUBSCRIBED event to GUI
self.automatically_added = []
# list of jid to auto-authorize
self._jids_for_auto_auth = set()
def _presence_received(self, _con, stanza, properties):
if properties.from_muc:
# MUC occupant presences are already handled in MUC module
return
muc = self._con.get_module('MUC').get_manager().get(properties.jid)
if muc is not None:
# Presence from the MUC itself, used for MUC avatar
# handled in VCardAvatars module
return
self._log.info('Received from %s', properties.jid)
if properties.type == PresenceType.ERROR:
self._log.info('Error: %s %s', properties.jid, properties.error)
return
if self._account == 'Local':
app.nec.push_incoming_event(
NetworkEvent('raw-pres-received',
conn=self._con,
stanza=stanza))
return
if properties.is_self_presence:
app.nec.push_incoming_event(
NetworkEvent('our-show',
account=self._account,
show=properties.show.value))
return
jid = properties.jid.bare
roster_item = self._con.get_module('Roster').get_item(jid)
if not properties.is_self_bare and roster_item is None:
# Handle only presence from roster contacts
self._log.warning('Unknown presence received')
self._log.warning(stanza)
return
show = properties.show.value
if properties.type.is_unavailable:
show = 'offline'
event_attrs = {
'conn': self._con,
'stanza': stanza,
'prio': properties.priority,
'need_add_in_roster': False,
'popup': False,
'ptype': properties.type.value,
'jid': properties.jid.bare,
'resource': properties.jid.resource,
'id_': properties.id,
'fjid': str(properties.jid),
'timestamp': properties.timestamp,
'avatar_sha': properties.avatar_sha,
'user_nick': properties.nickname,
'idle_time': properties.idle_timestamp,
'show': show,
'new_show': show,
'old_show': 0,
'status': properties.status,
'contact_list': [],
'contact': None,
}
event_ = NetworkEvent('presence-received', **event_attrs)
# TODO: Refactor
self._update_contact(event_, properties)
app.nec.push_incoming_event(event_)
def _update_contact(self, event, properties):
# Note: A similar method also exists in connection_zeroconf
jid = properties.jid.bare
resource = properties.jid.resource
status_strings = ['offline', 'error', 'online', 'chat', 'away',
'xa', 'dnd']
event.new_show = status_strings.index(event.show)
# Update contact
contact_list = app.contacts.get_contacts(self._account, jid)
if not contact_list:
self._log.warning('No contact found')
return
event.contact_list = contact_list
contact = app.contacts.get_contact_strict(self._account,
properties.jid.bare,
properties.jid.resource)
if contact is None:
contact = app.contacts.get_first_contact_from_jid(self._account,
jid)
if contact is None:
self._log.warning('First contact not found')
return
if (self._is_resource_known(contact_list) and
not app.jid_is_transport(jid)):
# Another resource of an existing contact connected
# Add new contact
event.old_show = 0
contact = app.contacts.copy_contact(contact)
contact.resource = resource
app.contacts.add_contact(self._account, contact)
else:
# Convert the initial roster contact to a contact with resource
contact.resource = resource
event.old_show = 0
if contact.show in status_strings:
event.old_show = status_strings.index(contact.show)
event.need_add_in_roster = True
elif contact.show in status_strings:
event.old_show = status_strings.index(contact.show)
# Update contact with presence data
contact.show = event.show
contact.status = properties.status
contact.priority = properties.priority
contact.idle_time = properties.idle_timestamp
event.contact = contact
if not app.jid_is_transport(jid) and len(contact_list) == 1:
# It's not an agent
if event.old_show == 0 and event.new_show > 1:
if not jid in app.newly_added[self._account]:
app.newly_added[self._account].append(jid)
if jid in app.to_be_removed[self._account]:
app.to_be_removed[self._account].remove(jid)
elif event.old_show > 1 and event.new_show == 0 and \
self._con.state.is_available:
if not jid in app.to_be_removed[self._account]:
app.to_be_removed[self._account].append(jid)
if jid in app.newly_added[self._account]:
app.newly_added[self._account].remove(jid)
if app.jid_is_transport(jid):
return
if properties.type.is_unavailable:
# TODO: This causes problems when another
# resource signs off!
self._con.get_module('Bytestream').stop_all_active_file_transfers(
contact)
self._log_presence(properties)
@staticmethod
def _is_resource_known(contact_list):
if len(contact_list) > 1:
return True
if contact_list[0].resource == '':
return False
return contact_list[0].show not in ('not in roster', 'offline')
def _log_presence(self, properties):
if not app.settings.get('log_contact_status_changes'):
return
if not should_log(self._account, properties.jid.bare):
return
show = ShowConstant[properties.show.name]
if properties.type.is_unavailable:
show = ShowConstant.OFFLINE
app.storage.archive.insert_into_logs(self._account,
properties.jid.bare,
time.time(),
KindConstant.STATUS,
message=properties.status,
show=show)
def _subscribe_received(self, _con, _stanza, properties):
jid = properties.jid.bare
fjid = str(properties.jid)
is_transport = app.jid_is_transport(fjid)
auto_auth = app.settings.get_account_setting(self._account, 'autoauth')
self._log.info('Received Subscribe: %s, transport: %s, '
'auto_auth: %s, user_nick: %s',
properties.jid, is_transport,
auto_auth, properties.nickname)
if auto_auth or jid in self._jids_for_auto_auth:
self.send_presence(fjid, 'subscribed')
self._jids_for_auto_auth.discard(jid)
self._log.info('Auto respond with subscribed: %s', jid)
return
status = (properties.status or
_('I would like to add you to my roster.'))
app.nec.push_incoming_event(NetworkEvent(
'subscribe-presence-received',
conn=self._con,
jid=jid,
fjid=fjid,
status=status,
user_nick=properties.nickname,
is_transport=is_transport))
raise nbxmpp.NodeProcessed
def _subscribed_received(self, _con, _stanza, properties):
jid = properties.jid.bare
self._log.info('Received Subscribed: %s', properties.jid)
if jid in self.automatically_added:
self.automatically_added.remove(jid)
raise nbxmpp.NodeProcessed
app.nec.push_incoming_event(NetworkEvent(
'subscribed-presence-received',
account=self._account,
jid=properties.jid))
raise nbxmpp.NodeProcessed
def _unsubscribe_received(self, _con, _stanza, properties):
self._log.info('Received Unsubscribe: %s', properties.jid)
raise nbxmpp.NodeProcessed
def _unsubscribed_received(self, _con, _stanza, properties):
self._log.info('Received Unsubscribed: %s', properties.jid)
app.nec.push_incoming_event(NetworkEvent(
'unsubscribed-presence-received',
conn=self._con, jid=properties.jid.bare))
raise nbxmpp.NodeProcessed
def subscribed(self, jid):
if not app.account_is_available(self._account):
return
self._log.info('Subscribed: %s', jid)
self.send_presence(jid, 'subscribed')
def unsubscribed(self, jid):
if not app.account_is_available(self._account):
return
self._log.info('Unsubscribed: %s', jid)
self._jids_for_auto_auth.discard(jid)
self.send_presence(jid, 'unsubscribed')
def unsubscribe(self, jid, remove_auth=True):
if not app.account_is_available(self._account):
return
if remove_auth:
self._con.get_module('Roster').del_item(jid)
else:
self._log.info('Unsubscribe from %s', jid)
self._jids_for_auto_auth.discard(jid)
self._con.get_module('Roster').unsubscribe(jid)
self._con.get_module('Roster').set_item(jid)
def subscribe(self, jid, msg=None, name='', groups=None, auto_auth=False):
if not app.account_is_available(self._account):
return
if groups is None:
groups = []
self._log.info('Request Subscription to %s', jid)
if auto_auth:
self._jids_for_auto_auth.add(jid)
infos = {'jid': jid}
if name:
infos['name'] = name
iq = nbxmpp.Iq('set', Namespace.ROSTER)
query = iq.setQuery()
item = query.addChild('item', attrs=infos)
for group in groups:
item.addChild('group').setData(group)
self._con.connection.send(iq)
self.send_presence(jid,
'subscribe',
status=msg,
nick=app.nicks[self._account])
def get_presence(self, to=None, typ=None, priority=None,
show=None, status=None, nick=None, caps=True,
idle_time=False):
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=Namespace.NICK)
nick_tag.setData(nick)
if (idle_time and
app.is_installed('IDLE') and
app.settings.get('autoaway')):
idle_sec = idle.Monitor.get_idle_sec()
time_ = time.strftime('%Y-%m-%dT%H:%M:%SZ',
time.gmtime(time.time() - idle_sec))
idle_node = presence.setTag('idle', namespace=Namespace.IDLE)
idle_node.setAttr('since', time_)
caps = self._con.get_module('Caps').caps
if caps is not None and typ != 'unavailable':
presence.setTag('c',
namespace=Namespace.CAPS,
attrs=caps._asdict())
return presence
def send_presence(self, *args, **kwargs):
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)
def get_instance(*args, **kwargs):
return Presence(*args, **kwargs), 'Presence'

View file

@ -0,0 +1,85 @@
# Copyright (C) 2006 Tomasz Melcer <liori AT exroot.org>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
# Copyright (C) 2008 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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/>.
# XEP-0060: Publish-Subscribe
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.modules.base import BaseModule
class PubSub(BaseModule):
_nbxmpp_extends = 'PubSub'
_nbxmpp_methods = [
'publish',
'delete',
'set_node_configuration',
'get_node_configuration',
'get_access_model',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.publish_options = False
def pass_disco(self, info):
if Namespace.PUBSUB_PUBLISH_OPTIONS in info.features:
self._log.info('Discovered Pubsub publish options: %s', info.jid)
self.publish_options = True
def send_pb_subscription_query(self, jid, cb, **kwargs):
if not app.account_is_available(self._account):
return
query = nbxmpp.Iq('get', to=jid)
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
pb.addChild('subscriptions')
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
def send_pb_subscribe(self, jid, node, cb, **kwargs):
if not app.account_is_available(self._account):
return
our_jid = app.get_jid_from_account(self._account)
query = nbxmpp.Iq('set', to=jid)
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
pb.addChild('subscribe', {'node': node, 'jid': our_jid})
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
def send_pb_unsubscribe(self, jid, node, cb, **kwargs):
if not app.account_is_available(self._account):
return
our_jid = app.get_jid_from_account(self._account)
query = nbxmpp.Iq('set', to=jid)
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
pb.addChild('unsubscribe', {'node': node, 'jid': our_jid})
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
def get_instance(*args, **kwargs):
return PubSub(*args, **kwargs), 'PubSub'

View file

@ -0,0 +1,112 @@
# 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/>.
# XEP-0184: Message Delivery Receipts
import nbxmpp
from nbxmpp.structs import StanzaHandler
from nbxmpp.namespaces import Namespace
from nbxmpp.modules.receipts import build_receipt
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
class Receipts(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='message',
callback=self._process_message_receipt,
ns=Namespace.RECEIPTS,
priority=46),
]
def _process_message_receipt(self, _con, stanza, properties):
if not properties.is_receipt:
return
if properties.type.is_error:
if properties.receipt.is_request:
return
# Don't propagate this event further
raise nbxmpp.NodeProcessed
if (properties.type.is_groupchat or
properties.is_self_message or
properties.is_mam_message or
properties.is_carbon_message and properties.carbon.is_sent):
if properties.receipt.is_received:
# Don't propagate this event further
raise nbxmpp.NodeProcessed
return
if properties.receipt.is_request:
if not app.settings.get_account_setting(self._account,
'answer_receipts'):
return
if properties.eme is not None:
# Don't send receipt for message which couldn't be decrypted
if not properties.is_encrypted:
return
contact = self._get_contact(properties)
if contact is None:
return
self._log.info('Send receipt: %s', properties.jid)
self._con.connection.send(build_receipt(stanza))
return
if properties.receipt.is_received:
self._log.info('Receipt from %s %s',
properties.jid,
properties.receipt.id)
jid = properties.jid
if not properties.is_muc_pm:
jid = jid.new_as_bare()
app.storage.archive.set_marker(
app.get_jid_from_account(self._account),
jid,
properties.receipt.id,
'received')
app.nec.push_incoming_event(
NetworkEvent('receipt-received',
account=self._account,
jid=jid,
receipt_id=properties.receipt.id))
raise nbxmpp.NodeProcessed
def _get_contact(self, properties):
if properties.is_muc_pm:
return app.contacts.get_gc_contact(self._account,
properties.jid.bare,
properties.jid.resource)
contact = app.contacts.get_contact(self._account,
properties.jid.bare)
if contact is not None and contact.sub not in ('to', 'none'):
return contact
return None
def get_instance(*args, **kwargs):
return Receipts(*args, **kwargs), 'Receipts'

View file

@ -0,0 +1,44 @@
# 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/>.
# XEP-0077: In-Band Registration
from nbxmpp.namespaces import Namespace
from gajim.common.modules.base import BaseModule
class Register(BaseModule):
_nbxmpp_extends = 'Register'
_nbxmpp_methods = [
'unregister',
'change_password',
'change_password_with_form',
'request_register_form',
'submit_register_form',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.supported = False
def pass_disco(self, info):
self.supported = Namespace.REGISTER in info.features
def get_instance(*args, **kwargs):
return Register(*args, **kwargs), 'Register'

View file

@ -0,0 +1,379 @@
# 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/>.
# Roster
from collections import namedtuple
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
RosterItem = namedtuple('RosterItem', 'jid data')
class Roster(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self._roster_push_received,
typ='set',
ns=Namespace.ROSTER),
StanzaHandler(name='presence',
callback=self._presence_received),
]
self._data = {}
self._set = None
def load_roster(self):
self._log.info('Load from database')
data = app.storage.cache.load_roster(self._account)
if data:
self.set_raw(data)
for jid, item in self._data.items():
self._log.debug('%s: %s', jid, item)
app.nec.push_incoming_event(NetworkEvent(
'roster-info',
conn=self._con,
jid=jid,
nickname=item['name'],
sub=item['subscription'],
ask=item['ask'],
groups=item['groups'],
avatar_sha=item.get('avatar_sha')))
else:
self._log.info('Database empty, reset roster version')
app.settings.set_account_setting(
self._account, 'roster_version', '')
app.nec.push_incoming_event(NetworkEvent(
'roster-received',
conn=self._con,
roster=self._data.copy(),
received_from_server=False))
def _store_roster(self):
app.storage.cache.store_roster(self._account, self._data)
def request_roster(self):
version = None
features = self._con.connection.features
if features.has_roster_version:
version = app.settings.get_account_setting(self._account,
'roster_version')
self._log.info('Requested from server')
iq = nbxmpp.Iq('get', Namespace.ROSTER)
if version is not None:
iq.setTagAttr('query', 'ver', version)
self._log.info('Request version: %s', version)
self._con.connection.SendAndCallForResponse(
iq, self._roster_received)
def _roster_received(self, _nbxmpp_client, stanza):
if not nbxmpp.isResultNode(stanza):
self._log.warning('Unable to retrieve roster: %s',
stanza.getError())
else:
self._log.info('Received Roster')
received_from_server = False
if stanza.getTag('query') is not None:
# clear Roster
self._data = {}
version = self._parse_roster(stanza)
self._log.info('New version: %s', version)
self._store_roster()
app.settings.set_account_setting(self._account,
'roster_version',
version)
received_from_server = True
app.nec.push_incoming_event(NetworkEvent(
'roster-received',
conn=self._con,
roster=self._data.copy(),
received_from_server=received_from_server))
self._con.connect_machine()
def _roster_push_received(self, _con, stanza, _properties):
self._log.info('Push received')
sender = stanza.getFrom()
if sender is not None:
if not self._con.get_own_jid().bare_match(sender):
self._log.warning('Wrong JID %s', stanza.getFrom())
return
push_items, version = self._parse_push(stanza)
self._ack_roster_push(stanza)
for item in push_items:
attrs = item.data
app.nec.push_incoming_event(NetworkEvent(
'roster-info',
conn=self._con,
jid=item.jid,
nickname=attrs['name'],
sub=attrs['subscription'],
ask=attrs['ask'],
groups=attrs['groups'],
avatar_sha=None))
self._store_roster()
self._log.info('New version: %s', version)
app.settings.set_account_setting(self._account,
'roster_version',
version)
raise nbxmpp.NodeProcessed
def _parse_roster(self, stanza):
query = stanza.getTag('query')
version = query.getAttr('ver')
for item in query.getTags('item'):
jid = item.getAttr('jid')
self._data[jid] = self._get_item_attrs(item)
self._log.info('Item %s: %s', jid, self._data[jid])
return version
@staticmethod
def _get_item_attrs(item, update=False):
'''
update: True
Omit avatar_sha from the returned attrs
update: False
Include the default value from avatar_sha in the returned attrs
'''
default_attrs = {'name': None,
'ask': None,
'subscription': None,
'groups': []}
if not update:
default_attrs['avatar_sha'] = None
attrs = item.getAttrs()
del attrs['jid']
groups = {group.getData() for group in item.getTags('group')}
attrs['groups'] = list(groups)
default_attrs.update(attrs)
return default_attrs
def _parse_push(self, stanza):
query = stanza.getTag('query')
version = query.getAttr('ver')
push_items = []
for item in query.getTags('item'):
push_items.append(self._update_roster_item(item))
for item in push_items:
self._log.info('Push: %s', item)
return push_items, version
def _update_roster_item(self, item):
jid = item.getAttr('jid')
if item.getAttr('subscription') == 'remove':
self._data.pop(jid, None)
attrs = self._get_item_attrs(item)
return RosterItem(jid, attrs)
if jid not in self._data:
self._data[jid] = self._get_item_attrs(item)
else:
self._data[jid].update(self._get_item_attrs(item, update=True))
return RosterItem(jid, self._data[jid])
def _ack_roster_push(self, stanza):
iq = nbxmpp.Iq('result',
to=stanza.getFrom(),
frm=stanza.getTo(),
attrs={'id': stanza.getID()})
self._con.connection.send(iq)
def _presence_received(self, _con, pres, _properties):
'''
Add contacts that request subscription to our internal
roster and also to the database. The contact is put into the
'Not in contact list' group and because we save it to the database
it is also after a restart available.
'''
if pres.getType() != 'subscribe':
return
jid = pres.getFrom().bare
if jid in self._data:
return
self._log.info('Add Contact from presence %s', jid)
self._data[jid] = {'name': None,
'ask': None,
'subscription':
'none',
'groups': ['Not in contact list']}
self._store_roster()
def _get_item_data(self, jid, dataname):
"""
Return specific jid's representation in internal format.
"""
jid = jid[:(jid + '/').find('/')]
return self._data[jid][dataname]
def del_item(self, jid):
"""
Delete contact 'jid' from roster
"""
self._con.connection.send(
nbxmpp.Iq('set', Namespace.ROSTER, payload=[
nbxmpp.Node('item', {'jid': jid, 'subscription': 'remove'})]))
def get_groups(self, jid):
"""
Return groups list that contact 'jid' belongs to
"""
return self._get_item_data(jid, 'groups')
def get_name(self, jid):
"""
Return name of contact 'jid'
"""
return self._get_item_data(jid, 'name')
def update_contact(self, jid, name, groups):
if app.account_is_available(self._account):
self.set_item(jid=jid, name=name, groups=groups)
def update_contacts(self, contacts):
"""
Update multiple roster items
"""
if app.account_is_available(self._account):
self.set_item_multi(contacts)
def set_item(self, jid, name=None, groups=None):
"""
Rename contact 'jid' and sets the groups list that it now belongs to
"""
iq = nbxmpp.Iq('set', Namespace.ROSTER)
query = iq.getTag('query')
attrs = {'jid': jid}
if name:
attrs['name'] = name
item = query.setTag('item', attrs)
if groups is not None:
for group in groups:
item.addChild(node=nbxmpp.Node('group', payload=[group]))
self._con.connection.send(iq)
def set_item_multi(self, items):
"""
Rename multiple contacts and sets their group lists
"""
for i in items:
iq = nbxmpp.Iq('set', Namespace.ROSTER)
query = iq.getTag('query')
attrs = {'jid': i['jid']}
if i['name']:
attrs['name'] = i['name']
item = query.setTag('item', attrs)
for group in i['groups']:
item.addChild(node=nbxmpp.Node('group', payload=[group]))
self._con.connection.send(iq)
def get_items(self):
"""
Return list of all [bare] JIDs that the roster is currently tracks
"""
return list(self._data.keys())
def keys(self):
"""
Same as get_items. Provided for the sake of dictionary interface
"""
return list(self._data.keys())
def __getitem__(self, item):
"""
Get the contact in the internal format.
Raises KeyError if JID 'item' is not in roster
"""
return self._data[item]
def get_item(self, item):
"""
Get the contact in the internal format (or None if JID 'item' is not in
roster)
"""
if item in self._data:
return self._data[item]
return None
def unsubscribe(self, jid):
"""
Ask for removing our subscription for JID 'jid'
"""
self._con.connection.send(nbxmpp.Presence(jid, 'unsubscribe'))
def get_raw(self):
"""
Return the internal data representation of the roster
"""
return self._data
def set_raw(self, data):
"""
Set the internal data representation of the roster
"""
own_jid = self._con.get_own_jid().bare
self._data = data
self._data[own_jid] = {
'resources': {},
'name': None,
'ask': None,
'subscription': None,
'groups': None,
'avatar_sha': None
}
def set_avatar_sha(self, jid, sha):
if jid not in self._data:
return
self._data[jid]['avatar_sha'] = sha
self._store_roster()
def get_instance(*args, **kwargs):
return Roster(*args, **kwargs), 'Roster'

View file

@ -0,0 +1,130 @@
# 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/>.
# XEP-0144: Roster Item Exchange
import nbxmpp
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import _
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.modules.base import BaseModule
class RosterItemExchange(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='iq',
callback=self.received_item,
typ='set',
ns=Namespace.ROSTERX),
StanzaHandler(name='message',
callback=self.received_item,
ns=Namespace.ROSTERX),
]
def received_item(self, _con, stanza, _properties):
# stanza can be a message or a iq
self._log.info('Received roster items from %s', stanza.getFrom())
exchange_items_list = {}
items_list = stanza.getTag(
'x', namespace=Namespace.ROSTERX).getChildren()
if items_list is None:
raise nbxmpp.NodeProcessed
action = items_list[0].getAttr('action')
if not action:
action = 'add'
for item in items_list:
try:
jid = helpers.parse_jid(item.getAttr('jid'))
except helpers.InvalidFormat:
self._log.warning('Invalid JID: %s, ignoring it',
item.getAttr('jid'))
continue
name = item.getAttr('name')
contact = app.contacts.get_contact(self._account, jid)
groups = []
same_groups = True
for group in item.getTags('group'):
groups.append(group.getData())
# check that all suggested groups are in the groups we have for
# this contact
if not contact or group not in contact.groups:
same_groups = False
if contact:
# check that all groups we have for this contact are in the
# suggested groups
for group in contact.groups:
if group not in groups:
same_groups = False
if contact.sub in ('both', 'to') and same_groups:
continue
exchange_items_list[jid] = [name, groups]
if not exchange_items_list:
raise nbxmpp.NodeProcessed
self._log.info('Items: %s', exchange_items_list)
app.nec.push_incoming_event(RosterItemExchangeEvent(
None, conn=self._con,
fjid=str(stanza.getFrom()),
exchange_items_list=exchange_items_list,
action=action))
raise nbxmpp.NodeProcessed
def send_contacts(self, contacts, fjid, type_='message'):
if not app.account_is_available(self._account):
return
if type_ == 'message':
if len(contacts) == 1:
msg = _('Sent contact: "%(jid)s" (%(name)s)') % {
'jid': contacts[0].get_full_jid(),
'name': contacts[0].get_shown_name()}
else:
msg = _('Sent contacts:')
for contact in contacts:
msg += '\n "%s" (%s)' % (contact.get_full_jid(),
contact.get_shown_name())
stanza = nbxmpp.Message(to=app.get_jid_without_resource(fjid),
body=msg)
elif type_ == 'iq':
stanza = nbxmpp.Iq(to=fjid, typ='set')
xdata = stanza.addChild(name='x', namespace=Namespace.ROSTERX)
for contact in contacts:
name = contact.get_shown_name()
xdata.addChild(name='item', attrs={'action': 'add',
'jid': contact.jid,
'name': name})
self._log.info('Send contact: %s %s', contact.jid, name)
self._con.connection.send(stanza)
class RosterItemExchangeEvent(NetworkIncomingEvent):
name = 'roster-item-exchange-received'
def get_instance(*args, **kwargs):
return RosterItemExchange(*args, **kwargs), 'RosterItemExchange'

View file

@ -0,0 +1,111 @@
# 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/>.
# XEP-0055: Jabber Search
import nbxmpp
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.modules.base import BaseModule
class Search(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
def request_search_fields(self, jid):
self._log.info('Request search fields from %s', jid)
iq = nbxmpp.Iq(typ='get', to=jid, queryNS=Namespace.SEARCH)
self._con.connection.SendAndCallForResponse(iq, self._fields_received)
def _fields_received(self, _nbxmpp_client, stanza):
data = None
is_dataform = False
if nbxmpp.isResultNode(stanza):
self._log.info('Received search fields from %s', stanza.getFrom())
tag = stanza.getTag('query', namespace=Namespace.SEARCH)
if tag is None:
self._log.info('Invalid stanza: %s', stanza)
return
data = tag.getTag('x', namespace=Namespace.DATA)
if data is not None:
is_dataform = True
else:
data = {}
for i in stanza.getQueryPayload():
data[i.getName()] = i.getData()
else:
self._log.info('Error: %s', stanza.getError())
app.nec.push_incoming_event(
SearchFormReceivedEvent(None, conn=self._con,
is_dataform=is_dataform,
data=data))
def send_search_form(self, jid, form, is_form):
iq = nbxmpp.Iq(typ='set', to=jid, queryNS=Namespace.SEARCH)
item = iq.setQuery()
if is_form:
item.addChild(node=form)
else:
for i in form.keys():
item.setTagData(i, form[i])
self._con.connection.SendAndCallForResponse(iq, self._received_result)
def _received_result(self, _nbxmpp_client, stanza):
data = None
is_dataform = False
if nbxmpp.isResultNode(stanza):
self._log.info('Received result from %s', stanza.getFrom())
tag = stanza.getTag('query', namespace=Namespace.SEARCH)
if tag is None:
self._log.info('Invalid stanza: %s', stanza)
return
data = tag.getTag('x', namespace=Namespace.DATA)
if data is not None:
is_dataform = True
else:
data = []
for item in tag.getTags('item'):
# We also show attributes. jid is there
field = item.attrs
for i in item.getPayload():
field[i.getName()] = i.getData()
data.append(field)
else:
self._log.info('Error: %s', stanza.getError())
app.nec.push_incoming_event(
SearchResultReceivedEvent(None, conn=self._con,
is_dataform=is_dataform,
data=data))
class SearchFormReceivedEvent(NetworkIncomingEvent):
name = 'search-form-received'
class SearchResultReceivedEvent(NetworkIncomingEvent):
name = 'search-result-received'
def get_instance(*args, **kwargs):
return Search(*args, **kwargs), 'Search'

View file

@ -0,0 +1,70 @@
# 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/>.
# XEP-0258: Security Labels in XMPP
from nbxmpp.namespaces import Namespace
from nbxmpp.errors import is_error
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import as_task
class SecLabels(BaseModule):
_nbxmpp_extends = 'SecurityLabels'
_nbxmpp_methods = [
'request_catalog',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._catalogs = {}
self.supported = False
def pass_disco(self, info):
if Namespace.SECLABEL not in info.features:
return
self.supported = True
self._log.info('Discovered security labels: %s', info.jid)
@as_task
def request_catalog(self, jid):
_task = yield
catalog = yield self._nbxmpp('SecurityLabels').request_catalog(jid)
if is_error(catalog):
self._log.info(catalog)
return
self._catalogs[jid] = catalog
self._log.info('Received catalog: %s', jid)
app.nec.push_incoming_event(NetworkEvent('sec-catalog-received',
account=self._account,
jid=jid,
catalog=catalog))
def get_catalog(self, jid):
return self._catalogs.get(jid)
def get_instance(*args, **kwargs):
return SecLabels(*args, **kwargs), 'SecLabels'

View file

@ -0,0 +1,46 @@
# 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/>.
# XEP-0092: Software Version
from gajim.common import app
from gajim.common.helpers import get_os_info
from gajim.common.modules.base import BaseModule
class SoftwareVersion(BaseModule):
_nbxmpp_extends = 'SoftwareVersion'
_nbxmpp_methods = [
'set_software_version',
'request_software_version',
'disable',
]
def __init__(self, con):
BaseModule.__init__(self, con)
def set_enabled(self, enabled):
if enabled:
if not app.settings.get_account_setting(self._account,
'send_os_info'):
return
self._nbxmpp('SoftwareVersion').set_software_version(
'Gajim', app.version, get_os_info())
else:
self._nbxmpp('SoftwareVersion').disable()
def get_instance(*args, **kwargs):
return SoftwareVersion(*args, **kwargs), 'SoftwareVersion'

View file

@ -0,0 +1,91 @@
# 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/>.
# XEP-0108: User Activity
from typing import Any
from typing import Tuple
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import ActivityData
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
from gajim.common.const import PEPEventType
class UserActivity(BaseModule):
_nbxmpp_extends = 'Activity'
_nbxmpp_methods = [
'set_activity',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._activity_received)
self._current_activity = None
def get_current_activity(self):
return self._current_activity
@event_node(Namespace.ACTIVITY)
def _activity_received(self, _con, _stanza, properties):
if properties.pubsub_event.retracted:
return
data = properties.pubsub_event.data
for contact in app.contacts.get_contacts(self._account,
str(properties.jid)):
if data is not None:
contact.pep[PEPEventType.ACTIVITY] = data
else:
contact.pep.pop(PEPEventType.ACTIVITY, None)
if properties.is_self_message:
if data is not None:
self._con.pep[PEPEventType.ACTIVITY] = data
else:
self._con.pep.pop(PEPEventType.ACTIVITY, None)
self._current_activity = data
app.nec.push_incoming_event(
NetworkEvent('activity-received',
account=self._account,
jid=properties.jid.bare,
activity=data,
is_self_message=properties.is_self_message))
def set_activity(self, activity):
if activity is not None:
activity = ActivityData(*activity, None)
if activity == self._current_activity:
return
self._current_activity = activity
if activity is None:
self._log.info('Remove user activity')
else:
self._log.info('Set %s', activity)
self._nbxmpp('Activity').set_activity(activity)
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserActivity, str]:
return UserActivity(*args, **kwargs), 'UserActivity'

View file

@ -0,0 +1,99 @@
# 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/>.
# XEP-0084: User Avatar
from nbxmpp.namespaces import Namespace
from nbxmpp.modules.util import is_error
from gajim.common import app
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
from gajim.common.modules.util import as_task
class UserAvatar(BaseModule):
_nbxmpp_extends = 'UserAvatar'
_nbxmpp_methods = [
'request_avatar_metadata',
'request_avatar_data',
'set_avatar',
'set_access_model'
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._avatar_metadata_received)
@event_node(Namespace.AVATAR_METADATA)
def _avatar_metadata_received(self, _con, _stanza, properties):
if properties.pubsub_event.retracted:
return
metadata = properties.pubsub_event.data
jid = str(properties.jid)
if metadata is None or not metadata.infos:
self._log.info('No avatar published: %s', jid)
app.contacts.set_avatar(self._account, jid, None)
self._con.get_module('Roster').set_avatar_sha(jid, None)
app.interface.update_avatar(self._account, jid)
else:
if properties.is_self_message:
sha = app.settings.get_account_setting(self._account,
'avatar_sha')
else:
sha = app.contacts.get_avatar_sha(self._account, jid)
if sha in metadata.avatar_shas:
self._log.info('Avatar already known: %s %s', jid, sha)
return
avatar_info = metadata.infos[0]
self._log.info('Request: %s %s', jid, avatar_info.id)
self._request_avatar_data(jid, avatar_info)
@as_task
def _request_avatar_data(self, jid, avatar_info):
_task = yield
avatar = yield self._nbxmpp('UserAvatar').request_avatar_data(
avatar_info.id, jid=jid)
if avatar is None:
self._log.warning('%s advertised %s but data node is empty',
jid, avatar_info.id)
return
if is_error(avatar):
self._log.warning(avatar)
return
self._log.info('Received Avatar: %s %s', jid, avatar.sha)
app.interface.save_avatar(avatar.data)
if self._con.get_own_jid().bare_match(jid):
app.settings.set_account_setting(self._account,
'avatar_sha',
avatar.sha)
else:
self._con.get_module('Roster').set_avatar_sha(
str(jid), avatar.sha)
app.contacts.set_avatar(self._account, str(jid), avatar.sha)
app.interface.update_avatar(self._account, str(jid))
def get_instance(*args, **kwargs):
return UserAvatar(*args, **kwargs), 'UserAvatar'

View file

@ -0,0 +1,78 @@
# 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/>.
# XEP-0080: User Location
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
from gajim.common.modules.util import store_publish
from gajim.common.const import PEPEventType
class UserLocation(BaseModule):
_nbxmpp_extends = 'Location'
_nbxmpp_methods = [
'set_location',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._location_received)
self._current_location = None
def get_current_location(self):
return self._current_location
@event_node(Namespace.LOCATION)
def _location_received(self, _con, _stanza, properties):
if properties.pubsub_event.retracted:
return
data = properties.pubsub_event.data
for contact in app.contacts.get_contacts(self._account,
str(properties.jid)):
if data is not None:
contact.pep[PEPEventType.LOCATION] = data
else:
contact.pep.pop(PEPEventType.LOCATION, None)
if properties.is_self_message:
if data is not None:
self._con.pep[PEPEventType.LOCATION] = data
else:
self._con.pep.pop(PEPEventType.LOCATION, None)
self._current_location = data
app.nec.push_incoming_event(
NetworkEvent('location-received',
account=self._account,
jid=properties.jid.bare,
location=data,
is_self_message=properties.is_self_message))
@store_publish
def set_location(self, location):
self._current_location = location
self._log.info('Send %s', location)
self._nbxmpp('Location').set_location(location)
def get_instance(*args, **kwargs):
return UserLocation(*args, **kwargs), 'UserLocation'

View file

@ -0,0 +1,91 @@
# 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/>.
# XEP-0107: User Mood
from typing import Any
from typing import Tuple
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import MoodData
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
from gajim.common.const import PEPEventType
class UserMood(BaseModule):
_nbxmpp_extends = 'Mood'
_nbxmpp_methods = [
'set_mood',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._mood_received)
self._current_mood = None
def get_current_mood(self):
return self._current_mood
@event_node(Namespace.MOOD)
def _mood_received(self, _con, _stanza, properties):
if properties.pubsub_event.retracted:
return
data = properties.pubsub_event.data
for contact in app.contacts.get_contacts(self._account,
str(properties.jid)):
if data is not None:
contact.pep[PEPEventType.MOOD] = data
else:
contact.pep.pop(PEPEventType.MOOD, None)
if properties.is_self_message:
if data is not None:
self._con.pep[PEPEventType.MOOD] = data
else:
self._con.pep.pop(PEPEventType.MOOD, None)
self._current_mood = data
app.nec.push_incoming_event(
NetworkEvent('mood-received',
account=self._account,
jid=properties.jid.bare,
mood=data,
is_self_message=properties.is_self_message))
def set_mood(self, mood):
if mood is not None:
mood = MoodData(mood, None)
if mood == self._current_mood:
return
self._current_mood = mood
if mood is None:
self._log.info('Remove user mood')
else:
self._log.info('Set %s', mood)
self._nbxmpp('Mood').set_mood(mood)
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserMood, str]:
return UserMood(*args, **kwargs), 'UserMood'

View file

@ -0,0 +1,65 @@
# 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/>.
# XEP-0172: User Nickname
from typing import Any
from typing import Tuple
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
class UserNickname(BaseModule):
_nbxmpp_extends = 'Nickname'
_nbxmpp_methods = [
'set_nickname',
'set_access_model',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._nickname_received)
@event_node(Namespace.NICK)
def _nickname_received(self, _con, _stanza, properties):
if properties.pubsub_event.retracted:
return
nick = properties.pubsub_event.data
if properties.is_self_message:
if nick is None:
nick = app.settings.get_account_setting(self._account, 'name')
app.nicks[self._account] = nick
for contact in app.contacts.get_contacts(self._account,
str(properties.jid)):
contact.contact_name = nick
self._log.info('Nickname for %s: %s', properties.jid, nick)
app.nec.push_incoming_event(
NetworkEvent('nickname-received',
account=self._account,
jid=properties.jid.bare,
nickname=nick))
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserNickname, str]:
return UserNickname(*args, **kwargs), 'UserNickname'

View file

@ -0,0 +1,124 @@
# 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/>.
# XEP-0118: User Tune
from typing import Any
from typing import Tuple
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import ged
from gajim.common.nec import NetworkEvent
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
from gajim.common.modules.util import store_publish
from gajim.common.const import PEPEventType
from gajim.common.dbus.music_track import MusicTrackListener
from gajim.common.helpers import event_filter
class UserTune(BaseModule):
_nbxmpp_extends = 'Tune'
_nbxmpp_methods = [
'set_tune',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._register_pubsub_handler(self._tune_received)
self._tune_data = None
self.register_events([
('music-track-changed', ged.CORE, self._on_music_track_changed),
('signed-in', ged.CORE, self._on_signed_in),
])
def get_current_tune(self):
return self._tune_data
@event_node(Namespace.TUNE)
def _tune_received(self, _con, _stanza, properties):
if properties.pubsub_event.retracted:
return
data = properties.pubsub_event.data
for contact in app.contacts.get_contacts(self._account,
str(properties.jid)):
if data is not None:
contact.pep[PEPEventType.TUNE] = data
else:
contact.pep.pop(PEPEventType.TUNE, None)
if properties.is_self_message:
if data is not None:
self._con.pep[PEPEventType.TUNE] = data
else:
self._con.pep.pop(PEPEventType.TUNE, None)
self._tune_data = data
app.nec.push_incoming_event(
NetworkEvent('tune-received',
account=self._account,
jid=properties.jid.bare,
tune=data,
is_self_message=properties.is_self_message))
@store_publish
def set_tune(self, tune):
if not self._con.get_module('PEP').supported:
return
if not app.settings.get_account_setting(self._account, 'publish_tune'):
return
if tune == self._tune_data:
return
self._tune_data = tune
self._log.info('Send %s', tune)
self._nbxmpp('Tune').set_tune(tune)
def set_enabled(self, enable):
if enable:
app.settings.set_account_setting(self._account,
'publish_tune',
True)
self._publish_current_tune()
else:
self.set_tune(None)
app.settings.set_account_setting(self._account,
'publish_tune',
False)
def _publish_current_tune(self):
self.set_tune(MusicTrackListener.get().current_tune)
@event_filter(['account'])
def _on_signed_in(self, _event):
self._publish_current_tune()
def _on_music_track_changed(self, event):
if self._tune_data == event.info:
return
self.set_tune(event.info)
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserTune, str]:
return UserTune(*args, **kwargs), 'UserTune'

View file

@ -0,0 +1,106 @@
# 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/>.
# Util module
from typing import Union
from logging import LoggerAdapter
from functools import wraps
from functools import partial
from nbxmpp.task import Task
from gajim.common import app
from gajim.common.const import EME_MESSAGES
def from_xs_boolean(value: Union[str, bool]) -> bool:
if isinstance(value, bool):
return value
if value in ('1', 'true', 'True'):
return True
if value in ('0', 'false', 'False', ''):
return False
raise ValueError('Cant convert %s to python boolean' % value)
def to_xs_boolean(value: Union[bool, None]) -> str:
# Convert to xs:boolean ('true', 'false')
# from a python boolean (True, False) or None
if value is True:
return 'true'
if value is False:
return 'false'
if value is None:
return 'false'
raise ValueError(
'Cant convert %s to xs:boolean' % value)
def event_node(node):
def event_node_decorator(func):
@wraps(func)
def func_wrapper(self, _con, _stanza, properties):
if not properties.is_pubsub_event:
return
if properties.pubsub_event.node != node:
return
func(self, _con, _stanza, properties)
return func_wrapper
return event_node_decorator
def store_publish(func):
@wraps(func)
def func_wrapper(self, *args, **kwargs):
# pylint: disable=protected-access
if not app.account_is_connected(self._account):
self._stored_publish = partial(func, self, *args, **kwargs)
return None
return func(self, *args, **kwargs)
return func_wrapper
def get_eme_message(eme_data):
try:
return EME_MESSAGES[eme_data.namespace]
except KeyError:
return EME_MESSAGES['fallback'] % eme_data.name
class LogAdapter(LoggerAdapter):
def process(self, msg, kwargs):
return '(%s) %s' % (self.extra['account'], msg), kwargs
def as_task(func):
@wraps(func)
def func_wrapper(self, *args, callback=None, user_data=None, **kwargs):
task_ = Task(func(self, *args, **kwargs))
app.register_task(self, task_)
task_.set_finalize_func(app.remove_task, id(self))
task_.set_user_data(user_data)
if callback is not None:
task_.add_done_callback(callback)
task_.start()
return task_
return func_wrapper

View file

@ -0,0 +1,33 @@
# 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/>.
# XEP-0292: vCard4 Over XMPP
from gajim.common.modules.base import BaseModule
class VCard4(BaseModule):
_nbxmpp_extends = 'VCard4'
_nbxmpp_methods = [
'request_vcard',
'set_vcard',
]
def __init__(self, con):
BaseModule.__init__(self, con)
def get_instance(*args, **kwargs):
return VCard4(*args, **kwargs), 'VCard4'

View file

@ -0,0 +1,204 @@
# 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/>.
# XEP-0153: vCard-Based Avatars
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import AvatarState
from nbxmpp.modules.util import is_error
from gajim.common import app
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import as_task
class VCardAvatars(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con)
self._requested_shas = []
self.handlers = [
StanzaHandler(name='presence',
callback=self._presence_received,
ns=Namespace.VCARD_UPDATE,
priority=51),
]
self.avatar_conversion_available = False
def pass_disco(self, info):
is_available = Namespace.VCARD_CONVERSION in info.features
self.avatar_conversion_available = is_available
self._log.info('Discovered Avatar Conversion')
@as_task
def _request_vcard(self, jid, expected_sha, type_):
_task = yield
vcard = yield self._con.get_module('VCardTemp').request_vcard(jid=jid)
if is_error(vcard):
self._log.warning(vcard)
return
avatar, avatar_sha = vcard.get_avatar()
if avatar is None:
self._log.warning('Avatar missing: %s %s', jid, expected_sha)
return
if expected_sha != avatar_sha:
self._log.warning('Avatar mismatch: %s %s != %s',
jid,
expected_sha,
avatar_sha)
return
self._log.info('Received: %s %s', jid, avatar_sha)
app.interface.save_avatar(avatar)
if type_ == 'contact':
self._con.get_module('Roster').set_avatar_sha(jid, avatar_sha)
app.contacts.set_avatar(self._account, jid, avatar_sha)
app.interface.update_avatar(self._account, jid)
elif type_ == 'muc':
app.storage.cache.set_muc_avatar_sha(jid, avatar_sha)
app.contacts.set_avatar(self._account, jid, avatar_sha)
app.interface.update_avatar(self._account, jid, room_avatar=True)
elif type_ == 'muc-user':
contact = app.contacts.get_gc_contact(self._account,
jid.bare,
jid.resource)
if contact is not None:
contact.avatar_sha = avatar_sha
app.interface.update_avatar(contact=contact)
def _presence_received(self, _con, _stanza, properties):
if not properties.type.is_available:
return
if properties.avatar_state in (AvatarState.IGNORE,
AvatarState.NOT_READY):
return
if self._con.get_own_jid().bare_match(properties.jid):
return
if properties.from_muc:
self._gc_update_received(properties)
else:
# Check if presence is from a MUC service
contact = app.contacts.get_groupchat_contact(self._account,
str(properties.jid))
self._update_received(properties, room=contact is not None)
def muc_disco_info_update(self, disco_info):
if not disco_info.supports(Namespace.VCARD):
return
field_var = '{http://modules.prosody.im/mod_vcard_muc}avatar#sha1'
if not disco_info.has_field(Namespace.MUC_INFO, field_var):
# Workaround so we dont delete the avatar for servers that dont
# support sha in disco info. Once there is a accepted XEP this
# can be removed
return
avatar_sha = disco_info.get_field_value(Namespace.MUC_INFO, field_var)
state = AvatarState.EMPTY if not avatar_sha else AvatarState.ADVERTISED
self._process_update(str(disco_info.jid), state, avatar_sha, True)
def _update_received(self, properties, room=False):
self._process_update(properties.jid.bare,
properties.avatar_state,
properties.avatar_sha,
room)
def _process_update(self, jid, state, avatar_sha, room):
if state == AvatarState.EMPTY:
# Empty <photo/> tag, means no avatar is advertised
self._log.info('%s has no avatar published', jid)
app.contacts.set_avatar(self._account, jid, None)
if room:
app.storage.cache.set_muc_avatar_sha(jid, None)
else:
self._con.get_module('Roster').set_avatar_sha(jid, None)
app.interface.update_avatar(self._account, jid, room_avatar=room)
else:
self._log.info('Update: %s %s', jid, avatar_sha)
current_sha = app.contacts.get_avatar_sha(self._account, jid)
if avatar_sha == current_sha:
self._log.info('Avatar already known: %s %s', jid, avatar_sha)
return
if app.interface.avatar_exists(avatar_sha):
# Check if the avatar is already in storage
self._log.info('Found avatar in storage')
if room:
app.storage.cache.set_muc_avatar_sha(jid, avatar_sha)
else:
self._con.get_module('Roster').set_avatar_sha(jid,
avatar_sha)
app.contacts.set_avatar(self._account, jid, avatar_sha)
app.interface.update_avatar(
self._account, jid, room_avatar=room)
return
if avatar_sha not in self._requested_shas:
self._requested_shas.append(avatar_sha)
if room:
self._request_vcard(jid, avatar_sha, 'muc')
else:
self._request_vcard(jid, avatar_sha, 'contact')
def _gc_update_received(self, properties):
nick = properties.jid.resource
gc_contact = app.contacts.get_gc_contact(
self._account, properties.jid.bare, nick)
if gc_contact is None:
self._log.error('no gc contact found: %s', nick)
return
if properties.avatar_state == AvatarState.EMPTY:
# Empty <photo/> tag, means no avatar is advertised
self._log.info('%s has no avatar published', nick)
gc_contact.avatar_sha = None
app.interface.update_avatar(contact=gc_contact)
else:
self._log.info('Update: %s %s', nick, properties.avatar_sha)
if not app.interface.avatar_exists(properties.avatar_sha):
if properties.avatar_sha not in self._requested_shas:
app.log('avatar').info('Request: %s', nick)
self._requested_shas.append(properties.avatar_sha)
self._request_vcard(properties.jid,
properties.avatar_sha,
'muc-user')
return
if gc_contact.avatar_sha != properties.avatar_sha:
self._log.info('%s changed their Avatar: %s',
nick, properties.avatar_sha)
gc_contact.avatar_sha = properties.avatar_sha
app.interface.update_avatar(contact=gc_contact)
else:
self._log.info('Avatar already known: %s', nick)
def get_instance(*args, **kwargs):
return VCardAvatars(*args, **kwargs), 'VCardAvatars'

View file

@ -0,0 +1,45 @@
# 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/>.
# XEP-0054: vcard-temp
from nbxmpp.namespaces import Namespace
from gajim.common.modules.base import BaseModule
class VCardTemp(BaseModule):
_nbxmpp_extends = 'VCardTemp'
_nbxmpp_methods = [
'request_vcard',
'set_vcard',
]
def __init__(self, con):
BaseModule.__init__(self, con)
self._own_vcard = None
self.supported = False
def pass_disco(self, info):
if Namespace.VCARD not in info.features:
return
self.supported = True
self._log.info('Discovered vcard-temp: %s', info.jid)
def get_instance(*args, **kwargs):
return VCardTemp(*args, **kwargs), 'VCardTemp'

View file

@ -0,0 +1,115 @@
# Copyright (C) 2009 Thibaut GIRKA <thib AT sitedethib.com>
#
# 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 logging
try:
from gi.repository import Gst
except Exception:
pass
from gajim.common.i18n import _
log = logging.getLogger('gajim.c.multimedia_helpers')
class DeviceManager:
def __init__(self):
self.devices = {}
def detect(self):
self.devices = {}
def get_devices(self):
if not self.devices:
self.detect()
return self.devices
def detect_element(self, name, text, pipe='%s'):
if Gst.ElementFactory.find(name):
element = Gst.ElementFactory.make(name, '%spresencetest' % name)
if element is None:
log.warning('could not create %spresencetest', name)
return
if hasattr(element.props, 'device'):
element.set_state(Gst.State.READY)
devices = element.get_properties('device')
if devices:
self.devices[text % _('Default device')] = pipe % name
for device in devices:
if device is None:
continue
element.set_state(Gst.State.NULL)
element.set_property('device', device)
element.set_state(Gst.State.READY)
device_name = element.get_property('device-name')
self.devices[text % device_name] = pipe % \
'%s device=%s' % (name, device)
element.set_state(Gst.State.NULL)
else:
self.devices[text] = pipe % name
else:
log.info('element %s not found', name)
class AudioInputManager(DeviceManager):
def detect(self):
self.devices = {}
# Test src
self.detect_element('audiotestsrc', _('Audio test'),
'%s is-live=true name=gajim_vol')
# Auto src
self.detect_element('autoaudiosrc', _('Autodetect'),
'%s ! volume name=gajim_vol')
# Alsa src
self.detect_element('alsasrc', _('ALSA: %s'),
'%s ! volume name=gajim_vol')
# Pulseaudio src
self.detect_element('pulsesrc', _('Pulse: %s'),
'%s ! volume name=gajim_vol')
class AudioOutputManager(DeviceManager):
def detect(self):
self.devices = {}
# Fake sink
self.detect_element('fakesink', _('Fake audio output'))
# Auto sink
self.detect_element('autoaudiosink', _('Autodetect'))
# Alsa sink
self.detect_element('alsasink', _('ALSA: %s'), '%s sync=false')
# Pulseaudio sink
self.detect_element('pulsesink', _('Pulse: %s'), '%s sync=true')
class VideoInputManager(DeviceManager):
def detect(self):
self.devices = {}
# Test src
self.detect_element('videotestsrc', _('Video test'),
'%s is-live=true ! video/x-raw,framerate=10/1 ! videoconvert')
# Auto src
self.detect_element('autovideosrc', _('Autodetect'))
# Best source on Linux, for both camera and screen sharing
self.detect_element('pipewiresrc', _('Pipewire'))
# Camera source on Linux
self.detect_element('v4l2src', _('V4L2: %s'))
# X11 screen sharing on Linux
self.detect_element('ximagesrc', _('X11'))
# Recommended source on Windows
self.detect_element('ksvideosrc', _('Windows'))
# Recommended source on OS X
self.detect_element('avfvideosrc', _('macOS'))

Some files were not shown because too many files have changed in this diff Show more