9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz
This commit is contained in:
parent
a5b3822651
commit
4c1b226bff
1045 changed files with 753037 additions and 18 deletions
27
gajim/plugins/__init__.py
Normal file
27
gajim/plugins/__init__.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
Main file of plugins package.
|
||||
|
||||
:author: Mateusz Biliński <mateusz@bilinski.it>
|
||||
:since: 05/30/2008
|
||||
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
|
||||
:license: GPL
|
||||
'''
|
||||
|
||||
from gajim.plugins.pluginmanager import PluginManager
|
||||
from gajim.plugins.gajimplugin import GajimPlugin
|
||||
|
||||
__all__ = ['PluginManager', 'GajimPlugin']
|
285
gajim/plugins/gajimplugin.py
Normal file
285
gajim/plugins/gajimplugin.py
Normal file
|
@ -0,0 +1,285 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
Base class for implementing plugin.
|
||||
|
||||
:author: Mateusz Biliński <mateusz@bilinski.it>
|
||||
:since: 1st June 2008
|
||||
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
|
||||
:license: GPL
|
||||
'''
|
||||
|
||||
from typing import List # pylint: disable=W0611
|
||||
from typing import Tuple # pylint: disable=W0611
|
||||
from typing import Dict # pylint: disable=W0611
|
||||
from typing import Any # pylint: disable=W0611
|
||||
|
||||
import os
|
||||
import locale
|
||||
import logging
|
||||
import pickle
|
||||
|
||||
from gajim.common import configpaths
|
||||
from gajim.common.types import PluginExtensionPoints # pylint: disable=W0611
|
||||
from gajim.common.types import EventHandlersDict # pylint: disable=W0611
|
||||
from gajim.common.types import PluginEvents # pylint: disable=W0611
|
||||
|
||||
from gajim.plugins.helpers import log
|
||||
from gajim.plugins.gui import GajimPluginConfigDialog
|
||||
|
||||
|
||||
log = logging.getLogger('gajim.p.plugin')
|
||||
|
||||
|
||||
class GajimPlugin:
|
||||
'''
|
||||
Base class for implementing Gajim plugins.
|
||||
'''
|
||||
name = ''
|
||||
'''
|
||||
Name of plugin.
|
||||
|
||||
Will be shown in plugins management GUI.
|
||||
|
||||
:type: str
|
||||
'''
|
||||
short_name = ''
|
||||
'''
|
||||
Short name of plugin.
|
||||
|
||||
Used for quick identification of plugin.
|
||||
|
||||
:type: str
|
||||
|
||||
:todo: decide whether we really need this one, because class name (with
|
||||
module name) can act as such short name
|
||||
'''
|
||||
encryption_name = ''
|
||||
'''
|
||||
Name of the encryption scheme.
|
||||
|
||||
The name that Gajim displays in the encryption menu.
|
||||
Leave empty if the plugin is not an encryption plugin.
|
||||
|
||||
:type: str
|
||||
|
||||
'''
|
||||
version = ''
|
||||
'''
|
||||
Version of plugin.
|
||||
|
||||
:type: str
|
||||
|
||||
:todo: decide how to compare version between each other (which one
|
||||
is higher). Also rethink: do we really need to compare versions
|
||||
of plugins between each other? This would be only useful if we detect
|
||||
same plugin class but with different version and we want only the newest
|
||||
one to be active - is such policy good?
|
||||
'''
|
||||
description = ''
|
||||
'''
|
||||
Plugin description.
|
||||
|
||||
:type: str
|
||||
|
||||
:todo: should be allow rich text here (like HTML or reStructuredText)?
|
||||
'''
|
||||
authors = [] # type: List[str]
|
||||
'''
|
||||
Plugin authors.
|
||||
|
||||
:type: [] of str
|
||||
|
||||
:todo: should we decide on any particular format of author strings?
|
||||
Especially: should we force format of giving author's e-mail?
|
||||
'''
|
||||
homepage = ''
|
||||
'''
|
||||
URL to plug-in's homepage.
|
||||
|
||||
:type: str
|
||||
|
||||
:todo: should we check whether provided string is valid URI? (Maybe
|
||||
using 'property')
|
||||
'''
|
||||
gui_extension_points = {} # type: PluginExtensionPoints
|
||||
'''
|
||||
Extension points that plugin wants to connect with and handlers to be used.
|
||||
|
||||
Keys of this string should be strings with name of GUI extension point
|
||||
to handles. Values should be 2-element tuples with references to handling
|
||||
functions. First function will be used to connect plugin with extpoint,
|
||||
the second one to successfully disconnect from it. Connecting takes places
|
||||
when plugin is activated and extpoint already exists, or when plugin is
|
||||
already activated but extpoint is being created (eg. chat window opens).
|
||||
Disconnecting takes place when plugin is deactivated and extpoint exists
|
||||
or when extpoint is destroyed and plugin is activate (eg. chat window
|
||||
closed).
|
||||
'''
|
||||
config_default_values = {} # type: Dict[str, Tuple[Any, str]]
|
||||
'''
|
||||
Default values for keys that should be stored in plug-in config.
|
||||
|
||||
This dict is used when when someone calls for config option but it has not
|
||||
been set yet.
|
||||
|
||||
Values are tuples: (default_value, option_description). The first one can
|
||||
be anything (this is the advantage of using shelve/pickle instead of
|
||||
custom-made config I/O handling); the second one should be str (gettext
|
||||
can be used if need and/or translation is planned).
|
||||
|
||||
:type: {} of 2-element tuples
|
||||
'''
|
||||
events_handlers = {} # type: EventHandlersDict
|
||||
'''
|
||||
Dictionary with events handlers.
|
||||
|
||||
Keys are event names. Values should be 2-element tuples with handler
|
||||
priority as first element and reference to handler function as second
|
||||
element. Priority is integer. See `ged` module for predefined priorities
|
||||
like `ged.PRECORE`, `ged.CORE` or `ged.POSTCORE`.
|
||||
|
||||
:type: {} with 2-element tuples
|
||||
'''
|
||||
events = [] # type: PluginEvents
|
||||
'''
|
||||
New network event classes to be registered in Network Events Controller.
|
||||
|
||||
:type: [] of `nec.NetworkIncomingEvent` or `nec.NetworkOutgoingEvent`
|
||||
subclasses.
|
||||
'''
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.config = GajimPluginConfig(self)
|
||||
'''
|
||||
Plug-in configuration dictionary.
|
||||
|
||||
Automatically saved and loaded and plug-in (un)load.
|
||||
|
||||
:type: `plugins.plugin.GajimPluginConfig`
|
||||
'''
|
||||
self.activatable = True
|
||||
self.available_text = ''
|
||||
self.load_config()
|
||||
self.config_dialog = GajimPluginConfigDialog(self)
|
||||
self.init()
|
||||
|
||||
def save_config(self) -> None:
|
||||
self.config.save()
|
||||
|
||||
def load_config(self) -> None:
|
||||
self.config.load()
|
||||
|
||||
def __eq__(self, plugin):
|
||||
if self.short_name == plugin.short_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def __ne__(self, plugin):
|
||||
if self.short_name != plugin.short_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def local_file_path(self, file_name):
|
||||
return os.path.join(self.__path__, file_name)
|
||||
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def activate(self):
|
||||
pass
|
||||
|
||||
def deactivate(self):
|
||||
pass
|
||||
|
||||
|
||||
class GajimPluginConfig():
|
||||
def __init__(self, plugin):
|
||||
self.plugin = plugin
|
||||
self.FILE_PATH = (configpaths.get('PLUGINS_CONFIG_DIR') /
|
||||
self.plugin.short_name)
|
||||
self.data = {}
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not key in self.data:
|
||||
self.data[key] = self.plugin.config_default_values[key][0]
|
||||
self.save()
|
||||
|
||||
return self.data[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.data[key] = value
|
||||
self.save()
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.data[key]
|
||||
self.save()
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.data
|
||||
|
||||
def __iter__(self):
|
||||
for k in self.data.keys():
|
||||
yield k
|
||||
|
||||
def keys(self):
|
||||
return self.data.keys()
|
||||
|
||||
def items(self):
|
||||
return self.data.items()
|
||||
|
||||
def save(self):
|
||||
with open(self.FILE_PATH, 'wb') as fd:
|
||||
pickle.dump(self.data, fd)
|
||||
|
||||
def load(self):
|
||||
if not self.FILE_PATH.is_file():
|
||||
self.data = {}
|
||||
self.save()
|
||||
return
|
||||
with open(self.FILE_PATH, 'rb') as fd:
|
||||
try:
|
||||
self.data = pickle.load(fd)
|
||||
except Exception:
|
||||
try:
|
||||
import shelve
|
||||
s = shelve.open(self.FILE_PATH)
|
||||
for (k, v) in s.items():
|
||||
self.data[k] = v
|
||||
if not isinstance(self.data, dict):
|
||||
raise GajimPluginException
|
||||
s.close()
|
||||
self.save()
|
||||
except Exception:
|
||||
enc = locale.getpreferredencoding()
|
||||
filename = self.FILE_PATH.decode(enc) + '.bak'
|
||||
log.warning(
|
||||
'%s plugin config file not readable. Saving it as '
|
||||
'%s and creating a new one',
|
||||
self.plugin.short_name, filename)
|
||||
if os.path.exists(self.FILE_PATH + '.bak'):
|
||||
os.remove(self.FILE_PATH + '.bak')
|
||||
os.rename(self.FILE_PATH, self.FILE_PATH + '.bak')
|
||||
self.data = {}
|
||||
self.save()
|
||||
|
||||
|
||||
class GajimPluginException(Exception):
|
||||
pass
|
||||
|
||||
class GajimPluginInitError(GajimPluginException):
|
||||
pass
|
345
gajim/plugins/gui.py
Normal file
345
gajim/plugins/gui.py
Normal file
|
@ -0,0 +1,345 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
GUI classes related to plug-in management.
|
||||
|
||||
:author: Mateusz Biliński <mateusz@bilinski.it>
|
||||
:since: 6th June 2008
|
||||
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
|
||||
:license: GPL
|
||||
'''
|
||||
|
||||
import os
|
||||
from enum import IntEnum
|
||||
from enum import unique
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import Gdk
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import ged
|
||||
from gajim.common.exceptions import PluginsystemError
|
||||
from gajim.common.helpers import open_uri
|
||||
from gajim.common.nec import EventHelper
|
||||
|
||||
from gajim.plugins.helpers import GajimPluginActivateException
|
||||
from gajim.plugins.plugins_i18n import _
|
||||
|
||||
from gajim.gui.dialogs import WarningDialog
|
||||
from gajim.gui.dialogs import DialogButton
|
||||
from gajim.gui.dialogs import ConfirmationDialog
|
||||
from gajim.gui.filechoosers import ArchiveChooserDialog
|
||||
from gajim.gui.util import get_builder
|
||||
from gajim.gui.util import load_icon
|
||||
|
||||
|
||||
@unique
|
||||
class Column(IntEnum):
|
||||
PLUGIN = 0
|
||||
NAME = 1
|
||||
ACTIVE = 2
|
||||
ACTIVATABLE = 3
|
||||
ICON = 4
|
||||
|
||||
|
||||
class PluginsWindow(Gtk.ApplicationWindow, EventHelper):
|
||||
def __init__(self):
|
||||
Gtk.ApplicationWindow.__init__(self)
|
||||
EventHelper.__init__(self)
|
||||
|
||||
self.set_application(app.app)
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
self.set_default_size(650, 500)
|
||||
self.set_show_menubar(False)
|
||||
self.set_title(_('Plugins'))
|
||||
|
||||
self._ui = get_builder('plugins_window.ui')
|
||||
self.add(self._ui.plugins_notebook)
|
||||
|
||||
# Disable 'Install from ZIP' for Flatpak installs
|
||||
if app.is_flatpak():
|
||||
self._ui.install_plugin_button.set_tooltip_text(
|
||||
_('Click to view Gajim\'s wiki page on how to install plugins '
|
||||
'in Flatpak.'))
|
||||
|
||||
self.installed_plugins_model = Gtk.ListStore(object, str, bool, bool,
|
||||
GdkPixbuf.Pixbuf)
|
||||
self._ui.installed_plugins_treeview.set_model(
|
||||
self.installed_plugins_model)
|
||||
|
||||
renderer = Gtk.CellRendererText()
|
||||
col = Gtk.TreeViewColumn(_('Plugin')) # , renderer, text=Column.NAME)
|
||||
cell = Gtk.CellRendererPixbuf()
|
||||
col.pack_start(cell, False)
|
||||
col.add_attribute(cell, 'pixbuf', Column.ICON)
|
||||
col.pack_start(renderer, True)
|
||||
col.add_attribute(renderer, 'text', Column.NAME)
|
||||
col.set_property('expand', True)
|
||||
self._ui.installed_plugins_treeview.append_column(col)
|
||||
|
||||
renderer = Gtk.CellRendererToggle()
|
||||
renderer.connect('toggled', self._installed_plugin_toggled)
|
||||
col = Gtk.TreeViewColumn(_('Active'), renderer, active=Column.ACTIVE,
|
||||
activatable=Column.ACTIVATABLE)
|
||||
self._ui.installed_plugins_treeview.append_column(col)
|
||||
|
||||
self.def_icon = load_icon('preferences-desktop', self, pixbuf=True)
|
||||
|
||||
# connect signal for selection change
|
||||
selection = self._ui.installed_plugins_treeview.get_selection()
|
||||
selection.connect(
|
||||
'changed', self._installed_plugins_treeview_selection_changed)
|
||||
selection.set_mode(Gtk.SelectionMode.SINGLE)
|
||||
|
||||
self._clear_installed_plugin_info()
|
||||
|
||||
self._fill_installed_plugins_model()
|
||||
root_iter = self.installed_plugins_model.get_iter_first()
|
||||
if root_iter:
|
||||
selection.select_iter(root_iter)
|
||||
|
||||
self.connect('destroy', self._on_destroy)
|
||||
self.connect('key-press-event', self._on_key_press)
|
||||
self._ui.connect_signals(self)
|
||||
|
||||
self._ui.plugins_notebook.set_current_page(0)
|
||||
|
||||
# Adding GUI extension point for Plugins that want to hook
|
||||
# the Plugin Window
|
||||
app.plugin_manager.gui_extension_point('plugin_window', self)
|
||||
|
||||
self.register_events([
|
||||
('plugin-removed', ged.GUI1, self._on_plugin_removed),
|
||||
('plugin-added', ged.GUI1, self._on_plugin_added),
|
||||
])
|
||||
|
||||
self.show_all()
|
||||
|
||||
def get_notebook(self):
|
||||
# Used by plugins
|
||||
return self._ui.plugins_notebook
|
||||
|
||||
def _on_key_press(self, _widget, event):
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
self.destroy()
|
||||
|
||||
def _on_destroy(self, *args):
|
||||
self.unregister_events()
|
||||
app.plugin_manager.remove_gui_extension_point('plugin_window', self)
|
||||
|
||||
def _installed_plugins_treeview_selection_changed(self, treeview_selection):
|
||||
model, iter_ = treeview_selection.get_selected()
|
||||
if iter_:
|
||||
plugin = model.get_value(iter_, Column.PLUGIN)
|
||||
self._display_installed_plugin_info(plugin)
|
||||
else:
|
||||
self._clear_installed_plugin_info()
|
||||
|
||||
def _display_installed_plugin_info(self, plugin):
|
||||
self._ui.plugin_name_label.set_text(plugin.name)
|
||||
self._ui.plugin_version_label.set_text(plugin.version)
|
||||
self._ui.plugin_authors_label.set_text(plugin.authors)
|
||||
markup = '<a href="%s">%s</a>' % (plugin.homepage, plugin.homepage)
|
||||
self._ui.plugin_homepage_linkbutton.set_markup(markup)
|
||||
|
||||
if plugin.available_text:
|
||||
text = _('Warning: %s') % plugin.available_text
|
||||
self._ui.available_text_label.set_text(text)
|
||||
self._ui.available_text.show()
|
||||
# Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=710888
|
||||
self._ui.available_text.queue_resize()
|
||||
else:
|
||||
self._ui.available_text.hide()
|
||||
|
||||
self._ui.description.set_text(plugin.description)
|
||||
|
||||
self._ui.uninstall_plugin_button.set_sensitive(True)
|
||||
self._ui.configure_plugin_button.set_sensitive(
|
||||
plugin.config_dialog is not None and plugin.active)
|
||||
|
||||
def _clear_installed_plugin_info(self):
|
||||
self._ui.plugin_name_label.set_text('')
|
||||
self._ui.plugin_version_label.set_text('')
|
||||
self._ui.plugin_authors_label.set_text('')
|
||||
self._ui.plugin_homepage_linkbutton.set_markup('')
|
||||
|
||||
self._ui.description.set_text('')
|
||||
self._ui.uninstall_plugin_button.set_sensitive(False)
|
||||
self._ui.configure_plugin_button.set_sensitive(False)
|
||||
|
||||
def _fill_installed_plugins_model(self):
|
||||
pm = app.plugin_manager
|
||||
self.installed_plugins_model.clear()
|
||||
self.installed_plugins_model.set_sort_column_id(1,
|
||||
Gtk.SortType.ASCENDING)
|
||||
|
||||
for plugin in pm.plugins:
|
||||
icon = self._get_plugin_icon(plugin)
|
||||
self.installed_plugins_model.append(
|
||||
[plugin,
|
||||
plugin.name,
|
||||
plugin.active and plugin.activatable,
|
||||
plugin.activatable,
|
||||
icon])
|
||||
|
||||
def _get_plugin_icon(self, plugin):
|
||||
icon_file = os.path.join(plugin.__path__, os.path.split(
|
||||
plugin.__path__)[1]) + '.png'
|
||||
icon = self.def_icon
|
||||
if os.path.isfile(icon_file):
|
||||
icon = GdkPixbuf.Pixbuf.new_from_file_at_size(icon_file, 16, 16)
|
||||
return icon
|
||||
|
||||
def _installed_plugin_toggled(self, _cell, path):
|
||||
is_active = self.installed_plugins_model[path][Column.ACTIVE]
|
||||
plugin = self.installed_plugins_model[path][Column.PLUGIN]
|
||||
|
||||
if is_active:
|
||||
app.plugin_manager.deactivate_plugin(plugin)
|
||||
else:
|
||||
try:
|
||||
app.plugin_manager.activate_plugin(plugin)
|
||||
except GajimPluginActivateException as e:
|
||||
WarningDialog(_('Plugin failed'), str(e),
|
||||
transient_for=self)
|
||||
return
|
||||
|
||||
self._ui.configure_plugin_button.set_sensitive(
|
||||
plugin.config_dialog is not None and not is_active)
|
||||
self.installed_plugins_model[path][Column.ACTIVE] = not is_active
|
||||
|
||||
def _on_configure_plugin(self, _widget):
|
||||
selection = self._ui.installed_plugins_treeview.get_selection()
|
||||
model, iter_ = selection.get_selected()
|
||||
if iter_:
|
||||
plugin = model.get_value(iter_, Column.PLUGIN)
|
||||
|
||||
if isinstance(plugin.config_dialog, GajimPluginConfigDialog):
|
||||
plugin.config_dialog.run(self)
|
||||
else:
|
||||
plugin.config_dialog(self)
|
||||
|
||||
else:
|
||||
# No plugin selected. this should never be reached. As configure
|
||||
# plugin button should only be clickable when plugin is selected.
|
||||
# XXX: maybe throw exception here?
|
||||
pass
|
||||
|
||||
def _on_uninstall_plugin(self, _widget):
|
||||
selection = self._ui.installed_plugins_treeview.get_selection()
|
||||
model, iter_ = selection.get_selected()
|
||||
if iter_:
|
||||
plugin = model.get_value(iter_, Column.PLUGIN)
|
||||
try:
|
||||
app.plugin_manager.uninstall_plugin(plugin)
|
||||
except PluginsystemError as e:
|
||||
WarningDialog(_('Unable to properly remove the plugin'),
|
||||
str(e), self)
|
||||
return
|
||||
|
||||
def _on_plugin_removed(self, event):
|
||||
for row in self.installed_plugins_model:
|
||||
if row[Column.PLUGIN] == event.plugin:
|
||||
self.installed_plugins_model.remove(row.iter)
|
||||
break
|
||||
|
||||
def _on_plugin_added(self, event):
|
||||
icon = self._get_plugin_icon(event.plugin)
|
||||
self.installed_plugins_model.append([event.plugin,
|
||||
event.plugin.name,
|
||||
False,
|
||||
event.plugin.activatable,
|
||||
icon])
|
||||
|
||||
def _on_install_plugin(self, _widget):
|
||||
if app.is_flatpak():
|
||||
open_uri('https://dev.gajim.org/gajim/gajim/wikis/help/flathub')
|
||||
return
|
||||
|
||||
def _show_warn_dialog():
|
||||
text = _('Archive is malformed')
|
||||
dialog = WarningDialog(text, '', transient_for=self)
|
||||
dialog.set_modal(False)
|
||||
dialog.popup()
|
||||
|
||||
def _on_plugin_exists(zip_filename):
|
||||
def _on_yes():
|
||||
plugin = app.plugin_manager.install_from_zip(zip_filename,
|
||||
overwrite=True)
|
||||
if not plugin:
|
||||
_show_warn_dialog()
|
||||
return
|
||||
|
||||
ConfirmationDialog(
|
||||
_('Overwrite Plugin?'),
|
||||
_('Plugin already exists'),
|
||||
_('Do you want to overwrite the currently installed version?'),
|
||||
[DialogButton.make('Cancel'),
|
||||
DialogButton.make('Remove',
|
||||
text=_('_Overwrite'),
|
||||
callback=_on_yes)],
|
||||
transient_for=self).show()
|
||||
|
||||
def _try_install(zip_filename):
|
||||
try:
|
||||
plugin = app.plugin_manager.install_from_zip(zip_filename)
|
||||
except PluginsystemError as er_type:
|
||||
error_text = str(er_type)
|
||||
if error_text == _('Plugin already exists'):
|
||||
_on_plugin_exists(zip_filename)
|
||||
return
|
||||
|
||||
WarningDialog(error_text, '"%s"' % zip_filename, self)
|
||||
return
|
||||
if not plugin:
|
||||
_show_warn_dialog()
|
||||
return
|
||||
|
||||
ArchiveChooserDialog(_try_install, transient_for=self)
|
||||
|
||||
|
||||
class GajimPluginConfigDialog(Gtk.Dialog):
|
||||
def __init__(self, plugin, **kwargs):
|
||||
Gtk.Dialog.__init__(self, title='%s %s' % (plugin.name,
|
||||
_('Configuration')), **kwargs)
|
||||
self.plugin = plugin
|
||||
button = self.add_button('gtk-close', Gtk.ResponseType.CLOSE)
|
||||
button.connect('clicked', self.on_close_button_clicked)
|
||||
|
||||
self.get_child().set_spacing(3)
|
||||
|
||||
self.init()
|
||||
|
||||
def on_close_dialog(self, widget, data):
|
||||
self.hide()
|
||||
return True
|
||||
|
||||
def on_close_button_clicked(self, widget):
|
||||
self.hide()
|
||||
|
||||
def run(self, parent=None):
|
||||
self.set_transient_for(parent)
|
||||
self.on_run()
|
||||
self.show_all()
|
||||
self.connect('delete-event', self.on_close_dialog)
|
||||
result = super(GajimPluginConfigDialog, self)
|
||||
return result
|
||||
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def on_run(self):
|
||||
pass
|
132
gajim/plugins/helpers.py
Normal file
132
gajim/plugins/helpers.py
Normal file
|
@ -0,0 +1,132 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
Helper code related to plug-ins management system.
|
||||
|
||||
:author: Mateusz Biliński <mateusz@bilinski.it>
|
||||
:since: 30th May 2008
|
||||
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
|
||||
:license: GPL
|
||||
'''
|
||||
|
||||
__all__ = ['log', 'log_calls']
|
||||
|
||||
from typing import List
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
from gajim.common import configpaths
|
||||
from gajim.plugins import plugins_i18n
|
||||
from gajim.gui.util import Builder
|
||||
|
||||
log = logging.getLogger('gajim.plugin_system')
|
||||
'''
|
||||
Logger for code related to plug-in system.
|
||||
|
||||
:type: logging.Logger
|
||||
'''
|
||||
|
||||
class GajimPluginActivateException(Exception):
|
||||
'''
|
||||
Raised when activation failed
|
||||
'''
|
||||
|
||||
class log_calls:
|
||||
'''
|
||||
Decorator class for functions to easily log when they are entered and left.
|
||||
'''
|
||||
|
||||
filter_out_classes = ['GajimPluginConfig', 'PluginManager',
|
||||
'GajimPluginConfigDialog', 'PluginsWindow']
|
||||
'''
|
||||
List of classes from which no logs should be emitted when methods are
|
||||
called, even though `log_calls` decorator is used.
|
||||
'''
|
||||
|
||||
def __init__(self, classname=''):
|
||||
'''
|
||||
:Keywords:
|
||||
classname : str
|
||||
Name of class to prefix function name (if function is a method).
|
||||
log : logging.Logger
|
||||
Logger to use when outputting debug information on when function has
|
||||
been entered and when left. By default: `plugins.helpers.log`
|
||||
is used.
|
||||
'''
|
||||
|
||||
self.full_func_name = ''
|
||||
'''
|
||||
Full name of function, with class name (as prefix) if given
|
||||
to decorator.
|
||||
|
||||
Otherwise, it's only function name retrieved from function object
|
||||
for which decorator was called.
|
||||
|
||||
:type: str
|
||||
'''
|
||||
self.log_this_class = True
|
||||
'''
|
||||
Determines whether wrapper of given function should log calls of this
|
||||
function or not.
|
||||
|
||||
:type: bool
|
||||
'''
|
||||
|
||||
if classname:
|
||||
self.full_func_name = classname+'.'
|
||||
|
||||
if classname in self.filter_out_classes:
|
||||
self.log_this_class = False
|
||||
|
||||
def __call__(self, f):
|
||||
'''
|
||||
:param f: function to be wrapped with logging statements
|
||||
|
||||
:return: given function wrapped by *log.debug* statements
|
||||
:rtype: function
|
||||
'''
|
||||
|
||||
self.full_func_name += f.__name__
|
||||
if self.log_this_class:
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
|
||||
log.debug('%s() <entered>', self.full_func_name)
|
||||
result = f(*args, **kwargs)
|
||||
log.debug('%s() <left>', self.full_func_name)
|
||||
return result
|
||||
else:
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
result = f(*args, **kwargs)
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def get_builder(file_name: str, widgets: List[str] = None) -> Builder:
|
||||
return Builder(file_name,
|
||||
widgets,
|
||||
domain=plugins_i18n.DOMAIN,
|
||||
gettext_=plugins_i18n._)
|
||||
|
||||
|
||||
def is_shipped_plugin(path):
|
||||
base = configpaths.get('PLUGINS_BASE')
|
||||
if not base.exists():
|
||||
return False
|
||||
plugin_parent = path.parent
|
||||
return base.samefile(plugin_parent)
|
758
gajim/plugins/pluginmanager.py
Normal file
758
gajim/plugins/pluginmanager.py
Normal file
|
@ -0,0 +1,758 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
Plug-in management related classes.
|
||||
|
||||
:author: Mateusz Biliński <mateusz@bilinski.it>
|
||||
:since: 30th May 2008
|
||||
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
|
||||
:license: GPL
|
||||
'''
|
||||
|
||||
__all__ = ['PluginManager']
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from importlib.util import spec_from_file_location
|
||||
from importlib.util import module_from_spec
|
||||
from shutil import rmtree, move
|
||||
import configparser
|
||||
from dataclasses import dataclass
|
||||
from packaging.version import Version as V
|
||||
|
||||
import gajim
|
||||
from gajim.common import app
|
||||
from gajim.common import nec
|
||||
from gajim.common import configpaths
|
||||
from gajim.common import modules
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.exceptions import PluginsystemError
|
||||
from gajim.common.helpers import Singleton
|
||||
from gajim.plugins.plugins_i18n import _ as p_
|
||||
|
||||
from gajim.plugins.helpers import log
|
||||
from gajim.plugins.helpers import GajimPluginActivateException
|
||||
from gajim.plugins.helpers import is_shipped_plugin
|
||||
from gajim.plugins.gajimplugin import GajimPlugin, GajimPluginException
|
||||
|
||||
|
||||
FIELDS = ('name',
|
||||
'short_name',
|
||||
'version',
|
||||
'min_gajim_version',
|
||||
'max_gajim_version',
|
||||
'description',
|
||||
'authors',
|
||||
'homepage')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plugin:
|
||||
name: str
|
||||
short_name: str
|
||||
description: str
|
||||
authors: str
|
||||
homepage: str
|
||||
version: V
|
||||
min_gajim_version: V
|
||||
max_gajim_version: V
|
||||
shipped: bool
|
||||
path: Path
|
||||
|
||||
@classmethod
|
||||
def from_manifest(cls, path):
|
||||
shipped = is_shipped_plugin(path)
|
||||
manifest = path / 'manifest.ini'
|
||||
if not manifest.exists() and not manifest.is_dir():
|
||||
raise ValueError(f'Not a plugin path: {path}')
|
||||
|
||||
conf = configparser.ConfigParser()
|
||||
conf.remove_section('info')
|
||||
|
||||
with manifest.open() as conf_file:
|
||||
try:
|
||||
conf.read_file(conf_file)
|
||||
except configparser.Error as error:
|
||||
raise ValueError(f'Error while parsing manifest: '
|
||||
f'{path.name}, {error}')
|
||||
|
||||
for field in FIELDS:
|
||||
try:
|
||||
value = conf.get('info', field, fallback=None)
|
||||
except configparser.Error as error:
|
||||
raise ValueError(f'Error while parsing manifest: '
|
||||
f'{path.name}, {error}')
|
||||
|
||||
if value is None:
|
||||
raise ValueError(f'No {field} found for {path.name}')
|
||||
|
||||
name = conf.get('info', 'name')
|
||||
short_name = conf.get('info', 'short_name')
|
||||
description = p_(conf.get('info', 'description'))
|
||||
authors = conf.get('info', 'authors')
|
||||
homepage = conf.get('info', 'homepage')
|
||||
version = V(conf.get('info', 'version'))
|
||||
min_gajim_version = V(conf.get('info', 'min_gajim_version'))
|
||||
max_gajim_version = V(conf.get('info', 'max_gajim_version'))
|
||||
gajim_version = V(gajim.__version__.split('+', 1)[0])
|
||||
|
||||
if not min_gajim_version <= gajim_version <= max_gajim_version:
|
||||
raise ValueError(
|
||||
f'Plugin {path.name} not loaded, '
|
||||
f'newer version of gajim required: '
|
||||
f'{min_gajim_version} <= {gajim_version} <= {max_gajim_version}'
|
||||
)
|
||||
|
||||
return cls(name=name,
|
||||
short_name=short_name,
|
||||
description=description,
|
||||
authors=authors,
|
||||
homepage=homepage,
|
||||
version=version,
|
||||
min_gajim_version=min_gajim_version,
|
||||
max_gajim_version=max_gajim_version,
|
||||
shipped=shipped,
|
||||
path=path)
|
||||
|
||||
def load_module(self):
|
||||
module_path = self.path / '__init__.py'
|
||||
if not module_path.exists():
|
||||
# On Windows we only ship compiled files
|
||||
module_path = self.path/ '__init__.pyc'
|
||||
|
||||
module_name = self.path.stem
|
||||
|
||||
try:
|
||||
spec = spec_from_file_location(module_name, module_path)
|
||||
if spec is None:
|
||||
return None
|
||||
module = module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
except Exception as error:
|
||||
log.warning('Error while loading module: %s', error)
|
||||
return None
|
||||
|
||||
for module_attr_name in dir(module):
|
||||
module_attr = getattr(module, module_attr_name)
|
||||
if issubclass(module_attr, GajimPlugin):
|
||||
for field in FIELDS:
|
||||
setattr(module_attr, field, str(getattr(self, field)))
|
||||
setattr(module_attr, '__path__', str(self.path))
|
||||
return module_attr
|
||||
return None
|
||||
|
||||
|
||||
class PluginManager(metaclass=Singleton):
|
||||
'''
|
||||
Main plug-in management class.
|
||||
|
||||
Currently:
|
||||
- scans for plugins
|
||||
- activates them
|
||||
- handles GUI extension points, when called by GUI objects after
|
||||
plugin is activated (by dispatching info about call to handlers
|
||||
in plugins)
|
||||
|
||||
:todo: add more info about how GUI extension points work
|
||||
:todo: add list of available GUI extension points
|
||||
:todo: implement mechanism to dynamically load plugins where GUI extension
|
||||
points have been already called (i.e. when plugin is activated
|
||||
after GUI object creation). [DONE?]
|
||||
:todo: implement mechanism to dynamically deactivate plugins (call plugin's
|
||||
deactivation handler) [DONE?]
|
||||
:todo: when plug-in is deactivated all GUI extension points are removed
|
||||
from `PluginManager.gui_extension_points_handlers`. But when
|
||||
object that invoked GUI extension point is abandoned by Gajim,
|
||||
eg. closed ChatControl object, the reference to called GUI
|
||||
extension points is still in `PluginManager.gui_extension_points`
|
||||
These should be removed, so that object can be destroyed by
|
||||
Python.
|
||||
Possible solution: add call to clean up method in classes
|
||||
'destructors' (classes that register GUI extension points)
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
self.plugins = []
|
||||
'''
|
||||
Detected plugin classes.
|
||||
|
||||
Each class object in list is `GajimPlugin` subclass.
|
||||
|
||||
:type: [] of class objects
|
||||
'''
|
||||
self.active_plugins = []
|
||||
'''
|
||||
Instance objects of active plugins.
|
||||
|
||||
These are object instances of classes held `plugins`, but only those
|
||||
that were activated.
|
||||
|
||||
:type: [] of `GajimPlugin` based objects
|
||||
'''
|
||||
self.gui_extension_points = {}
|
||||
'''
|
||||
Registered GUI extension points.
|
||||
'''
|
||||
|
||||
self.gui_extension_points_handlers = {}
|
||||
'''
|
||||
Registered handlers of GUI extension points.
|
||||
'''
|
||||
|
||||
self.encryption_plugins = {}
|
||||
'''
|
||||
Registered names with instances of encryption Plugins.
|
||||
'''
|
||||
|
||||
self.update_plugins()
|
||||
self._load_plugins()
|
||||
|
||||
def update_plugins(self, replace=True, activate=False, plugin_name=None):
|
||||
'''
|
||||
Move plugins from the downloaded folder to the user plugin folder
|
||||
|
||||
:param replace: replace plugin files if they already exist.
|
||||
:type replace: boolean
|
||||
:param activate: load and activate the plugin
|
||||
:type activate: boolean
|
||||
:param plugin_name: if provided, update only this plugin
|
||||
:type plugin_name: str
|
||||
:return: list of updated plugins (files have been installed)
|
||||
:rtype: [] of str
|
||||
'''
|
||||
updated_plugins = []
|
||||
user_dir = configpaths.get('PLUGINS_USER')
|
||||
dl_dir = configpaths.get('PLUGINS_DOWNLOAD')
|
||||
to_update = [plugin_name] if plugin_name else next(os.walk(dl_dir))[1]
|
||||
for directory in to_update:
|
||||
src_dir = dl_dir / directory
|
||||
dst_dir = user_dir / directory
|
||||
try:
|
||||
if dst_dir.exists():
|
||||
if not replace:
|
||||
continue
|
||||
self.delete_plugin_files(dst_dir)
|
||||
move(src_dir, dst_dir)
|
||||
except Exception:
|
||||
log.exception('Upgrade of plugin %s failed. '
|
||||
'Impossible to move files from "%s" to "%s"',
|
||||
directory, src_dir, dst_dir)
|
||||
continue
|
||||
updated_plugins.append(directory)
|
||||
if activate:
|
||||
plugin = self._load_plugin(Path(dst_dir))
|
||||
if plugin is None:
|
||||
log.warning('Error while updating plugin')
|
||||
continue
|
||||
|
||||
self.add_plugin(plugin, activate=True)
|
||||
return updated_plugins
|
||||
|
||||
def init_plugins(self):
|
||||
for plugin in self.plugins:
|
||||
if not app.settings.get_plugin_setting(plugin.short_name, 'active'):
|
||||
continue
|
||||
if not plugin.activatable:
|
||||
continue
|
||||
|
||||
try:
|
||||
self.activate_plugin(plugin)
|
||||
except GajimPluginActivateException:
|
||||
pass
|
||||
|
||||
def add_plugin(self, plugin, activate=False):
|
||||
plugin_class = plugin.load_module()
|
||||
if plugin_class is None:
|
||||
return None
|
||||
|
||||
if plugin in self.plugins:
|
||||
log.info('Not loading plugin %s v %s. Plugin already loaded.',
|
||||
plugin.short_name, plugin.version)
|
||||
return None
|
||||
|
||||
try:
|
||||
plugin_obj = plugin_class()
|
||||
except Exception:
|
||||
log.exception('Error while loading a plugin')
|
||||
return None
|
||||
|
||||
if plugin.short_name not in app.settings.get_plugins():
|
||||
app.settings.set_plugin_setting(plugin.short_name,
|
||||
'active',
|
||||
plugin.shipped)
|
||||
|
||||
self.plugins.append(plugin_obj)
|
||||
plugin_obj.active = False
|
||||
|
||||
if activate:
|
||||
self.activate_plugin(plugin_obj)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('plugin-added', plugin=plugin_obj))
|
||||
|
||||
return plugin_obj
|
||||
|
||||
def remove_plugin(self, plugin):
|
||||
'''
|
||||
removes the plugin from the plugin list and deletes all loaded modules
|
||||
from sys. This way we will have a fresh start when the plugin gets added
|
||||
again.
|
||||
'''
|
||||
if plugin.active:
|
||||
self.deactivate_plugin(plugin)
|
||||
|
||||
self.plugins.remove(plugin)
|
||||
|
||||
# remove modules from cache
|
||||
base_package = plugin.__module__.split('.')[0]
|
||||
# get the subpackages/-modules of the base_package. Add a dot to the
|
||||
# name to avoid name problems (removing module_abc if base_package is
|
||||
# module_ab)
|
||||
modules_to_remove = [module for module in sys.modules
|
||||
if module.startswith('{}.'.format(base_package))]
|
||||
# remove the base_package itself
|
||||
if base_package in sys.modules:
|
||||
modules_to_remove.append(base_package)
|
||||
|
||||
for module_to_remove in modules_to_remove:
|
||||
del sys.modules[module_to_remove]
|
||||
|
||||
def get_active_plugin(self, plugin_name):
|
||||
for plugin in self.active_plugins:
|
||||
if plugin.short_name == plugin_name:
|
||||
return plugin
|
||||
return None
|
||||
|
||||
def get_plugin(self, short_name):
|
||||
for plugin in self.plugins:
|
||||
if plugin.short_name == short_name:
|
||||
return plugin
|
||||
return None
|
||||
|
||||
def extension_point(self, gui_extpoint_name, *args):
|
||||
'''
|
||||
Invokes all handlers (from plugins) for a particular extension point, but
|
||||
doesn't add it to collection for further processing.
|
||||
For example if you pass a message for encryption via extension point to a
|
||||
plugin, its undesired that the call is stored and replayed on activating the
|
||||
plugin. For example after an update.
|
||||
|
||||
:param gui_extpoint_name: name of GUI extension point.
|
||||
:type gui_extpoint_name: str
|
||||
:param args: parameters to be passed to extension point handlers
|
||||
(typically and object that invokes `gui_extension_point`;
|
||||
however, this can be practically anything)
|
||||
:type args: tuple
|
||||
'''
|
||||
|
||||
self._execute_all_handlers_of_gui_extension_point(gui_extpoint_name,
|
||||
*args)
|
||||
|
||||
def gui_extension_point(self, gui_extpoint_name, *args):
|
||||
'''
|
||||
Invokes all handlers (from plugins) for particular GUI extension point
|
||||
and adds it to collection for further processing (eg. by plugins not
|
||||
active yet).
|
||||
|
||||
:param gui_extpoint_name: name of GUI extension point.
|
||||
:type gui_extpoint_name: str
|
||||
:param args: parameters to be passed to extension point handlers
|
||||
(typically and object that invokes `gui_extension_point`;
|
||||
however, this can be practically anything)
|
||||
:type args: tuple
|
||||
|
||||
:todo: GUI extension points must be documented well - names with
|
||||
parameters that will be passed to handlers (in plugins). Such
|
||||
documentation must be obeyed both in core and in plugins. This
|
||||
is a loosely coupled approach and is pretty natural in Python.
|
||||
|
||||
:bug: what if only some handlers are successfully connected? we should
|
||||
revert all those connections that where successfully made. Maybe
|
||||
call 'self._deactivate_plugin()' or sth similar.
|
||||
Looking closer - we only rewrite tuples here. Real check should
|
||||
be made in method that invokes gui_extpoints handlers.
|
||||
'''
|
||||
|
||||
self._add_gui_extension_point_call_to_list(gui_extpoint_name, *args)
|
||||
self._execute_all_handlers_of_gui_extension_point(gui_extpoint_name,
|
||||
*args)
|
||||
|
||||
def remove_gui_extension_point(self, gui_extpoint_name, *args):
|
||||
'''
|
||||
Removes GUI extension point from collection held by `PluginManager`.
|
||||
|
||||
From this point this particular extension point won't be visible
|
||||
to plugins (eg. it won't invoke any handlers when plugin is activated).
|
||||
|
||||
GUI extension point is removed completely (there is no way to recover it
|
||||
from inside `PluginManager`).
|
||||
|
||||
Removal is needed when instance object that given extension point was
|
||||
connect with is destroyed (eg. ChatControl is closed or context menu
|
||||
is hidden).
|
||||
|
||||
Each `PluginManager.gui_extension_point` call should have a call of
|
||||
`PluginManager.remove_gui_extension_point` related to it.
|
||||
|
||||
:note: in current implementation different arguments mean different
|
||||
extension points. The same arguments and the same name mean
|
||||
the same extension point.
|
||||
:todo: instead of using argument to identify which extpoint should be
|
||||
removed, maybe add additional 'id' argument - this would work
|
||||
similar hash in Python objects. 'id' would be calculated based
|
||||
on arguments passed or on anything else (even could be constant)
|
||||
This would give core developers (that add new extpoints) more
|
||||
freedom, but is this necessary?
|
||||
|
||||
:param gui_extpoint_name: name of GUI extension point.
|
||||
:type gui_extpoint_name: str
|
||||
:param args: arguments that `PluginManager.gui_extension_point` was
|
||||
called with for this extension point. This is used (along with
|
||||
extension point name) to identify element to be removed.
|
||||
:type args: tuple
|
||||
'''
|
||||
if gui_extpoint_name in self.gui_extension_points:
|
||||
extension_points = list(self.gui_extension_points[gui_extpoint_name])
|
||||
for ext_point in extension_points:
|
||||
if args[0] in ext_point:
|
||||
self.gui_extension_points[gui_extpoint_name].remove(
|
||||
ext_point)
|
||||
|
||||
if gui_extpoint_name not in self.gui_extension_points_handlers:
|
||||
return
|
||||
|
||||
for handlers in self.gui_extension_points_handlers[gui_extpoint_name]:
|
||||
disconnect_handler = handlers[1]
|
||||
if disconnect_handler is not None:
|
||||
disconnect_handler(args[0])
|
||||
|
||||
def _add_gui_extension_point_call_to_list(self, gui_extpoint_name, *args):
|
||||
'''
|
||||
Adds GUI extension point call to list of calls.
|
||||
|
||||
This is done only if such call hasn't been added already
|
||||
(same extension point name and same arguments).
|
||||
|
||||
:note: This is assumption that GUI extension points are different only
|
||||
if they have different name or different arguments.
|
||||
|
||||
:param gui_extpoint_name: GUI extension point name used to identify it
|
||||
by plugins.
|
||||
:type gui_extpoint_name: str
|
||||
|
||||
:param args: parameters to be passed to extension point handlers
|
||||
(typically and object that invokes `gui_extension_point`;
|
||||
however, this can be practically anything)
|
||||
:type args: tuple
|
||||
|
||||
'''
|
||||
if ((gui_extpoint_name not in self.gui_extension_points)
|
||||
or (args not in self.gui_extension_points[gui_extpoint_name])):
|
||||
self.gui_extension_points.setdefault(gui_extpoint_name, []).append(
|
||||
args)
|
||||
|
||||
def _execute_all_handlers_of_gui_extension_point(self, gui_extpoint_name,
|
||||
*args):
|
||||
if gui_extpoint_name in self.gui_extension_points_handlers:
|
||||
for handlers in self.gui_extension_points_handlers[
|
||||
gui_extpoint_name]:
|
||||
try:
|
||||
handlers[0](*args)
|
||||
except Exception:
|
||||
log.warning('Error executing %s',
|
||||
handlers[0], exc_info=True)
|
||||
|
||||
def _register_events_handlers_in_ged(self, plugin):
|
||||
for event_name, handler in plugin.events_handlers.items():
|
||||
priority = handler[0]
|
||||
handler_function = handler[1]
|
||||
app.ged.register_event_handler(event_name, priority,
|
||||
handler_function)
|
||||
|
||||
def _remove_events_handler_from_ged(self, plugin):
|
||||
for event_name, handler in plugin.events_handlers.items():
|
||||
priority = handler[0]
|
||||
handler_function = handler[1]
|
||||
app.ged.remove_event_handler(event_name, priority,
|
||||
handler_function)
|
||||
|
||||
def _register_network_events_in_nec(self, plugin):
|
||||
for event_class in plugin.events:
|
||||
setattr(event_class, 'plugin', plugin)
|
||||
if issubclass(event_class, nec.NetworkIncomingEvent):
|
||||
app.nec.register_incoming_event(event_class)
|
||||
elif issubclass(event_class, nec.NetworkOutgoingEvent):
|
||||
app.nec.register_outgoing_event(event_class)
|
||||
|
||||
def _remove_network_events_from_nec(self, plugin):
|
||||
for event_class in plugin.events:
|
||||
if issubclass(event_class, nec.NetworkIncomingEvent):
|
||||
app.nec.unregister_incoming_event(event_class)
|
||||
elif issubclass(event_class, nec.NetworkOutgoingEvent):
|
||||
app.nec.unregister_outgoing_event(event_class)
|
||||
|
||||
def _remove_name_from_encryption_plugins(self, plugin):
|
||||
if plugin.encryption_name:
|
||||
del self.encryption_plugins[plugin.encryption_name]
|
||||
|
||||
def _register_modules_with_handlers(self, plugin):
|
||||
if not hasattr(plugin, 'modules'):
|
||||
return
|
||||
for con in app.connections.values():
|
||||
for module in plugin.modules:
|
||||
if not module.zeroconf and con.name == 'Local':
|
||||
continue
|
||||
instance, name = module.get_instance(con)
|
||||
modules.register_single_module(con, instance, name)
|
||||
|
||||
for handler in instance.handlers:
|
||||
con.connection.register_handler(handler)
|
||||
|
||||
def _unregister_modules_with_handlers(self, plugin):
|
||||
if not hasattr(plugin, 'modules'):
|
||||
return
|
||||
for con in app.connections.values():
|
||||
for module in plugin.modules:
|
||||
instance = con.get_module(module.name)
|
||||
modules.unregister_single_module(con, module.name)
|
||||
|
||||
for handler in instance.handlers:
|
||||
con.connection.unregister_handler(handler)
|
||||
|
||||
def activate_plugin(self, plugin):
|
||||
'''
|
||||
:param plugin: plugin to be activated
|
||||
:type plugin: class object of `GajimPlugin` subclass
|
||||
'''
|
||||
if not plugin.active and plugin.activatable:
|
||||
|
||||
self._add_gui_extension_points_handlers_from_plugin(plugin)
|
||||
self._add_encryption_name_from_plugin(plugin)
|
||||
self._handle_all_gui_extension_points_with_plugin(plugin)
|
||||
self._register_events_handlers_in_ged(plugin)
|
||||
self._register_network_events_in_nec(plugin)
|
||||
self._register_modules_with_handlers(plugin)
|
||||
|
||||
self.active_plugins.append(plugin)
|
||||
try:
|
||||
plugin.activate()
|
||||
except GajimPluginException as e:
|
||||
self.deactivate_plugin(plugin)
|
||||
raise GajimPluginActivateException(str(e))
|
||||
app.settings.set_plugin_setting(plugin.short_name, 'active', True)
|
||||
plugin.active = True
|
||||
|
||||
def deactivate_plugin(self, plugin):
|
||||
# remove GUI extension points handlers (provided by plug-in) from
|
||||
# handlers list
|
||||
for gui_extpoint_name, gui_extpoint_handlers in \
|
||||
plugin.gui_extension_points.items():
|
||||
self.gui_extension_points_handlers[gui_extpoint_name].remove(
|
||||
gui_extpoint_handlers)
|
||||
|
||||
# detaching plug-in from handler GUI extension points (calling
|
||||
# cleaning up method that must be provided by plug-in developer
|
||||
# for each handled GUI extension point)
|
||||
for gui_extpoint_name, gui_extpoint_handlers in \
|
||||
plugin.gui_extension_points.items():
|
||||
if gui_extpoint_name in self.gui_extension_points:
|
||||
for gui_extension_point_args in self.gui_extension_points[
|
||||
gui_extpoint_name]:
|
||||
handler = gui_extpoint_handlers[1]
|
||||
if handler:
|
||||
try:
|
||||
handler(*gui_extension_point_args)
|
||||
except Exception:
|
||||
log.warning('Error executing %s',
|
||||
handler, exc_info=True)
|
||||
|
||||
self._remove_events_handler_from_ged(plugin)
|
||||
self._remove_network_events_from_nec(plugin)
|
||||
self._remove_name_from_encryption_plugins(plugin)
|
||||
self._unregister_modules_with_handlers(plugin)
|
||||
|
||||
# removing plug-in from active plug-ins list
|
||||
plugin.deactivate()
|
||||
self.active_plugins.remove(plugin)
|
||||
app.settings.set_plugin_setting(plugin.short_name, 'active', False)
|
||||
plugin.active = False
|
||||
|
||||
def _add_gui_extension_points_handlers_from_plugin(self, plugin):
|
||||
for gui_extpoint_name, gui_extpoint_handlers in \
|
||||
plugin.gui_extension_points.items():
|
||||
self.gui_extension_points_handlers.setdefault(gui_extpoint_name,
|
||||
[]).append(gui_extpoint_handlers)
|
||||
|
||||
def _add_encryption_name_from_plugin(self, plugin):
|
||||
if plugin.encryption_name:
|
||||
self.encryption_plugins[plugin.encryption_name] = plugin
|
||||
|
||||
def _handle_all_gui_extension_points_with_plugin(self, plugin):
|
||||
for gui_extpoint_name, gui_extpoint_handlers in \
|
||||
plugin.gui_extension_points.items():
|
||||
if gui_extpoint_name in self.gui_extension_points:
|
||||
for gui_extension_point_args in self.gui_extension_points[
|
||||
gui_extpoint_name]:
|
||||
handler = gui_extpoint_handlers[0]
|
||||
if handler:
|
||||
try:
|
||||
handler(*gui_extension_point_args)
|
||||
except Exception:
|
||||
log.warning('Error executing %s',
|
||||
handler, exc_info=True)
|
||||
|
||||
def register_modules_for_account(self, con):
|
||||
'''
|
||||
A new account has been added, register modules
|
||||
of all active plugins
|
||||
'''
|
||||
for plugin in self.plugins:
|
||||
if not plugin.active:
|
||||
continue
|
||||
|
||||
if not hasattr(plugin, 'modules'):
|
||||
continue
|
||||
|
||||
for module in plugin.modules:
|
||||
instance, name = module.get_instance(con)
|
||||
if not module.zeroconf and con.name == 'Local':
|
||||
continue
|
||||
modules.register_single_module(con, instance, name)
|
||||
|
||||
for handler in instance.handlers:
|
||||
con.connection.register_handler(handler)
|
||||
|
||||
@staticmethod
|
||||
def _load_plugin(plugin_path):
|
||||
try:
|
||||
return Plugin.from_manifest(plugin_path)
|
||||
except Exception as error:
|
||||
log.warning(error)
|
||||
|
||||
def _load_plugins(self):
|
||||
plugins = {}
|
||||
for plugin_dir in configpaths.get_plugin_dirs():
|
||||
if not plugin_dir.is_dir():
|
||||
continue
|
||||
|
||||
for plugin_path in plugin_dir.iterdir():
|
||||
plugin = self._load_plugin(plugin_path)
|
||||
if plugin is None:
|
||||
continue
|
||||
|
||||
same_plugin = plugins.get(plugin.short_name)
|
||||
if same_plugin is not None:
|
||||
if same_plugin.version > plugin.version:
|
||||
continue
|
||||
|
||||
log.info('Found plugin %s %s',
|
||||
plugin.short_name, plugin.version)
|
||||
plugins[plugin.short_name] = plugin
|
||||
|
||||
for plugin in plugins.values():
|
||||
self.add_plugin(plugin)
|
||||
|
||||
def install_from_zip(self, zip_filename, overwrite=None):
|
||||
'''
|
||||
Install plugin from zip and return plugin
|
||||
'''
|
||||
try:
|
||||
zip_file = zipfile.ZipFile(zip_filename)
|
||||
except zipfile.BadZipfile:
|
||||
# it is not zip file
|
||||
raise PluginsystemError(_('Archive corrupted'))
|
||||
except IOError:
|
||||
raise PluginsystemError(_('Archive empty'))
|
||||
|
||||
if zip_file.testzip():
|
||||
# CRC error
|
||||
raise PluginsystemError(_('Archive corrupted'))
|
||||
|
||||
dirs = []
|
||||
manifest = None
|
||||
for filename in zip_file.namelist():
|
||||
if filename.startswith('.') or filename.startswith('/') or \
|
||||
('/' not in filename):
|
||||
# members not safe
|
||||
raise PluginsystemError(_('Archive is malformed'))
|
||||
if filename.endswith('/') and filename.find('/', 0, -1) < 0:
|
||||
dirs.append(filename.strip('/'))
|
||||
if 'manifest.ini' in filename.split('/')[1]:
|
||||
manifest = True
|
||||
if not manifest:
|
||||
return None
|
||||
if len(dirs) > 1:
|
||||
raise PluginsystemError(_('Archive is malformed'))
|
||||
|
||||
plugin_name = dirs[0]
|
||||
user_dir = configpaths.get('PLUGINS_USER')
|
||||
plugin_path = user_dir / plugin_name
|
||||
|
||||
if plugin_path.exists():
|
||||
# Plugin dir already exists
|
||||
if not overwrite:
|
||||
raise PluginsystemError(_('Plugin already exists'))
|
||||
self.uninstall_plugin(self.get_plugin_by_path(str(plugin_path)))
|
||||
|
||||
zip_file.extractall(user_dir)
|
||||
zip_file.close()
|
||||
|
||||
plugin = self._load_plugin(plugin_path)
|
||||
if plugin is None:
|
||||
log.warning('Error while installing from zip')
|
||||
rmtree(plugin_path)
|
||||
raise PluginsystemError(_('Installation failed'))
|
||||
|
||||
return self.add_plugin(plugin)
|
||||
|
||||
def delete_plugin_files(self, plugin_path):
|
||||
def on_error(func, path, error):
|
||||
if func == os.path.islink:
|
||||
# if symlink
|
||||
os.unlink(path)
|
||||
return
|
||||
# access is denied or other
|
||||
raise PluginsystemError(str(error[1]))
|
||||
|
||||
rmtree(plugin_path, False, on_error)
|
||||
|
||||
def uninstall_plugin(self, plugin):
|
||||
'''
|
||||
Deactivate and remove plugin from `plugins` list
|
||||
'''
|
||||
if not plugin:
|
||||
return
|
||||
|
||||
self.remove_plugin(plugin)
|
||||
self.delete_plugin_files(plugin.__path__)
|
||||
if not is_shipped_plugin(Path(plugin.__path__)):
|
||||
path = configpaths.get('PLUGINS_BASE') / plugin.short_name
|
||||
if path.exists():
|
||||
self.delete_plugin_files(str(path))
|
||||
|
||||
app.settings.remove_plugin(plugin.short_name)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('plugin-removed', plugin=plugin))
|
||||
|
||||
def get_plugin_by_path(self, plugin_dir):
|
||||
for plugin in self.plugins:
|
||||
if plugin.__path__ in plugin_dir:
|
||||
return plugin
|
||||
return None
|
42
gajim/plugins/plugins_i18n.py
Normal file
42
gajim/plugins/plugins_i18n.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Copyright (C) 2010-2011 Denis Fomin <fominde 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 locale
|
||||
import gettext
|
||||
from pathlib import Path
|
||||
|
||||
from gajim.common import configpaths
|
||||
|
||||
DOMAIN = 'gajim_plugins'
|
||||
try:
|
||||
plugin_user_dir = configpaths.get('PLUGINS_USER')
|
||||
except KeyError:
|
||||
# This allows to import the module for tests
|
||||
print('No plugin translation path available')
|
||||
plugin_user_dir = Path.cwd()
|
||||
|
||||
|
||||
# python 3.7 gettext module does not support Path objects
|
||||
plugins_locale_dir = str(plugin_user_dir / 'locale')
|
||||
|
||||
try:
|
||||
t = gettext.translation(DOMAIN, plugins_locale_dir)
|
||||
_ = t.gettext
|
||||
except OSError:
|
||||
_ = gettext.gettext
|
||||
|
||||
if hasattr(locale, 'bindtextdomain'):
|
||||
locale.bindtextdomain(DOMAIN, plugins_locale_dir) # type: ignore
|
Loading…
Add table
Add a link
Reference in a new issue