cq: Format with black and isort

This commit is contained in:
Philipp Hörist
2025-01-25 19:15:37 +01:00
parent e6e71d82bf
commit 841b1fb25e
44 changed files with 1641 additions and 1660 deletions

View File

@@ -1,40 +1,40 @@
from typing import Any, Iterator from typing import Any
from typing import Iterator
import os
import json
import functools import functools
from shutil import make_archive import json
import os
from ftplib import FTP_TLS from ftplib import FTP_TLS
from pathlib import Path from pathlib import Path
from shutil import make_archive
import requests import requests
from rich.console import Console from rich.console import Console
PackageT = tuple[dict[str, Any], Path] PackageT = tuple[dict[str, Any], Path]
ManifestT = dict[str, Any] ManifestT = dict[str, Any]
PackageIndexT = dict[str, Any] PackageIndexT = dict[str, Any]
FTP_URL = 'panoramix.gajim.org' FTP_URL = "panoramix.gajim.org"
FTP_USER = os.environ['FTP_USER'] FTP_USER = os.environ["FTP_USER"]
FTP_PASS = os.environ['FTP_PASS'] FTP_PASS = os.environ["FTP_PASS"]
REPOSITORY_FOLDER = 'plugins/master' REPOSITORY_FOLDER = "plugins/master"
PACKAGE_INDEX_URL = 'https://ftp.gajim.org/plugins/master/package_index.json' PACKAGE_INDEX_URL = "https://ftp.gajim.org/plugins/master/package_index.json"
REPO_ROOT = Path(__file__).parent.parent REPO_ROOT = Path(__file__).parent.parent
BUILD_PATH = REPO_ROOT / 'build' BUILD_PATH = REPO_ROOT / "build"
REQUIRED_KEYS = { REQUIRED_KEYS = {
'authors', "authors",
'description', "description",
'homepage', "homepage",
'name', "name",
'platforms', "platforms",
'requirements', "requirements",
'short_name', "short_name",
'version' "version",
} }
@@ -45,11 +45,12 @@ def ftp_connection(func: Any) -> Any:
@functools.wraps(func) @functools.wraps(func)
def func_wrapper(*args: Any) -> None: def func_wrapper(*args: Any) -> None:
ftp = FTP_TLS(FTP_URL, FTP_USER, FTP_PASS) ftp = FTP_TLS(FTP_URL, FTP_USER, FTP_PASS)
console.print('Successfully connected to', FTP_URL) console.print("Successfully connected to", FTP_URL)
func(ftp, *args) func(ftp, *args)
ftp.quit() ftp.quit()
console.print('Quit') console.print("Quit")
return return
return func_wrapper return func_wrapper
@@ -59,7 +60,7 @@ def is_manifest_valid(manifest: ManifestT) -> bool:
def download_package_index() -> ManifestT: def download_package_index() -> ManifestT:
console.print('Download package index') console.print("Download package index")
r = requests.get(PACKAGE_INDEX_URL) r = requests.get(PACKAGE_INDEX_URL)
if r.status_code == 404: if r.status_code == 404:
return {} return {}
@@ -70,7 +71,7 @@ def download_package_index() -> ManifestT:
def iter_manifests() -> Iterator[PackageT]: def iter_manifests() -> Iterator[PackageT]:
for path in REPO_ROOT.rglob('plugin-manifest.json'): for path in REPO_ROOT.rglob("plugin-manifest.json"):
with path.open() as f: with path.open() as f:
manifest = json.load(f) manifest = json.load(f)
yield manifest, path.parent yield manifest, path.parent
@@ -80,43 +81,41 @@ def find_plugins_to_publish(index: PackageIndexT) -> list[PackageT]:
packages_to_publish: list[PackageT] = [] packages_to_publish: list[PackageT] = []
for manifest, path in iter_manifests(): for manifest, path in iter_manifests():
if not is_manifest_valid(manifest): if not is_manifest_valid(manifest):
exit('Invalid manifest found') exit("Invalid manifest found")
short_name = manifest['short_name'] short_name = manifest["short_name"]
version = manifest['version'] version = manifest["version"]
try: try:
index['plugins'][short_name][version] index["plugins"][short_name][version]
except KeyError: except KeyError:
packages_to_publish.append((manifest, path)) packages_to_publish.append((manifest, path))
console.print('Found package to publish:', path.stem) console.print("Found package to publish:", path.stem)
return packages_to_publish return packages_to_publish
def get_release_zip_name(manifest: ManifestT) -> str: def get_release_zip_name(manifest: ManifestT) -> str:
short_name = manifest['short_name'] short_name = manifest["short_name"]
version = manifest['version'] version = manifest["version"]
return f'{short_name}_{version}' return f"{short_name}_{version}"
def get_dir_list(ftp: FTP_TLS) -> set[str]: def get_dir_list(ftp: FTP_TLS) -> set[str]:
return {x[0] for x in ftp.mlsd()} return {x[0] for x in ftp.mlsd()}
def upload_file(ftp: FTP_TLS, def upload_file(ftp: FTP_TLS, filepath: Path) -> None:
filepath: Path) -> None:
name = filepath.name name = filepath.name
console.print('Upload file', name) console.print("Upload file", name)
with open(filepath, 'rb') as f: with open(filepath, "rb") as f:
ftp.storbinary('STOR ' + name, f) ftp.storbinary("STOR " + name, f)
def create_release_folder(ftp: FTP_TLS, def create_release_folder(ftp: FTP_TLS, packages_to_publish: list[PackageT]) -> None:
packages_to_publish: list[PackageT]) -> None:
folders = {manifest['short_name'] for manifest, _ in packages_to_publish} folders = {manifest["short_name"] for manifest, _ in packages_to_publish}
dir_list = get_dir_list(ftp) dir_list = get_dir_list(ftp)
missing_folders = folders - dir_list missing_folders = folders - dir_list
for folder in missing_folders: for folder in missing_folders:
@@ -129,26 +128,26 @@ def deploy(ftp: FTP_TLS, packages_to_publish: list[PackageT]) -> None:
create_release_folder(ftp, packages_to_publish) create_release_folder(ftp, packages_to_publish)
for manifest, path in packages_to_publish: for manifest, path in packages_to_publish:
package_name = manifest['short_name'] package_name = manifest["short_name"]
zip_name = get_release_zip_name(manifest) zip_name = get_release_zip_name(manifest)
zip_path = BUILD_PATH / f'{zip_name}.zip' zip_path = BUILD_PATH / f"{zip_name}.zip"
image_path = path / f'{package_name}.png' image_path = path / f"{package_name}.png"
make_archive(str(BUILD_PATH / zip_name), 'zip', path) make_archive(str(BUILD_PATH / zip_name), "zip", path)
ftp.cwd(package_name) ftp.cwd(package_name)
upload_file(ftp, zip_path) upload_file(ftp, zip_path)
if image_path.exists(): if image_path.exists():
upload_file(ftp, image_path) upload_file(ftp, image_path)
ftp.cwd('..') ftp.cwd("..")
console.print('Deployed', package_name) console.print("Deployed", package_name)
if __name__ == '__main__': if __name__ == "__main__":
index = download_package_index() index = download_package_index()
packages_to_publish = find_plugins_to_publish(index) packages_to_publish = find_plugins_to_publish(index)
if not packages_to_publish: if not packages_to_publish:
console.print('No new packages deployed') console.print("No new packages deployed")
else: else:
deploy(packages_to_publish) deploy(packages_to_publish)

View File

@@ -19,8 +19,8 @@ from __future__ import annotations
import json import json
import logging import logging
from pathlib import Path
from functools import partial from functools import partial
from pathlib import Path
from gi.repository import GLib from gi.repository import GLib
from gi.repository import GObject from gi.repository import GObject
@@ -29,28 +29,27 @@ from gi.repository import Gtk
from gajim.common import configpaths from gajim.common import configpaths
from gajim.common import types from gajim.common import types
from gajim.common.modules.contacts import GroupchatContact from gajim.common.modules.contacts import GroupchatContact
from gajim.gtk.message_input import MessageInputTextView from gajim.gtk.message_input import MessageInputTextView
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from acronyms_expander.acronyms import DEFAULT_DATA from acronyms_expander.acronyms import DEFAULT_DATA
from acronyms_expander.gtk.config import ConfigDialog from acronyms_expander.gtk.config import ConfigDialog
log = logging.getLogger('gajim.p.acronyms') log = logging.getLogger("gajim.p.acronyms")
class AcronymsExpanderPlugin(GajimPlugin): class AcronymsExpanderPlugin(GajimPlugin):
def init(self) -> None: def init(self) -> None:
self.description = _('Replaces acronyms (or other strings) ' self.description = _(
'with given expansions/substitutes.') "Replaces acronyms (or other strings) " "with given expansions/substitutes."
)
self.config_dialog = partial(ConfigDialog, self) self.config_dialog = partial(ConfigDialog, self)
self.gui_extension_points = { self.gui_extension_points = {
'message_input': (self._connect, None), "message_input": (self._connect, None),
'switch_contact': (self._on_switch_contact, None) "switch_contact": (self._on_switch_contact, None),
} }
self._invoker = ' ' self._invoker = " "
self._replace_in_progress = False self._replace_in_progress = False
self._signal_id = None self._signal_id = None
@@ -62,42 +61,40 @@ class AcronymsExpanderPlugin(GajimPlugin):
@staticmethod @staticmethod
def _load_acronyms() -> dict[str, str]: def _load_acronyms() -> dict[str, str]:
try: try:
data_path = Path(configpaths.get('PLUGINS_DATA')) data_path = Path(configpaths.get("PLUGINS_DATA"))
except KeyError: except KeyError:
# PLUGINS_DATA was added in 1.0.99.1 # PLUGINS_DATA was added in 1.0.99.1
return DEFAULT_DATA return DEFAULT_DATA
path = data_path / 'acronyms' / 'acronyms' path = data_path / "acronyms" / "acronyms"
if not path.exists(): if not path.exists():
return DEFAULT_DATA return DEFAULT_DATA
with path.open('r') as file: with path.open("r") as file:
acronyms = json.load(file) acronyms = json.load(file)
return acronyms return acronyms
@staticmethod @staticmethod
def _save_acronyms(acronyms: dict[str, str]) -> None: def _save_acronyms(acronyms: dict[str, str]) -> None:
try: try:
data_path = Path(configpaths.get('PLUGINS_DATA')) data_path = Path(configpaths.get("PLUGINS_DATA"))
except KeyError: except KeyError:
# PLUGINS_DATA was added in 1.0.99.1 # PLUGINS_DATA was added in 1.0.99.1
return return
path = data_path / 'acronyms' path = data_path / "acronyms"
if not path.exists(): if not path.exists():
path.mkdir(parents=True) path.mkdir(parents=True)
filepath = path / 'acronyms' filepath = path / "acronyms"
with filepath.open('w') as file: with filepath.open("w") as file:
json.dump(acronyms, file) json.dump(acronyms, file)
def set_acronyms(self, acronyms: dict[str, str]) -> None: def set_acronyms(self, acronyms: dict[str, str]) -> None:
self.acronyms = acronyms self.acronyms = acronyms
self._save_acronyms(acronyms) self._save_acronyms(acronyms)
def _on_buffer_changed(self, def _on_buffer_changed(self, message_input: MessageInputTextView) -> None:
message_input: MessageInputTextView
) -> None:
if self._contact is None: if self._contact is None:
# If no chat has been activated yet # If no chat has been activated yet
@@ -126,9 +123,8 @@ class AcronymsExpanderPlugin(GajimPlugin):
# Get to the start of the last word # Get to the start of the last word
# word_start_iter = insert_iter.copy() # word_start_iter = insert_iter.copy()
result = insert_iter.backward_search( result = insert_iter.backward_search(
self._invoker, self._invoker, Gtk.TextSearchFlags.VISIBLE_ONLY, None
Gtk.TextSearchFlags.VISIBLE_ONLY, )
None)
if result is None: if result is None:
word_start_iter = buffer_.get_start_iter() word_start_iter = buffer_.get_start_iter()
@@ -140,31 +136,30 @@ class AcronymsExpanderPlugin(GajimPlugin):
if isinstance(self._contact, GroupchatContact): if isinstance(self._contact, GroupchatContact):
if last_word in self._contact.get_user_nicknames(): if last_word in self._contact.get_user_nicknames():
log.info('Groupchat participant has same nick as acronym') log.info("Groupchat participant has same nick as acronym")
return return
if self._contact.is_pm_contact: if self._contact.is_pm_contact:
if last_word == self._contact.name: if last_word == self._contact.name:
log.info('Contact name equals acronym') log.info("Contact name equals acronym")
return return
substitute = self.acronyms.get(last_word) substitute = self.acronyms.get(last_word)
if substitute is None: if substitute is None:
log.debug('%s not an acronym', last_word) log.debug("%s not an acronym", last_word)
return return
GLib.idle_add(self._replace_text, GLib.idle_add(
buffer_, self._replace_text, buffer_, word_start_iter, insert_iter, substitute
word_start_iter, )
insert_iter,
substitute)
def _replace_text(self, def _replace_text(
buffer_: Gtk.TextBuffer, self,
start: Gtk.TextIter, buffer_: Gtk.TextBuffer,
end: Gtk.TextIter, start: Gtk.TextIter,
substitute: str end: Gtk.TextIter,
) -> None: substitute: str,
) -> None:
self._replace_in_progress = True self._replace_in_progress = True
buffer_.delete(start, end) buffer_.delete(start, end)
@@ -176,11 +171,12 @@ class AcronymsExpanderPlugin(GajimPlugin):
def _connect(self, message_input: MessageInputTextView) -> None: def _connect(self, message_input: MessageInputTextView) -> None:
self._message_input = message_input self._message_input = message_input
self._signal_id = message_input.connect('buffer-changed', self._on_buffer_changed) self._signal_id = message_input.connect(
"buffer-changed", self._on_buffer_changed
)
def deactivate(self) -> None: def deactivate(self) -> None:
assert self._message_input is not None assert self._message_input is not None
assert self._signal_id is not None assert self._signal_id is not None
if GObject.signal_handler_is_connected( if GObject.signal_handler_is_connected(self._message_input, self._signal_id):
self._message_input, self._signal_id):
self._message_input.disconnect(self._signal_id) self._message_input.disconnect(self._signal_id)

View File

@@ -23,24 +23,20 @@ from pathlib import Path
from gi.repository import Gtk from gi.repository import Gtk
from gajim.gtk.widgets import GajimAppWindow from gajim.gtk.widgets import GajimAppWindow
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING: if TYPE_CHECKING:
from ..acronyms_expander import AcronymsExpanderPlugin from ..acronyms_expander import AcronymsExpanderPlugin
class ConfigDialog(GajimAppWindow): class ConfigDialog(GajimAppWindow):
def __init__(self, def __init__(self, plugin: AcronymsExpanderPlugin, transient: Gtk.Window) -> None:
plugin: AcronymsExpanderPlugin,
transient: Gtk.Window
) -> None:
GajimAppWindow.__init__( GajimAppWindow.__init__(
self, self,
name="AcronymsConfigDialog", name="AcronymsConfigDialog",
title=_('Acronyms Configuration'), title=_("Acronyms Configuration"),
default_width=400, default_width=400,
default_height=400, default_height=400,
transient_for=transient, transient_for=transient,
@@ -48,7 +44,7 @@ class ConfigDialog(GajimAppWindow):
) )
ui_path = Path(__file__).parent ui_path = Path(__file__).parent
self._ui = get_builder(str(ui_path.resolve() / 'config.ui')) self._ui = get_builder(str(ui_path.resolve() / "config.ui"))
self._plugin = plugin self._plugin = plugin
@@ -71,29 +67,24 @@ class ConfigDialog(GajimAppWindow):
for acronym, substitute in self._plugin.acronyms.items(): for acronym, substitute in self._plugin.acronyms.items():
self._ui.acronyms_store.append([acronym, substitute]) self._ui.acronyms_store.append([acronym, substitute])
def _on_acronym_edited(self, def _on_acronym_edited(
_renderer: Gtk.CellRendererText, self, _renderer: Gtk.CellRendererText, path: str, new_text: str
path: str, ) -> None:
new_text: str
) -> None:
iter_ = self._ui.acronyms_store.get_iter(path) iter_ = self._ui.acronyms_store.get_iter(path)
self._ui.acronyms_store.set_value(iter_, 0, new_text) self._ui.acronyms_store.set_value(iter_, 0, new_text)
def _on_substitute_edited(self, def _on_substitute_edited(
_renderer: Gtk.CellRendererText, self, _renderer: Gtk.CellRendererText, path: str, new_text: str
path: str, ) -> None:
new_text: str
) -> None:
iter_ = self._ui.acronyms_store.get_iter(path) iter_ = self._ui.acronyms_store.get_iter(path)
self._ui.acronyms_store.set_value(iter_, 1, new_text) self._ui.acronyms_store.set_value(iter_, 1, new_text)
def _on_add_clicked(self, _button: Gtk.Button) -> None: def _on_add_clicked(self, _button: Gtk.Button) -> None:
self._ui.acronyms_store.append(['', '']) self._ui.acronyms_store.append(["", ""])
row = self._ui.acronyms_store[-1] row = self._ui.acronyms_store[-1]
self._ui.acronyms_treeview.scroll_to_cell( self._ui.acronyms_treeview.scroll_to_cell(row.path, None, False, 0, 0)
row.path, None, False, 0, 0)
self._ui.selection.unselect_all() self._ui.selection.unselect_all()
self._ui.selection.select_path(row.path) self._ui.selection.select_path(row.path)

View File

@@ -12,37 +12,39 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
''' """
:author: Yann Leboulanger <asterix@lagaule.org> :author: Yann Leboulanger <asterix@lagaule.org>
:since: 16 August 2012 :since: 16 August 2012
:copyright: Copyright (2012) Yann Leboulanger <asterix@lagaule.org> :copyright: Copyright (2012) Yann Leboulanger <asterix@lagaule.org>
:license: GPLv3 :license: GPLv3
''' """
from functools import partial from functools import partial
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from anti_spam.modules import anti_spam
from anti_spam.config_dialog import AntiSpamConfigDialog from anti_spam.config_dialog import AntiSpamConfigDialog
from anti_spam.modules import anti_spam
class AntiSpamPlugin(GajimPlugin): class AntiSpamPlugin(GajimPlugin):
def init(self) -> None: def init(self) -> None:
self.description = _('Allows you to block various kinds of incoming ' self.description = _(
'messages (Spam, XHTML formatting, etc.)') "Allows you to block various kinds of incoming "
"messages (Spam, XHTML formatting, etc.)"
)
self.config_dialog = partial(AntiSpamConfigDialog, self) self.config_dialog = partial(AntiSpamConfigDialog, self)
self.config_default_values = { self.config_default_values = {
'disable_xhtml_muc': (False, ''), "disable_xhtml_muc": (False, ""),
'disable_xhtml_pm': (False, ''), "disable_xhtml_pm": (False, ""),
'block_subscription_requests': (False, ''), "block_subscription_requests": (False, ""),
'msgtxt_limit': (0, ''), "msgtxt_limit": (0, ""),
'msgtxt_question': ('12 x 12 = ?', ''), "msgtxt_question": ("12 x 12 = ?", ""),
'msgtxt_answer': ('', ''), "msgtxt_answer": ("", ""),
'antispam_for_conference': (False, ''), "antispam_for_conference": (False, ""),
'block_domains': ('', ''), "block_domains": ("", ""),
'whitelist': ([], ''), "whitelist": ([], ""),
} }
self.gui_extension_points = {} self.gui_extension_points = {}
self.modules = [anti_spam] self.modules = [anti_spam]

View File

@@ -21,11 +21,10 @@ from typing import TYPE_CHECKING
from gi.repository import Gtk from gi.repository import Gtk
from gajim.gtk.settings import SettingsDialog
from gajim.gtk.const import Setting from gajim.gtk.const import Setting
from gajim.gtk.const import SettingKind from gajim.gtk.const import SettingKind
from gajim.gtk.const import SettingType from gajim.gtk.const import SettingType
from gajim.gtk.settings import SettingsDialog
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -35,72 +34,89 @@ if TYPE_CHECKING:
class AntiSpamConfigDialog(SettingsDialog): class AntiSpamConfigDialog(SettingsDialog):
def __init__(self, plugin: AntiSpamPlugin, parent: Gtk.Window) -> None: def __init__(self, plugin: AntiSpamPlugin, parent: Gtk.Window) -> None:
self.plugin = plugin self.plugin = plugin
msgtxt_limit = cast(int, self.plugin.config['msgtxt_limit']) msgtxt_limit = cast(int, self.plugin.config["msgtxt_limit"])
max_length = '' if msgtxt_limit == 0 else msgtxt_limit max_length = "" if msgtxt_limit == 0 else msgtxt_limit
settings = [ settings = [
Setting(SettingKind.ENTRY, Setting(
_('Limit Message Length'), SettingKind.ENTRY,
SettingType.VALUE, _("Limit Message Length"),
str(max_length), SettingType.VALUE,
callback=self._on_length_setting, str(max_length),
data='msgtxt_limit', callback=self._on_length_setting,
desc=_('Limits maximum message length (leave empty to ' data="msgtxt_limit",
'disable)')), desc=_("Limits maximum message length (leave empty to " "disable)"),
Setting(SettingKind.SWITCH, ),
_('Deny Subscription Requests'), Setting(
SettingType.VALUE, SettingKind.SWITCH,
self.plugin.config['block_subscription_requests'], _("Deny Subscription Requests"),
callback=self._on_setting, SettingType.VALUE,
data='block_subscription_requests'), self.plugin.config["block_subscription_requests"],
Setting(SettingKind.SWITCH, callback=self._on_setting,
_('Disable XHTML for Group Chats'), data="block_subscription_requests",
SettingType.VALUE, ),
self.plugin.config['disable_xhtml_muc'], Setting(
callback=self._on_setting, SettingKind.SWITCH,
data='disable_xhtml_muc', _("Disable XHTML for Group Chats"),
desc=_('Removes XHTML formatting from group chat ' SettingType.VALUE,
'messages')), self.plugin.config["disable_xhtml_muc"],
Setting(SettingKind.SWITCH, callback=self._on_setting,
_('Disable XHTML for PMs'), data="disable_xhtml_muc",
SettingType.VALUE, desc=_("Removes XHTML formatting from group chat " "messages"),
self.plugin.config['disable_xhtml_pm'], ),
callback=self._on_setting, Setting(
data='disable_xhtml_pm', SettingKind.SWITCH,
desc=_('Removes XHTML formatting from private messages ' _("Disable XHTML for PMs"),
'in group chats')), SettingType.VALUE,
Setting(SettingKind.ENTRY, self.plugin.config["disable_xhtml_pm"],
_('Anti Spam Question'), callback=self._on_setting,
SettingType.VALUE, data="disable_xhtml_pm",
self.plugin.config['msgtxt_question'], desc=_(
callback=self._on_setting, "Removes XHTML formatting from private messages " "in group chats"
data='msgtxt_question', ),
desc=_('Question has to be answered in order to ' ),
'contact you')), Setting(
Setting(SettingKind.ENTRY, SettingKind.ENTRY,
_('Anti Spam Answer'), _("Anti Spam Question"),
SettingType.VALUE, SettingType.VALUE,
self.plugin.config['msgtxt_answer'], self.plugin.config["msgtxt_question"],
callback=self._on_setting, callback=self._on_setting,
data='msgtxt_answer', data="msgtxt_question",
desc=_('Correct answer to your Anti Spam Question ' desc=_("Question has to be answered in order to " "contact you"),
'(leave empty to disable question)')), ),
Setting(SettingKind.SWITCH, Setting(
_('Anti Spam Question in Group Chats'), SettingKind.ENTRY,
SettingType.VALUE, _("Anti Spam Answer"),
self.plugin.config['antispam_for_conference'], SettingType.VALUE,
callback=self._on_setting, self.plugin.config["msgtxt_answer"],
data='antispam_for_conference', callback=self._on_setting,
desc=_('Enables anti spam question for private messages ' data="msgtxt_answer",
'in group chats')), desc=_(
] "Correct answer to your Anti Spam Question "
"(leave empty to disable question)"
),
),
Setting(
SettingKind.SWITCH,
_("Anti Spam Question in Group Chats"),
SettingType.VALUE,
self.plugin.config["antispam_for_conference"],
callback=self._on_setting,
data="antispam_for_conference",
desc=_(
"Enables anti spam question for private messages " "in group chats"
),
),
]
SettingsDialog.__init__(self, SettingsDialog.__init__(
parent, self,
_('Anti Spam Configuration'), parent,
Gtk.DialogFlags.MODAL, _("Anti Spam Configuration"),
settings, Gtk.DialogFlags.MODAL,
'') settings,
"",
)
def _on_setting(self, value: Any, data: Any) -> None: def _on_setting(self, value: Any, data: Any) -> None:
self.plugin.config[data] = value self.plugin.config[data] = value

View File

@@ -32,7 +32,7 @@ from gajim.common.events import MessageSent
from gajim.common.modules.base import BaseModule from gajim.common.modules.base import BaseModule
# Module name # Module name
name = 'AntiSpam' name = "AntiSpam"
zeroconf = False zeroconf = False
@@ -41,21 +41,23 @@ class AntiSpam(BaseModule):
BaseModule.__init__(self, client, plugin=True) BaseModule.__init__(self, client, plugin=True)
self.handlers = [ self.handlers = [
StanzaHandler(name='message', StanzaHandler(name="message", callback=self._message_received, priority=48),
callback=self._message_received, StanzaHandler(
priority=48), name="presence",
StanzaHandler(name='presence', callback=self._subscribe_received,
callback=self._subscribe_received, typ="subscribe",
typ='subscribe', priority=48,
priority=48), ),
] ]
self.register_events([ self.register_events(
('message-sent', ged.GUI2, self._on_message_sent), [
]) ("message-sent", ged.GUI2, self._on_message_sent),
]
)
for plugin in app.plugin_manager.plugins: for plugin in app.plugin_manager.plugins:
if plugin.manifest.short_name == 'anti_spam': if plugin.manifest.short_name == "anti_spam":
self._config = plugin.config self._config = plugin.config
self._contacted_jids: set[JID] = set() self._contacted_jids: set[JID] = set()
@@ -66,11 +68,9 @@ class AntiSpam(BaseModule):
# This set contains JIDs of all outgoing chats. # This set contains JIDs of all outgoing chats.
self._contacted_jids.add(event.jid) self._contacted_jids.add(event.jid)
def _message_received(self, def _message_received(
_con: Client, self, _con: Client, _stanza: Message, properties: MessageProperties
_stanza: Message, ) -> None:
properties: MessageProperties
) -> None:
if properties.is_sent_carbon: if properties.is_sent_carbon:
# Another device already sent a message # Another device already sent a message
@@ -86,33 +86,35 @@ class AntiSpam(BaseModule):
raise NodeProcessed raise NodeProcessed
msg_from = properties.jid msg_from = properties.jid
limit = cast(int, self._config['msgtxt_limit']) limit = cast(int, self._config["msgtxt_limit"])
if limit > 0 and len(msg_body) > limit: if limit > 0 and len(msg_body) > limit:
self._log.info('Discarded message from %s: message ' self._log.info(
'length exceeded' % msg_from) "Discarded message from %s: message " "length exceeded" % msg_from
)
raise NodeProcessed raise NodeProcessed
if self._config['disable_xhtml_muc'] and properties.type.is_groupchat: if self._config["disable_xhtml_muc"] and properties.type.is_groupchat:
properties.xhtml = None properties.xhtml = None
self._log.info('Stripped message from %s: message ' self._log.info(
'contained XHTML' % msg_from) "Stripped message from %s: message " "contained XHTML" % msg_from
)
if self._config['disable_xhtml_pm'] and properties.is_muc_pm: if self._config["disable_xhtml_pm"] and properties.is_muc_pm:
properties.xhtml = None properties.xhtml = None
self._log.info('Stripped message from %s: message ' self._log.info(
'contained XHTML' % msg_from) "Stripped message from %s: message " "contained XHTML" % msg_from
)
def _ask_question(self, properties: MessageProperties) -> bool: def _ask_question(self, properties: MessageProperties) -> bool:
answer = cast(str, self._config['msgtxt_answer']) answer = cast(str, self._config["msgtxt_answer"])
if len(answer) == 0: if len(answer) == 0:
return False return False
is_muc_pm = properties.is_muc_pm is_muc_pm = properties.is_muc_pm
if is_muc_pm and not self._config['antispam_for_conference']: if is_muc_pm and not self._config["antispam_for_conference"]:
return False return False
if (properties.type.value not in ('chat', 'normal') or if properties.type.value not in ("chat", "normal") or properties.is_mam_message:
properties.is_mam_message):
return False return False
assert properties.jid assert properties.jid
@@ -126,15 +128,15 @@ class AntiSpam(BaseModule):
# If we receive a PM or a message from an unknown user, our anti spam # If we receive a PM or a message from an unknown user, our anti spam
# question will silently be sent in the background # question will silently be sent in the background
whitelist = cast(list[str], self._config['whitelist']) whitelist = cast(list[str], self._config["whitelist"])
if str(msg_from) in whitelist: if str(msg_from) in whitelist:
return False return False
roster_item = self._client.get_module('Roster').get_item(msg_from) roster_item = self._client.get_module("Roster").get_item(msg_from)
if is_muc_pm or roster_item is None: if is_muc_pm or roster_item is None:
assert properties.body assert properties.body
if answer in properties.body.split('\n'): if answer in properties.body.split("\n"):
if str(msg_from) not in whitelist: if str(msg_from) not in whitelist:
whitelist.append(str(msg_from)) whitelist.append(str(msg_from))
# We need to explicitly save, because 'append' does not # We need to explicitly save, because 'append' does not
@@ -146,26 +148,24 @@ class AntiSpam(BaseModule):
return False return False
def _send_question(self, properties: MessageProperties, jid: JID) -> None: def _send_question(self, properties: MessageProperties, jid: JID) -> None:
message = 'Anti Spam Question: %s' % self._config['msgtxt_question'] message = "Anti Spam Question: %s" % self._config["msgtxt_question"]
stanza = Message(to=jid, body=message, typ=properties.type.value) stanza = Message(to=jid, body=message, typ=properties.type.value)
self._client.connection.send_stanza(stanza) self._client.connection.send_stanza(stanza)
self._log.info('Anti spam question sent to %s', jid) self._log.info("Anti spam question sent to %s", jid)
def _subscribe_received(self, def _subscribe_received(
_con: Client, self, _con: Client, _stanza: Presence, properties: PresenceProperties
_stanza: Presence, ) -> None:
properties: PresenceProperties
) -> None:
msg_from = properties.jid msg_from = properties.jid
block_sub = self._config['block_subscription_requests'] block_sub = self._config["block_subscription_requests"]
roster_item = self._client.get_module('Roster').get_item(msg_from) roster_item = self._client.get_module("Roster").get_item(msg_from)
if block_sub and roster_item is None: if block_sub and roster_item is None:
self._client.get_module('Presence').unsubscribed(msg_from) self._client.get_module("Presence").unsubscribed(msg_from)
self._log.info('Denied subscription request from %s' % msg_from) self._log.info("Denied subscription request from %s" % msg_from)
raise NodeProcessed raise NodeProcessed
def get_instance(*args: Any, **kwargs: Any) -> tuple[AntiSpam, str]: def get_instance(*args: Any, **kwargs: Any) -> tuple[AntiSpam, str]:
return AntiSpam(*args, **kwargs), 'AntiSpam' return AntiSpam(*args, **kwargs), "AntiSpam"

View File

@@ -39,7 +39,7 @@ def get_variations(client_name: str) -> list[str]:
if client_name is None: if client_name is None:
return [] return []
alts = client_name.split() alts = client_name.split()
alts = [' '.join(alts[:(i + 1)]) for i in range(len(alts))] alts = [" ".join(alts[: (i + 1)]) for i in range(len(alts))]
alts.reverse() alts.reverse()
return alts return alts
@@ -48,23 +48,23 @@ class ClientsDict(UserDict[str, ClientData]):
def get_client_data(self, name: str, node: str) -> tuple[str, str]: def get_client_data(self, name: str, node: str) -> tuple[str, str]:
client_data = self.get(node) client_data = self.get(node)
if client_data is None: if client_data is None:
return _('Unknown'), 'xmpp-client-unknown' return _("Unknown"), "xmpp-client-unknown"
if client_data.variations is None: if client_data.variations is None:
assert client_data.default is not None assert client_data.default is not None
client_name, icon_name = client_data.default client_name, icon_name = client_data.default
return client_name, f'xmpp-client-{icon_name}' return client_name, f"xmpp-client-{icon_name}"
variations = get_variations(name) variations = get_variations(name)
for var in variations: for var in variations:
try: try:
return var, f'xmpp-client-{client_data.variations[var]}' return var, f"xmpp-client-{client_data.variations[var]}"
except KeyError: except KeyError:
pass pass
assert client_data.default is not None assert client_data.default is not None
client_name, icon_name = client_data.default client_name, icon_name = client_data.default
return client_name, f'xmpp-client-{icon_name}' return client_name, f"xmpp-client-{icon_name}"
# ClientData( # ClientData(
@@ -73,137 +73,159 @@ class ClientsDict(UserDict[str, ClientData]):
# ) # )
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
CLIENTS = ClientsDict({ CLIENTS = ClientsDict(
'http://gajim.org': ClientData(('Gajim', 'gajim')), {
'https://gajim.org': ClientData(('Gajim', 'gajim')), "http://gajim.org": ClientData(("Gajim", "gajim")),
'http://conversations.im': ClientData( "https://gajim.org": ClientData(("Gajim", "gajim")),
default=('Conversations', 'conversations'), "http://conversations.im": ClientData(
variations={'Conversations Legacy': 'conversations-legacy'} default=("Conversations", "conversations"),
), variations={"Conversations Legacy": "conversations-legacy"},
'http://jabber.pix-art.de': ClientData(('Pix-Art Messenger', 'pixart')), ),
'http://blabber.im': ClientData(('blabber.im', 'blabber')), "http://jabber.pix-art.de": ClientData(("Pix-Art Messenger", "pixart")),
'http://monocles.de': ClientData(('monocles chat', 'monocles-chat')), "http://blabber.im": ClientData(("blabber.im", "blabber")),
'http://pidgin.im/': ClientData(('Pidgin', 'pidgin')), "http://monocles.de": ClientData(("monocles chat", "monocles-chat")),
'https://poez.io': ClientData(('Poezio', 'poezio')), "http://pidgin.im/": ClientData(("Pidgin", "pidgin")),
'https://yaxim.org/': ClientData(('yaxim', 'yaxim')), "https://poez.io": ClientData(("Poezio", "poezio")),
'https://yaxim.org/bruno/': ClientData(('Bruno', 'bruno')), "https://yaxim.org/": ClientData(("yaxim", "yaxim")),
'http://mcabber.com/caps': ClientData(('MCabber', 'mcabber')), "https://yaxim.org/bruno/": ClientData(("Bruno", "bruno")),
'http://psi-plus.com': ClientData(('Psi+', 'psiplus')), "http://mcabber.com/caps": ClientData(("MCabber", "mcabber")),
'https://psi-plus.com': ClientData(('Psi+', 'psiplus')), "http://psi-plus.com": ClientData(("Psi+", "psiplus")),
'https://dino.im': ClientData(('Dino', 'dino')), "https://psi-plus.com": ClientData(("Psi+", "psiplus")),
'http://monal.im/': ClientData(('Monal', 'monal')), "https://dino.im": ClientData(("Dino", "dino")),
'http://slixmpp.com/ver/1.2.4': ClientData(('Bot', 'bot')), "http://monal.im/": ClientData(("Monal", "monal")),
'http://slixmpp.com/ver/1.3.0': ClientData(('Bot', 'bot')), "http://slixmpp.com/ver/1.2.4": ClientData(("Bot", "bot")),
'https://www.xabber.com/': ClientData(('Xabber', 'xabber')), "http://slixmpp.com/ver/1.3.0": ClientData(("Bot", "bot")),
'http://www.profanity.im': ClientData(('Profanity', 'profanity')), "https://www.xabber.com/": ClientData(("Xabber", "xabber")),
'http://swift.im': ClientData(('Swift', 'swift')), "http://www.profanity.im": ClientData(("Profanity", "profanity")),
'https://salut-a-toi.org': ClientData(('Salut à Toi', 'sat')), "http://swift.im": ClientData(("Swift", "swift")),
'https://conversejs.org': ClientData(('Converse', 'conversejs')), "https://salut-a-toi.org": ClientData(("Salut à Toi", "sat")),
'http://bitlbee.org/xmpp/caps': ClientData(('BitlBee', 'bitlbee')), "https://conversejs.org": ClientData(("Converse", "conversejs")),
'http://tkabber.jabber.ru/': ClientData(('Tkabber', 'tkabber')), "http://bitlbee.org/xmpp/caps": ClientData(("BitlBee", "bitlbee")),
'http://miranda-ng.org/caps': ClientData(('Miranda NG', 'miranda_ng')), "http://tkabber.jabber.ru/": ClientData(("Tkabber", "tkabber")),
'http://www.adium.im/': ClientData(('Adium', 'adium')), "http://miranda-ng.org/caps": ClientData(("Miranda NG", "miranda_ng")),
'http://www.adiumx.com/caps': ClientData(('Adium', 'adium')), "http://www.adium.im/": ClientData(("Adium", "adium")),
'http://www.adiumx.com': ClientData(('Adium', 'adium')), "http://www.adiumx.com/caps": ClientData(("Adium", "adium")),
'http://aqq.eu/': ClientData(('Aqq', 'aqq')), "http://www.adiumx.com": ClientData(("Adium", "adium")),
'http://www.asterisk.org/xmpp/client/caps': ClientData(('Asterisk', 'asterisk')), "http://aqq.eu/": ClientData(("Aqq", "aqq")),
'http://ayttm.souceforge.net/caps': ClientData(('Ayttm', 'ayttm')), "http://www.asterisk.org/xmpp/client/caps": ClientData(
'http://www.barobin.com/caps': ClientData(('Bayanicq', 'bayanicq')), ("Asterisk", "asterisk")
'http://simpleapps.ru/caps#blacksmith': ClientData(('Blacksmith', 'bot')), ),
'http://blacksmith-2.googlecode.com/svn/': ClientData(('Blacksmith-2', 'bot')), "http://ayttm.souceforge.net/caps": ClientData(("Ayttm", "ayttm")),
'http://coccinella.sourceforge.net/protocol/caps': ClientData(('Coccinella', 'coccinella')), "http://www.barobin.com/caps": ClientData(("Bayanicq", "bayanicq")),
'http://digsby.com/caps': ClientData(('Digsby', 'digsby')), "http://simpleapps.ru/caps#blacksmith": ClientData(("Blacksmith", "bot")),
'http://emacs-jabber.sourceforge.net': ClientData(('Emacs Jabber Client', 'emacs')), "http://blacksmith-2.googlecode.com/svn/": ClientData(("Blacksmith-2", "bot")),
'http://emess.eqx.su/caps': ClientData(('Emess', 'emess')), "http://coccinella.sourceforge.net/protocol/caps": ClientData(
'http://live.gnome.org/empathy/caps': ClientData(('Empathy', 'telepathy.freedesktop.org')), ("Coccinella", "coccinella")
'http://eqo.com/': ClientData(('Eqo', 'libpurple')), ),
'http://exodus.jabberstudio.org/caps': ClientData(('Exodus', 'exodus')), "http://digsby.com/caps": ClientData(("Digsby", "digsby")),
'http://fatal-bot.spb.ru/caps': ClientData(('Fatal-bot', 'bot')), "http://emacs-jabber.sourceforge.net": ClientData(
'http://svn.posix.ru/fatal-bot/trunk': ClientData(('Fatal-bot', 'bot')), ("Emacs Jabber Client", "emacs")
'http://isida.googlecode.com': ClientData(('Isida', 'isida-bot')), ),
'http://isida-bot.com': ClientData(('Isida', 'isida-bot')), "http://emess.eqx.su/caps": ClientData(("Emess", "emess")),
'http://jabga.ru': ClientData(('Fin jabber', 'fin')), "http://live.gnome.org/empathy/caps": ClientData(
'http://chat.freize.org/caps': ClientData(('Freize', 'freize')), ("Empathy", "telepathy.freedesktop.org")
'http://gabber.sourceforge.net': ClientData(('Gabber', 'gabber')), ),
'http://glu.net/': ClientData(('Glu', 'glu')), "http://eqo.com/": ClientData(("Eqo", "libpurple")),
'http://mail.google.com/xmpp/client/caps': ClientData(('GMail', 'google.com')), "http://exodus.jabberstudio.org/caps": ClientData(("Exodus", "exodus")),
'http://www.android.com/gtalk/client/caps': ClientData(('GTalk', 'talk.google.com')), "http://fatal-bot.spb.ru/caps": ClientData(("Fatal-bot", "bot")),
'talk.google.com': ClientData(('GTalk', 'talk.google.com')), "http://svn.posix.ru/fatal-bot/trunk": ClientData(("Fatal-bot", "bot")),
'http://talkgadget.google.com/client/caps': ClientData(('GTalk', 'google')), "http://isida.googlecode.com": ClientData(("Isida", "isida-bot")),
'http://talk.google.com/xmpp/bot/caps': ClientData(('GTalk', 'google')), "http://isida-bot.com": ClientData(("Isida", "isida-bot")),
'http://aspro.users.ru/historian-bot/': ClientData(('Historian-bot', 'bot')), "http://jabga.ru": ClientData(("Fin jabber", "fin")),
'http://www.apple.com/ichat/caps': ClientData(('IChat', 'ichat')), "http://chat.freize.org/caps": ClientData(("Freize", "freize")),
'http://instantbird.com/': ClientData(('Instantbird', 'instantbird')), "http://gabber.sourceforge.net": ClientData(("Gabber", "gabber")),
'http://j-tmb.ru/caps': ClientData(('J-tmb', 'bot')), "http://glu.net/": ClientData(("Glu", "glu")),
'http://jabbroid.akuz.de': ClientData(('Jabbroid', 'android')), "http://mail.google.com/xmpp/client/caps": ClientData(("GMail", "google.com")),
'http://jabbroid.akuz.de/caps': ClientData(('Jabbroid', 'android')), "http://www.android.com/gtalk/client/caps": ClientData(
'http://dev.jabbim.cz/jabbim/caps': ClientData(('Jabbim', 'jabbim')), ("GTalk", "talk.google.com")
'http://jabbrik.ru/caps': ClientData(('Jabbrik', 'bot')), ),
'http://jabrvista.net.ru': ClientData(('Jabvista', 'bot')), "talk.google.com": ClientData(("GTalk", "talk.google.com")),
'http://jajc.jrudevels.org/caps': ClientData(('JAJC', 'jajc')), "http://talkgadget.google.com/client/caps": ClientData(("GTalk", "google")),
'http://qabber.ru/jame-bot': ClientData(('Jame-bot', 'bot')), "http://talk.google.com/xmpp/bot/caps": ClientData(("GTalk", "google")),
'https://www.jappix.com/': ClientData(('Jappix', 'jappix')), "http://aspro.users.ru/historian-bot/": ClientData(("Historian-bot", "bot")),
'http://japyt.googlecode.com': ClientData(('Japyt', 'japyt')), "http://www.apple.com/ichat/caps": ClientData(("IChat", "ichat")),
'http://jasmineicq.ru/caps': ClientData(('Jasmine', 'jasmine')), "http://instantbird.com/": ClientData(("Instantbird", "instantbird")),
'http://jimm.net.ru/caps': ClientData(('Jimm', 'jimm-aspro')), "http://j-tmb.ru/caps": ClientData(("J-tmb", "bot")),
'http://jitsi.org': ClientData(('Jitsi', 'jitsi')), "http://jabbroid.akuz.de": ClientData(("Jabbroid", "android")),
'http://jtalk.ustyugov.net/caps': ClientData(('Jtalk', 'jtalk')), "http://jabbroid.akuz.de/caps": ClientData(("Jabbroid", "android")),
'http://pjc.googlecode.com/caps': ClientData(('Jubo', 'jubo')), "http://dev.jabbim.cz/jabbim/caps": ClientData(("Jabbim", "jabbim")),
'http://juick.com/caps': ClientData(('Juick', 'juick')), "http://jabbrik.ru/caps": ClientData(("Jabbrik", "bot")),
'http://kopete.kde.org/jabber/caps': ClientData(('Kopete', 'kopete')), "http://jabrvista.net.ru": ClientData(("Jabvista", "bot")),
'http://bluendo.com/protocol/caps': ClientData(('Lampiro', 'lampiro')), "http://jajc.jrudevels.org/caps": ClientData(("JAJC", "jajc")),
'http://lytgeygen.ru/caps': ClientData(('Lytgeygen', 'bot')), "http://qabber.ru/jame-bot": ClientData(("Jame-bot", "bot")),
'http://agent.mail.ru/caps': ClientData(('Mailruagent', 'mailruagent')), "https://www.jappix.com/": ClientData(("Jappix", "jappix")),
'http://agent.mail.ru/': ClientData(('Mailruagent', 'mailruagent')), "http://japyt.googlecode.com": ClientData(("Japyt", "japyt")),
'http://tomclaw.com/mandarin_im/caps': ClientData(('Mandarin', 'mandarin')), "http://jasmineicq.ru/caps": ClientData(("Jasmine", "jasmine")),
'http://mchat.mgslab.com/': ClientData(('Mchat', 'mchat')), "http://jimm.net.ru/caps": ClientData(("Jimm", "jimm-aspro")),
'https://www.meebo.com/': ClientData(('Meebo', 'meebo')), "http://jitsi.org": ClientData(("Jitsi", "jitsi")),
'http://megafonvolga.ru/': ClientData(('Megafon', 'megafon')), "http://jtalk.ustyugov.net/caps": ClientData(("Jtalk", "jtalk")),
'http://miranda-im.org/caps': ClientData(('Miranda', 'miranda')), "http://pjc.googlecode.com/caps": ClientData(("Jubo", "jubo")),
'https://movim.eu/': ClientData(('Movim', 'movim')), "http://juick.com/caps": ClientData(("Juick", "juick")),
'http://moxl.movim.eu/': ClientData(('Movim', 'movim')), "http://kopete.kde.org/jabber/caps": ClientData(("Kopete", "kopete")),
'nimbuzz:caps': ClientData(('Nimbuzz', 'nimbuzz')), "http://bluendo.com/protocol/caps": ClientData(("Lampiro", "lampiro")),
'http://nimbuzz.com/caps': ClientData(('Nimbuzz', 'nimbuzz')), "http://lytgeygen.ru/caps": ClientData(("Lytgeygen", "bot")),
'http://home.gna.org/': ClientData(('Omnipresence', 'omnipresence')), "http://agent.mail.ru/caps": ClientData(("Mailruagent", "mailruagent")),
'http://oneteam.im/caps': ClientData(('OneTeam', 'oneteamiphone')), "http://agent.mail.ru/": ClientData(("Mailruagent", "mailruagent")),
'http://www.process-one.net/en/solutions/oneteam_iphone/': ClientData(('OneTeam-IPhone', 'oneteamiphone')), "http://tomclaw.com/mandarin_im/caps": ClientData(("Mandarin", "mandarin")),
'rss@isida-bot.com': ClientData(('Osiris', 'osiris')), "http://mchat.mgslab.com/": ClientData(("Mchat", "mchat")),
'http://chat.ovi.com/caps': ClientData(('Ovi-chat', 'ovi-chat')), "https://www.meebo.com/": ClientData(("Meebo", "meebo")),
'http://opensource.palm.com/packages.html': ClientData(('Palm', 'palm')), "http://megafonvolga.ru/": ClientData(("Megafon", "megafon")),
'http://palringo.com/caps': ClientData(('Palringo', 'palringo')), "http://miranda-im.org/caps": ClientData(("Miranda", "miranda")),
'http://pandion.im/': ClientData(('Pandion', 'pandion')), "https://movim.eu/": ClientData(("Movim", "movim")),
'http://pigeon.vpro.ru/caps': ClientData(('Pigeon', 'pigeon')), "http://moxl.movim.eu/": ClientData(("Movim", "movim")),
'psto@psto.net': ClientData(('Psto', 'psto')), "nimbuzz:caps": ClientData(("Nimbuzz", "nimbuzz")),
'http://qq-im.com/caps': ClientData(('QQ', 'qq')), "http://nimbuzz.com/caps": ClientData(("Nimbuzz", "nimbuzz")),
'http://qq.com/caps': ClientData(('QQ', 'qq')), "http://home.gna.org/": ClientData(("Omnipresence", "omnipresence")),
'http://2010.qip.ru/caps': ClientData(('Qip', 'qip')), "http://oneteam.im/caps": ClientData(("OneTeam", "oneteamiphone")),
'http://qip.ru/caps': ClientData(('Qip', 'qip')), "http://www.process-one.net/en/solutions/oneteam_iphone/": ClientData(
'http://qip.ru/caps?QIP': ClientData(('Qip', 'qip')), ("OneTeam-IPhone", "oneteamiphone")
'http://pda.qip.ru/caps': ClientData(('Qip-PDA', 'qippda')), ),
'http://qutim.org': ClientData(('QutIM', 'qutim')), "rss@isida-bot.com": ClientData(("Osiris", "osiris")),
'http://qutim.org/': ClientData(('QutIM', 'qutim')), "http://chat.ovi.com/caps": ClientData(("Ovi-chat", "ovi-chat")),
'http://apps.radio-t.com/caps': ClientData(('Radio-t', 'radio-t')), "http://opensource.palm.com/packages.html": ClientData(("Palm", "palm")),
'http://sim-im.org/caps': ClientData(('Sim', 'sim')), "http://palringo.com/caps": ClientData(("Palringo", "palringo")),
'http://www.lonelycatgames.com/slick/caps': ClientData(('Slick', 'slick')), "http://pandion.im/": ClientData(("Pandion", "pandion")),
'http://snapi-bot.googlecode.com/caps': ClientData(('Snapi-bot', 'bot')), "http://pigeon.vpro.ru/caps": ClientData(("Pigeon", "pigeon")),
'http://www.igniterealtime.org/project/spark/caps': ClientData(('Spark', 'spark')), "psto@psto.net": ClientData(("Psto", "psto")),
'http://spectrum.im/': ClientData(('Spectrum', 'spectrum')), "http://qq-im.com/caps": ClientData(("QQ", "qq")),
'http://storm-bot.googlecode.com/svn/trunk': ClientData(('Storm-bot', 'bot')), "http://qq.com/caps": ClientData(("QQ", "qq")),
'http://jabber-net.ru/caps/talisman-bot': ClientData(('Talisman-bot', 'bot')), "http://2010.qip.ru/caps": ClientData(("Qip", "qip")),
'http://jabber-net.ru/talisman-bot/caps': ClientData(('Talisman-bot', 'bot')), "http://qip.ru/caps": ClientData(("Qip", "qip")),
'http://www.google.com/xmpp/client/caps': ClientData(('Talkonaut', 'talkonaut')), "http://qip.ru/caps?QIP": ClientData(("Qip", "qip")),
'http://telepathy.freedesktop.org/caps': ClientData(('SlicTelepathyk', 'telepathy.freedesktop.org')), "http://pda.qip.ru/caps": ClientData(("Qip-PDA", "qippda")),
'http://tigase.org/messenger': ClientData(('Tigase', 'tigase')), "http://qutim.org": ClientData(("QutIM", "qutim")),
'http://trillian.im/caps': ClientData(('Trillian', 'trillian')), "http://qutim.org/": ClientData(("QutIM", "qutim")),
'http://vacuum-im.googlecode.com': ClientData(('Vacuum', 'vacuum')), "http://apps.radio-t.com/caps": ClientData(("Radio-t", "radio-t")),
'http://code.google.com/p/vacuum-im/': ClientData(('Vacuum', 'vacuum')), "http://sim-im.org/caps": ClientData(("Sim", "sim")),
'http://witcher-team.ucoz.ru/': ClientData(('Witcher', 'bot')), "http://www.lonelycatgames.com/slick/caps": ClientData(("Slick", "slick")),
'http://online.yandex.ru/caps': ClientData(('Yaonline', 'yaonline')), "http://snapi-bot.googlecode.com/caps": ClientData(("Snapi-bot", "bot")),
'http://www.igniterealtime.org/projects/smack/': ClientData(('Xabber', 'xabber')), "http://www.igniterealtime.org/project/spark/caps": ClientData(
'http://www.xfire.com/': ClientData(('Xfire', 'xfire')), ("Spark", "spark")
'http://www.xfire.com/caps': ClientData(('Xfire', 'xfire')), ),
'http://xu-6.jabbrik.ru/caps': ClientData(('XU-6', 'bot')), "http://spectrum.im/": ClientData(("Spectrum", "spectrum")),
}) "http://storm-bot.googlecode.com/svn/trunk": ClientData(("Storm-bot", "bot")),
"http://jabber-net.ru/caps/talisman-bot": ClientData(("Talisman-bot", "bot")),
"http://jabber-net.ru/talisman-bot/caps": ClientData(("Talisman-bot", "bot")),
"http://www.google.com/xmpp/client/caps": ClientData(
("Talkonaut", "talkonaut")
),
"http://telepathy.freedesktop.org/caps": ClientData(
("SlicTelepathyk", "telepathy.freedesktop.org")
),
"http://tigase.org/messenger": ClientData(("Tigase", "tigase")),
"http://trillian.im/caps": ClientData(("Trillian", "trillian")),
"http://vacuum-im.googlecode.com": ClientData(("Vacuum", "vacuum")),
"http://code.google.com/p/vacuum-im/": ClientData(("Vacuum", "vacuum")),
"http://witcher-team.ucoz.ru/": ClientData(("Witcher", "bot")),
"http://online.yandex.ru/caps": ClientData(("Yaonline", "yaonline")),
"http://www.igniterealtime.org/projects/smack/": ClientData(
("Xabber", "xabber")
),
"http://www.xfire.com/": ClientData(("Xfire", "xfire")),
"http://www.xfire.com/caps": ClientData(("Xfire", "xfire")),
"http://xu-6.jabbrik.ru/caps": ClientData(("XU-6", "bot")),
}
)
# pylint: enable=too-many-lines # pylint: enable=too-many-lines

View File

@@ -18,42 +18,39 @@ from __future__ import annotations
from typing import cast from typing import cast
import logging import logging
from pathlib import Path
from functools import partial from functools import partial
from pathlib import Path
from gi.repository import Gtk from gi.repository import Gtk
from nbxmpp.structs import DiscoInfo from nbxmpp.structs import DiscoInfo
from gajim.common import app from gajim.common import app
from gajim.common.modules.contacts import GroupchatParticipant from gajim.common.modules.contacts import GroupchatParticipant
from gajim.common.modules.contacts import ResourceContact from gajim.common.modules.contacts import ResourceContact
from gajim.gtk.util import load_icon_surface
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from gajim.gtk.util import load_icon_surface
from clients_icons import clients from clients_icons import clients
from clients_icons.config_dialog import ClientsIconsConfigDialog from clients_icons.config_dialog import ClientsIconsConfigDialog
log = logging.getLogger('gajim.p.client_icons') log = logging.getLogger("gajim.p.client_icons")
class ClientsIconsPlugin(GajimPlugin): class ClientsIconsPlugin(GajimPlugin):
def init(self) -> None: def init(self) -> None:
self.description = _('Shows client icons in your contact list' self.description = _("Shows client icons in your contact list" " in a tooltip.")
' in a tooltip.')
self.config_dialog = partial(ClientsIconsConfigDialog, self) self.config_dialog = partial(ClientsIconsConfigDialog, self)
self.gui_extension_points = { self.gui_extension_points = {
'roster_tooltip_resource_populate': ( "roster_tooltip_resource_populate": (
self._roster_tooltip_resource_populate, self._roster_tooltip_resource_populate,
None), None,
),
} }
self.config_default_values = { self.config_default_values = {
'show_unknown_icon': (True, ''), "show_unknown_icon": (True, ""),
} }
_icon_theme = Gtk.IconTheme.get_default() _icon_theme = Gtk.IconTheme.get_default()
@@ -63,15 +60,13 @@ class ClientsIconsPlugin(GajimPlugin):
@staticmethod @staticmethod
def _get_client_identity_name(disco_info: DiscoInfo) -> str | None: def _get_client_identity_name(disco_info: DiscoInfo) -> str | None:
for identity in disco_info.identities: for identity in disco_info.identities:
if identity.category == 'client': if identity.category == "client":
return identity.name return identity.name
return None return None
def _get_image_and_client_name(self, def _get_image_and_client_name(
contact: self, contact: GroupchatParticipant | ResourceContact, _widget: Gtk.Widget
GroupchatParticipant | ResourceContact, ) -> tuple[Gtk.Image, str] | None:
_widget: Gtk.Widget
) -> tuple[Gtk.Image, str] | None:
disco_info = app.storage.cache.get_last_disco_info(contact.jid) disco_info = app.storage.cache.get_last_disco_info(contact.jid)
if disco_info is None: if disco_info is None:
@@ -80,18 +75,17 @@ class ClientsIconsPlugin(GajimPlugin):
if disco_info.node is None: if disco_info.node is None:
return None return None
node = disco_info.node.split('#')[0] node = disco_info.node.split("#")[0]
client_name = self._get_client_identity_name(disco_info) client_name = self._get_client_identity_name(disco_info)
log.info('Lookup client: %s %s', client_name, node) log.info("Lookup client: %s %s", client_name, node)
client_name, icon_name = clients.get_data(client_name, node) client_name, icon_name = clients.get_data(client_name, node)
surface = load_icon_surface(icon_name) surface = load_icon_surface(icon_name)
return Gtk.Image.new_from_surface(surface), client_name return Gtk.Image.new_from_surface(surface), client_name
def _roster_tooltip_resource_populate(self, def _roster_tooltip_resource_populate(
resource_box: Gtk.Box, self, resource_box: Gtk.Box, resource: ResourceContact
resource: ResourceContact ) -> None:
) -> None:
result = self._get_image_and_client_name(resource, resource_box) result = self._get_image_and_client_name(resource, resource_box)
if result is None: if result is None:
@@ -100,8 +94,9 @@ class ClientsIconsPlugin(GajimPlugin):
image, client_name = result image, client_name = result
label = Gtk.Label(label=client_name) label = Gtk.Label(label=client_name)
client_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, client_box = Gtk.Box(
halign=Gtk.Align.START) orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.START
)
client_box.add(image) client_box.add(image)
client_box.add(label) client_box.add(label)

View File

@@ -20,12 +20,11 @@ from typing import TYPE_CHECKING
from gi.repository import Gtk from gi.repository import Gtk
from gajim.plugins.plugins_i18n import _
from gajim.gtk.settings import SettingsDialog
from gajim.gtk.const import Setting from gajim.gtk.const import Setting
from gajim.gtk.const import SettingKind from gajim.gtk.const import SettingKind
from gajim.gtk.const import SettingType from gajim.gtk.const import SettingType
from gajim.gtk.settings import SettingsDialog
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING: if TYPE_CHECKING:
from .clients_icons import ClientsIconsPlugin from .clients_icons import ClientsIconsPlugin
@@ -36,20 +35,24 @@ class ClientsIconsConfigDialog(SettingsDialog):
self.plugin = plugin self.plugin = plugin
settings = [ settings = [
Setting(SettingKind.SWITCH, Setting(
_('Show Icon for Unknown Clients'), SettingKind.SWITCH,
SettingType.VALUE, _("Show Icon for Unknown Clients"),
self.plugin.config['show_unknown_icon'], SettingType.VALUE,
callback=self._on_setting, self.plugin.config["show_unknown_icon"],
data='show_unknown_icon'), callback=self._on_setting,
data="show_unknown_icon",
),
] ]
SettingsDialog.__init__(self, SettingsDialog.__init__(
parent, self,
_('Clients Icons Configuration'), parent,
Gtk.DialogFlags.MODAL, _("Clients Icons Configuration"),
settings, Gtk.DialogFlags.MODAL,
'') settings,
"",
)
def _on_setting(self, value: Any, data: Any) -> None: def _on_setting(self, value: Any, data: Any) -> None:
self.plugin.config[data] = value self.plugin.config[data] = value

View File

@@ -20,11 +20,10 @@ from typing import TYPE_CHECKING
from gi.repository import Gtk from gi.repository import Gtk
from gajim.gtk.settings import SettingsDialog
from gajim.gtk.const import Setting from gajim.gtk.const import Setting
from gajim.gtk.const import SettingKind from gajim.gtk.const import SettingKind
from gajim.gtk.const import SettingType from gajim.gtk.const import SettingType
from gajim.gtk.settings import SettingsDialog
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -32,52 +31,56 @@ if TYPE_CHECKING:
class LengthNotifierConfigDialog(SettingsDialog): class LengthNotifierConfigDialog(SettingsDialog):
def __init__(self, def __init__(self, plugin: LengthNotifierPlugin, parent: Gtk.Window) -> None:
plugin: LengthNotifierPlugin,
parent: Gtk.Window
) -> None:
self.plugin = plugin self.plugin = plugin
jids = self.plugin.config['JIDS'] or '' jids = self.plugin.config["JIDS"] or ""
if isinstance(jids, list): if isinstance(jids, list):
# Gajim 1.0 stored this as list[str] # Gajim 1.0 stored this as list[str]
jids = ','.join(jids) jids = ",".join(jids)
settings = [ settings = [
Setting(SettingKind.SPIN, Setting(
_('Message Length'), SettingKind.SPIN,
SettingType.VALUE, _("Message Length"),
str(self.plugin.config['MESSAGE_WARNING_LENGTH']), SettingType.VALUE,
callback=self._on_setting, str(self.plugin.config["MESSAGE_WARNING_LENGTH"]),
data='MESSAGE_WARNING_LENGTH', callback=self._on_setting,
desc=_('Message length at which the highlight is shown'), data="MESSAGE_WARNING_LENGTH",
props={'range_': (1, 1000, 1)}, desc=_("Message length at which the highlight is shown"),
), props={"range_": (1, 1000, 1)},
Setting(SettingKind.COLOR, ),
_('Color'), Setting(
SettingType.VALUE, SettingKind.COLOR,
self.plugin.config['WARNING_COLOR'], _("Color"),
callback=self._on_setting, SettingType.VALUE,
data='WARNING_COLOR', self.plugin.config["WARNING_COLOR"],
desc=_('Highlight color for the message input'), callback=self._on_setting,
), data="WARNING_COLOR",
Setting(SettingKind.ENTRY, desc=_("Highlight color for the message input"),
_('Selected Addresses'), ),
SettingType.VALUE, Setting(
jids, SettingKind.ENTRY,
callback=self._on_setting, _("Selected Addresses"),
data='JIDS', SettingType.VALUE,
desc=_('Enable the plugin for selected XMPP addresses ' jids,
'only (comma separated)'), callback=self._on_setting,
), data="JIDS",
] desc=_(
"Enable the plugin for selected XMPP addresses "
"only (comma separated)"
),
),
]
SettingsDialog.__init__(self, SettingsDialog.__init__(
parent, self,
_('Length Notifier Configuration'), parent,
Gtk.DialogFlags.MODAL, _("Length Notifier Configuration"),
settings, Gtk.DialogFlags.MODAL,
'') settings,
"",
)
def _on_setting(self, value: Any, data: Any) -> None: def _on_setting(self, value: Any, data: Any) -> None:
if isinstance(value, str): if isinstance(value, str):

View File

@@ -12,14 +12,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
''' """
Message length notifier plugin. Message length notifier plugin.
:author: Mateusz Biliński <mateusz@bilinski.it> :author: Mateusz Biliński <mateusz@bilinski.it>
:since: 1st June 2008 :since: 1st June 2008
:copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it> :copyright: Copyright (2008) Mateusz Biliński <mateusz@bilinski.it>
:license: GPL :license: GPL
''' """
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
@@ -31,48 +31,50 @@ from functools import partial
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GObject from gi.repository import GObject
from gi.repository import Gtk from gi.repository import Gtk
from nbxmpp.protocol import JID from nbxmpp.protocol import JID
from gajim.common import app from gajim.common import app
from gajim.common import types from gajim.common import types
from gajim.gtk.message_actions_box import MessageActionsBox from gajim.gtk.message_actions_box import MessageActionsBox
from gajim.gtk.message_input import MessageInputTextView from gajim.gtk.message_input import MessageInputTextView
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.gajimplugin import GajimPluginConfig from gajim.plugins.gajimplugin import GajimPluginConfig
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from length_notifier.config_dialog import LengthNotifierConfigDialog from length_notifier.config_dialog import LengthNotifierConfigDialog
log = logging.getLogger('gajim.p.length_notifier') log = logging.getLogger("gajim.p.length_notifier")
class LengthNotifierPlugin(GajimPlugin): class LengthNotifierPlugin(GajimPlugin):
def init(self) -> None: def init(self) -> None:
self.description = _('Highlights the chat windows message input if ' self.description = _(
'a specified message length is exceeded.') "Highlights the chat windows message input if "
"a specified message length is exceeded."
)
self.config_dialog = partial(LengthNotifierConfigDialog, self) self.config_dialog = partial(LengthNotifierConfigDialog, self)
self.gui_extension_points = { self.gui_extension_points = {
'message_actions_box': (self._message_actions_box_created, None), "message_actions_box": (self._message_actions_box_created, None),
'switch_contact': (self._on_switch_contact, None) "switch_contact": (self._on_switch_contact, None),
} }
self.config_default_values = { self.config_default_values = {
'MESSAGE_WARNING_LENGTH': ( "MESSAGE_WARNING_LENGTH": (
140, 140,
'Message length at which the highlight is shown'), "Message length at which the highlight is shown",
'WARNING_COLOR': ( ),
'rgb(240, 220, 60)', "WARNING_COLOR": (
'Highlight color for the message input'), "rgb(240, 220, 60)",
'JIDS': ( "Highlight color for the message input",
'', ),
'Enable the plugin for selected XMPP addresses ' "JIDS": (
'only (comma separated)') "",
} "Enable the plugin for selected XMPP addresses "
"only (comma separated)",
),
}
self._message_action_box = None self._message_action_box = None
self._actions_box_widget = None self._actions_box_widget = None
@@ -92,14 +94,12 @@ class LengthNotifierPlugin(GajimPlugin):
def _create_counter(self) -> None: def _create_counter(self) -> None:
assert self._message_action_box is not None assert self._message_action_box is not None
assert self._actions_box_widget is not None assert self._actions_box_widget is not None
self._counter = Counter(self._message_action_box.msg_textview, self._counter = Counter(self._message_action_box.msg_textview, self.config)
self.config)
self._actions_box_widget.pack_end(self._counter, False, False, 0) self._actions_box_widget.pack_end(self._counter, False, False, 0)
def _message_actions_box_created(self, def _message_actions_box_created(
message_actions_box: MessageActionsBox, self, message_actions_box: MessageActionsBox, gtk_box: Gtk.Box
gtk_box: Gtk.Box ) -> None:
) -> None:
self._message_action_box = message_actions_box self._message_action_box = message_actions_box
self._actions_box_widget = gtk_box self._actions_box_widget = gtk_box
@@ -117,14 +117,13 @@ class LengthNotifierPlugin(GajimPlugin):
class Counter(Gtk.Label): class Counter(Gtk.Label):
def __init__(self, def __init__(
message_input: MessageInputTextView, self, message_input: MessageInputTextView, config: GajimPluginConfig
config: GajimPluginConfig ) -> None:
) -> None:
Gtk.Label.__init__(self) Gtk.Label.__init__(self)
self.set_tooltip_text(_('Number of typed characters')) self.set_tooltip_text(_("Number of typed characters"))
self.get_style_context().add_class('dim-label') self.get_style_context().add_class("dim-label")
self._config = config self._config = config
@@ -135,47 +134,48 @@ class Counter(Gtk.Label):
self._inverted_color = None self._inverted_color = None
self._textview = message_input self._textview = message_input
self._signal_id = self._textview.connect('buffer-changed', self._update) self._signal_id = self._textview.connect("buffer-changed", self._update)
self._provider = None self._provider = None
self._parse_config() self._parse_config()
self._set_css() self._set_css()
self.connect('destroy', self._on_destroy) self.connect("destroy", self._on_destroy)
def _on_destroy(self, _widget: Counter) -> None: def _on_destroy(self, _widget: Counter) -> None:
self._context.remove_class('length-warning') self._context.remove_class("length-warning")
assert self._signal_id is not None assert self._signal_id is not None
if GObject.signal_handler_is_connected( if GObject.signal_handler_is_connected(self._textview, self._signal_id):
self._textview, self._signal_id):
self._textview.disconnect(self._signal_id) self._textview.disconnect(self._signal_id)
app.check_finalize(self) app.check_finalize(self)
def _parse_config(self) -> None: def _parse_config(self) -> None:
self._max_length = cast(int, self._config['MESSAGE_WARNING_LENGTH']) self._max_length = cast(int, self._config["MESSAGE_WARNING_LENGTH"])
self._color = cast(str, self._config['WARNING_COLOR']) self._color = cast(str, self._config["WARNING_COLOR"])
rgba = Gdk.RGBA() rgba = Gdk.RGBA()
rgba.parse(self._color) rgba.parse(self._color)
red = int(255 - rgba.red * 255) red = int(255 - rgba.red * 255)
green = int(255 - rgba.green * 255) green = int(255 - rgba.green * 255)
blue = int(255 - rgba.blue * 255) blue = int(255 - rgba.blue * 255)
self._inverted_color = f'rgb({red}, {green}, {blue})' self._inverted_color = f"rgb({red}, {green}, {blue})"
def _set_css(self) -> None: def _set_css(self) -> None:
self._context = self._textview.get_style_context() self._context = self._textview.get_style_context()
if self._provider is not None: if self._provider is not None:
self._context.remove_provider(self._provider) self._context.remove_provider(self._provider)
css = ''' css = """
.length-warning > * { .length-warning > * {
color: %s; color: %s;
background-color: %s; background-color: %s;
} }
''' % (self._inverted_color, self._color) """ % (
self._inverted_color,
self._color,
)
self._provider = Gtk.CssProvider() self._provider = Gtk.CssProvider()
self._provider.load_from_data(bytes(css.encode())) self._provider.load_from_data(bytes(css.encode()))
self._context.add_provider( self._context.add_provider(self._provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def _set_count(self, count: int) -> None: def _set_count(self, count: int) -> None:
self.set_label(str(count)) self.set_label(str(count))
@@ -196,38 +196,38 @@ class Counter(Gtk.Label):
len_text = len(text) len_text = len(text)
self._set_count(len_text) self._set_count(len_text)
if len_text > self._max_length: if len_text > self._max_length:
self._context.add_class('length-warning') self._context.add_class("length-warning")
else: else:
self._context.remove_class('length-warning') self._context.remove_class("length-warning")
else: else:
self._set_count(0) self._set_count(0)
self._context.remove_class('length-warning') self._context.remove_class("length-warning")
return False return False
def _jid_allowed(self, current_jid: JID) -> bool: def _jid_allowed(self, current_jid: JID) -> bool:
jids = self._config['JIDS'] jids = self._config["JIDS"]
if isinstance(jids, list): if isinstance(jids, list):
# Gajim 1.0 stored this as list[str] # Gajim 1.0 stored this as list[str]
jids = ','.join(jids) jids = ",".join(jids)
assert isinstance(jids, str) assert isinstance(jids, str)
if not len(jids): if not len(jids):
# Not restricted to any JIDs # Not restricted to any JIDs
return True return True
allowed_jids = jids.split(',') allowed_jids = jids.split(",")
for allowed_jid in allowed_jids: for allowed_jid in allowed_jids:
try: try:
address = JID.from_string(allowed_jid.strip()) address = JID.from_string(allowed_jid.strip())
except Exception as error: except Exception as error:
log.error('Error parsing JID: %s (%s)' % (error, allowed_jid)) log.error("Error parsing JID: %s (%s)" % (error, allowed_jid))
continue continue
if address.is_domain: if address.is_domain:
if current_jid.domain == address: if current_jid.domain == address:
log.debug('Show counter for Domain %s' % address) log.debug("Show counter for Domain %s" % address)
return True return True
if current_jid == address: if current_jid == address:
log.debug('Show counter for JID %s' % address) log.debug("Show counter for JID %s" % address)
return True return True
return False return False
@@ -241,6 +241,6 @@ class Counter(Gtk.Label):
self._update() self._update()
def reset(self) -> None: def reset(self) -> None:
self._context.remove_class('length-warning') self._context.remove_class("length-warning")
self._parse_config() self._parse_config()
self._set_css() self._set_css()

View File

@@ -21,11 +21,10 @@ from typing import TYPE_CHECKING
from gi.repository import Gtk from gi.repository import Gtk
from gajim.gtk.settings import SettingsDialog
from gajim.gtk.const import Setting from gajim.gtk.const import Setting
from gajim.gtk.const import SettingKind from gajim.gtk.const import SettingKind
from gajim.gtk.const import SettingType from gajim.gtk.const import SettingType
from gajim.gtk.settings import SettingsDialog
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -37,22 +36,26 @@ class MessageBoxSizeConfigDialog(SettingsDialog):
self.plugin = plugin self.plugin = plugin
settings = [ settings = [
Setting(SettingKind.SPIN, Setting(
_('Height in pixels'), SettingKind.SPIN,
SettingType.VALUE, _("Height in pixels"),
str(self.plugin.config['HEIGHT']), SettingType.VALUE,
callback=self._on_setting, str(self.plugin.config["HEIGHT"]),
data='HEIGHT', callback=self._on_setting,
desc=_('Size of message input in pixels'), data="HEIGHT",
props={'range_': (20, 200, 1)}), desc=_("Size of message input in pixels"),
] props={"range_": (20, 200, 1)},
),
]
SettingsDialog.__init__(self, SettingsDialog.__init__(
parent, self,
_('Message Box Size Configuration'), parent,
Gtk.DialogFlags.MODAL, _("Message Box Size Configuration"),
settings, Gtk.DialogFlags.MODAL,
'') settings,
"",
)
def _on_setting(self, value: Any, data: Any) -> None: def _on_setting(self, value: Any, data: Any) -> None:
self.plugin.config[data] = value self.plugin.config[data] = value

View File

@@ -21,7 +21,6 @@ from typing import cast
from functools import partial from functools import partial
from gajim.gtk.message_input import MessageInputTextView from gajim.gtk.message_input import MessageInputTextView
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
@@ -31,23 +30,20 @@ from message_box_size.config_dialog import MessageBoxSizeConfigDialog
class MsgBoxSizePlugin(GajimPlugin): class MsgBoxSizePlugin(GajimPlugin):
def init(self) -> None: def init(self) -> None:
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
self.description = _('Allows you to adjust the height ' self.description = _("Allows you to adjust the height " "of the message input.")
'of the message input.')
self.config_dialog = partial(MessageBoxSizeConfigDialog, self) self.config_dialog = partial(MessageBoxSizeConfigDialog, self)
self.gui_extension_points = { self.gui_extension_points = {
'message_input': (self._on_message_input_created, None) "message_input": (self._on_message_input_created, None)
} }
self.config_default_values = { self.config_default_values = {
'HEIGHT': (20, ''), "HEIGHT": (20, ""),
} }
self._message_input = None self._message_input = None
def _on_message_input_created(self, def _on_message_input_created(self, message_input: MessageInputTextView) -> None:
message_input: MessageInputTextView
) -> None:
self._message_input = message_input self._message_input = message_input
self.set_input_height(cast(int, self.config['HEIGHT'])) self.set_input_height(cast(int, self.config["HEIGHT"]))
def deactivate(self) -> None: def deactivate(self) -> None:
self.set_input_height(-1) self.set_input_height(-1)

View File

@@ -20,12 +20,11 @@ from typing import TYPE_CHECKING
from gi.repository import Gtk from gi.repository import Gtk
from gajim.plugins.plugins_i18n import _
from gajim.gtk.const import Setting from gajim.gtk.const import Setting
from gajim.gtk.const import SettingKind from gajim.gtk.const import SettingKind
from gajim.gtk.const import SettingType from gajim.gtk.const import SettingType
from gajim.gtk.settings import SettingsDialog from gajim.gtk.settings import SettingsDialog
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING: if TYPE_CHECKING:
from ..now_listen import NowListenPlugin from ..now_listen import NowListenPlugin
@@ -36,19 +35,24 @@ class NowListenConfigDialog(SettingsDialog):
self.plugin = plugin self.plugin = plugin
settings = [ settings = [
Setting(SettingKind.ENTRY, Setting(
_('Format string'), SettingKind.ENTRY,
SettingType.VALUE, _("Format string"),
self.plugin.config['format_string'], SettingType.VALUE,
callback=self._on_setting, data='format_string') self.plugin.config["format_string"],
] callback=self._on_setting,
data="format_string",
)
]
SettingsDialog.__init__(self, SettingsDialog.__init__(
parent, self,
_('Now Listen Configuration'), parent,
Gtk.DialogFlags.MODAL, _("Now Listen Configuration"),
settings, Gtk.DialogFlags.MODAL,
'') settings,
"",
)
def _on_setting(self, value: Any, data: Any) -> None: def _on_setting(self, value: Any, data: Any) -> None:
self.plugin.config[data] = value self.plugin.config[data] = value

View File

@@ -17,45 +17,42 @@ from __future__ import annotations
from typing import cast from typing import cast
import sys
import logging import logging
import sys
from functools import partial from functools import partial
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GObject from gi.repository import GObject
from nbxmpp.structs import TuneData from nbxmpp.structs import TuneData
from gajim.common.dbus.music_track import MusicTrackListener
from gajim.gtk.message_input import MessageInputTextView from gajim.gtk.message_input import MessageInputTextView
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from gajim.common.dbus.music_track import MusicTrackListener
from now_listen.gtk.config import NowListenConfigDialog from now_listen.gtk.config import NowListenConfigDialog
log = logging.getLogger("gajim.p.now_listen")
log = logging.getLogger('gajim.p.now_listen')
class NowListenPlugin(GajimPlugin): class NowListenPlugin(GajimPlugin):
def init(self) -> None: def init(self) -> None:
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
self.description = _('Copy tune info of playing music to conversation ' self.description = _(
'input box at cursor position (Alt + N)') "Copy tune info of playing music to conversation "
"input box at cursor position (Alt + N)"
)
self.config_dialog = partial(NowListenConfigDialog, self) self.config_dialog = partial(NowListenConfigDialog, self)
self.gui_extension_points = { self.gui_extension_points = {
'message_input': (self._on_message_input_created, None) "message_input": (self._on_message_input_created, None)
} }
self.config_default_values = { self.config_default_values = {
'format_string': "format_string": (_('Now listening to: "%title" by %artist'), ""),
(_('Now listening to: "%title" by %artist'), ''),
} }
if sys.platform != 'linux': if sys.platform != "linux":
self.available_text = _('Plugin only available for Linux') self.available_text = _("Plugin only available for Linux")
self.activatable = False self.activatable = False
self._signal_id = None self._signal_id = None
@@ -64,29 +61,24 @@ class NowListenPlugin(GajimPlugin):
def deactivate(self) -> None: def deactivate(self) -> None:
assert self._message_input is not None assert self._message_input is not None
assert self._signal_id is not None assert self._signal_id is not None
if GObject.signal_handler_is_connected( if GObject.signal_handler_is_connected(self._message_input, self._signal_id):
self._message_input, self._signal_id):
self._message_input.disconnect(self._signal_id) self._message_input.disconnect(self._signal_id)
def _on_message_input_created(self, def _on_message_input_created(self, message_input: MessageInputTextView) -> None:
message_input: MessageInputTextView
) -> None:
self._message_input = message_input self._message_input = message_input
self._signal_id = message_input.connect( self._signal_id = message_input.connect("key-press-event", self._on_key_press)
'key-press-event', self._on_key_press)
def _get_tune_string(self, info: TuneData) -> str: def _get_tune_string(self, info: TuneData) -> str:
format_string = cast(str, self.config['format_string']) format_string = cast(str, self.config["format_string"])
tune_string = format_string.replace( tune_string = format_string.replace("%artist", info.artist or "").replace(
'%artist', info.artist or '').replace( "%title", info.title or ""
'%title', info.title or '') )
return tune_string return tune_string
def _on_key_press(self, def _on_key_press(
textview: MessageInputTextView, self, textview: MessageInputTextView, event: Gdk.EventKey
event: Gdk.EventKey ) -> bool:
) -> bool:
# Insert text to message input box, at cursor position # Insert text to message input box, at cursor position
if event.keyval != Gdk.KEY_n: if event.keyval != Gdk.KEY_n:
@@ -96,7 +88,7 @@ class NowListenPlugin(GajimPlugin):
info = MusicTrackListener.get().current_tune info = MusicTrackListener.get().current_tune
if info is None: if info is None:
log.info('No current tune available') log.info("No current tune available")
return False return False
tune_string = self._get_tune_string(info) tune_string = self._get_tune_string(info)

View File

@@ -16,15 +16,14 @@
import logging import logging
from nbxmpp.protocol import JID
import gpg import gpg
from gpg.results import ImportResult from gpg.results import ImportResult
from nbxmpp.protocol import JID
from openpgp.backend.util import parse_uid from openpgp.backend.util import parse_uid
from openpgp.modules.util import DecryptionFailed from openpgp.modules.util import DecryptionFailed
log = logging.getLogger('gajim.p.openpgp.gpgme') log = logging.getLogger("gajim.p.openpgp.gpgme")
class KeyringItem: class KeyringItem:
@@ -73,31 +72,33 @@ class GPGME:
def __init__(self, jid, gnuhome): def __init__(self, jid, gnuhome):
self._jid = jid self._jid = jid
self._context_args = { self._context_args = {
'home_dir': str(gnuhome), "home_dir": str(gnuhome),
'offline': True, "offline": True,
'armor': False, "armor": False,
} }
def generate_key(self): def generate_key(self):
with gpg.Context(**self._context_args) as context: with gpg.Context(**self._context_args) as context:
result = context.create_key(f'xmpp:{str(self._jid)}', result = context.create_key(
algorithm='default', f"xmpp:{str(self._jid)}",
expires=False, algorithm="default",
passphrase=None, expires=False,
force=False) passphrase=None,
force=False,
)
log.info('Generated new key: %s', result.fpr) log.info("Generated new key: %s", result.fpr)
def get_key(self, fingerprint): def get_key(self, fingerprint):
with gpg.Context(**self._context_args) as context: with gpg.Context(**self._context_args) as context:
try: try:
key = context.get_key(fingerprint) key = context.get_key(fingerprint)
except gpg.errors.KeyNotFound as error: except gpg.errors.KeyNotFound as error:
log.warning('key not found: %s', error.keystr) log.warning("key not found: %s", error.keystr)
return return
except Exception as error: except Exception as error:
log.warning('get_key() error: %s', error) log.warning("get_key() error: %s", error)
return return
return key return key
@@ -121,7 +122,7 @@ class GPGME:
for key in context.keylist(): for key in context.keylist():
keyring_item = KeyringItem(key) keyring_item = KeyringItem(key)
if not keyring_item.is_xmpp_key: if not keyring_item.is_xmpp_key:
log.warning('Key not suited for xmpp: %s', key.fpr) log.warning("Key not suited for xmpp: %s", key.fpr)
self.delete_key(keyring_item.fingerprint) self.delete_key(keyring_item.fingerprint)
continue continue
@@ -157,12 +158,12 @@ class GPGME:
recipients.append(key) recipients.append(key)
if not recipients: if not recipients:
return None, 'No keys found to encrypt to' return None, "No keys found to encrypt to"
with gpg.Context(**self._context_args) as context: with gpg.Context(**self._context_args) as context:
result = context.encrypt(str(plaintext).encode(), result = context.encrypt(
recipients, str(plaintext).encode(), recipients, always_trust=True
always_trust=True) )
ciphertext, result, _sign_result = result ciphertext, result, _sign_result = result
return ciphertext, None return ciphertext, None
@@ -172,7 +173,7 @@ class GPGME:
try: try:
result = context.decrypt(ciphertext) result = context.decrypt(ciphertext)
except Exception as error: except Exception as error:
raise DecryptionFailed('Decryption failed: %s' % error) raise DecryptionFailed("Decryption failed: %s" % error)
plaintext, result, verify_result = result plaintext, result, verify_result = result
plaintext = plaintext.decode() plaintext = plaintext.decode()
@@ -181,16 +182,16 @@ class GPGME:
if not fingerprints or len(fingerprints) > 1: if not fingerprints or len(fingerprints) > 1:
log.error(result) log.error(result)
log.error(verify_result) log.error(verify_result)
raise DecryptionFailed('Verification failed') raise DecryptionFailed("Verification failed")
return plaintext, fingerprints[0] return plaintext, fingerprints[0]
def import_key(self, data, jid): def import_key(self, data, jid):
log.info('Import key from %s', jid) log.info("Import key from %s", jid)
with gpg.Context(**self._context_args) as context: with gpg.Context(**self._context_args) as context:
result = context.key_import(data) result = context.key_import(data)
if not isinstance(result, ImportResult) or result.imported != 1: if not isinstance(result, ImportResult) or result.imported != 1:
log.error('Key import failed: %s', jid) log.error("Key import failed: %s", jid)
log.error(result) log.error(result)
return return
@@ -198,7 +199,7 @@ class GPGME:
key = self.get_key(fingerprint) key = self.get_key(fingerprint)
item = KeyringItem(key) item = KeyringItem(key)
if not item.is_valid(jid): if not item.is_valid(jid):
log.warning('Invalid key found') log.warning("Invalid key found")
log.warning(key) log.warning(key)
self.delete_key(item.fingerprint) self.delete_key(item.fingerprint)
return return
@@ -206,7 +207,7 @@ class GPGME:
return item return item
def delete_key(self, fingerprint): def delete_key(self, fingerprint):
log.info('Delete Key: %s', fingerprint) log.info("Delete Key: %s", fingerprint)
key = self.get_key(fingerprint) key = self.get_key(fingerprint)
with gpg.Context(**self._context_args) as context: with gpg.Context(**self._context_args) as context:
context.op_delete(key, True) context.op_delete(key, True)

View File

@@ -23,10 +23,9 @@ from nbxmpp.protocol import JID
from openpgp.backend.util import parse_uid from openpgp.backend.util import parse_uid
from openpgp.modules.util import DecryptionFailed from openpgp.modules.util import DecryptionFailed
log = logging.getLogger("gajim.p.openpgp.pygnupg")
log = logging.getLogger('gajim.p.openpgp.pygnupg')
if log.getEffectiveLevel() == logging.DEBUG: if log.getEffectiveLevel() == logging.DEBUG:
log = logging.getLogger('gnupg') log = logging.getLogger("gnupg")
log.addHandler(logging.StreamHandler()) log.addHandler(logging.StreamHandler())
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)
@@ -50,10 +49,10 @@ class KeyringItem:
@property @property
def keyid(self) -> str: def keyid(self) -> str:
return self._key['keyid'] return self._key["keyid"]
def _get_uid(self) -> str | None: def _get_uid(self) -> str | None:
for uid in self._key['uids']: for uid in self._key["uids"]:
try: try:
return parse_uid(uid) return parse_uid(uid)
except Exception: except Exception:
@@ -61,7 +60,7 @@ class KeyringItem:
@property @property
def fingerprint(self): def fingerprint(self):
return self._key['fingerprint'] return self._key["fingerprint"]
@property @property
def uid(self): def uid(self):
@@ -79,28 +78,28 @@ class KeyringItem:
class PythonGnuPG(gnupg.GPG): class PythonGnuPG(gnupg.GPG):
def __init__(self, jid: str, gnupghome: Path) -> None: def __init__(self, jid: str, gnupghome: Path) -> None:
gnupg.GPG.__init__(self, gpgbinary='gpg', gnupghome=str(gnupghome)) gnupg.GPG.__init__(self, gpgbinary="gpg", gnupghome=str(gnupghome))
self._jid = jid self._jid = jid
self._own_fingerprint = None self._own_fingerprint = None
@staticmethod @staticmethod
def _get_key_params(jid): def _get_key_params(jid):
''' """
Generate --gen-key input Generate --gen-key input
''' """
params = { params = {
'Key-Type': 'RSA', "Key-Type": "RSA",
'Key-Length': 2048, "Key-Length": 2048,
'Name-Real': 'xmpp:%s' % jid, "Name-Real": "xmpp:%s" % jid,
} }
out = 'Key-Type: %s\n' % params.pop('Key-Type') out = "Key-Type: %s\n" % params.pop("Key-Type")
for key, val in list(params.items()): for key, val in list(params.items()):
out += '%s: %s\n' % (key, val) out += "%s: %s\n" % (key, val)
out += '%no-protection\n' out += "%no-protection\n"
out += '%commit\n' out += "%commit\n"
return out return out
def generate_key(self): def generate_key(self):
@@ -108,18 +107,20 @@ class PythonGnuPG(gnupg.GPG):
def encrypt(self, payload, keys): def encrypt(self, payload, keys):
recipients = [key.fingerprint for key in keys] recipients = [key.fingerprint for key in keys]
log.info('encrypt to:') log.info("encrypt to:")
for fingerprint in recipients: for fingerprint in recipients:
log.info(fingerprint) log.info(fingerprint)
result = super().encrypt(str(payload).encode('utf8'), result = super().encrypt(
recipients, str(payload).encode("utf8"),
armor=False, recipients,
sign=self._own_fingerprint, armor=False,
always_trust=True) sign=self._own_fingerprint,
always_trust=True,
)
if result.ok: if result.ok:
error = '' error = ""
else: else:
error = result.status error = result.status
@@ -130,7 +131,7 @@ class PythonGnuPG(gnupg.GPG):
if not result.ok: if not result.ok:
raise DecryptionFailed(result.status) raise DecryptionFailed(result.status)
return result.data.decode('utf8'), result.fingerprint return result.data.decode("utf8"), result.fingerprint
def get_key(self, fingerprint): def get_key(self, fingerprint):
return super().list_keys(keys=[fingerprint]) return super().list_keys(keys=[fingerprint])
@@ -141,7 +142,7 @@ class PythonGnuPG(gnupg.GPG):
for key in result: for key in result:
item = KeyringItem(key) item = KeyringItem(key)
if not item.is_xmpp_key: if not item.is_xmpp_key:
log.warning('Invalid key found, deleting key') log.warning("Invalid key found, deleting key")
log.warning(key) log.warning(key)
self.delete_key(item.fingerprint) self.delete_key(item.fingerprint)
continue continue
@@ -149,17 +150,17 @@ class PythonGnuPG(gnupg.GPG):
return keys return keys
def import_key(self, data, jid): def import_key(self, data, jid):
log.info('Import key from %s', jid) log.info("Import key from %s", jid)
result = super().import_keys(data) result = super().import_keys(data)
if not result: if not result:
log.error('Could not import key') log.error("Could not import key")
log.error(result) log.error(result)
return return
key = self.get_key(result.results[0]['fingerprint']) key = self.get_key(result.results[0]["fingerprint"])
item = KeyringItem(key[0]) item = KeyringItem(key[0])
if not item.is_valid(jid): if not item.is_valid(jid):
log.warning('Invalid key found, deleting key') log.warning("Invalid key found, deleting key")
log.warning(key) log.warning(key)
self.delete_key(item.fingerprint) self.delete_key(item.fingerprint)
return return
@@ -172,17 +173,16 @@ class PythonGnuPG(gnupg.GPG):
return None, None return None, None
if len(result) > 1: if len(result) > 1:
log.error('More than one secret key found') log.error("More than one secret key found")
return None, None return None, None
self._own_fingerprint = result[0]['fingerprint'] self._own_fingerprint = result[0]["fingerprint"]
return self._own_fingerprint, int(result[0]['date']) return self._own_fingerprint, int(result[0]["date"])
def export_key(self, fingerprint): def export_key(self, fingerprint):
key = super().export_keys( key = super().export_keys(fingerprint, secret=False, armor=False, minimal=True)
fingerprint, secret=False, armor=False, minimal=True)
return key return key
def delete_key(self, fingerprint): def delete_key(self, fingerprint):
log.info('Delete Key: %s', fingerprint) log.info("Delete Key: %s", fingerprint)
super().delete_keys(fingerprint) super().delete_keys(fingerprint)

View File

@@ -14,13 +14,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import sqlite3
import logging import logging
import sqlite3
from collections import namedtuple from collections import namedtuple
log = logging.getLogger('gajim.p.openpgp.sql') log = logging.getLogger("gajim.p.openpgp.sql")
TABLE_LAYOUT = ''' TABLE_LAYOUT = """
CREATE TABLE contacts ( CREATE TABLE contacts (
jid TEXT, jid TEXT,
fingerprint TEXT, fingerprint TEXT,
@@ -29,13 +29,14 @@ TABLE_LAYOUT = '''
timestamp INTEGER, timestamp INTEGER,
comment TEXT comment TEXT
); );
CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);''' CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);"""
class Storage: class Storage:
def __init__(self, folder_path): def __init__(self, folder_path):
self._con = sqlite3.connect(str(folder_path / 'contacts.db'), self._con = sqlite3.connect(
detect_types=sqlite3.PARSE_COLNAMES) str(folder_path / "contacts.db"), detect_types=sqlite3.PARSE_COLNAMES
)
self._con.row_factory = self._namedtuple_factory self._con.row_factory = self._namedtuple_factory
self._create_database() self._create_database()
@@ -51,11 +52,11 @@ class Storage:
return named_row return named_row
def _user_version(self): def _user_version(self):
return self._con.execute('PRAGMA user_version').fetchone()[0] return self._con.execute("PRAGMA user_version").fetchone()[0]
def _create_database(self): def _create_database(self):
if not self._user_version(): if not self._user_version():
log.info('Create contacts.db') log.info("Create contacts.db")
self._execute_query(TABLE_LAYOUT) self._execute_query(TABLE_LAYOUT)
def _execute_query(self, query): def _execute_query(self, query):
@@ -64,41 +65,43 @@ class Storage:
%s %s
PRAGMA user_version=1; PRAGMA user_version=1;
END TRANSACTION; END TRANSACTION;
""" % (query) """ % (
query
)
self._con.executescript(transaction) self._con.executescript(transaction)
def _migrate_database(self): def _migrate_database(self):
pass pass
def load_contacts(self): def load_contacts(self):
sql = '''SELECT jid as "jid [jid]", sql = """SELECT jid as "jid [jid]",
fingerprint, fingerprint,
active, active,
trust, trust,
timestamp, timestamp,
comment comment
FROM contacts''' FROM contacts"""
return self._con.execute(sql).fetchall() return self._con.execute(sql).fetchall()
def save_contact(self, db_values): def save_contact(self, db_values):
sql = '''REPLACE INTO sql = """REPLACE INTO
contacts(jid, fingerprint, active, trust, timestamp, comment) contacts(jid, fingerprint, active, trust, timestamp, comment)
VALUES(?, ?, ?, ?, ?, ?)''' VALUES(?, ?, ?, ?, ?, ?)"""
for values in db_values: for values in db_values:
log.info('Store key: %s', values) log.info("Store key: %s", values)
self._con.execute(sql, values) self._con.execute(sql, values)
self._con.commit() self._con.commit()
def set_trust(self, jid, fingerprint, trust): def set_trust(self, jid, fingerprint, trust):
sql = 'UPDATE contacts SET trust = ? WHERE jid = ? AND fingerprint = ?' sql = "UPDATE contacts SET trust = ? WHERE jid = ? AND fingerprint = ?"
log.info('Set Trust: %s %s %s', trust, jid, fingerprint) log.info("Set Trust: %s %s %s", trust, jid, fingerprint)
self._con.execute(sql, (trust, jid, fingerprint)) self._con.execute(sql, (trust, jid, fingerprint))
self._con.commit() self._con.commit()
def delete_key(self, jid, fingerprint): def delete_key(self, jid, fingerprint):
sql = 'DELETE from contacts WHERE jid = ? AND fingerprint = ?' sql = "DELETE from contacts WHERE jid = ? AND fingerprint = ?"
log.info('Delete Key: %s %s', jid, fingerprint) log.info("Delete Key: %s %s", jid, fingerprint)
self._con.execute(sql, (jid, fingerprint)) self._con.execute(sql, (jid, fingerprint))
self._con.commit() self._con.commit()

View File

@@ -1,13 +1,12 @@
from __future__ import annotations from __future__ import annotations
def parse_uid(uid: str, compat=False) -> str: def parse_uid(uid: str, compat=False) -> str:
if uid.startswith('xmpp:'): if uid.startswith("xmpp:"):
return uid[5:] return uid[5:]
# Compat with uids of form "Name <xmpp:my@jid.com>" # Compat with uids of form "Name <xmpp:my@jid.com>"
if compat and '<xmpp:' in uid and uid.endswith('>'): if compat and "<xmpp:" in uid and uid.endswith(">"):
return uid[:-1].split('<xmpp:', maxsplit=1)[1] return uid[:-1].split("<xmpp:", maxsplit=1)[1]
raise ValueError('Uknown UID format: %s' % uid) raise ValueError("Uknown UID format: %s" % uid)

View File

@@ -20,41 +20,31 @@ import time
from gi.repository import Gtk from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.gtk.dialogs import ConfirmationDialog from gajim.gtk.dialogs import ConfirmationDialog
from gajim.gtk.dialogs import DialogButton from gajim.gtk.dialogs import DialogButton
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from openpgp.modules.util import Trust from openpgp.modules.util import Trust
log = logging.getLogger('gajim.p.openpgp.keydialog') log = logging.getLogger("gajim.p.openpgp.keydialog")
TRUST_DATA = { TRUST_DATA = {
Trust.NOT_TRUSTED: ('dialog-error-symbolic', Trust.NOT_TRUSTED: ("dialog-error-symbolic", _("Not Trusted"), "error-color"),
_('Not Trusted'), Trust.UNKNOWN: ("security-low-symbolic", _("Not Decided"), "warning-color"),
'error-color'), Trust.BLIND: ("security-medium-symbolic", _("Blind Trust"), "encrypted-color"),
Trust.UNKNOWN: ('security-low-symbolic', Trust.VERIFIED: ("security-high-symbolic", _("Verified"), "encrypted-color"),
_('Not Decided'),
'warning-color'),
Trust.BLIND: ('security-medium-symbolic',
_('Blind Trust'),
'encrypted-color'),
Trust.VERIFIED: ('security-high-symbolic',
_('Verified'),
'encrypted-color')
} }
class KeyDialog(Gtk.Dialog): class KeyDialog(Gtk.Dialog):
def __init__(self, account, jid, transient): def __init__(self, account, jid, transient):
super().__init__(title=_('Public Keys for %s') % jid, super().__init__(title=_("Public Keys for %s") % jid, destroy_with_parent=True)
destroy_with_parent=True)
self.set_transient_for(transient) self.set_transient_for(transient)
self.set_resizable(True) self.set_resizable(True)
self.set_default_size(500, 300) self.set_default_size(500, 300)
self.get_style_context().add_class('openpgp-key-dialog') self.get_style_context().add_class("openpgp-key-dialog")
self._client = app.get_client(account) self._client = app.get_client(account)
@@ -62,17 +52,15 @@ class KeyDialog(Gtk.Dialog):
self._listbox.set_selection_mode(Gtk.SelectionMode.NONE) self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
self._scrolled = Gtk.ScrolledWindow() self._scrolled = Gtk.ScrolledWindow()
self._scrolled.set_policy(Gtk.PolicyType.NEVER, self._scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
Gtk.PolicyType.AUTOMATIC)
self._scrolled.add(self._listbox) self._scrolled.add(self._listbox)
box = self.get_content_area() box = self.get_content_area()
box.pack_start(self._scrolled, True, True, 0) box.pack_start(self._scrolled, True, True, 0)
keys = self._client.get_module('OpenPGP').get_keys( keys = self._client.get_module("OpenPGP").get_keys(jid, only_trusted=False)
jid, only_trusted=False)
for key in keys: for key in keys:
log.info('Load: %s', key.fingerprint) log.info("Load: %s", key.fingerprint)
self._listbox.add(KeyRow(key)) self._listbox.add(KeyRow(key))
self.show_all() self.show_all()
@@ -92,11 +80,10 @@ class KeyRow(Gtk.ListBoxRow):
box.add(self._trust_button) box.add(self._trust_button)
label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
fingerprint = Gtk.Label( fingerprint = Gtk.Label(label=self._format_fingerprint(key.fingerprint))
label=self._format_fingerprint(key.fingerprint)) fingerprint.get_style_context().add_class("openpgp-mono")
fingerprint.get_style_context().add_class('openpgp-mono')
if not key.active: if not key.active:
fingerprint.get_style_context().add_class('openpgp-inactive-color') fingerprint.get_style_context().add_class("openpgp-inactive-color")
fingerprint.set_selectable(True) fingerprint.set_selectable(True)
fingerprint.set_halign(Gtk.Align.START) fingerprint.set_halign(Gtk.Align.START)
fingerprint.set_valign(Gtk.Align.START) fingerprint.set_valign(Gtk.Align.START)
@@ -105,9 +92,9 @@ class KeyRow(Gtk.ListBoxRow):
date = Gtk.Label(label=self._format_timestamp(key.timestamp)) date = Gtk.Label(label=self._format_timestamp(key.timestamp))
date.set_halign(Gtk.Align.START) date.set_halign(Gtk.Align.START)
date.get_style_context().add_class('openpgp-mono') date.get_style_context().add_class("openpgp-mono")
if not key.active: if not key.active:
date.get_style_context().add_class('openpgp-inactive-color') date.get_style_context().add_class("openpgp-inactive-color")
label_box.add(date) label_box.add(date)
box.add(label_box) box.add(label_box)
@@ -122,12 +109,12 @@ class KeyRow(Gtk.ListBoxRow):
self.destroy() self.destroy()
ConfirmationDialog( ConfirmationDialog(
_('Delete Public Key?'), _("Delete Public Key?"),
_('This will permanently delete this public key'), _("This will permanently delete this public key"),
[DialogButton.make('Cancel'), [
DialogButton.make('Remove', DialogButton.make("Cancel"),
text=_('Delete'), DialogButton.make("Remove", text=_("Delete"), callback=_remove),
callback=_remove)], ],
).show() ).show()
def set_trust(self, trust): def set_trust(self, trust):
@@ -140,22 +127,21 @@ class KeyRow(Gtk.ListBoxRow):
def _format_fingerprint(fingerprint): def _format_fingerprint(fingerprint):
fplen = len(fingerprint) fplen = len(fingerprint)
wordsize = fplen // 8 wordsize = fplen // 8
buf = '' buf = ""
for w in range(0, fplen, wordsize): for w in range(0, fplen, wordsize):
buf += '{0} '.format(fingerprint[w:w + wordsize]) buf += "{0} ".format(fingerprint[w : w + wordsize])
return buf.rstrip() return buf.rstrip()
@staticmethod @staticmethod
def _format_timestamp(timestamp): def _format_timestamp(timestamp):
return time.strftime('%Y-%m-%d %H:%M:%S', return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp))
time.localtime(timestamp))
class TrustButton(Gtk.MenuButton): class TrustButton(Gtk.MenuButton):
def __init__(self, row): def __init__(self, row):
Gtk.MenuButton.__init__(self) Gtk.MenuButton.__init__(self)
self._row = row self._row = row
self._css_class = '' self._css_class = ""
self.set_popover(TrustPopver(row)) self.set_popover(TrustPopver(row))
self.update() self.update()
@@ -167,8 +153,8 @@ class TrustButton(Gtk.MenuButton):
image.get_style_context().remove_class(self._css_class) image.get_style_context().remove_class(self._css_class)
if not self._row.key.active: if not self._row.key.active:
css_class = 'openpgp-inactive-color' css_class = "openpgp-inactive-color"
tooltip = '%s - %s' % (_('Inactive'), tooltip) tooltip = "%s - %s" % (_("Inactive"), tooltip)
image.get_style_context().add_class(css_class) image.get_style_context().add_class(css_class)
self._css_class = css_class self._css_class = css_class
@@ -188,8 +174,8 @@ class TrustPopver(Gtk.Popover):
self._listbox.add(DeleteOption()) self._listbox.add(DeleteOption())
self.add(self._listbox) self.add(self._listbox)
self._listbox.show_all() self._listbox.show_all()
self._listbox.connect('row-activated', self._activated) self._listbox.connect("row-activated", self._activated)
self.get_style_context().add_class('openpgp-trust-popover') self.get_style_context().add_class("openpgp-trust-popover")
def _activated(self, listbox, row): def _activated(self, listbox, row):
self.popdown() self.popdown()
@@ -215,8 +201,7 @@ class MenuOption(Gtk.ListBoxRow):
box = Gtk.Box() box = Gtk.Box()
box.set_spacing(6) box.set_spacing(6)
image = Gtk.Image.new_from_icon_name(self.icon, image = Gtk.Image.new_from_icon_name(self.icon, Gtk.IconSize.MENU)
Gtk.IconSize.MENU)
label = Gtk.Label(label=self.label) label = Gtk.Label(label=self.label)
image.get_style_context().add_class(self.color) image.get_style_context().add_class(self.color)
@@ -229,9 +214,9 @@ class MenuOption(Gtk.ListBoxRow):
class VerifiedOption(MenuOption): class VerifiedOption(MenuOption):
type_ = Trust.VERIFIED type_ = Trust.VERIFIED
icon = 'security-high-symbolic' icon = "security-high-symbolic"
label = _('Verified') label = _("Verified")
color = 'encrypted-color' color = "encrypted-color"
def __init__(self): def __init__(self):
MenuOption.__init__(self) MenuOption.__init__(self)
@@ -240,9 +225,9 @@ class VerifiedOption(MenuOption):
class NotTrustedOption(MenuOption): class NotTrustedOption(MenuOption):
type_ = Trust.NOT_TRUSTED type_ = Trust.NOT_TRUSTED
icon = 'dialog-error-symbolic' icon = "dialog-error-symbolic"
label = _('Not Trusted') label = _("Not Trusted")
color = 'error-color' color = "error-color"
def __init__(self): def __init__(self):
MenuOption.__init__(self) MenuOption.__init__(self)
@@ -251,9 +236,9 @@ class NotTrustedOption(MenuOption):
class DeleteOption(MenuOption): class DeleteOption(MenuOption):
type_ = None type_ = None
icon = 'user-trash-symbolic' icon = "user-trash-symbolic"
label = _('Delete') label = _("Delete")
color = '' color = ""
def __init__(self): def __init__(self):
MenuOption.__init__(self) MenuOption.__init__(self)

View File

@@ -18,13 +18,13 @@ import logging
import threading import threading
from enum import IntEnum from enum import IntEnum
from gi.repository import Gtk
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
log = logging.getLogger('gajim.p.openpgp.wizard') log = logging.getLogger("gajim.p.openpgp.wizard")
class Page(IntEnum): class Page(IntEnum):
@@ -51,7 +51,7 @@ class KeyWizard(Gtk.Assistant):
self.set_position(Gtk.WindowPosition.CENTER) self.set_position(Gtk.WindowPosition.CENTER)
self.set_default_size(600, 400) self.set_default_size(600, 400)
self.get_style_context().add_class('dialog-margin') self.get_style_context().add_class("dialog-margin")
self._add_page(WelcomePage()) self._add_page(WelcomePage())
# self._add_page(BackupKeyPage()) # self._add_page(BackupKeyPage())
@@ -60,9 +60,9 @@ class KeyWizard(Gtk.Assistant):
self._add_page(SuccessfulPage()) self._add_page(SuccessfulPage())
self._add_page(ErrorPage()) self._add_page(ErrorPage())
self.connect('prepare', self._on_page_change) self.connect("prepare", self._on_page_change)
self.connect('cancel', self._on_cancel) self.connect("cancel", self._on_cancel)
self.connect('close', self._on_cancel) self.connect("close", self._on_cancel)
self._remove_sidebar() self._remove_sidebar()
self.show_all() self.show_all()
@@ -79,12 +79,12 @@ class KeyWizard(Gtk.Assistant):
main_box.remove(sidebar) main_box.remove(sidebar)
def _activate_encryption(self): def _activate_encryption(self):
action = app.window.lookup_action('set-encryption') action = app.window.lookup_action("set-encryption")
action.activate(GLib.Variant('s', self._plugin.encryption_name)) action.activate(GLib.Variant("s", self._plugin.encryption_name))
def _on_page_change(self, assistant, page): def _on_page_change(self, assistant, page):
if self.get_current_page() == Page.NEWKEY: if self.get_current_page() == Page.NEWKEY:
if self._client.get_module('OpenPGP').secret_key_available: if self._client.get_module("OpenPGP").secret_key_available:
self.set_current_page(Page.SUCCESS) self.set_current_page(Page.SUCCESS)
else: else:
page.generate() page.generate()
@@ -98,15 +98,14 @@ class KeyWizard(Gtk.Assistant):
class WelcomePage(Gtk.Box): class WelcomePage(Gtk.Box):
type_ = Gtk.AssistantPageType.INTRO type_ = Gtk.AssistantPageType.INTRO
title = _('Welcome') title = _("Welcome")
complete = True complete = True
def __init__(self): def __init__(self):
super().__init__(orientation=Gtk.Orientation.VERTICAL) super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.set_spacing(18) self.set_spacing(18)
title_label = Gtk.Label(label=_('Setup OpenPGP')) title_label = Gtk.Label(label=_("Setup OpenPGP"))
text_label = Gtk.Label( text_label = Gtk.Label(label=_("Gajim will now try to setup OpenPGP for you"))
label=_('Gajim will now try to setup OpenPGP for you'))
self.add(title_label) self.add(title_label)
self.add(text_label) self.add(text_label)
@@ -114,7 +113,7 @@ class WelcomePage(Gtk.Box):
class RequestPage(Gtk.Box): class RequestPage(Gtk.Box):
type_ = Gtk.AssistantPageType.INTRO type_ = Gtk.AssistantPageType.INTRO
title = _('Request OpenPGP Key') title = _("Request OpenPGP Key")
complete = False complete = False
def __init__(self): def __init__(self):
@@ -146,7 +145,7 @@ class RequestPage(Gtk.Box):
class NewKeyPage(RequestPage): class NewKeyPage(RequestPage):
type_ = Gtk.AssistantPageType.PROGRESS type_ = Gtk.AssistantPageType.PROGRESS
title = _('Generating new Key') title = _("Generating new Key")
complete = False complete = False
def __init__(self, assistant, client): def __init__(self, assistant, client):
@@ -155,14 +154,14 @@ class NewKeyPage(RequestPage):
self._client = client self._client = client
def generate(self): def generate(self):
log.info('Creating Key') log.info("Creating Key")
thread = threading.Thread(target=self.worker) thread = threading.Thread(target=self.worker)
thread.start() thread.start()
def worker(self): def worker(self):
text = None text = None
try: try:
self._client.get_module('OpenPGP').generate_key() self._client.get_module("OpenPGP").generate_key()
except Exception as error: except Exception as error:
text = str(error) text = str(error)
@@ -170,9 +169,9 @@ class NewKeyPage(RequestPage):
def finished(self, error): def finished(self, error):
if error is None: if error is None:
self._client.get_module('OpenPGP').get_own_key_details() self._client.get_module("OpenPGP").get_own_key_details()
self._client.get_module('OpenPGP').set_public_key() self._client.get_module("OpenPGP").set_public_key()
self._client.get_module('OpenPGP').request_keylist() self._client.get_module("OpenPGP").request_keylist()
self._assistant.set_current_page(Page.SUCCESS) self._assistant.set_current_page(Page.SUCCESS)
else: else:
error_page = self._assistant.get_nth_page(Page.ERROR) error_page = self._assistant.get_nth_page(Page.ERROR)
@@ -199,7 +198,7 @@ class NewKeyPage(RequestPage):
class SuccessfulPage(Gtk.Box): class SuccessfulPage(Gtk.Box):
type_ = Gtk.AssistantPageType.SUMMARY type_ = Gtk.AssistantPageType.SUMMARY
title = _('Setup successful') title = _("Setup successful")
complete = True complete = True
def __init__(self): def __init__(self):
@@ -207,12 +206,13 @@ class SuccessfulPage(Gtk.Box):
self.set_spacing(12) self.set_spacing(12)
self.set_homogeneous(True) self.set_homogeneous(True)
icon = Gtk.Image.new_from_icon_name('object-select-symbolic', icon = Gtk.Image.new_from_icon_name(
Gtk.IconSize.DIALOG) "object-select-symbolic", Gtk.IconSize.DIALOG
icon.get_style_context().add_class('success-color') )
icon.get_style_context().add_class("success-color")
icon.set_valign(Gtk.Align.END) icon.set_valign(Gtk.Align.END)
label = Gtk.Label(label=_('Setup successful')) label = Gtk.Label(label=_("Setup successful"))
label.get_style_context().add_class('bold16') label.get_style_context().add_class("bold16")
label.set_valign(Gtk.Align.START) label.set_valign(Gtk.Align.START)
self.add(icon) self.add(icon)
@@ -222,7 +222,7 @@ class SuccessfulPage(Gtk.Box):
class ErrorPage(Gtk.Box): class ErrorPage(Gtk.Box):
type_ = Gtk.AssistantPageType.SUMMARY type_ = Gtk.AssistantPageType.SUMMARY
title = _('Setup failed') title = _("Setup failed")
complete = True complete = True
def __init__(self): def __init__(self):
@@ -230,12 +230,13 @@ class ErrorPage(Gtk.Box):
self.set_spacing(12) self.set_spacing(12)
self.set_homogeneous(True) self.set_homogeneous(True)
icon = Gtk.Image.new_from_icon_name('dialog-error-symbolic', icon = Gtk.Image.new_from_icon_name(
Gtk.IconSize.DIALOG) "dialog-error-symbolic", Gtk.IconSize.DIALOG
icon.get_style_context().add_class('error-color') )
icon.get_style_context().add_class("error-color")
icon.set_valign(Gtk.Align.END) icon.set_valign(Gtk.Align.END)
self._label = Gtk.Label() self._label = Gtk.Label()
self._label.get_style_context().add_class('bold16') self._label.get_style_context().add_class("bold16")
self._label.set_valign(Gtk.Align.START) self._label.set_valign(Gtk.Align.START)
self.add(icon) self.add(icon)

View File

@@ -18,13 +18,14 @@ import logging
from openpgp.modules.util import Trust from openpgp.modules.util import Trust
log = logging.getLogger('gajim.p.openpgp.store') log = logging.getLogger("gajim.p.openpgp.store")
class KeyData: class KeyData:
''' """
Holds all data related to a certain key Holds all data related to a certain key
''' """
def __init__(self, contact_data): def __init__(self, contact_data):
self._contact_data = contact_data self._contact_data = contact_data
self.fingerprint = None self.fingerprint = None
@@ -40,11 +41,8 @@ class KeyData:
@trust.setter @trust.setter
def trust(self, value): def trust(self, value):
if value not in (Trust.NOT_TRUSTED, if value not in (Trust.NOT_TRUSTED, Trust.UNKNOWN, Trust.BLIND, Trust.VERIFIED):
Trust.UNKNOWN, raise ValueError("Trust value not allowed: %s" % value)
Trust.BLIND,
Trust.VERIFIED):
raise ValueError('Trust value not allowed: %s' % value)
self._trust = value self._trust = value
self._contact_data.set_trust(self.fingerprint, self._trust) self._contact_data.set_trust(self.fingerprint, self._trust)
@@ -72,9 +70,10 @@ class KeyData:
class ContactData: class ContactData:
''' """
Holds all data related to a contact Holds all data related to a contact
''' """
def __init__(self, jid, storage, pgp): def __init__(self, jid, storage, pgp):
self.jid = jid self.jid = jid
self._key_store = {} self._key_store = {}
@@ -84,8 +83,8 @@ class ContactData:
@property @property
def userid(self): def userid(self):
if self.jid is None: if self.jid is None:
raise ValueError('JID not set') raise ValueError("JID not set")
return 'xmpp:%s' % self.jid return "xmpp:%s" % self.jid
@property @property
def default_trust(self): def default_trust(self):
@@ -96,12 +95,14 @@ class ContactData:
def db_values(self): def db_values(self):
for key in self._key_store.values(): for key in self._key_store.values():
yield (self.jid, yield (
key.fingerprint, self.jid,
key.active, key.fingerprint,
key.trust, key.active,
key.timestamp, key.trust,
key.comment) key.timestamp,
key.comment,
)
def add_from_key(self, key): def add_from_key(self, key):
try: try:
@@ -109,7 +110,7 @@ class ContactData:
except KeyError: except KeyError:
keydata = KeyData.from_key(self, key, self.default_trust) keydata = KeyData.from_key(self, key, self.default_trust)
self._key_store[key.fingerprint] = keydata self._key_store[key.fingerprint] = keydata
log.info('Add from key: %s %s', self.jid, keydata.fingerprint) log.info("Add from key: %s %s", self.jid, keydata.fingerprint)
return keydata return keydata
def add_from_db(self, row): def add_from_db(self, row):
@@ -118,11 +119,11 @@ class ContactData:
except KeyError: except KeyError:
keydata = KeyData.from_row(self, row) keydata = KeyData.from_row(self, row)
self._key_store[row.fingerprint] = keydata self._key_store[row.fingerprint] = keydata
log.info('Add from row: %s %s', self.jid, row.fingerprint) log.info("Add from row: %s %s", self.jid, row.fingerprint)
return keydata return keydata
def process_keylist(self, keylist): def process_keylist(self, keylist):
log.info('Process keylist: %s %s', self.jid, keylist) log.info("Process keylist: %s %s", self.jid, keylist)
if keylist is None: if keylist is None:
for keydata in self._key_store.values(): for keydata in self._key_store.values():
@@ -133,7 +134,7 @@ class ContactData:
missing_pub_keys = [] missing_pub_keys = []
fingerprints = set([key.fingerprint for key in keylist]) fingerprints = set([key.fingerprint for key in keylist])
if fingerprints == self._key_store.keys(): if fingerprints == self._key_store.keys():
log.info('No updates found') log.info("No updates found")
for key in self._key_store.values(): for key in self._key_store.values():
if not key.has_pubkey: if not key.has_pubkey:
missing_pub_keys.append(key.fingerprint) missing_pub_keys.append(key.fingerprint)
@@ -159,18 +160,20 @@ class ContactData:
try: try:
keydata = self._key_store[fingerprint] keydata = self._key_store[fingerprint]
except KeyError: except KeyError:
log.warning('Set public key on unknown fingerprint: %s %s', log.warning(
self.jid, fingerprint) "Set public key on unknown fingerprint: %s %s", self.jid, fingerprint
)
else: else:
keydata.has_pubkey = True keydata.has_pubkey = True
log.info('Set public key: %s %s', self.jid, fingerprint) log.info("Set public key: %s %s", self.jid, fingerprint)
def get_keys(self, only_trusted=True): def get_keys(self, only_trusted=True):
keys = list(self._key_store.values()) keys = list(self._key_store.values())
if not only_trusted: if not only_trusted:
return keys return keys
return [k for k in keys if k.active and k.trust in (Trust.VERIFIED, return [
Trust.BLIND)] k for k in keys if k.active and k.trust in (Trust.VERIFIED, Trust.BLIND)
]
def get_key(self, fingerprint): def get_key(self, fingerprint):
return self._key_store.get(fingerprint, None) return self._key_store.get(fingerprint, None)
@@ -185,9 +188,10 @@ class ContactData:
class PGPContacts: class PGPContacts:
''' """
Holds all contacts available for PGP encryption Holds all contacts available for PGP encryption
''' """
def __init__(self, pgp, storage): def __init__(self, pgp, storage):
self._contacts = {} self._contacts = {}
self._storage = storage self._storage = storage
@@ -196,20 +200,20 @@ class PGPContacts:
self._load_from_keyring() self._load_from_keyring()
def _load_from_keyring(self): def _load_from_keyring(self):
log.info('Load keys from keyring') log.info("Load keys from keyring")
keyring = self._pgp.get_keys() keyring = self._pgp.get_keys()
for key in keyring: for key in keyring:
log.info('Found: %s %s', key.jid, key.fingerprint) log.info("Found: %s %s", key.jid, key.fingerprint)
self.set_public_key(key.jid, key.fingerprint) self.set_public_key(key.jid, key.fingerprint)
def _load_from_storage(self): def _load_from_storage(self):
log.info('Load contacts from storage') log.info("Load contacts from storage")
rows = self._storage.load_contacts() rows = self._storage.load_contacts()
if rows is None: if rows is None:
return return
for row in rows: for row in rows:
log.info('Found: %s %s', row.jid, row.fingerprint) log.info("Found: %s %s", row.jid, row.fingerprint)
try: try:
contact_data = self._contacts[row.jid] contact_data = self._contacts[row.jid]
except KeyError: except KeyError:
@@ -235,7 +239,7 @@ class PGPContacts:
try: try:
contact_data = self._contacts[jid] contact_data = self._contacts[jid]
except KeyError: except KeyError:
log.warning('ContactData not found: %s %s', jid, fingerprint) log.warning("ContactData not found: %s %s", jid, fingerprint)
else: else:
contact_data.set_public_key(fingerprint) contact_data.set_public_key(fingerprint)

View File

@@ -14,24 +14,24 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging
import sys import sys
import time import time
import logging
from pathlib import Path from pathlib import Path
from nbxmpp.namespaces import Namespace
from nbxmpp import Node from nbxmpp import Node
from nbxmpp import StanzaMalformed from nbxmpp import StanzaMalformed
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import StanzaError
from nbxmpp.exceptions import StanzaDecrypted from nbxmpp.exceptions import StanzaDecrypted
from nbxmpp.modules.openpgp import create_message_stanza
from nbxmpp.modules.openpgp import create_signcrypt_node
from nbxmpp.modules.openpgp import parse_signcrypt
from nbxmpp.modules.openpgp import PGPKeyMetadata
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import EncryptionData from nbxmpp.structs import EncryptionData
from nbxmpp.structs import MessageProperties from nbxmpp.structs import MessageProperties
from nbxmpp.structs import StanzaHandler from nbxmpp.structs import StanzaHandler
from nbxmpp.errors import StanzaError
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.modules.openpgp import PGPKeyMetadata
from nbxmpp.modules.openpgp import parse_signcrypt
from nbxmpp.modules.openpgp import create_signcrypt_node
from nbxmpp.modules.openpgp import create_message_stanza
from gajim.common import app from gajim.common import app
from gajim.common import configpaths from gajim.common import configpaths
@@ -40,22 +40,22 @@ from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node from gajim.common.modules.util import event_node
from gajim.common.structs import OutgoingMessage from gajim.common.structs import OutgoingMessage
from openpgp.modules.util import ENCRYPTION_NAME
from openpgp.modules.util import NOT_ENCRYPTED_TAGS
from openpgp.modules.util import Key
from openpgp.modules.util import Trust
from openpgp.modules.util import DecryptionFailed
from openpgp.modules.util import prepare_stanza
from openpgp.modules.key_store import PGPContacts
from openpgp.backend.sql import Storage from openpgp.backend.sql import Storage
from openpgp.modules.key_store import PGPContacts
from openpgp.modules.util import DecryptionFailed
from openpgp.modules.util import ENCRYPTION_NAME
from openpgp.modules.util import Key
from openpgp.modules.util import NOT_ENCRYPTED_TAGS
from openpgp.modules.util import prepare_stanza
from openpgp.modules.util import Trust
if sys.platform == 'win32': if sys.platform == "win32":
from openpgp.backend.pygpg import PythonGnuPG as PGPBackend from openpgp.backend.pygpg import PythonGnuPG as PGPBackend
else: else:
from openpgp.backend.gpgme import GPGME as PGPBackend from openpgp.backend.gpgme import GPGME as PGPBackend
log = logging.getLogger('gajim.p.openpgp') log = logging.getLogger("gajim.p.openpgp")
# Module name # Module name
@@ -65,24 +65,26 @@ zeroconf = False
class OpenPGP(BaseModule): class OpenPGP(BaseModule):
_nbxmpp_extends = 'OpenPGP' _nbxmpp_extends = "OpenPGP"
_nbxmpp_methods = [ _nbxmpp_methods = [
'set_keylist', "set_keylist",
'request_keylist', "request_keylist",
'set_public_key', "set_public_key",
'request_public_key', "request_public_key",
'set_secret_key', "set_secret_key",
'request_secret_key', "request_secret_key",
] ]
def __init__(self, client): def __init__(self, client):
BaseModule.__init__(self, client) BaseModule.__init__(self, client)
self.handlers = [ self.handlers = [
StanzaHandler(name='message', StanzaHandler(
callback=self.decrypt_message, name="message",
ns=Namespace.OPENPGP, callback=self.decrypt_message,
priority=9), ns=Namespace.OPENPGP,
priority=9,
),
] ]
self._register_pubsub_handler(self._keylist_notification_received) self._register_pubsub_handler(self._keylist_notification_received)
@@ -90,7 +92,7 @@ class OpenPGP(BaseModule):
self.own_jid = self._client.get_own_jid() self.own_jid = self._client.get_own_jid()
own_bare_jid = self.own_jid.bare own_bare_jid = self.own_jid.bare
path = Path(configpaths.get('MY_DATA')) / 'openpgp' / own_bare_jid path = Path(configpaths.get("MY_DATA")) / "openpgp" / own_bare_jid
if not path.exists(): if not path.exists():
path.mkdir(mode=0o700, parents=True) path.mkdir(mode=0o700, parents=True)
@@ -98,7 +100,7 @@ class OpenPGP(BaseModule):
self._storage = Storage(path) self._storage = Storage(path)
self._contacts = PGPContacts(self._pgp, self._storage) self._contacts = PGPContacts(self._pgp, self._storage)
self._fingerprint, self._date = self.get_own_key_details() self._fingerprint, self._date = self.get_own_key_details()
log.info('Own Fingerprint at start: %s', self._fingerprint) log.info("Own Fingerprint at start: %s", self._fingerprint)
@property @property
def secret_key_available(self): def secret_key_available(self):
@@ -112,27 +114,22 @@ class OpenPGP(BaseModule):
self._pgp.generate_key() self._pgp.generate_key()
def set_public_key(self): def set_public_key(self):
log.info('%s => Publish public key', self._account) log.info("%s => Publish public key", self._account)
key = self._pgp.export_key(self._fingerprint) key = self._pgp.export_key(self._fingerprint)
self._nbxmpp('OpenPGP').set_public_key( self._nbxmpp("OpenPGP").set_public_key(key, self._fingerprint, self._date)
key, self._fingerprint, self._date)
def request_public_key(self, jid, fingerprint): def request_public_key(self, jid, fingerprint):
log.info('%s => Request public key %s - %s', log.info("%s => Request public key %s - %s", self._account, fingerprint, jid)
self._account, fingerprint, jid) self._nbxmpp("OpenPGP").request_public_key(
self._nbxmpp('OpenPGP').request_public_key( jid, fingerprint, callback=self._public_key_received, user_data=fingerprint
jid, )
fingerprint,
callback=self._public_key_received,
user_data=fingerprint)
def _public_key_received(self, task): def _public_key_received(self, task):
fingerprint = task.get_user_data() fingerprint = task.get_user_data()
try: try:
result = task.finish() result = task.finish()
except (StanzaError, MalformedStanzaError) as error: except (StanzaError, MalformedStanzaError) as error:
log.error('%s => Public Key not found: %s', log.error("%s => Public Key not found: %s", self._account, error)
self._account, error)
return return
imported_key = self._pgp.import_key(result.key, result.jid) imported_key = self._pgp.import_key(result.key, result.jid)
@@ -142,8 +139,8 @@ class OpenPGP(BaseModule):
def set_keylist(self, keylist=None): def set_keylist(self, keylist=None):
if keylist is None: if keylist is None:
keylist = [PGPKeyMetadata(None, self._fingerprint, self._date)] keylist = [PGPKeyMetadata(None, self._fingerprint, self._date)]
log.info('%s => Publish keylist', self._account) log.info("%s => Publish keylist", self._account)
self._nbxmpp('OpenPGP').set_keylist(keylist) self._nbxmpp("OpenPGP").set_keylist(keylist)
@event_node(Namespace.OPENPGP_PK) @event_node(Namespace.OPENPGP_PK)
def _keylist_notification_received(self, _con, _stanza, properties): def _keylist_notification_received(self, _con, _stanza, properties):
@@ -157,46 +154,43 @@ class OpenPGP(BaseModule):
def request_keylist(self, jid=None): def request_keylist(self, jid=None):
if jid is None: if jid is None:
jid = self.own_jid jid = self.own_jid
log.info('%s => Fetch keylist %s', self._account, jid) log.info("%s => Fetch keylist %s", self._account, jid)
self._nbxmpp('OpenPGP').request_keylist( self._nbxmpp("OpenPGP").request_keylist(
jid, jid, callback=self._keylist_received, user_data=jid
callback=self._keylist_received, )
user_data=jid)
def _keylist_received(self, task): def _keylist_received(self, task):
jid = task.get_user_data() jid = task.get_user_data()
try: try:
keylist = task.finish() keylist = task.finish()
except (StanzaError, MalformedStanzaError) as error: except (StanzaError, MalformedStanzaError) as error:
log.error('%s => Keylist query failed: %s', log.error("%s => Keylist query failed: %s", self._account, error)
self._account, error)
if self.own_jid.bare_match(jid) and self._fingerprint is not None: if self.own_jid.bare_match(jid) and self._fingerprint is not None:
self.set_keylist() self.set_keylist()
return return
log.info('Keylist received from %s', jid) log.info("Keylist received from %s", jid)
self._process_keylist(keylist, jid) self._process_keylist(keylist, jid)
def _process_keylist(self, keylist, from_jid): def _process_keylist(self, keylist, from_jid):
if not keylist: if not keylist:
log.warning('%s => Empty keylist received from %s', log.warning("%s => Empty keylist received from %s", self._account, from_jid)
self._account, from_jid)
self._contacts.process_keylist(self.own_jid, keylist) self._contacts.process_keylist(self.own_jid, keylist)
if self.own_jid.bare_match(from_jid) and self._fingerprint is not None: if self.own_jid.bare_match(from_jid) and self._fingerprint is not None:
self.set_keylist() self.set_keylist()
return return
if self.own_jid.bare_match(from_jid): if self.own_jid.bare_match(from_jid):
log.info('Received own keylist') log.info("Received own keylist")
for key in keylist: for key in keylist:
log.info(key.fingerprint) log.info(key.fingerprint)
for key in keylist: for key in keylist:
# Check if own fingerprint is published # Check if own fingerprint is published
if key.fingerprint == self._fingerprint: if key.fingerprint == self._fingerprint:
log.info('Own key found in keys list') log.info("Own key found in keys list")
return return
log.info('Own key not published') log.info("Own key not published")
if self._fingerprint is not None: if self._fingerprint is not None:
keylist.append(Key(self._fingerprint, self._date)) keylist.append(Key(self._fingerprint, self._date))
self.set_keylist(keylist) self.set_keylist(keylist)
@@ -228,31 +222,29 @@ class OpenPGP(BaseModule):
try: try:
payload, recipients, _timestamp = parse_signcrypt(signcrypt) payload, recipients, _timestamp = parse_signcrypt(signcrypt)
except StanzaMalformed as error: except StanzaMalformed as error:
log.warning('Decryption failed: %s', error) log.warning("Decryption failed: %s", error)
log.warning(payload) log.warning(payload)
return return
if not any(map(self.own_jid.bare_match, recipients)): if not any(map(self.own_jid.bare_match, recipients)):
log.warning('to attr not valid') log.warning("to attr not valid")
log.warning(signcrypt) log.warning(signcrypt)
return return
keys = self._contacts.get_keys(remote_jid) keys = self._contacts.get_keys(remote_jid)
fingerprints = [key.fingerprint for key in keys] fingerprints = [key.fingerprint for key in keys]
if fingerprint not in fingerprints: if fingerprint not in fingerprints:
log.warning('Invalid fingerprint on message: %s', fingerprint) log.warning("Invalid fingerprint on message: %s", fingerprint)
log.warning('Expected: %s', fingerprints) log.warning("Expected: %s", fingerprints)
return return
log.info('Received OpenPGP message from: %s', properties.jid) log.info("Received OpenPGP message from: %s", properties.jid)
prepare_stanza(stanza, payload) prepare_stanza(stanza, payload)
trust = self._contacts.get_trust(remote_jid, fingerprint) trust = self._contacts.get_trust(remote_jid, fingerprint)
properties.encrypted = EncryptionData( properties.encrypted = EncryptionData(
protocol=ENCRYPTION_NAME, protocol=ENCRYPTION_NAME, key=fingerprint, trust=trust
key=fingerprint,
trust=trust
) )
raise StanzaDecrypted raise StanzaDecrypted
@@ -262,39 +254,38 @@ class OpenPGP(BaseModule):
keys = self._contacts.get_keys(remote_jid) keys = self._contacts.get_keys(remote_jid)
if not keys: if not keys:
log.error('Droping stanza to %s, because we have no key', remote_jid) log.error("Droping stanza to %s, because we have no key", remote_jid)
return return
keys += self._contacts.get_keys(self.own_jid) keys += self._contacts.get_keys(self.own_jid)
keys += [Key(self._fingerprint, None)] keys += [Key(self._fingerprint, None)]
payload = create_signcrypt_node(message.get_stanza(), payload = create_signcrypt_node(
[remote_jid], message.get_stanza(), [remote_jid], NOT_ENCRYPTED_TAGS
NOT_ENCRYPTED_TAGS) )
encrypted_payload, error = self._pgp.encrypt(payload, keys) encrypted_payload, error = self._pgp.encrypt(payload, keys)
if error: if error:
log.error('Error: %s', error) log.error("Error: %s", error)
text = message.get_text(with_fallback=False) or '' text = message.get_text(with_fallback=False) or ""
app.ged.raise_event( app.ged.raise_event(
MessageNotSent(client=self._client, MessageNotSent(
jid=str(remote_jid), client=self._client,
message=text, jid=str(remote_jid),
error=error, message=text,
time=time.time())) error=error,
time=time.time(),
)
)
return return
create_message_stanza( create_message_stanza(
message.get_stanza(), message.get_stanza(), encrypted_payload, bool(message.get_text())
encrypted_payload,
bool(message.get_text())
) )
message.set_encryption( message.set_encryption(
EncryptionData( EncryptionData(
protocol=ENCRYPTION_NAME, protocol=ENCRYPTION_NAME, key="Unknown", trust=Trust.VERIFIED
key='Unknown',
trust=Trust.VERIFIED
) )
) )
@@ -302,12 +293,12 @@ class OpenPGP(BaseModule):
@staticmethod @staticmethod
def print_msg_to_log(stanza): def print_msg_to_log(stanza):
""" Prints a stanza in a fancy way to the log """ """Prints a stanza in a fancy way to the log"""
log.debug('-'*15) log.debug("-" * 15)
stanzastr = '\n' + stanza.__str__(fancy=True) stanzastr = "\n" + stanza.__str__(fancy=True)
stanzastr = stanzastr[0:-1] stanzastr = stanzastr[0:-1]
log.debug(stanzastr) log.debug(stanzastr)
log.debug('-'*15) log.debug("-" * 15)
def get_keys(self, jid=None, only_trusted=True): def get_keys(self, jid=None, only_trusted=True):
if jid is None: if jid is None:
@@ -324,4 +315,4 @@ class OpenPGP(BaseModule):
def get_instance(*args, **kwargs): def get_instance(*args, **kwargs):
return OpenPGP(*args, **kwargs), 'OpenPGP' return OpenPGP(*args, **kwargs), "OpenPGP"

View File

@@ -14,24 +14,23 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from enum import IntEnum
from collections import namedtuple from collections import namedtuple
from enum import IntEnum
from nbxmpp.namespaces import Namespace from nbxmpp.namespaces import Namespace
ENCRYPTION_NAME = "OpenPGP"
ENCRYPTION_NAME = 'OpenPGP'
NOT_ENCRYPTED_TAGS = [ NOT_ENCRYPTED_TAGS = [
('no-store', Namespace.HINTS), ("no-store", Namespace.HINTS),
('store', Namespace.HINTS), ("store", Namespace.HINTS),
('no-copy', Namespace.HINTS), ("no-copy", Namespace.HINTS),
('no-permanent-store', Namespace.HINTS), ("no-permanent-store", Namespace.HINTS),
('origin-id', Namespace.SID), ("origin-id", Namespace.SID),
('thread', None) ("thread", None),
] ]
Key = namedtuple('Key', 'fingerprint date') Key = namedtuple("Key", "fingerprint date")
class Trust(IntEnum): class Trust(IntEnum):
@@ -42,8 +41,8 @@ class Trust(IntEnum):
def prepare_stanza(stanza, payload): def prepare_stanza(stanza, payload):
delete_nodes(stanza, 'openpgp', Namespace.OPENPGP) delete_nodes(stanza, "openpgp", Namespace.OPENPGP)
delete_nodes(stanza, 'body') delete_nodes(stanza, "body")
nodes = [(node.getName(), node.getNamespace()) for node in payload] nodes = [(node.getName(), node.getNamespace()) for node in payload]
for name, namespace in nodes: for name, namespace in nodes:
@@ -56,7 +55,7 @@ def prepare_stanza(stanza, payload):
def delete_nodes(stanza, name, namespace=None): def delete_nodes(stanza, name, namespace=None):
attrs = None attrs = None
if namespace is not None: if namespace is not None:
attrs = {'xmlns': Namespace.OPENPGP} attrs = {"xmlns": Namespace.OPENPGP}
nodes = stanza.getTags(name, attrs) nodes = stanza.getTags(name, attrs)
for node in nodes: for node in nodes:
stanza.delChild(node) stanza.delChild(node)

View File

@@ -17,22 +17,21 @@
import logging import logging
from pathlib import Path from pathlib import Path
from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from nbxmpp.namespaces import Namespace from gi.repository import Gtk
from nbxmpp import JID from nbxmpp import JID
from nbxmpp.namespaces import Namespace
from gajim.common import app from gajim.common import app
from gajim.common import ged
from gajim.common import configpaths from gajim.common import configpaths
from gajim.common import ged
from gajim.common.const import CSSPriority from gajim.common.const import CSSPriority
from gajim.gtk.dialogs import SimpleDialog from gajim.gtk.dialogs import SimpleDialog
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from openpgp.modules.util import ENCRYPTION_NAME from openpgp.modules.util import ENCRYPTION_NAME
try: try:
from openpgp.modules import openpgp from openpgp.modules import openpgp
except (ImportError, OSError) as e: except (ImportError, OSError) as e:
@@ -40,7 +39,7 @@ except (ImportError, OSError) as e:
else: else:
ERROR_MSG = None ERROR_MSG = None
log = logging.getLogger('gajim.p.openpgp') log = logging.getLogger("gajim.p.openpgp")
class OpenPGPPlugin(GajimPlugin): class OpenPGPPlugin(GajimPlugin):
@@ -52,23 +51,21 @@ class OpenPGPPlugin(GajimPlugin):
return return
self.events_handlers = { self.events_handlers = {
'signed-in': (ged.PRECORE, self.signed_in), "signed-in": (ged.PRECORE, self.signed_in),
} }
self.modules = [openpgp] self.modules = [openpgp]
self.encryption_name = ENCRYPTION_NAME self.encryption_name = ENCRYPTION_NAME
self.config_dialog = None self.config_dialog = None
self.gui_extension_points = { self.gui_extension_points = {
'encrypt' + self.encryption_name: (self._encrypt_message, None), "encrypt" + self.encryption_name: (self._encrypt_message, None),
'send_message' + self.encryption_name: ( "send_message" + self.encryption_name: (self._before_sendmessage, None),
self._before_sendmessage, None), "encryption_dialog"
'encryption_dialog' + self.encryption_name: ( + self.encryption_name: (self.on_encryption_button_clicked, None),
self.on_encryption_button_clicked, None), "encryption_state" + self.encryption_name: (self.encryption_state, None),
'encryption_state' + self.encryption_name: ( "update_caps": (self._update_caps, None),
self.encryption_state, None), }
'update_caps': (self._update_caps, None),
}
self.connections = {} self.connections = {}
@@ -80,74 +77,78 @@ class OpenPGPPlugin(GajimPlugin):
self._load_css() self._load_css()
def _load_css(self): def _load_css(self):
path = Path(__file__).parent / 'gtk' / 'style.css' path = Path(__file__).parent / "gtk" / "style.css"
try: try:
with path.open('r') as f: with path.open("r") as f:
css = f.read() css = f.read()
except Exception as exc: except Exception as exc:
log.error('Error loading css: %s', exc) log.error("Error loading css: %s", exc)
return return
try: try:
provider = Gtk.CssProvider() provider = Gtk.CssProvider()
provider.load_from_data(bytes(css.encode('utf-8'))) provider.load_from_data(bytes(css.encode("utf-8")))
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), Gtk.StyleContext.add_provider_for_screen(
provider, Gdk.Screen.get_default(), provider, CSSPriority.DEFAULT_THEME
CSSPriority.DEFAULT_THEME) )
except Exception: except Exception:
log.exception('Error loading application css') log.exception("Error loading application css")
@staticmethod @staticmethod
def _create_paths(): def _create_paths():
keyring_path = Path(configpaths.get('MY_DATA')) / 'openpgp' keyring_path = Path(configpaths.get("MY_DATA")) / "openpgp"
if not keyring_path.exists(): if not keyring_path.exists():
keyring_path.mkdir() keyring_path.mkdir()
def signed_in(self, event): def signed_in(self, event):
client = app.get_client(event.account) client = app.get_client(event.account)
if client.get_module('OpenPGP').secret_key_available: if client.get_module("OpenPGP").secret_key_available:
log.info('%s => Publish keylist and public key after sign in', log.info(
event.account) "%s => Publish keylist and public key after sign in", event.account
client.get_module('OpenPGP').request_keylist() )
client.get_module('OpenPGP').set_public_key() client.get_module("OpenPGP").request_keylist()
client.get_module("OpenPGP").set_public_key()
def activate(self): def activate(self):
for account in app.settings.get_active_accounts(): for account in app.settings.get_active_accounts():
client = app.get_client(account) client = app.get_client(account)
client.get_module('Caps').update_caps() client.get_module("Caps").update_caps()
if app.account_is_connected(account): if app.account_is_connected(account):
if client.get_module('OpenPGP').secret_key_available: if client.get_module("OpenPGP").secret_key_available:
log.info('%s => Publish keylist and public key ' log.info(
'after plugin activation', account) "%s => Publish keylist and public key "
client.get_module('OpenPGP').request_keylist() "after plugin activation",
client.get_module('OpenPGP').set_public_key() account,
)
client.get_module("OpenPGP").request_keylist()
client.get_module("OpenPGP").set_public_key()
def deactivate(self): def deactivate(self):
pass pass
@staticmethod @staticmethod
def _update_caps(_account, features): def _update_caps(_account, features):
features.append('%s+notify' % Namespace.OPENPGP_PK) features.append("%s+notify" % Namespace.OPENPGP_PK)
def activate_encryption(self, chat_control): def activate_encryption(self, chat_control):
account = chat_control.account account = chat_control.account
jid = chat_control.contact.jid jid = chat_control.contact.jid
client = app.get_client(account) client = app.get_client(account)
if client.get_module('OpenPGP').secret_key_available: if client.get_module("OpenPGP").secret_key_available:
keys = client.get_module('OpenPGP').get_keys( keys = client.get_module("OpenPGP").get_keys(jid, only_trusted=False)
jid, only_trusted=False)
if not keys: if not keys:
client.get_module('OpenPGP').request_keylist(JID.from_string(jid)) client.get_module("OpenPGP").request_keylist(JID.from_string(jid))
return True return True
from openpgp.gtk.wizard import KeyWizard from openpgp.gtk.wizard import KeyWizard
KeyWizard(self, account, chat_control) KeyWizard(self, account, chat_control)
return False return False
@staticmethod @staticmethod
def encryption_state(_chat_control, state): def encryption_state(_chat_control, state):
state['authenticated'] = True state["authenticated"] = True
state['visible'] = True state["visible"] = True
@staticmethod @staticmethod
def on_encryption_button_clicked(chat_control): def on_encryption_button_clicked(chat_control):
@@ -155,6 +156,7 @@ class OpenPGPPlugin(GajimPlugin):
jid = chat_control.contact.jid jid = chat_control.contact.jid
from openpgp.gtk.key import KeyDialog from openpgp.gtk.key import KeyDialog
KeyDialog(account, jid, app.window) KeyDialog(account, jid, app.window)
def _before_sendmessage(self, chat_control): def _before_sendmessage(self, chat_control):
@@ -162,20 +164,21 @@ class OpenPGPPlugin(GajimPlugin):
jid = chat_control.contact.jid jid = chat_control.contact.jid
client = app.get_client(account) client = app.get_client(account)
if not client.get_module('OpenPGP').secret_key_available: if not client.get_module("OpenPGP").secret_key_available:
from openpgp.gtk.wizard import KeyWizard from openpgp.gtk.wizard import KeyWizard
KeyWizard(self, account, chat_control) KeyWizard(self, account, chat_control)
return return
keys = client.get_module('OpenPGP').get_keys(jid) keys = client.get_module("OpenPGP").get_keys(jid)
if not keys: if not keys:
SimpleDialog( SimpleDialog(
_('Not Trusted'), _("Not Trusted"), _("There was no trusted and active key found")
_('There was no trusted and active key found')) )
chat_control.sendmessage = False chat_control.sendmessage = False
@staticmethod @staticmethod
def _encrypt_message(client, obj, callback): def _encrypt_message(client, obj, callback):
if not client.get_module('OpenPGP').secret_key_available: if not client.get_module("OpenPGP").secret_key_available:
return return
client.get_module('OpenPGP').encrypt_message(obj, callback) client.get_module("OpenPGP").encrypt_message(obj, callback)

View File

@@ -20,8 +20,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import os
import logging import logging
import os
from functools import lru_cache from functools import lru_cache
import gnupg import gnupg
@@ -30,56 +30,51 @@ from gajim.common.util.classes import Singleton
from pgp.exceptions import SignError from pgp.exceptions import SignError
logger = logging.getLogger("gajim.p.pgplegacy")
logger = logging.getLogger('gajim.p.pgplegacy')
if logger.getEffectiveLevel() == logging.DEBUG: if logger.getEffectiveLevel() == logging.DEBUG:
logger = logging.getLogger('gnupg') logger = logging.getLogger("gnupg")
logger.addHandler(logging.StreamHandler()) logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
class PGP(gnupg.GPG, metaclass=Singleton): class PGP(gnupg.GPG, metaclass=Singleton):
def __init__(self, binary, encoding=None): def __init__(self, binary, encoding=None):
super().__init__(gpgbinary=binary, super().__init__(gpgbinary=binary, use_agent=True)
use_agent=True)
if encoding is not None: if encoding is not None:
self.encoding = encoding self.encoding = encoding
self.decode_errors = 'replace' self.decode_errors = "replace"
def encrypt(self, payload, recipients, always_trust=False): def encrypt(self, payload, recipients, always_trust=False):
if not always_trust: if not always_trust:
# check that we'll be able to encrypt # check that we'll be able to encrypt
result = self.get_key(recipients[0]) result = self.get_key(recipients[0])
for key in result: for key in result:
if key['trust'] not in ('f', 'u'): if key["trust"] not in ("f", "u"):
return '', 'NOT_TRUSTED ' + key['keyid'][-8:] return "", "NOT_TRUSTED " + key["keyid"][-8:]
result = super().encrypt( result = super().encrypt(
payload.encode('utf8'), payload.encode("utf8"), recipients, always_trust=always_trust
recipients, )
always_trust=always_trust)
if result.ok: if result.ok:
error = '' error = ""
else: else:
error = result.status error = result.status
return self._strip_header_footer(str(result)), error return self._strip_header_footer(str(result)), error
def decrypt(self, payload): def decrypt(self, payload):
data = self._add_header_footer(payload, 'MESSAGE') data = self._add_header_footer(payload, "MESSAGE")
result = super().decrypt(data.encode('utf8')) result = super().decrypt(data.encode("utf8"))
return result.data.decode('utf8') return result.data.decode("utf8")
@lru_cache(maxsize=8) @lru_cache(maxsize=8)
def sign(self, payload, key_id): def sign(self, payload, key_id):
if payload is None: if payload is None:
payload = '' payload = ""
result = super().sign(payload.encode('utf8'), result = super().sign(payload.encode("utf8"), keyid=key_id, detach=True)
keyid=key_id,
detach=True)
if result.fingerprint: if result.fingerprint:
return self._strip_header_footer(str(result)) return self._strip_header_footer(str(result))
@@ -91,19 +86,20 @@ class PGP(gnupg.GPG, metaclass=Singleton):
# Text name for hash algorithms from RFC 4880 - section 9.4 # Text name for hash algorithms from RFC 4880 - section 9.4
if payload is None: if payload is None:
payload = '' payload = ""
hash_algorithms = ['SHA512', 'SHA384', 'SHA256', hash_algorithms = ["SHA512", "SHA384", "SHA256", "SHA224", "SHA1", "RIPEMD160"]
'SHA224', 'SHA1', 'RIPEMD160']
for algo in hash_algorithms: for algo in hash_algorithms:
data = os.linesep.join( data = os.linesep.join(
['-----BEGIN PGP SIGNED MESSAGE-----', [
'Hash: ' + algo, "-----BEGIN PGP SIGNED MESSAGE-----",
'', "Hash: " + algo,
payload, "",
self._add_header_footer(signed, 'SIGNATURE')] payload,
) self._add_header_footer(signed, "SIGNATURE"),
result = super().verify(data.encode('utf8')) ]
)
result = super().verify(data.encode("utf8"))
if result.valid: if result.valid:
return result.fingerprint return result.fingerprint
@@ -116,7 +112,7 @@ class PGP(gnupg.GPG, metaclass=Singleton):
for key in result: for key in result:
# Take first not empty uid # Take first not empty uid
keys[key['fingerprint']] = next(uid for uid in key['uids'] if uid) keys[key["fingerprint"]] = next(uid for uid in key["uids"] if uid)
return keys return keys
@staticmethod @staticmethod
@@ -125,19 +121,19 @@ class PGP(gnupg.GPG, metaclass=Singleton):
Remove header and footer from data Remove header and footer from data
""" """
if not data: if not data:
return '' return ""
lines = data.splitlines() lines = data.splitlines()
while lines[0] != '': while lines[0] != "":
lines.remove(lines[0]) lines.remove(lines[0])
while lines[0] == '': while lines[0] == "":
lines.remove(lines[0]) lines.remove(lines[0])
i = 0 i = 0
for line in lines: for line in lines:
if line: if line:
if line[0] == '-': if line[0] == "-":
break break
i = i+1 i = i + 1
line = '\n'.join(lines[0:i]) line = "\n".join(lines[0:i])
return line return line
@staticmethod @staticmethod

View File

@@ -34,31 +34,30 @@ class KeyStore:
self._account = account self._account = account
own_bare_jid = own_jid.bare own_bare_jid = own_jid.bare
path = Path(configpaths.get('PLUGINS_DATA')) / 'pgplegacy' / own_bare_jid path = Path(configpaths.get("PLUGINS_DATA")) / "pgplegacy" / own_bare_jid
if not path.exists(): if not path.exists():
path.mkdir(parents=True) path.mkdir(parents=True)
self._store_path = path / 'store' self._store_path = path / "store"
if self._store_path.exists(): if self._store_path.exists():
# having store v2 or higher # having store v2 or higher
with self._store_path.open('r') as file: with self._store_path.open("r") as file:
try: try:
self._store = json.load(file) self._store = json.load(file)
except Exception: except Exception:
log.exception('Could not load config') log.exception("Could not load config")
self._store = self._empty_store() self._store = self._empty_store()
ver = self._store.get('_version', 2) ver = self._store.get("_version", 2)
if ver > CURRENT_STORE_VERSION: if ver > CURRENT_STORE_VERSION:
raise Exception('Unknown store version! ' raise Exception("Unknown store version! " "Please upgrade pgp plugin.")
'Please upgrade pgp plugin.')
elif ver == 2: elif ver == 2:
self._migrate_v2_store() self._migrate_v2_store()
self._save_store() self._save_store()
elif ver != CURRENT_STORE_VERSION: elif ver != CURRENT_STORE_VERSION:
# garbled version # garbled version
self._store = self._empty_store() self._store = self._empty_store()
log.warning('Bad pgp key store version. Initializing new.') log.warning("Bad pgp key store version. Initializing new.")
else: else:
# having store v1 or fresh install # having store v1 or fresh install
self._store = self._empty_store() self._store = self._empty_store()
@@ -69,15 +68,16 @@ class KeyStore:
@staticmethod @staticmethod
def _empty_store(): def _empty_store():
return { return {
'_version': CURRENT_STORE_VERSION, "_version": CURRENT_STORE_VERSION,
'own_key_data': None, "own_key_data": None,
'contact_key_data': {}, "contact_key_data": {},
} }
def _migrate_v1_store(self): def _migrate_v1_store(self):
keys = {} keys = {}
attached_keys = app.settings.get_account_setting( attached_keys = app.settings.get_account_setting(
self._account, 'attached_gpg_keys') self._account, "attached_gpg_keys"
)
if not attached_keys: if not attached_keys:
return return
attached_keys = attached_keys.split() attached_keys = attached_keys.split()
@@ -86,23 +86,25 @@ class KeyStore:
keys[attached_keys[2 * i]] = attached_keys[2 * i + 1] keys[attached_keys[2 * i]] = attached_keys[2 * i + 1]
for jid, key_id in keys.items(): for jid, key_id in keys.items():
self._set_contact_key_data_nosync(jid, (key_id, '')) self._set_contact_key_data_nosync(jid, (key_id, ""))
own_key_id = app.settings.get_account_setting(self._account, 'keyid') own_key_id = app.settings.get_account_setting(self._account, "keyid")
own_key_user = app.settings.get_account_setting( own_key_user = app.settings.get_account_setting(self._account, "keyname")
self._account, 'keyname')
if own_key_id: if own_key_id:
self._set_own_key_data_nosync((own_key_id, own_key_user)) self._set_own_key_data_nosync((own_key_id, own_key_user))
attached_keys = app.settings.set_account_setting( attached_keys = app.settings.set_account_setting(
self._account, 'attached_gpg_keys', '') self._account, "attached_gpg_keys", ""
self._log.info('Migration from store v1 was successful') )
self._log.info("Migration from store v1 was successful")
def _migrate_v2_store(self): def _migrate_v2_store(self):
own_key_data = self.get_own_key_data() own_key_data = self.get_own_key_data()
if own_key_data is not None: if own_key_data is not None:
own_key_id, own_key_user = (own_key_data['key_id'], own_key_id, own_key_user = (
own_key_data['key_user']) own_key_data["key_id"],
own_key_data["key_user"],
)
try: try:
own_key_fp = self._resolve_short_id(own_key_id, has_secret=True) own_key_fp = self._resolve_short_id(own_key_id, has_secret=True)
self._set_own_key_data_nosync((own_key_fp, own_key_user)) self._set_own_key_data_nosync((own_key_fp, own_key_user))
@@ -111,38 +113,41 @@ class KeyStore:
prune_list = [] prune_list = []
for dict_key, key_data in self._store['contact_key_data'].items(): for dict_key, key_data in self._store["contact_key_data"].items():
try: try:
key_data['key_id'] = self._resolve_short_id(key_data['key_id']) key_data["key_id"] = self._resolve_short_id(key_data["key_id"])
except KeyResolveError: except KeyResolveError:
prune_list.append(dict_key) prune_list.append(dict_key)
for dict_key in prune_list: for dict_key in prune_list:
del self._store['contact_key_data'][dict_key] del self._store["contact_key_data"][dict_key]
self._store['_version'] = CURRENT_STORE_VERSION self._store["_version"] = CURRENT_STORE_VERSION
self._log.info('Migration from store v2 was successful') self._log.info("Migration from store v2 was successful")
def _save_store(self): def _save_store(self):
with self._store_path.open('w') as file: with self._store_path.open("w") as file:
json.dump(self._store, file) json.dump(self._store, file)
def _get_dict_key(self, jid): def _get_dict_key(self, jid):
return '%s-%s' % (self._account, jid) return "%s-%s" % (self._account, jid)
def _resolve_short_id(self, short_id, has_secret=False): def _resolve_short_id(self, short_id, has_secret=False):
candidates = self._list_keys_func( candidates = self._list_keys_func(
secret=has_secret, keys=(short_id,)).fingerprints secret=has_secret, keys=(short_id,)
).fingerprints
if len(candidates) == 1: if len(candidates) == 1:
return candidates[0] return candidates[0]
elif len(candidates) > 1: elif len(candidates) > 1:
self._log.critical('Key collision during migration. ' self._log.critical(
'Key ID is %s. Removing binding...', "Key collision during migration. " "Key ID is %s. Removing binding...",
repr(short_id)) repr(short_id),
)
else: else:
self._log.warning('Key %s was not found during migration. ' self._log.warning(
'Removing binding...', "Key %s was not found during migration. " "Removing binding...",
repr(short_id)) repr(short_id),
)
raise KeyResolveError raise KeyResolveError
def set_own_key_data(self, key_data): def set_own_key_data(self, key_data):
@@ -151,18 +156,18 @@ class KeyStore:
def _set_own_key_data_nosync(self, key_data): def _set_own_key_data_nosync(self, key_data):
if key_data is None: if key_data is None:
self._store['own_key_data'] = None self._store["own_key_data"] = None
else: else:
self._store['own_key_data'] = { self._store["own_key_data"] = {
'key_id': key_data[0], "key_id": key_data[0],
'key_user': key_data[1] "key_user": key_data[1],
} }
def get_own_key_data(self): def get_own_key_data(self):
return self._store['own_key_data'] return self._store["own_key_data"]
def get_contact_key_data(self, jid): def get_contact_key_data(self, jid):
key_ids = self._store['contact_key_data'] key_ids = self._store["contact_key_data"]
dict_key = self._get_dict_key(jid) dict_key = self._get_dict_key(jid)
return key_ids.get(dict_key) return key_ids.get(dict_key)
@@ -171,12 +176,9 @@ class KeyStore:
self._save_store() self._save_store()
def _set_contact_key_data_nosync(self, jid, key_data): def _set_contact_key_data_nosync(self, jid, key_data):
key_ids = self._store['contact_key_data'] key_ids = self._store["contact_key_data"]
dict_key = self._get_dict_key(jid) dict_key = self._get_dict_key(jid)
if key_data is None: if key_data is None:
key_ids[dict_key] = None key_ids[dict_key] = None
else: else:
key_ids[dict_key] = { key_ids[dict_key] = {"key_id": key_data[0], "key_user": key_data[1]}
'key_id': key_data[0],
'key_user': key_data[1]
}

View File

@@ -14,11 +14,14 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
class SignError(Exception): class SignError(Exception):
pass pass
class KeyMismatch(Exception): class KeyMismatch(Exception):
pass pass
class NoKeyIdFound(Exception): class NoKeyIdFound(Exception):
pass pass

View File

@@ -16,12 +16,11 @@
from pathlib import Path from pathlib import Path
from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
@@ -33,14 +32,14 @@ class PGPConfigDialog(Gtk.ApplicationWindow):
Gtk.ApplicationWindow.__init__(self) Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app) self.set_application(app.app)
self.set_show_menubar(False) self.set_show_menubar(False)
self.set_title(_('PGP Configuration')) self.set_title(_("PGP Configuration"))
self.set_transient_for(parent) self.set_transient_for(parent)
self.set_resizable(True) self.set_resizable(True)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG) self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.set_destroy_with_parent(True) self.set_destroy_with_parent(True)
ui_path = Path(__file__).parent ui_path = Path(__file__).parent
self._ui = get_builder(ui_path.resolve() / 'config.ui') self._ui = get_builder(ui_path.resolve() / "config.ui")
self.add(self._ui.config_box) self.add(self._ui.config_box)
@@ -50,9 +49,7 @@ class PGPConfigDialog(Gtk.ApplicationWindow):
for account in app.settings.get_active_accounts(): for account in app.settings.get_active_accounts():
page = Page(plugin, account) page = Page(plugin, account)
self._ui.stack.add_titled(page, self._ui.stack.add_titled(page, account, app.get_account_label(account))
account,
app.get_account_label(account))
self.show_all() self.show_all()
@@ -64,11 +61,11 @@ class Page(Gtk.Box):
self._client = app.get_client(account) self._client = app.get_client(account)
self._plugin = plugin self._plugin = plugin
self._label = Gtk.Label() self._label = Gtk.Label()
self._button = Gtk.Button(label=_('Assign Key')) self._button = Gtk.Button(label=_("Assign Key"))
self._button.get_style_context().add_class('suggested-action') self._button.get_style_context().add_class("suggested-action")
self._button.set_halign(Gtk.Align.CENTER) self._button.set_halign(Gtk.Align.CENTER)
self._button.set_margin_top(18) self._button.set_margin_top(18)
self._button.connect('clicked', self._on_assign) self._button.connect("clicked", self._on_assign)
self._load_key() self._load_key()
self.add(self._label) self.add(self._label)
@@ -76,34 +73,34 @@ class Page(Gtk.Box):
self.show_all() self.show_all()
def _on_assign(self, _button): def _on_assign(self, _button):
backend = self._client.get_module('PGPLegacy').pgp_backend backend = self._client.get_module("PGPLegacy").pgp_backend
secret_keys = backend.get_keys(secret=True) secret_keys = backend.get_keys(secret=True)
dialog = ChooseGPGKeyDialog(secret_keys, self.get_toplevel()) dialog = ChooseGPGKeyDialog(secret_keys, self.get_toplevel())
dialog.connect('response', self._on_response) dialog.connect("response", self._on_response)
def _load_key(self): def _load_key(self):
key_data = self._client.get_module('PGPLegacy').get_own_key_data() key_data = self._client.get_module("PGPLegacy").get_own_key_data()
if key_data is None: if key_data is None:
self._set_key(None) self._set_key(None)
else: else:
self._set_key((key_data['key_id'], key_data['key_user'])) self._set_key((key_data["key_id"], key_data["key_user"]))
def _on_response(self, dialog, response): def _on_response(self, dialog, response):
if response != Gtk.ResponseType.OK: if response != Gtk.ResponseType.OK:
return return
if dialog.selected_key is None: if dialog.selected_key is None:
self._client.get_module('PGPLegacy').set_own_key_data(None) self._client.get_module("PGPLegacy").set_own_key_data(None)
self._set_key(None) self._set_key(None)
else: else:
self._client.get_module('PGPLegacy').set_own_key_data( self._client.get_module("PGPLegacy").set_own_key_data(dialog.selected_key)
dialog.selected_key)
self._set_key(dialog.selected_key) self._set_key(dialog.selected_key)
def _set_key(self, key_data): def _set_key(self, key_data):
if key_data is None: if key_data is None:
self._label.set_text(_('No key assigned')) self._label.set_text(_("No key assigned"))
else: else:
key_id, key_user = key_data key_id, key_user = key_data
self._label.set_markup('<b><tt>%s</tt> %s</b>' % \ self._label.set_markup(
(key_id, GLib.markup_escape_text(key_user))) "<b><tt>%s</tt> %s</b>" % (key_id, GLib.markup_escape_text(key_user))
)

View File

@@ -16,18 +16,17 @@
from pathlib import Path from pathlib import Path
from gi.repository import Gtk
from gi.repository import GLib from gi.repository import GLib
from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
class KeyDialog(Gtk.Dialog): class KeyDialog(Gtk.Dialog):
def __init__(self, plugin, account, jid, transient): def __init__(self, plugin, account, jid, transient):
super().__init__(title=_('Assign key for %s') % jid, super().__init__(title=_("Assign key for %s") % jid, destroy_with_parent=True)
destroy_with_parent=True)
self.set_transient_for(transient) self.set_transient_for(transient)
self.set_resizable(True) self.set_resizable(True)
@@ -39,11 +38,11 @@ class KeyDialog(Gtk.Dialog):
self._label = Gtk.Label() self._label = Gtk.Label()
self._assign_button = Gtk.Button(label=_('Assign Key')) self._assign_button = Gtk.Button(label=_("Assign Key"))
self._assign_button.get_style_context().add_class('suggested-action') self._assign_button.get_style_context().add_class("suggested-action")
self._assign_button.set_halign(Gtk.Align.CENTER) self._assign_button.set_halign(Gtk.Align.CENTER)
self._assign_button.set_margin_top(18) self._assign_button.set_margin_top(18)
self._assign_button.connect('clicked', self._choose_key) self._assign_button.connect("clicked", self._choose_key)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.set_border_width(18) box.set_border_width(18)
@@ -57,13 +56,12 @@ class KeyDialog(Gtk.Dialog):
self.show_all() self.show_all()
def _choose_key(self, *args): def _choose_key(self, *args):
backend = self._client.get_module('PGPLegacy').pgp_backend backend = self._client.get_module("PGPLegacy").pgp_backend
dialog = ChooseGPGKeyDialog(backend.get_keys(), self) dialog = ChooseGPGKeyDialog(backend.get_keys(), self)
dialog.connect('response', self._on_response) dialog.connect("response", self._on_response)
def _load_key(self): def _load_key(self):
key_data = self._client.get_module('PGPLegacy').get_contact_key_data( key_data = self._client.get_module("PGPLegacy").get_contact_key_data(self._jid)
self._jid)
if key_data is None: if key_data is None:
self._set_key(None) self._set_key(None)
else: else:
@@ -74,42 +72,43 @@ class KeyDialog(Gtk.Dialog):
return return
if dialog.selected_key is None: if dialog.selected_key is None:
self._client.get_module('PGPLegacy').set_contact_key_data( self._client.get_module("PGPLegacy").set_contact_key_data(self._jid, None)
self._jid, None)
self._set_key(None) self._set_key(None)
else: else:
self._client.get_module('PGPLegacy').set_contact_key_data( self._client.get_module("PGPLegacy").set_contact_key_data(
self._jid, dialog.selected_key) self._jid, dialog.selected_key
)
self._set_key(dialog.selected_key) self._set_key(dialog.selected_key)
def _set_key(self, key_data): def _set_key(self, key_data):
if key_data is None: if key_data is None:
self._label.set_text(_('No key assigned')) self._label.set_text(_("No key assigned"))
else: else:
key_id, key_user = key_data key_id, key_user = key_data
self._label.set_markup('<b><tt>%s</tt> %s</b>' % \ self._label.set_markup(
(key_id, GLib.markup_escape_text(key_user))) "<b><tt>%s</tt> %s</b>" % (key_id, GLib.markup_escape_text(key_user))
)
class ChooseGPGKeyDialog(Gtk.Dialog): class ChooseGPGKeyDialog(Gtk.Dialog):
def __init__(self, secret_keys, transient_for): def __init__(self, secret_keys, transient_for):
Gtk.Dialog.__init__(self, Gtk.Dialog.__init__(
title=_('Assign PGP Key'), self, title=_("Assign PGP Key"), transient_for=transient_for
transient_for=transient_for) )
secret_keys[_('None')] = _('None') secret_keys[_("None")] = _("None")
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
self.set_resizable(True) self.set_resizable(True)
self.set_default_size(500, 300) self.set_default_size(500, 300)
self.add_button(_('Cancel'), Gtk.ResponseType.CANCEL) self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
self.add_button(_('OK'), Gtk.ResponseType.OK) self.add_button(_("OK"), Gtk.ResponseType.OK)
self._selected_key = None self._selected_key = None
ui_path = Path(__file__).parent ui_path = Path(__file__).parent
self._ui = get_builder(ui_path.resolve() / 'choose_key.ui') self._ui = get_builder(ui_path.resolve() / "choose_key.ui")
self._ui.keys_treeview = self._ui.keys_treeview self._ui.keys_treeview = self._ui.keys_treeview
@@ -124,7 +123,7 @@ class ChooseGPGKeyDialog(Gtk.Dialog):
self._ui.connect_signals(self) self._ui.connect_signals(self)
self.connect_after('response', self._on_response) self.connect_after("response", self._on_response)
self.show_all() self.show_all()
@@ -136,9 +135,9 @@ class ChooseGPGKeyDialog(Gtk.Dialog):
def _sort(model, iter1, iter2, _data): def _sort(model, iter1, iter2, _data):
value1 = model[iter1][1] value1 = model[iter1][1]
value2 = model[iter2][1] value2 = model[iter2][1]
if value1 == _('None'): if value1 == _("None"):
return -1 return -1
if value2 == _('None'): if value2 == _("None"):
return 1 return 1
if value1 < value2: if value1 < value2:
return -1 return -1
@@ -154,7 +153,7 @@ class ChooseGPGKeyDialog(Gtk.Dialog):
self._selected_key = None self._selected_key = None
else: else:
key_id, key_user = model[iter_][0], model[iter_][1] key_id, key_user = model[iter_][0], model[iter_][1]
if key_id == _('None'): if key_id == _("None"):
self._selected_key = None self._selected_key = None
else: else:
self._selected_key = key_id, key_user self._selected_key = key_id, key_user

View File

@@ -14,7 +14,8 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Callable from typing import Any
from typing import Callable
from dataclasses import dataclass from dataclasses import dataclass
from dataclasses import field from dataclasses import field
@@ -24,12 +25,12 @@ from gajim.common.events import ApplicationEvent
@dataclass @dataclass
class PGPNotTrusted(ApplicationEvent): class PGPNotTrusted(ApplicationEvent):
name: str = field(init=False, default='pgp-not-trusted') name: str = field(init=False, default="pgp-not-trusted")
on_yes: Callable[..., Any] on_yes: Callable[..., Any]
on_no: Callable[..., Any] on_no: Callable[..., Any]
@dataclass @dataclass
class PGPFileEncryptionError(ApplicationEvent): class PGPFileEncryptionError(ApplicationEvent):
name: str = field(init=False, default='pgp-file-encryption-error') name: str = field(init=False, default="pgp-file-encryption-error")
error: str error: str

View File

@@ -15,57 +15,55 @@
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import os import os
import time
import threading import threading
import time
import nbxmpp import nbxmpp
from gi.repository import GLib
from nbxmpp.namespaces import Namespace from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import Message from nbxmpp.protocol import Message
from nbxmpp.structs import EncryptionData from nbxmpp.structs import EncryptionData
from nbxmpp.structs import StanzaHandler from nbxmpp.structs import StanzaHandler
from gi.repository import GLib
from gajim.common import app from gajim.common import app
from gajim.common.const import Trust from gajim.common.const import Trust
from gajim.common.events import MessageNotSent from gajim.common.events import MessageNotSent
from gajim.common.structs import OutgoingMessage
from gajim.common.modules.base import BaseModule from gajim.common.modules.base import BaseModule
from gajim.common.structs import OutgoingMessage
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from pgp.backend.python_gnupg import PGP from pgp.backend.python_gnupg import PGP
from pgp.backend.store import KeyStore
from pgp.exceptions import KeyMismatch
from pgp.exceptions import NoKeyIdFound
from pgp.exceptions import SignError
from pgp.modules.events import PGPFileEncryptionError from pgp.modules.events import PGPFileEncryptionError
from pgp.modules.events import PGPNotTrusted from pgp.modules.events import PGPNotTrusted
from pgp.modules.util import prepare_stanza from pgp.modules.util import prepare_stanza
from pgp.backend.store import KeyStore
from pgp.exceptions import SignError
from pgp.exceptions import KeyMismatch
from pgp.exceptions import NoKeyIdFound
# Module name # Module name
name = 'PGPLegacy' name = "PGPLegacy"
zeroconf = True zeroconf = True
ENCRYPTION_NAME = 'PGP' ENCRYPTION_NAME = "PGP"
ALLOWED_TAGS = [ ALLOWED_TAGS = [
('request', Namespace.RECEIPTS), ("request", Namespace.RECEIPTS),
('active', Namespace.CHATSTATES), ("active", Namespace.CHATSTATES),
('gone', Namespace.CHATSTATES), ("gone", Namespace.CHATSTATES),
('inactive', Namespace.CHATSTATES), ("inactive", Namespace.CHATSTATES),
('paused', Namespace.CHATSTATES), ("paused", Namespace.CHATSTATES),
('composing', Namespace.CHATSTATES), ("composing", Namespace.CHATSTATES),
('markable', Namespace.CHATMARKERS), ("markable", Namespace.CHATMARKERS),
('no-store', Namespace.HINTS), ("no-store", Namespace.HINTS),
('store', Namespace.HINTS), ("store", Namespace.HINTS),
('no-copy', Namespace.HINTS), ("no-copy", Namespace.HINTS),
('no-permanent-store', Namespace.HINTS), ("no-permanent-store", Namespace.HINTS),
('replace', Namespace.CORRECT), ("replace", Namespace.CORRECT),
('thread', None), ("thread", None),
('reply', Namespace.REPLY), ("reply", Namespace.REPLY),
('fallback', Namespace.FALLBACK), ("fallback", Namespace.FALLBACK),
('origin-id', Namespace.SID), ("origin-id", Namespace.SID),
('reactions', Namespace.REACTIONS), ("reactions", Namespace.REACTIONS),
] ]
@@ -74,21 +72,26 @@ class PGPLegacy(BaseModule):
BaseModule.__init__(self, client, plugin=True) BaseModule.__init__(self, client, plugin=True)
self.handlers = [ self.handlers = [
StanzaHandler(name='message', StanzaHandler(
callback=self._message_received, name="message",
ns=Namespace.ENCRYPTED, callback=self._message_received,
priority=9), ns=Namespace.ENCRYPTED,
StanzaHandler(name='presence', priority=9,
callback=self._on_presence_received, ),
ns=Namespace.SIGNED, StanzaHandler(
priority=48), name="presence",
callback=self._on_presence_received,
ns=Namespace.SIGNED,
priority=48,
),
] ]
self.own_jid = self._client.get_own_jid() self.own_jid = self._client.get_own_jid()
self._pgp = PGP() self._pgp = PGP()
self._store = KeyStore(self._account, self.own_jid, self._log, self._store = KeyStore(
self._pgp.list_keys) self._account, self.own_jid, self._log, self._pgp.list_keys
)
self._always_trust = [] self._always_trust = []
self._presence_fingerprint_store = {} self._presence_fingerprint_store = {}
@@ -112,7 +115,7 @@ class PGPLegacy(BaseModule):
key_data = self.get_contact_key_data(jid) key_data = self.get_contact_key_data(jid)
if key_data is None: if key_data is None:
return False return False
key_id = key_data['key_id'] key_id = key_data["key_id"]
announced_fingerprint = self._presence_fingerprint_store.get(jid) announced_fingerprint = self._presence_fingerprint_store.get(jid)
if announced_fingerprint is None: if announced_fingerprint is None:
@@ -130,24 +133,31 @@ class PGPLegacy(BaseModule):
fingerprint = self._pgp.verify(properties.status, properties.signed) fingerprint = self._pgp.verify(properties.status, properties.signed)
if fingerprint is None: if fingerprint is None:
self._log.info('Presence from %s was signed but no corresponding ' self._log.info(
'key was found', jid) "Presence from %s was signed but no corresponding " "key was found", jid
)
return return
self._presence_fingerprint_store[jid] = fingerprint self._presence_fingerprint_store[jid] = fingerprint
self._log.info('Presence from %s was verified successfully, ' self._log.info(
'fingerprint: %s', jid, fingerprint) "Presence from %s was verified successfully, " "fingerprint: %s",
jid,
fingerprint,
)
key_data = self.get_contact_key_data(jid) key_data = self.get_contact_key_data(jid)
if key_data is None: if key_data is None:
self._log.info('No key assigned for contact: %s', jid) self._log.info("No key assigned for contact: %s", jid)
return return
if key_data['key_id'] != fingerprint: if key_data["key_id"] != fingerprint:
self._log.warning('Fingerprint mismatch, ' self._log.warning(
'Presence was signed with fingerprint: %s, ' "Fingerprint mismatch, "
'Assigned key fingerprint: %s', "Presence was signed with fingerprint: %s, "
fingerprint, key_data['key_id']) "Assigned key fingerprint: %s",
fingerprint,
key_data["key_id"],
)
return return
def _message_received(self, _con, stanza, properties): def _message_received(self, _con, stanza, properties):
@@ -155,15 +165,13 @@ class PGPLegacy(BaseModule):
return return
remote_jid = properties.remote_jid remote_jid = properties.remote_jid
self._log.info('Message received from: %s', remote_jid) self._log.info("Message received from: %s", remote_jid)
payload = self._pgp.decrypt(properties.pgp_legacy) payload = self._pgp.decrypt(properties.pgp_legacy)
prepare_stanza(stanza, payload) prepare_stanza(stanza, payload)
properties.encrypted = EncryptionData( properties.encrypted = EncryptionData(
protocol=ENCRYPTION_NAME, protocol=ENCRYPTION_NAME, key="Unknown", trust=Trust.UNDECIDED
key='Unknown',
trust=Trust.UNDECIDED
) )
def encrypt_message(self, con, message: OutgoingMessage, callback): def encrypt_message(self, con, message: OutgoingMessage, callback):
@@ -181,7 +189,9 @@ class PGPLegacy(BaseModule):
always_trust = key_id in self._always_trust always_trust = key_id in self._always_trust
self._encrypt(con, message, [key_id, own_key_id], callback, always_trust) self._encrypt(con, message, [key_id, own_key_id], callback, always_trust)
def _encrypt(self, con, message: OutgoingMessage, keys, callback, always_trust: bool): def _encrypt(
self, con, message: OutgoingMessage, keys, callback, always_trust: bool
):
result = self._pgp.encrypt(message.get_text(), keys, always_trust) result = self._pgp.encrypt(message.get_text(), keys, always_trust)
encrypted_payload, error = result encrypted_payload, error = result
if error: if error:
@@ -194,15 +204,18 @@ class PGPLegacy(BaseModule):
message.set_encryption( message.set_encryption(
EncryptionData( EncryptionData(
protocol=ENCRYPTION_NAME, protocol=ENCRYPTION_NAME,
key='Unknown', key="Unknown",
trust=Trust.VERIFIED, trust=Trust.VERIFIED,
) )
) )
callback(message) callback(message)
def _handle_encrypt_error(self, con, error: str, message: OutgoingMessage, keys, callback): def _handle_encrypt_error(
if error.startswith('NOT_TRUSTED'): self, con, error: str, message: OutgoingMessage, keys, callback
):
if error.startswith("NOT_TRUSTED"):
def on_yes(checked): def on_yes(checked):
if checked: if checked:
self._always_trust.append(keys[0]) self._always_trust.append(keys[0])
@@ -219,64 +232,67 @@ class PGPLegacy(BaseModule):
@staticmethod @staticmethod
def _raise_message_not_sent(con, message: OutgoingMessage, error: str): def _raise_message_not_sent(con, message: OutgoingMessage, error: str):
app.ged.raise_event( app.ged.raise_event(
MessageNotSent(client=con, MessageNotSent(
jid=str(message.contact.jid), client=con,
message=message.get_text(), jid=str(message.contact.jid),
error=_('Encryption error: %s') % error, message=message.get_text(),
time=time.time())) error=_("Encryption error: %s") % error,
time=time.time(),
)
)
def _create_pgp_legacy_message(self, stanza: Message, payload: str) -> None: def _create_pgp_legacy_message(self, stanza: Message, payload: str) -> None:
stanza.setBody(self._get_info_message()) stanza.setBody(self._get_info_message())
stanza.setTag('x', namespace=Namespace.ENCRYPTED).setData(payload) stanza.setTag("x", namespace=Namespace.ENCRYPTED).setData(payload)
eme_node = nbxmpp.Node('encryption', eme_node = nbxmpp.Node(
attrs={'xmlns': Namespace.EME, "encryption",
'namespace': Namespace.ENCRYPTED}) attrs={"xmlns": Namespace.EME, "namespace": Namespace.ENCRYPTED},
)
stanza.addChild(node=eme_node) stanza.addChild(node=eme_node)
def sign_presence(self, presence, status): def sign_presence(self, presence, status):
key_data = self.get_own_key_data() key_data = self.get_own_key_data()
if key_data is None: if key_data is None:
self._log.warning('No own key id found, cant sign presence') self._log.warning("No own key id found, cant sign presence")
return return
try: try:
result = self._pgp.sign(status, key_data['key_id']) result = self._pgp.sign(status, key_data["key_id"])
except SignError as error: except SignError as error:
self._log.warning('Sign Error: %s', error) self._log.warning("Sign Error: %s", error)
return return
# self._log.debug(self._pgp.sign.cache_info()) # self._log.debug(self._pgp.sign.cache_info())
self._log.info('Presence signed') self._log.info("Presence signed")
presence.setTag(Namespace.SIGNED + ' x').setData(result) presence.setTag(Namespace.SIGNED + " x").setData(result)
@staticmethod @staticmethod
def _get_info_message(): def _get_info_message():
msg = '[This message is *encrypted* (See :XEP:`27`)]' msg = "[This message is *encrypted* (See :XEP:`27`)]"
lang = os.getenv('LANG') lang = os.getenv("LANG")
if lang is not None and not lang.startswith('en'): if lang is not None and not lang.startswith("en"):
# we're not english: one in locale and one en # we're not english: one in locale and one en
msg = _('[This message is *encrypted* (See :XEP:`27`)]') + \ msg = _("[This message is *encrypted* (See :XEP:`27`)]") + " (" + msg + ")"
' (' + msg + ')'
return msg return msg
def _get_key_ids(self, jid): def _get_key_ids(self, jid):
key_data = self.get_contact_key_data(jid) key_data = self.get_contact_key_data(jid)
if key_data is None: if key_data is None:
raise NoKeyIdFound('No key id found for %s' % jid) raise NoKeyIdFound("No key id found for %s" % jid)
key_id = key_data['key_id'] key_id = key_data["key_id"]
own_key_data = self.get_own_key_data() own_key_data = self.get_own_key_data()
if own_key_data is None: if own_key_data is None:
raise NoKeyIdFound('Own key id not found') raise NoKeyIdFound("Own key id not found")
own_key_id = own_key_data['key_id'] own_key_id = own_key_data["key_id"]
return key_id, own_key_id return key_id, own_key_id
@staticmethod @staticmethod
def _cleanup_stanza(message: OutgoingMessage) -> None: def _cleanup_stanza(message: OutgoingMessage) -> None:
''' We make sure only allowed tags are in the stanza ''' """We make sure only allowed tags are in the stanza"""
original_stanza = message.get_stanza() original_stanza = message.get_stanza()
stanza = nbxmpp.Message( stanza = nbxmpp.Message(
to=original_stanza.getTo(), to=original_stanza.getTo(), typ=original_stanza.getType()
typ=original_stanza.getType()) )
stanza.setID(original_stanza.getID()) stanza.setID(original_stanza.getID())
stanza.setThread(original_stanza.getThread()) stanza.setThread(original_stanza.getThread())
for tag, ns in ALLOWED_TAGS: for tag, ns in ALLOWED_TAGS:
@@ -286,8 +302,9 @@ class PGPLegacy(BaseModule):
message.set_stanza(stanza) message.set_stanza(stanza)
def encrypt_file(self, file, callback): def encrypt_file(self, file, callback):
thread = threading.Thread(target=self._encrypt_file_thread, thread = threading.Thread(
args=(file, callback)) target=self._encrypt_file_thread, args=(file, callback)
)
thread.daemon = True thread.daemon = True
thread.start() thread.start()
@@ -299,8 +316,7 @@ class PGPLegacy(BaseModule):
return return
stream = open(file.path, "rb") stream = open(file.path, "rb")
encrypted = self._pgp.encrypt_file(stream, encrypted = self._pgp.encrypt_file(stream, [key_id, own_key_id])
[key_id, own_key_id])
stream.close() stream.close()
if not encrypted: if not encrypted:
@@ -308,7 +324,7 @@ class PGPLegacy(BaseModule):
return return
file.size = len(encrypted.data) file.size = len(encrypted.data)
file.set_uri_transform_func(lambda uri: '%s.pgp' % uri) file.set_uri_transform_func(lambda uri: "%s.pgp" % uri)
file.set_encrypted_data(encrypted.data) file.set_encrypted_data(encrypted.data)
GLib.idle_add(callback, file) GLib.idle_add(callback, file)
@@ -316,5 +332,6 @@ class PGPLegacy(BaseModule):
def _on_file_encryption_error(error): def _on_file_encryption_error(error):
app.ged.raise_event(PGPFileEncryptionError(error=error)) app.ged.raise_event(PGPFileEncryptionError(error=error))
def get_instance(*args, **kwargs): def get_instance(*args, **kwargs):
return PGPLegacy(*args, **kwargs), 'PGPLegacy' return PGPLegacy(*args, **kwargs), "PGPLegacy"

View File

@@ -21,8 +21,8 @@ from nbxmpp.namespaces import Namespace
def prepare_stanza(stanza, plaintext): def prepare_stanza(stanza, plaintext):
delete_nodes(stanza, 'encrypted', Namespace.ENCRYPTED) delete_nodes(stanza, "encrypted", Namespace.ENCRYPTED)
delete_nodes(stanza, 'body') delete_nodes(stanza, "body")
stanza.setBody(plaintext) stanza.setBody(plaintext)
@@ -34,16 +34,16 @@ def delete_nodes(stanza, name, namespace=None):
def find_gpg(): def find_gpg():
def _search(binary): def _search(binary):
if os.name == 'nt': if os.name == "nt":
gpg_cmd = binary + ' -h >nul 2>&1' gpg_cmd = binary + " -h >nul 2>&1"
else: else:
gpg_cmd = binary + ' -h >/dev/null 2>&1' gpg_cmd = binary + " -h >/dev/null 2>&1"
if subprocess.call(gpg_cmd, shell=True): if subprocess.call(gpg_cmd, shell=True):
return False return False
return True return True
if _search('gpg2'): if _search("gpg2"):
return 'gpg2' return "gpg2"
if _search('gpg'): if _search("gpg"):
return 'gpg' return "gpg"

View File

@@ -14,29 +14,29 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>. # along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import logging
import os import os
import sys import sys
import logging
from functools import partial from functools import partial
from packaging.version import Version as V from packaging.version import Version as V
from gajim.common import app from gajim.common import app
from gajim.common import ged from gajim.common import ged
from gajim.gtk.dialogs import ConfirmationCheckDialog
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import SimpleDialog
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from gajim.gtk.dialogs import SimpleDialog
from gajim.gtk.dialogs import DialogButton
from gajim.gtk.dialogs import ConfirmationCheckDialog
from pgp.gtk.key import KeyDialog
from pgp.gtk.config import PGPConfigDialog
from pgp.exceptions import KeyMismatch from pgp.exceptions import KeyMismatch
from pgp.gtk.config import PGPConfigDialog
from pgp.gtk.key import KeyDialog
from pgp.modules.util import find_gpg from pgp.modules.util import find_gpg
ENCRYPTION_NAME = 'PGP' ENCRYPTION_NAME = "PGP"
log = logging.getLogger('gajim.p.pgplegacy') log = logging.getLogger("gajim.p.pgplegacy")
ERROR = False ERROR = False
try: try:
@@ -51,29 +51,29 @@ else:
# on a much lower version number than gnupg # on a much lower version number than gnupg
# Also we need at least python-gnupg 0.3.8 # Also we need at least python-gnupg 0.3.8
v_gnupg = gnupg.__version__ v_gnupg = gnupg.__version__
if V(v_gnupg) < V('0.3.8') or V(v_gnupg) > V('1.0.0'): if V(v_gnupg) < V("0.3.8") or V(v_gnupg) > V("1.0.0"):
log.error('We need python-gnupg >= 0.3.8') log.error("We need python-gnupg >= 0.3.8")
ERROR = True ERROR = True
ERROR_MSG = None ERROR_MSG = None
BINARY = find_gpg() BINARY = find_gpg()
log.info('Found GPG executable: %s', BINARY) log.info("Found GPG executable: %s", BINARY)
if BINARY is None or ERROR: if BINARY is None or ERROR:
if os.name == 'nt': if os.name == "nt":
ERROR_MSG = _('Please install GnuPG / Gpg4win') ERROR_MSG = _("Please install GnuPG / Gpg4win")
else: else:
ERROR_MSG = _('Please install python-gnupg and gnupg') ERROR_MSG = _("Please install python-gnupg and gnupg")
else: else:
from pgp.modules import pgp_legacy
from pgp.backend.python_gnupg import PGP from pgp.backend.python_gnupg import PGP
from pgp.modules import pgp_legacy
class PGPPlugin(GajimPlugin): class PGPPlugin(GajimPlugin):
def init(self): def init(self):
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
self.description = _('PGP encryption as per XEP-0027') self.description = _("PGP encryption as per XEP-0027")
if ERROR_MSG: if ERROR_MSG:
self.activatable = False self.activatable = False
self.config_dialog = None self.config_dialog = None
@@ -84,30 +84,26 @@ class PGPPlugin(GajimPlugin):
self.encryption_name = ENCRYPTION_NAME self.encryption_name = ENCRYPTION_NAME
self.allow_zeroconf = True self.allow_zeroconf = True
self.gui_extension_points = { self.gui_extension_points = {
'encrypt' + ENCRYPTION_NAME: (self._encrypt_message, None), "encrypt" + ENCRYPTION_NAME: (self._encrypt_message, None),
'send_message' + ENCRYPTION_NAME: ( "send_message" + ENCRYPTION_NAME: (self._before_sendmessage, None),
self._before_sendmessage, None), "encryption_dialog" + ENCRYPTION_NAME: (self._on_encryption_dialog, None),
'encryption_dialog' + ENCRYPTION_NAME: ( "encryption_state" + ENCRYPTION_NAME: (self._encryption_state, None),
self._on_encryption_dialog, None), "send-presence": (self._on_send_presence, None),
'encryption_state' + ENCRYPTION_NAME: (
self._encryption_state, None),
'send-presence': (self._on_send_presence, None),
} }
self.modules = [pgp_legacy] self.modules = [pgp_legacy]
self.events_handlers = { self.events_handlers = {
'pgp-not-trusted': (ged.PRECORE, self._on_not_trusted), "pgp-not-trusted": (ged.PRECORE, self._on_not_trusted),
'pgp-file-encryption-error': (ged.PRECORE, "pgp-file-encryption-error": (ged.PRECORE, self._on_file_encryption_error),
self._on_file_encryption_error),
} }
encoding = 'utf8' if sys.platform == 'linux' else None encoding = "utf8" if sys.platform == "linux" else None
self._pgp = PGP(BINARY, encoding=encoding) self._pgp = PGP(BINARY, encoding=encoding)
@staticmethod @staticmethod
def get_pgp_module(account): def get_pgp_module(account):
return app.get_client(account).get_module('PGPLegacy') return app.get_client(account).get_module("PGPLegacy")
def activate(self): def activate(self):
pass pass
@@ -121,8 +117,8 @@ class PGPPlugin(GajimPlugin):
@staticmethod @staticmethod
def _encryption_state(_chat_control, state): def _encryption_state(_chat_control, state):
state['visible'] = True state["visible"] = True
state['authenticated'] = True state["authenticated"] = True
def _on_encryption_dialog(self, chat_control): def _on_encryption_dialog(self, chat_control):
account = chat_control.account account = chat_control.account
@@ -137,17 +133,20 @@ class PGPPlugin(GajimPlugin):
@staticmethod @staticmethod
def _on_not_trusted(event): def _on_not_trusted(event):
ConfirmationCheckDialog( ConfirmationCheckDialog(
_('Untrusted PGP key'), _("Untrusted PGP key"),
_('The PGP key used to encrypt this chat is not ' _(
'trusted. Do you really want to encrypt this ' "The PGP key used to encrypt this chat is not "
'message?'), "trusted. Do you really want to encrypt this "
_('_Do not ask me again'), "message?"
[DialogButton.make('Cancel', ),
text=_('_No'), _("_Do not ask me again"),
callback=event.on_no), [
DialogButton.make('OK', DialogButton.make("Cancel", text=_("_No"), callback=event.on_no),
text=_('_Encrypt Anyway'), DialogButton.make(
callback=event.on_yes)]).show() "OK", text=_("_Encrypt Anyway"), callback=event.on_yes
),
],
).show()
@staticmethod @staticmethod
def _before_sendmessage(chat_control): def _before_sendmessage(chat_control):
@@ -156,24 +155,30 @@ class PGPPlugin(GajimPlugin):
client = app.get_client(account) client = app.get_client(account)
try: try:
valid = client.get_module('PGPLegacy').has_valid_key_assigned(jid) valid = client.get_module("PGPLegacy").has_valid_key_assigned(jid)
except KeyMismatch as announced_key_id: except KeyMismatch as announced_key_id:
SimpleDialog( SimpleDialog(
_('PGP Key mismatch'), _("PGP Key mismatch"),
_('The contact\'s key (%s) <b>does not match</b> the key ' _(
'assigned in Gajim.') % announced_key_id) "The contact's key (%s) <b>does not match</b> the key "
"assigned in Gajim."
)
% announced_key_id,
)
chat_control.sendmessage = False chat_control.sendmessage = False
return return
if not valid: if not valid:
SimpleDialog( SimpleDialog(
_('No OpenPGP key assigned'), _("No OpenPGP key assigned"),
_('No OpenPGP key is assigned to this contact.')) _("No OpenPGP key is assigned to this contact."),
)
chat_control.sendmessage = False chat_control.sendmessage = False
elif client.get_module('PGPLegacy').get_own_key_data() is None: elif client.get_module("PGPLegacy").get_own_key_data() is None:
SimpleDialog( SimpleDialog(
_('No OpenPGP key assigned'), _("No OpenPGP key assigned"),
_('No OpenPGP key is assigned to your account.')) _("No OpenPGP key is assigned to your account."),
)
chat_control.sendmessage = False chat_control.sendmessage = False
def _encrypt_message(self, conn, event, callback): def _encrypt_message(self, conn, event, callback):
@@ -185,4 +190,4 @@ class PGPPlugin(GajimPlugin):
@staticmethod @staticmethod
def _on_file_encryption_error(event): def _on_file_encryption_error(event):
SimpleDialog(_('Error'), event.error) SimpleDialog(_("Error"), event.error)

View File

@@ -1 +1,3 @@
from .plugins_translations import PluginsTranslationsPlugin # type: ignore # noqa: F401 from .plugins_translations import ( # type: ignore # noqa: F401
PluginsTranslationsPlugin,
)

View File

@@ -23,49 +23,49 @@ from glob import glob
from pathlib import Path from pathlib import Path
from gajim.common import configpaths from gajim.common import configpaths
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
log = logging.getLogger('gajim.p.plugins_translations') log = logging.getLogger("gajim.p.plugins_translations")
class PluginsTranslationsPlugin(GajimPlugin): class PluginsTranslationsPlugin(GajimPlugin):
def init(self) -> None: def init(self) -> None:
self.description = _('This plugin contains translations for other ' self.description = _(
'Gajim plugins. Please restart Gajim after ' "This plugin contains translations for other "
'enabling this plugin.') "Gajim plugins. Please restart Gajim after "
"enabling this plugin."
)
self.config_dialog = None self.config_dialog = None
self.config_default_values = {'last_version': ('0', '')} self.config_default_values = {"last_version": ("0", "")}
self.locale_dir = Path(configpaths.get('PLUGINS_USER')) / 'locale' self.locale_dir = Path(configpaths.get("PLUGINS_USER")) / "locale"
def activate(self) -> None: def activate(self) -> None:
current_version = str(self.manifest.version) current_version = str(self.manifest.version)
if cast(str, self.config['last_version']) == current_version: if cast(str, self.config["last_version"]) == current_version:
return return
files = glob(self.__path__ + '/*.mo') files = glob(self.__path__ + "/*.mo")
self._remove_translations() self._remove_translations()
self.locale_dir.mkdir() self.locale_dir.mkdir()
locales = [ locales = [os.path.splitext(os.path.basename(name))[0] for name in files]
os.path.splitext(os.path.basename(name))[0] for name in files log.info("Installing new translations...")
]
log.info('Installing new translations...')
for locale in locales: for locale in locales:
dst = self.locale_dir / locale / 'LC_MESSAGES' dst = self.locale_dir / locale / "LC_MESSAGES"
dst.mkdir(parents=True) dst.mkdir(parents=True)
shutil.copy2(os.path.join(self.__path__, '%s.mo' % locale), shutil.copy2(
dst / 'gajim_plugins.mo') os.path.join(self.__path__, "%s.mo" % locale), dst / "gajim_plugins.mo"
)
self.config['last_version'] = current_version self.config["last_version"] = current_version
def _remove_translations(self) -> None: def _remove_translations(self) -> None:
log.info('Removing old translations...') log.info("Removing old translations...")
if self.locale_dir.exists(): if self.locale_dir.exists():
shutil.rmtree(str(self.locale_dir)) shutil.rmtree(str(self.locale_dir))
def deactivate(self) -> None: def deactivate(self) -> None:
self._remove_translations() self._remove_translations()
self.config['last_version'] = '0' self.config["last_version"] = "0"

View File

@@ -21,28 +21,24 @@ from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING: if TYPE_CHECKING:
from ..plugin import QuickRepliesPlugin from ..plugin import QuickRepliesPlugin
class ConfigDialog(Gtk.ApplicationWindow): class ConfigDialog(Gtk.ApplicationWindow):
def __init__(self, def __init__(self, plugin: QuickRepliesPlugin, transient: Gtk.Window) -> None:
plugin: QuickRepliesPlugin,
transient: Gtk.Window
) -> None:
Gtk.ApplicationWindow.__init__(self) Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app) self.set_application(app.app)
self.set_show_menubar(False) self.set_show_menubar(False)
self.set_title(_('Quick Replies Configuration')) self.set_title(_("Quick Replies Configuration"))
self.set_transient_for(transient) self.set_transient_for(transient)
self.set_default_size(400, 400) self.set_default_size(400, 400)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG) self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
@@ -50,7 +46,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
self.set_destroy_with_parent(True) self.set_destroy_with_parent(True)
ui_path = Path(__file__).parent ui_path = Path(__file__).parent
self._ui = get_builder(str(ui_path.resolve() / 'config.ui')) self._ui = get_builder(str(ui_path.resolve() / "config.ui"))
self._plugin = plugin self._plugin = plugin
@@ -60,26 +56,23 @@ class ConfigDialog(Gtk.ApplicationWindow):
self.show_all() self.show_all()
self._ui.connect_signals(self) self._ui.connect_signals(self)
self.connect('destroy', self._on_destroy) self.connect("destroy", self._on_destroy)
def _fill_list(self) -> None: def _fill_list(self) -> None:
for reply in self._plugin.quick_replies: for reply in self._plugin.quick_replies:
self._ui.replies_store.append([reply]) self._ui.replies_store.append([reply])
def _on_reply_edited(self, def _on_reply_edited(
_renderer: Gtk.CellRendererText, self, _renderer: Gtk.CellRendererText, path: str, new_text: str
path: str, ) -> None:
new_text: str
) -> None:
iter_ = self._ui.replies_store.get_iter(path) iter_ = self._ui.replies_store.get_iter(path)
self._ui.replies_store.set_value(iter_, 0, new_text) self._ui.replies_store.set_value(iter_, 0, new_text)
def _on_add_clicked(self, _button: Gtk.Button) -> None: def _on_add_clicked(self, _button: Gtk.Button) -> None:
self._ui.replies_store.append([_('New Quick Reply')]) self._ui.replies_store.append([_("New Quick Reply")])
row = self._ui.replies_store[-1] row = self._ui.replies_store[-1]
self._ui.replies_treeview.scroll_to_cell( self._ui.replies_treeview.scroll_to_cell(row.path, None, False, 0, 0)
row.path, None, False, 0, 0)
self._ui.selection.unselect_all() self._ui.selection.unselect_all()
self._ui.selection.select_path(row.path) self._ui.selection.select_path(row.path)
@@ -96,7 +89,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
def _on_destroy(self, *args: Any) -> None: def _on_destroy(self, *args: Any) -> None:
replies: list[str] = [] replies: list[str] = []
for row in self._ui.replies_store: for row in self._ui.replies_store:
if row[0] == '': if row[0] == "":
continue continue
replies.append(row[0]) replies.append(row[0])
self._plugin.set_quick_replies(replies) self._plugin.set_quick_replies(replies)

View File

@@ -18,8 +18,8 @@ from __future__ import annotations
from typing import cast from typing import cast
import json import json
from pathlib import Path
from functools import partial from functools import partial
from pathlib import Path
from gi.repository import Gio from gi.repository import Gio
from gi.repository import GLib from gi.repository import GLib
@@ -27,23 +27,21 @@ from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.common import configpaths from gajim.common import configpaths
from gajim.gtk.message_actions_box import MessageActionsBox from gajim.gtk.message_actions_box import MessageActionsBox
from gajim.gtk.message_input import MessageInputTextView from gajim.gtk.message_input import MessageInputTextView
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from quick_replies.quick_replies import DEFAULT_DATA
from quick_replies.gtk.config import ConfigDialog from quick_replies.gtk.config import ConfigDialog
from quick_replies.quick_replies import DEFAULT_DATA
class QuickRepliesPlugin(GajimPlugin): class QuickRepliesPlugin(GajimPlugin):
def init(self) -> None: def init(self) -> None:
self.description = _('Adds a menu with customizable quick replies') self.description = _("Adds a menu with customizable quick replies")
self.config_dialog = partial(ConfigDialog, self) self.config_dialog = partial(ConfigDialog, self)
self.gui_extension_points = { self.gui_extension_points = {
'message_actions_box': (self._message_actions_box_created, None), "message_actions_box": (self._message_actions_box_created, None),
} }
self._button = None self._button = None
self.quick_replies = self._load_quick_replies() self.quick_replies = self._load_quick_replies()
@@ -53,47 +51,44 @@ class QuickRepliesPlugin(GajimPlugin):
self._button.destroy() self._button.destroy()
del self._button del self._button
def _message_actions_box_created(self, def _message_actions_box_created(
message_actions_box: MessageActionsBox, self, message_actions_box: MessageActionsBox, gtk_box: Gtk.Box
gtk_box: Gtk.Box ) -> None:
) -> None:
self._button = QuickRepliesButton( self._button = QuickRepliesButton(self, message_actions_box.msg_textview)
self,
message_actions_box.msg_textview)
gtk_box.pack_start(self._button, False, False, 0) gtk_box.pack_start(self._button, False, False, 0)
self._button.show() self._button.show()
@staticmethod @staticmethod
def _load_quick_replies() -> list[str]: def _load_quick_replies() -> list[str]:
try: try:
data_path = Path(configpaths.get('PLUGINS_DATA')) data_path = Path(configpaths.get("PLUGINS_DATA"))
except KeyError: except KeyError:
# PLUGINS_DATA was added in 1.0.99.1 # PLUGINS_DATA was added in 1.0.99.1
return DEFAULT_DATA return DEFAULT_DATA
path = data_path / 'quick_replies' / 'quick_replies' path = data_path / "quick_replies" / "quick_replies"
if not path.exists(): if not path.exists():
return DEFAULT_DATA return DEFAULT_DATA
with path.open('r') as file: with path.open("r") as file:
quick_replies = json.load(file) quick_replies = json.load(file)
return quick_replies return quick_replies
@staticmethod @staticmethod
def _save_quick_replies(quick_replies: list[str]) -> None: def _save_quick_replies(quick_replies: list[str]) -> None:
try: try:
data_path = Path(configpaths.get('PLUGINS_DATA')) data_path = Path(configpaths.get("PLUGINS_DATA"))
except KeyError: except KeyError:
# PLUGINS_DATA was added in 1.0.99.1 # PLUGINS_DATA was added in 1.0.99.1
return return
path = data_path / 'quick_replies' path = data_path / "quick_replies"
if not path.exists(): if not path.exists():
path.mkdir(parents=True) path.mkdir(parents=True)
filepath = path / 'quick_replies' filepath = path / "quick_replies"
with filepath.open('w') as file: with filepath.open("w") as file:
json.dump(quick_replies, file) json.dump(quick_replies, file)
def set_quick_replies(self, quick_replies: list[str]) -> None: def set_quick_replies(self, quick_replies: list[str]) -> None:
@@ -104,20 +99,19 @@ class QuickRepliesPlugin(GajimPlugin):
class QuickRepliesButton(Gtk.MenuButton): class QuickRepliesButton(Gtk.MenuButton):
def __init__(self, def __init__(
plugin: QuickRepliesPlugin, self, plugin: QuickRepliesPlugin, message_input: MessageInputTextView
message_input: MessageInputTextView ) -> None:
) -> None:
Gtk.MenuButton.__init__(self) Gtk.MenuButton.__init__(self)
self.get_style_context().add_class('chatcontrol-actionbar-button') self.get_style_context().add_class("chatcontrol-actionbar-button")
self.set_property('relief', Gtk.ReliefStyle.NONE) self.set_property("relief", Gtk.ReliefStyle.NONE)
self.set_can_focus(False) self.set_can_focus(False)
plugin_path = Path(__file__).parent plugin_path = Path(__file__).parent
img_path = plugin_path.resolve() / 'quick_replies.png' img_path = plugin_path.resolve() / "quick_replies.png"
img = Gtk.Image.new_from_file(str(img_path)) img = Gtk.Image.new_from_file(str(img_path))
self.set_image(img) self.set_image(img)
self.set_tooltip_text(_('Quick Replies')) self.set_tooltip_text(_("Quick Replies"))
self._plugin = plugin self._plugin = plugin
self._message_input = message_input self._message_input = message_input
@@ -133,38 +127,36 @@ class QuickRepliesButton(Gtk.MenuButton):
self._menu.remove_all() self._menu.remove_all()
# Add config item # Add config item
action_data = GLib.Variant('s', 'plugin-configuration') action_data = GLib.Variant("s", "plugin-configuration")
menu_item = Gio.MenuItem() menu_item = Gio.MenuItem()
menu_item.set_label(_('Manage Replies…')) menu_item.set_label(_("Manage Replies…"))
menu_item.set_attribute_value('action-data', action_data) menu_item.set_attribute_value("action-data", action_data)
self._menu.append_item(menu_item) self._menu.append_item(menu_item)
# Add quick replies # Add quick replies
for reply in self._plugin.quick_replies: for reply in self._plugin.quick_replies:
assert isinstance(reply, str) assert isinstance(reply, str)
action_data = GLib.Variant('s', reply) action_data = GLib.Variant("s", reply)
menu_item = Gio.MenuItem() menu_item = Gio.MenuItem()
menu_item.set_label(reply) menu_item.set_label(reply)
menu_item.set_attribute_value('action-data', action_data) menu_item.set_attribute_value("action-data", action_data)
self._menu.append_item(menu_item) self._menu.append_item(menu_item)
menu_buttons = self._get_menu_buttons() menu_buttons = self._get_menu_buttons()
for button in menu_buttons: for button in menu_buttons:
button.connect( button.connect(
'clicked', "clicked", self._on_button_clicked, menu_buttons.index(button)
self._on_button_clicked, )
menu_buttons.index(button))
def _on_button_clicked(self, _button: Gtk.MenuButton, index: int) -> None: def _on_button_clicked(self, _button: Gtk.MenuButton, index: int) -> None:
variant = self._menu.get_item_attribute_value( variant = self._menu.get_item_attribute_value(index, "action-data")
index, 'action-data') if variant.get_string() == "plugin-configuration":
if variant.get_string() == 'plugin-configuration':
self._popover.popdown() self._popover.popdown()
self._plugin.config_dialog(app.window) self._plugin.config_dialog(app.window)
return return
message_buffer = self._message_input.get_buffer() message_buffer = self._message_input.get_buffer()
message_buffer.insert_at_cursor(variant.get_string().rstrip() + ' ') message_buffer.insert_at_cursor(variant.get_string().rstrip() + " ")
self._popover.popdown() self._popover.popdown()
self._message_input.grab_focus() self._message_input.grab_focus()

View File

@@ -1,5 +1,5 @@
DEFAULT_DATA = [ DEFAULT_DATA = [
'Hello!', "Hello!",
'How are you?', "How are you?",
'Good bye.', "Good bye.",
] ]

View File

@@ -1,40 +1,38 @@
# Keep this file python 3.7 compatible because it is executed on the server # Keep this file python 3.7 compatible because it is executed on the server
from typing import Any from typing import Any
from typing import Iterator from typing import Iterator
import sys
import json import json
import logging import logging
import sys
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from zipfile import ZipFile from zipfile import ZipFile
FORMAT = "%(asctime)s %(message)s"
FORMAT = '%(asctime)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.DEBUG) logging.basicConfig(format=FORMAT, level=logging.DEBUG)
log = logging.getLogger() log = logging.getLogger()
REQUIRED_KEYS: set[str] = { REQUIRED_KEYS: set[str] = {
'authors', "authors",
'description', "description",
'homepage', "homepage",
'config_dialog', "config_dialog",
'name', "name",
'platforms', "platforms",
'requirements', "requirements",
'short_name', "short_name",
'version' "version",
} }
PACKAGE_INDEX: dict[str, Any] = { PACKAGE_INDEX: dict[str, Any] = {
'metadata': { "metadata": {
'repository_name': 'master', "repository_name": "master",
'image_path': 'images.zip', "image_path": "images.zip",
}, },
'plugins': defaultdict(dict) "plugins": defaultdict(dict),
} }
@@ -44,53 +42,53 @@ def is_manifest_valid(manifest: dict[str, Any]) -> bool:
def iter_releases(release_folder: Path) -> Iterator[dict[str, Any]]: def iter_releases(release_folder: Path) -> Iterator[dict[str, Any]]:
for path in release_folder.rglob('*.zip'): for path in release_folder.rglob("*.zip"):
with ZipFile(path) as release_zip: with ZipFile(path) as release_zip:
if path.name == 'images.zip': if path.name == "images.zip":
continue continue
log.info('Check path: %s', path) log.info("Check path: %s", path)
try: try:
with release_zip.open('plugin-manifest.json') as file: with release_zip.open("plugin-manifest.json") as file:
manifest = json.load(file) manifest = json.load(file)
yield manifest yield manifest
except Exception: except Exception:
log.error('Error loading manifest') log.error("Error loading manifest")
log.exception('') log.exception("")
def build_package_index(release_folder: Path) -> None: def build_package_index(release_folder: Path) -> None:
log.info('Build package index') log.info("Build package index")
for manifest in iter_releases(release_folder): for manifest in iter_releases(release_folder):
if not is_manifest_valid(manifest): if not is_manifest_valid(manifest):
log.warning('Invalid manifest') log.warning("Invalid manifest")
log.warning(manifest) log.warning(manifest)
continue continue
short_name = manifest.pop('short_name') short_name = manifest.pop("short_name")
version = manifest.pop('version') version = manifest.pop("version")
PACKAGE_INDEX['plugins'][short_name][version] = manifest PACKAGE_INDEX["plugins"][short_name][version] = manifest
log.info('Found manifest: %s - %s', short_name, version) log.info("Found manifest: %s - %s", short_name, version)
path = release_folder / 'package_index.json' path = release_folder / "package_index.json"
with path.open('w') as f: with path.open("w") as f:
json.dump(PACKAGE_INDEX, f) json.dump(PACKAGE_INDEX, f)
def build_image_zip(release_folder: Path) -> None: def build_image_zip(release_folder: Path) -> None:
log.info('Build images.zip') log.info("Build images.zip")
with ZipFile(release_folder / 'images.zip', mode='w') as image_zip: with ZipFile(release_folder / "images.zip", mode="w") as image_zip:
for path in release_folder.iterdir(): for path in release_folder.iterdir():
if not path.is_dir(): if not path.is_dir():
continue continue
image = path / f'{path.name}.png' image = path / f"{path.name}.png"
if not image.exists(): if not image.exists():
continue continue
image_zip.write(image, arcname=image.name) image_zip.write(image, arcname=image.name)
if __name__ == '__main__': if __name__ == "__main__":
path = Path(sys.argv[1]) path = Path(sys.argv[1])
build_package_index(path) build_package_index(path)
build_image_zip(path) build_image_zip(path)
log.info('Finished') log.info("Finished")

View File

@@ -5,25 +5,24 @@ import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
REPO_DIR = Path(__file__).parent.parent REPO_DIR = Path(__file__).parent.parent
TRANS_DIR = REPO_DIR / 'po' TRANS_DIR = REPO_DIR / "po"
TRANS_TEMPLATE = TRANS_DIR / 'gajim_plugins.pot' TRANS_TEMPLATE = TRANS_DIR / "gajim_plugins.pot"
BUILD_DIR = REPO_DIR / 'plugins_translations' BUILD_DIR = REPO_DIR / "plugins_translations"
TRANSLATABLE_FILES = [ TRANSLATABLE_FILES = [
'*.py', "*.py",
'*.ui', "*.ui",
] ]
def template_is_equal(old_template_path: Path, new_template: str) -> bool: def template_is_equal(old_template_path: Path, new_template: str) -> bool:
with open(old_template_path, 'r') as f: with open(old_template_path, "r") as f:
old_template = f.read() old_template = f.read()
pattern = r'"POT-Creation-Date: .*\n"' pattern = r'"POT-Creation-Date: .*\n"'
old_template = re.sub(pattern, '', old_template, count=1) old_template = re.sub(pattern, "", old_template, count=1)
new_template = re.sub(pattern, '', new_template, count=1) new_template = re.sub(pattern, "", new_template, count=1)
return old_template == new_template return old_template == new_template
@@ -34,86 +33,77 @@ def update_translation_template() -> bool:
paths += list(REPO_DIR.rglob(file_path)) paths += list(REPO_DIR.rglob(file_path))
cmd = [ cmd = [
'xgettext', "xgettext",
'-o', '-', "-o",
'-c#', "-",
'--from-code=utf-8', "-c#",
'--keyword=Q_', "--from-code=utf-8",
'--no-location', "--keyword=Q_",
'--sort-output', "--no-location",
'--package-name=Gajim Plugins' "--sort-output",
"--package-name=Gajim Plugins",
] ]
for path in paths: for path in paths:
cmd.append(str(path)) cmd.append(str(path))
result = subprocess.run(cmd, result = subprocess.run(
cwd=REPO_DIR, cmd, cwd=REPO_DIR, text=True, check=True, capture_output=True
text=True, )
check=True,
capture_output=True)
template = result.stdout template = result.stdout
if (TRANS_TEMPLATE.exists() and if TRANS_TEMPLATE.exists() and template_is_equal(TRANS_TEMPLATE, template):
template_is_equal(TRANS_TEMPLATE, template)):
# No new strings were discovered # No new strings were discovered
return False return False
with open(TRANS_TEMPLATE, 'w') as f: with open(TRANS_TEMPLATE, "w") as f:
f.write(template) f.write(template)
return True return True
def update_translation_files() -> None: def update_translation_files() -> None:
for file in TRANS_DIR.glob('*.po'): for file in TRANS_DIR.glob("*.po"):
subprocess.run(['msgmerge', subprocess.run(
'-U', ["msgmerge", "-U", "--sort-output", str(file), TRANS_TEMPLATE],
'--sort-output', cwd=REPO_DIR,
str(file), check=True,
TRANS_TEMPLATE], )
cwd=REPO_DIR,
check=True)
def build_translations() -> None: def build_translations() -> None:
for po_file in TRANS_DIR.glob('*.po'): for po_file in TRANS_DIR.glob("*.po"):
lang = po_file.stem lang = po_file.stem
po_file = TRANS_DIR / f'{lang}.po' po_file = TRANS_DIR / f"{lang}.po"
mo_file = BUILD_DIR / f'{po_file.stem}.mo' mo_file = BUILD_DIR / f"{po_file.stem}.mo"
subprocess.run(['msgfmt', subprocess.run(
str(po_file), ["msgfmt", str(po_file), "-o", str(mo_file)], cwd=REPO_DIR, check=True
'-o', )
str(mo_file)],
cwd=REPO_DIR,
check=True)
def cleanup_translations() -> None: def cleanup_translations() -> None:
for po_file in TRANS_DIR.glob('*.po'): for po_file in TRANS_DIR.glob("*.po"):
subprocess.run(['msgattrib', subprocess.run(
'--output-file', ["msgattrib", "--output-file", str(po_file), "--no-obsolete", str(po_file)],
str(po_file), cwd=REPO_DIR,
'--no-obsolete', check=True,
str(po_file)], )
cwd=REPO_DIR,
check=True)
if __name__ == '__main__': if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Update Translations') parser = argparse.ArgumentParser(description="Update Translations")
parser.add_argument('command', choices=['update', 'build', 'cleanup']) parser.add_argument("command", choices=["update", "build", "cleanup"])
args = parser.parse_args() args = parser.parse_args()
if args.command == 'cleanup': if args.command == "cleanup":
cleanup_translations() cleanup_translations()
elif args.command == 'update': elif args.command == "update":
update_translation_template() update_translation_template()
update_translation_files() update_translation_files()
elif args.command == 'build': elif args.command == "build":
update_translation_template() update_translation_template()
update_translation_files() update_translation_files()
build_translations() build_translations()

View File

@@ -16,29 +16,28 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any
from gi.repository import Gdk
from gi.repository import Gtk
from gajim.common import app from gajim.common import app
from gajim.common.helpers import play_sound_file from gajim.common.helpers import play_sound_file
from gajim.common.util.status import get_uf_show from gajim.common.util.status import get_uf_show
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from gi.repository import Gdk, Gtk
if TYPE_CHECKING: if TYPE_CHECKING:
from ..triggers import Triggers from ..triggers import Triggers
EVENTS: dict[str, Any] = { EVENTS: dict[str, Any] = {
'message_received': [], "message_received": [],
} }
RECIPIENT_TYPES = [ RECIPIENT_TYPES = ["contact", "group", "groupchat", "all"]
'contact',
'group',
'groupchat',
'all'
]
class ConfigDialog(Gtk.ApplicationWindow): class ConfigDialog(Gtk.ApplicationWindow):
@@ -46,7 +45,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
Gtk.ApplicationWindow.__init__(self) Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app) self.set_application(app.app)
self.set_show_menubar(False) self.set_show_menubar(False)
self.set_title(_('Triggers Configuration')) self.set_title(_("Triggers Configuration"))
self.set_transient_for(transient) self.set_transient_for(transient)
self.set_default_size(600, 800) self.set_default_size(600, 800)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG) self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
@@ -54,7 +53,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
self.set_destroy_with_parent(True) self.set_destroy_with_parent(True)
ui_path = Path(__file__).parent ui_path = Path(__file__).parent
self._ui = get_builder(str(ui_path.resolve() / 'config.ui')) self._ui = get_builder(str(ui_path.resolve() / "config.ui"))
self._plugin = plugin self._plugin = plugin
@@ -67,7 +66,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
self._initialize() self._initialize()
self._ui.connect_signals(self) self._ui.connect_signals(self)
self.connect('destroy', self._on_destroy) self.connect("destroy", self._on_destroy)
def _on_destroy(self, *args: Any) -> None: def _on_destroy(self, *args: Any) -> None:
for num in list(self._plugin.config.keys()): for num in list(self._plugin.config.keys()):
@@ -78,31 +77,31 @@ class ConfigDialog(Gtk.ApplicationWindow):
def _initialize(self) -> None: def _initialize(self) -> None:
# Fill window # Fill window
widgets = [ widgets = [
'conditions_treeview', "conditions_treeview",
'config_box', "config_box",
'event_combobox', "event_combobox",
'recipient_type_combobox', "recipient_type_combobox",
'recipient_list_entry', "recipient_list_entry",
'delete_button', "delete_button",
'online_cb', "online_cb",
'away_cb', "away_cb",
'xa_cb', "xa_cb",
'dnd_cb', "dnd_cb",
'use_sound_cb', "use_sound_cb",
'disable_sound_cb', "disable_sound_cb",
'use_popup_cb', "use_popup_cb",
'disable_popup_cb', "disable_popup_cb",
'tab_opened_cb', "tab_opened_cb",
'not_tab_opened_cb', "not_tab_opened_cb",
'has_focus_cb', "has_focus_cb",
'not_has_focus_cb', "not_has_focus_cb",
'filechooser', "filechooser",
'sound_file_box', "sound_file_box",
'up_button', "up_button",
'down_button', "down_button",
'run_command_cb', "run_command_cb",
'command_entry', "command_entry",
'one_shot_cb' "one_shot_cb",
] ]
for widget in widgets: for widget in widgets:
self._ui.__dict__[widget] = self._ui.get_object(widget) self._ui.__dict__[widget] = self._ui.get_object(widget)
@@ -118,17 +117,17 @@ class ConfigDialog(Gtk.ApplicationWindow):
self._ui.conditions_treeview.set_model(model) self._ui.conditions_treeview.set_model(model)
# '#' Means number # '#' Means number
col = Gtk.TreeViewColumn(_('#')) col = Gtk.TreeViewColumn(_("#"))
self._ui.conditions_treeview.append_column(col) self._ui.conditions_treeview.append_column(col)
renderer = Gtk.CellRendererText() renderer = Gtk.CellRendererText()
col.pack_start(renderer, expand=False) col.pack_start(renderer, expand=False)
col.add_attribute(renderer, 'text', 0) col.add_attribute(renderer, "text", 0)
col = Gtk.TreeViewColumn(_('Condition')) col = Gtk.TreeViewColumn(_("Condition"))
self._ui.conditions_treeview.append_column(col) self._ui.conditions_treeview.append_column(col)
renderer = Gtk.CellRendererText() renderer = Gtk.CellRendererText()
col.pack_start(renderer, expand=True) col.pack_start(renderer, expand=True)
col.add_attribute(renderer, 'text', 1) col.add_attribute(renderer, "text", 1)
else: else:
model = self._ui.conditions_treeview.get_model() model = self._ui.conditions_treeview.get_model()
@@ -137,7 +136,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
# Fill conditions_treeview # Fill conditions_treeview
num = 0 num = 0
while num in self._config: while num in self._config:
iter_ = model.append((num, '')) iter_ = model.append((num, ""))
path = model.get_path(iter_) path = model.get_path(iter_)
self._ui.conditions_treeview.set_cursor(path) self._ui.conditions_treeview.set_cursor(path)
self._active_num = num self._active_num = num
@@ -154,13 +153,13 @@ class ConfigDialog(Gtk.ApplicationWindow):
self._ui.up_button.set_sensitive(False) self._ui.up_button.set_sensitive(False)
filter_ = Gtk.FileFilter() filter_ = Gtk.FileFilter()
filter_.set_name(_('All Files')) filter_.set_name(_("All Files"))
filter_.add_pattern('*') filter_.add_pattern("*")
self._ui.filechooser.add_filter(filter_) self._ui.filechooser.add_filter(filter_)
filter_ = Gtk.FileFilter() filter_ = Gtk.FileFilter()
filter_.set_name(_('Wav Sounds')) filter_.set_name(_("Wav Sounds"))
filter_.add_pattern('*.wav') filter_.add_pattern("*.wav")
self._ui.filechooser.add_filter(filter_) self._ui.filechooser.add_filter(filter_)
self._ui.filechooser.set_filter(filter_) self._ui.filechooser.set_filter(filter_)
@@ -172,96 +171,95 @@ class ConfigDialog(Gtk.ApplicationWindow):
return return
# event # event
value = self._config[self._active_num]['event'] value = self._config[self._active_num]["event"]
legacy_values = [ legacy_values = [
'contact_connected', "contact_connected",
'contact_disconnected', "contact_disconnected",
'contact_status_change'] "contact_status_change",
]
if value and value not in legacy_values: if value and value not in legacy_values:
self._ui.event_combobox.set_active( self._ui.event_combobox.set_active(list(EVENTS.keys()).index(value))
list(EVENTS.keys()).index(value))
else: else:
self._ui.event_combobox.set_active(-1) self._ui.event_combobox.set_active(-1)
# recipient_type # recipient_type
value = self._config[self._active_num]['recipient_type'] value = self._config[self._active_num]["recipient_type"]
if value: if value:
self._ui.recipient_type_combobox.set_active( self._ui.recipient_type_combobox.set_active(RECIPIENT_TYPES.index(value))
RECIPIENT_TYPES.index(value))
else: else:
self._ui.recipient_type_combobox.set_active(-1) self._ui.recipient_type_combobox.set_active(-1)
# recipient # recipient
value = self._config[self._active_num]['recipients'] value = self._config[self._active_num]["recipients"]
if not value: if not value:
value = '' value = ""
self._ui.recipient_list_entry.set_text(value) self._ui.recipient_list_entry.set_text(value)
# status # status
value = self._config[self._active_num]['status'] value = self._config[self._active_num]["status"]
if value == 'all': if value == "all":
self._ui.all_status_rb.set_active(True) self._ui.all_status_rb.set_active(True)
else: else:
self._ui.special_status_rb.set_active(True) self._ui.special_status_rb.set_active(True)
values = value.split() values = value.split()
for val in ('online', 'away', 'xa', 'dnd'): for val in ("online", "away", "xa", "dnd"):
if val in values: if val in values:
self._ui.__dict__[val + '_cb'].set_active(True) self._ui.__dict__[val + "_cb"].set_active(True)
else: else:
self._ui.__dict__[val + '_cb'].set_active(False) self._ui.__dict__[val + "_cb"].set_active(False)
self._on_status_radiobutton_toggled(self._ui.all_status_rb) self._on_status_radiobutton_toggled(self._ui.all_status_rb)
# tab_opened # tab_opened
value = self._config[self._active_num]['tab_opened'] value = self._config[self._active_num]["tab_opened"]
self._ui.tab_opened_cb.set_active(True) self._ui.tab_opened_cb.set_active(True)
self._ui.not_tab_opened_cb.set_active(True) self._ui.not_tab_opened_cb.set_active(True)
if value == 'no': if value == "no":
self._ui.tab_opened_cb.set_active(False) self._ui.tab_opened_cb.set_active(False)
elif value == 'yes': elif value == "yes":
self._ui.not_tab_opened_cb.set_active(False) self._ui.not_tab_opened_cb.set_active(False)
# has_focus # has_focus
if 'has_focus' not in self._config[self._active_num]: if "has_focus" not in self._config[self._active_num]:
self._config[self._active_num]['has_focus'] = 'both' self._config[self._active_num]["has_focus"] = "both"
value = self._config[self._active_num]['has_focus'] value = self._config[self._active_num]["has_focus"]
self._ui.has_focus_cb.set_active(True) self._ui.has_focus_cb.set_active(True)
self._ui.not_has_focus_cb.set_active(True) self._ui.not_has_focus_cb.set_active(True)
if value == 'no': if value == "no":
self._ui.has_focus_cb.set_active(False) self._ui.has_focus_cb.set_active(False)
elif value == 'yes': elif value == "yes":
self._ui.not_has_focus_cb.set_active(False) self._ui.not_has_focus_cb.set_active(False)
# sound_file # sound_file
value = self._config[self._active_num]['sound_file'] value = self._config[self._active_num]["sound_file"]
if value is None: if value is None:
self._ui.filechooser.unselect_all() self._ui.filechooser.unselect_all()
else: else:
self._ui.filechooser.set_filename(value) self._ui.filechooser.set_filename(value)
# sound, popup, auto_open, systray, roster # sound, popup, auto_open, systray, roster
for option in ('sound', 'popup'): for option in ("sound", "popup"):
value = self._config[self._active_num][option] value = self._config[self._active_num][option]
if value == 'yes': if value == "yes":
self._ui.__dict__['use_' + option + '_cb'].set_active(True) self._ui.__dict__["use_" + option + "_cb"].set_active(True)
else: else:
self._ui.__dict__['use_' + option + '_cb'].set_active(False) self._ui.__dict__["use_" + option + "_cb"].set_active(False)
if value == 'no': if value == "no":
self._ui.__dict__['disable_' + option + '_cb'].set_active(True) self._ui.__dict__["disable_" + option + "_cb"].set_active(True)
else: else:
self._ui.__dict__['disable_' + option + '_cb'].set_active(False) self._ui.__dict__["disable_" + option + "_cb"].set_active(False)
# run_command # run_command
value = self._config[self._active_num]['run_command'] value = self._config[self._active_num]["run_command"]
self._ui.run_command_cb.set_active(value) self._ui.run_command_cb.set_active(value)
# command # command
value = self._config[self._active_num]['command'] value = self._config[self._active_num]["command"]
self._ui.command_entry.set_text(value) self._ui.command_entry.set_text(value)
# one shot # one shot
if 'one_shot' in self._config[self._active_num]: if "one_shot" in self._config[self._active_num]:
value = self._config[self._active_num]['one_shot'] value = self._config[self._active_num]["one_shot"]
else: else:
value = False value = False
self._ui.one_shot_cb.set_active(value) self._ui.one_shot_cb.set_active(value)
@@ -272,34 +270,34 @@ class ConfigDialog(Gtk.ApplicationWindow):
if not iter_: if not iter_:
return return
ind = self._ui.event_combobox.get_active() ind = self._ui.event_combobox.get_active()
event = '' event = ""
if ind > -1: if ind > -1:
event = self._ui.event_combobox.get_model()[ind][0] event = self._ui.event_combobox.get_model()[ind][0]
ind = self._ui.recipient_type_combobox.get_active() ind = self._ui.recipient_type_combobox.get_active()
recipient_type = '' recipient_type = ""
if ind > -1: if ind > -1:
recipient_type_model = self._ui.recipient_type_combobox.get_model() recipient_type_model = self._ui.recipient_type_combobox.get_model()
recipient_type = recipient_type_model[ind][0] recipient_type = recipient_type_model[ind][0]
recipient = '' recipient = ""
if recipient_type != 'everybody': if recipient_type != "everybody":
recipient = self._ui.recipient_list_entry.get_text() recipient = self._ui.recipient_list_entry.get_text()
if self._ui.all_status_rb.get_active(): if self._ui.all_status_rb.get_active():
status = '' status = ""
else: else:
status = _('and I am ') status = _("and I am ")
for st in ('online', 'away', 'xa', 'dnd'): for st in ("online", "away", "xa", "dnd"):
if self._ui.__dict__[st + '_cb'].get_active(): if self._ui.__dict__[st + "_cb"].get_active():
status += get_uf_show(st) + ' ' status += get_uf_show(st) + " "
model[iter_][1] = _('%(event)s (%(recipient_type)s) %(recipient)s ' model[iter_][1] = _(
'%(status)s') % { "%(event)s (%(recipient_type)s) %(recipient)s " "%(status)s"
'event': event, ) % {
'recipient_type': recipient_type, "event": event,
'recipient': recipient, "recipient_type": recipient_type,
'status': status} "recipient": recipient,
"status": status,
}
def _on_conditions_treeview_cursor_changed(self, def _on_conditions_treeview_cursor_changed(self, widget: Gtk.TreeView) -> None:
widget: Gtk.TreeView
) -> None:
(model, iter_) = widget.get_selection().get_selected() (model, iter_) = widget.get_selection().get_selected()
if not iter_: if not iter_:
@@ -325,20 +323,20 @@ class ConfigDialog(Gtk.ApplicationWindow):
model = self._ui.conditions_treeview.get_model() model = self._ui.conditions_treeview.get_model()
num = self._ui.conditions_treeview.get_model().iter_n_children(None) num = self._ui.conditions_treeview.get_model().iter_n_children(None)
self._config[num] = { self._config[num] = {
'event': 'message_received', "event": "message_received",
'recipient_type': 'all', "recipient_type": "all",
'recipients': '', "recipients": "",
'status': 'all', "status": "all",
'tab_opened': 'both', "tab_opened": "both",
'has_focus': 'both', "has_focus": "both",
'sound': '', "sound": "",
'sound_file': '', "sound_file": "",
'popup': '', "popup": "",
'run_command': False, "run_command": False,
'command': '', "command": "",
'one_shot': False, "one_shot": False,
} }
iter_ = model.append((num, '')) iter_ = model.append((num, ""))
path = model.get_path(iter_) path = model.get_path(iter_)
self._ui.conditions_treeview.set_cursor(path) self._ui.conditions_treeview.set_cursor(path)
self._active_num = num self._active_num = num
@@ -380,8 +378,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
path = model.get_path(iter_) path = model.get_path(iter_)
iter_ = model.get_iter((path[0] - 1,)) iter_ = model.get_iter((path[0] - 1,))
model[iter_][0] = self._active_num model[iter_][0] = self._active_num
self._on_conditions_treeview_cursor_changed( self._on_conditions_treeview_cursor_changed(self._ui.conditions_treeview)
self._ui.conditions_treeview)
def _on_down_button_clicked(self, _button: Gtk.Button) -> None: def _on_down_button_clicked(self, _button: Gtk.Button) -> None:
selection = self._ui.conditions_treeview.get_selection() selection = self._ui.conditions_treeview.get_selection()
@@ -395,8 +392,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
model[iter_][0] = self._active_num + 1 model[iter_][0] = self._active_num + 1
iter_ = model.iter_next(iter_) iter_ = model.iter_next(iter_)
model[iter_][0] = self._active_num model[iter_][0] = self._active_num
self._on_conditions_treeview_cursor_changed( self._on_conditions_treeview_cursor_changed(self._ui.conditions_treeview)
self._ui.conditions_treeview)
def _on_event_combobox_changed(self, combo: Gtk.ComboBox) -> None: def _on_event_combobox_changed(self, combo: Gtk.ComboBox) -> None:
if self._active_num < 0: if self._active_num < 0:
@@ -405,21 +401,19 @@ class ConfigDialog(Gtk.ApplicationWindow):
if active == -1: if active == -1:
return return
event = list(EVENTS.keys())[active] event = list(EVENTS.keys())[active]
self._config[self._active_num]['event'] = event self._config[self._active_num]["event"] = event
for widget in EVENTS[event]: for widget in EVENTS[event]:
self._ui.__dict__[widget].set_sensitive(False) self._ui.__dict__[widget].set_sensitive(False)
self._ui.__dict__[widget].set_state(False) self._ui.__dict__[widget].set_state(False)
self._set_treeview_string() self._set_treeview_string()
def _on_recipient_type_combobox_changed(self, def _on_recipient_type_combobox_changed(self, widget: Gtk.ComboBox) -> None:
widget: Gtk.ComboBox
) -> None:
if self._active_num < 0: if self._active_num < 0:
return return
recipient_type = RECIPIENT_TYPES[widget.get_active()] recipient_type = RECIPIENT_TYPES[widget.get_active()]
self._config[self._active_num]['recipient_type'] = recipient_type self._config[self._active_num]["recipient_type"] = recipient_type
if recipient_type == 'all': if recipient_type == "all":
self._ui.recipient_list_entry.set_sensitive(False) self._ui.recipient_list_entry.set_sensitive(False)
else: else:
self._ui.recipient_list_entry.set_sensitive(True) self._ui.recipient_list_entry.set_sensitive(True)
@@ -430,19 +424,19 @@ class ConfigDialog(Gtk.ApplicationWindow):
return return
recipients = widget.get_text() recipients = widget.get_text()
# TODO: do some check # TODO: do some check
self._config[self._active_num]['recipients'] = recipients self._config[self._active_num]["recipients"] = recipients
self._set_treeview_string() self._set_treeview_string()
def _set_status_config(self) -> None: def _set_status_config(self) -> None:
if self._active_num < 0: if self._active_num < 0:
return return
status = '' status = ""
for st in ('online', 'away', 'xa', 'dnd'): for st in ("online", "away", "xa", "dnd"):
if self._ui.__dict__[st + '_cb'].get_active(): if self._ui.__dict__[st + "_cb"].get_active():
status += st + ' ' status += st + " "
if status: if status:
status = status[:-1] status = status[:-1]
self._config[self._active_num]['status'] = status self._config[self._active_num]["status"] = status
self._set_treeview_string() self._set_treeview_string()
def _on_status_radiobutton_toggled(self, _widget: Gtk.RadioButton) -> None: def _on_status_radiobutton_toggled(self, _widget: Gtk.RadioButton) -> None:
@@ -450,16 +444,16 @@ class ConfigDialog(Gtk.ApplicationWindow):
return return
if self._ui.all_status_rb.get_active(): if self._ui.all_status_rb.get_active():
self._ui.status_expander.set_expanded(False) self._ui.status_expander.set_expanded(False)
self._config[self._active_num]['status'] = 'all' self._config[self._active_num]["status"] = "all"
# 'All status' clicked # 'All status' clicked
for st in ('online', 'away', 'xa', 'dnd'): for st in ("online", "away", "xa", "dnd"):
self._ui.__dict__[st + '_cb'].set_sensitive(False) self._ui.__dict__[st + "_cb"].set_sensitive(False)
else: else:
self._ui.status_expander.set_expanded(True) self._ui.status_expander.set_expanded(True)
self._set_status_config() self._set_status_config()
# 'special status' clicked # 'special status' clicked
for st in ('online', 'away', 'xa', 'dnd'): for st in ("online", "away", "xa", "dnd"):
self._ui.__dict__[st + '_cb'].set_sensitive(True) self._ui.__dict__[st + "_cb"].set_sensitive(True)
self._set_treeview_string() self._set_treeview_string()
@@ -476,26 +470,26 @@ class ConfigDialog(Gtk.ApplicationWindow):
self._ui.has_focus_cb.set_sensitive(True) self._ui.has_focus_cb.set_sensitive(True)
self._ui.not_has_focus_cb.set_sensitive(True) self._ui.not_has_focus_cb.set_sensitive(True)
if self._ui.not_tab_opened_cb.get_active(): if self._ui.not_tab_opened_cb.get_active():
self._config[self._active_num]['tab_opened'] = 'both' self._config[self._active_num]["tab_opened"] = "both"
else: else:
self._config[self._active_num]['tab_opened'] = 'yes' self._config[self._active_num]["tab_opened"] = "yes"
else: else:
self._ui.has_focus_cb.set_sensitive(False) self._ui.has_focus_cb.set_sensitive(False)
self._ui.not_has_focus_cb.set_sensitive(False) self._ui.not_has_focus_cb.set_sensitive(False)
self._ui.not_tab_opened_cb.set_active(True) self._ui.not_tab_opened_cb.set_active(True)
self._config[self._active_num]['tab_opened'] = 'no' self._config[self._active_num]["tab_opened"] = "no"
def _on_not_tab_opened_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_not_tab_opened_cb_toggled(self, widget: Gtk.CheckButton) -> None:
if self._active_num < 0: if self._active_num < 0:
return return
if widget.get_active(): if widget.get_active():
if self._ui.tab_opened_cb.get_active(): if self._ui.tab_opened_cb.get_active():
self._config[self._active_num]['tab_opened'] = 'both' self._config[self._active_num]["tab_opened"] = "both"
else: else:
self._config[self._active_num]['tab_opened'] = 'no' self._config[self._active_num]["tab_opened"] = "no"
else: else:
self._ui.tab_opened_cb.set_active(True) self._ui.tab_opened_cb.set_active(True)
self._config[self._active_num]['tab_opened'] = 'yes' self._config[self._active_num]["tab_opened"] = "yes"
# has_focus OR (not xor) not_has_focus must be active # has_focus OR (not xor) not_has_focus must be active
def _on_has_focus_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_has_focus_cb_toggled(self, widget: Gtk.CheckButton) -> None:
@@ -503,87 +497,83 @@ class ConfigDialog(Gtk.ApplicationWindow):
return return
if widget.get_active(): if widget.get_active():
if self._ui.not_has_focus_cb.get_active(): if self._ui.not_has_focus_cb.get_active():
self._config[self._active_num]['has_focus'] = 'both' self._config[self._active_num]["has_focus"] = "both"
else: else:
self._config[self._active_num]['has_focus'] = 'yes' self._config[self._active_num]["has_focus"] = "yes"
else: else:
self._ui.not_has_focus_cb.set_active(True) self._ui.not_has_focus_cb.set_active(True)
self._config[self._active_num]['has_focus'] = 'no' self._config[self._active_num]["has_focus"] = "no"
def _on_not_has_focus_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_not_has_focus_cb_toggled(self, widget: Gtk.CheckButton) -> None:
if self._active_num < 0: if self._active_num < 0:
return return
if widget.get_active(): if widget.get_active():
if self._ui.has_focus_cb.get_active(): if self._ui.has_focus_cb.get_active():
self._config[self._active_num]['has_focus'] = 'both' self._config[self._active_num]["has_focus"] = "both"
else: else:
self._config[self._active_num]['has_focus'] = 'no' self._config[self._active_num]["has_focus"] = "no"
else: else:
self._ui.has_focus_cb.set_active(True) self._ui.has_focus_cb.set_active(True)
self._config[self._active_num]['has_focus'] = 'yes' self._config[self._active_num]["has_focus"] = "yes"
def _on_use_it_toggled(self, def _on_use_it_toggled(
widget: Gtk.CheckButton, self, widget: Gtk.CheckButton, opposite_widget: Gtk.CheckButton, option: str
opposite_widget: Gtk.CheckButton, ) -> None:
option: str
) -> None:
if widget.get_active(): if widget.get_active():
if opposite_widget.get_active(): if opposite_widget.get_active():
opposite_widget.set_active(False) opposite_widget.set_active(False)
self._config[self._active_num][option] = 'yes' self._config[self._active_num][option] = "yes"
elif opposite_widget.get_active(): elif opposite_widget.get_active():
self._config[self._active_num][option] = 'no' self._config[self._active_num][option] = "no"
else: else:
self._config[self._active_num][option] = '' self._config[self._active_num][option] = ""
def _on_disable_it_toggled(self, def _on_disable_it_toggled(
widget: Gtk.CheckButton, self, widget: Gtk.CheckButton, opposite_widget: Gtk.CheckButton, option: str
opposite_widget: Gtk.CheckButton, ) -> None:
option: str
) -> None:
if widget.get_active(): if widget.get_active():
if opposite_widget.get_active(): if opposite_widget.get_active():
opposite_widget.set_active(False) opposite_widget.set_active(False)
self._config[self._active_num][option] = 'no' self._config[self._active_num][option] = "no"
elif opposite_widget.get_active(): elif opposite_widget.get_active():
self._config[self._active_num][option] = 'yes' self._config[self._active_num][option] = "yes"
else: else:
self._config[self._active_num][option] = '' self._config[self._active_num][option] = ""
def _on_use_sound_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_use_sound_cb_toggled(self, widget: Gtk.CheckButton) -> None:
self._on_use_it_toggled(widget, self._ui.disable_sound_cb, 'sound') self._on_use_it_toggled(widget, self._ui.disable_sound_cb, "sound")
if widget.get_active(): if widget.get_active():
self._ui.sound_file_box.set_sensitive(True) self._ui.sound_file_box.set_sensitive(True)
else: else:
self._ui.sound_file_box.set_sensitive(False) self._ui.sound_file_box.set_sensitive(False)
def _on_sound_file_set(self, widget: Gtk.FileChooserButton) -> None: def _on_sound_file_set(self, widget: Gtk.FileChooserButton) -> None:
self._config[self._active_num]['sound_file'] = widget.get_filename() self._config[self._active_num]["sound_file"] = widget.get_filename()
def _on_play_button_clicked(self, _button: Gtk.Button) -> None: def _on_play_button_clicked(self, _button: Gtk.Button) -> None:
play_sound_file(self._ui.filechooser.get_filename()) play_sound_file(self._ui.filechooser.get_filename())
def _on_disable_sound_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_disable_sound_cb_toggled(self, widget: Gtk.CheckButton) -> None:
self._on_disable_it_toggled(widget, self._ui.use_sound_cb, 'sound') self._on_disable_it_toggled(widget, self._ui.use_sound_cb, "sound")
def _on_use_popup_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_use_popup_cb_toggled(self, widget: Gtk.CheckButton) -> None:
self._on_use_it_toggled(widget, self._ui.disable_popup_cb, 'popup') self._on_use_it_toggled(widget, self._ui.disable_popup_cb, "popup")
def _on_disable_popup_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_disable_popup_cb_toggled(self, widget: Gtk.CheckButton) -> None:
self._on_disable_it_toggled(widget, self._ui.use_popup_cb, 'popup') self._on_disable_it_toggled(widget, self._ui.use_popup_cb, "popup")
def _on_run_command_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_run_command_cb_toggled(self, widget: Gtk.CheckButton) -> None:
self._config[self._active_num]['run_command'] = widget.get_active() self._config[self._active_num]["run_command"] = widget.get_active()
if widget.get_active(): if widget.get_active():
self._ui.command_entry.set_sensitive(True) self._ui.command_entry.set_sensitive(True)
else: else:
self._ui.command_entry.set_sensitive(False) self._ui.command_entry.set_sensitive(False)
def _on_command_entry_changed(self, widget: Gtk.Entry) -> None: def _on_command_entry_changed(self, widget: Gtk.Entry) -> None:
self._config[self._active_num]['command'] = widget.get_text() self._config[self._active_num]["command"] = widget.get_text()
def _on_one_shot_cb_toggled(self, widget: Gtk.CheckButton) -> None: def _on_one_shot_cb_toggled(self, widget: Gtk.CheckButton) -> None:
self._config[self._active_num]['one_shot'] = widget.get_active() self._config[self._active_num]["one_shot"] = widget.get_active()
self._ui.command_entry.set_sensitive(widget.get_active()) self._ui.command_entry.set_sensitive(widget.get_active())

View File

@@ -17,23 +17,33 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from typing import Callable
from typing import cast
from typing import Union
import logging import logging
import subprocess import subprocess
from functools import partial from functools import partial
from typing import Any, Callable, Union, cast
from gajim.common import app, ged from nbxmpp.protocol import JID
from gajim.common.const import PROPAGATE_EVENT, STOP_EVENT
from gajim.common.events import MessageReceived, Notification, PresenceReceived from gajim.common import app
from gajim.common import ged
from gajim.common.const import PROPAGATE_EVENT
from gajim.common.const import STOP_EVENT
from gajim.common.events import MessageReceived
from gajim.common.events import Notification
from gajim.common.events import PresenceReceived
from gajim.common.helpers import play_sound_file from gajim.common.helpers import play_sound_file
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from nbxmpp.protocol import JID
from triggers.gtk.config import ConfigDialog from triggers.gtk.config import ConfigDialog
from triggers.util import RuleResult, log_result from triggers.util import log_result
from triggers.util import RuleResult
log = logging.getLogger('gajim.p.triggers') log = logging.getLogger("gajim.p.triggers")
ProcessableEventsT = Union[MessageReceived, Notification, PresenceReceived] ProcessableEventsT = Union[MessageReceived, Notification, PresenceReceived]
RuleT = dict[str, Any] RuleT = dict[str, Any]
@@ -42,36 +52,37 @@ RuleT = dict[str, Any]
class Triggers(GajimPlugin): class Triggers(GajimPlugin):
def init(self) -> None: def init(self) -> None:
self.description = _( self.description = _(
'Configure Gajims behaviour with triggers for each contact') "Configure Gajims behaviour with triggers for each contact"
)
self.config_dialog = partial(ConfigDialog, self) self.config_dialog = partial(ConfigDialog, self)
self.config_default_values = {} self.config_default_values = {}
self.events_handlers = { self.events_handlers = {
'notification': (ged.PREGUI, self._on_notification), "notification": (ged.PREGUI, self._on_notification),
'message-received': (ged.PREGUI2, self._on_message_received), "message-received": (ged.PREGUI2, self._on_message_received),
'gc-message-received': (ged.PREGUI2, self._on_message_received), "gc-message-received": (ged.PREGUI2, self._on_message_received),
# 'presence-received': (ged.PREGUI, self._on_presence_received), # 'presence-received': (ged.PREGUI, self._on_presence_received),
} }
def _on_notification(self, event: Notification) -> bool: def _on_notification(self, event: Notification) -> bool:
log.info('Process %s, %s', event.name, event.type) log.info("Process %s, %s", event.name, event.type)
result = self._check_all(event, result = self._check_all(
self._check_rule_apply_notification, event, self._check_rule_apply_notification, self._apply_rule
self._apply_rule) )
log.info('Result: %s', result) log.info("Result: %s", result)
return self._excecute_notification_rules(result, event) return self._excecute_notification_rules(result, event)
def _on_message_received(self, event: MessageReceived) -> bool: def _on_message_received(self, event: MessageReceived) -> bool:
log.info('Process %s', event.name) log.info("Process %s", event.name)
message = event.message message = event.message
if message.text is None: if message.text is None:
log.info('Discard event because it has no message text') log.info("Discard event because it has no message text")
return PROPAGATE_EVENT return PROPAGATE_EVENT
result = self._check_all(event, result = self._check_all(
self._check_rule_apply_msg_received, event, self._check_rule_apply_msg_received, self._apply_rule
self._apply_rule) )
log.info('Result: %s', result) log.info("Result: %s", result)
return self._excecute_message_rules(result) return self._excecute_message_rules(result)
def _on_presence_received(self, event: PresenceReceived) -> None: def _on_presence_received(self, event: PresenceReceived) -> None:
@@ -86,11 +97,12 @@ class Triggers(GajimPlugin):
check_func = self._check_rule_apply_status_changed check_func = self._check_rule_apply_status_changed
self._check_all(event, check_func, self._apply_rule) self._check_all(event, check_func, self._apply_rule)
def _check_all(self, def _check_all(
event: ProcessableEventsT, self,
check_func: Callable[..., bool], event: ProcessableEventsT,
apply_func: Callable[..., Any] check_func: Callable[..., bool],
) -> RuleResult: apply_func: Callable[..., Any],
) -> RuleResult:
result = RuleResult() result = RuleResult()
@@ -101,7 +113,7 @@ class Triggers(GajimPlugin):
rule = cast(RuleT, self.config[str(num)]) rule = cast(RuleT, self.config[str(num)])
if check_func(event, rule): if check_func(event, rule):
apply_func(result, rule) apply_func(result, rule)
if 'one_shot' in rule and rule['one_shot']: if "one_shot" in rule and rule["one_shot"]:
to_remove.append(num) to_remove.append(num)
decal = 0 decal = 0
@@ -121,47 +133,38 @@ class Triggers(GajimPlugin):
return result return result
@log_result @log_result
def _check_rule_apply_msg_received(self, def _check_rule_apply_msg_received(
event: MessageReceived, self, event: MessageReceived, rule: RuleT
rule: RuleT ) -> bool:
) -> bool:
return self._check_rule_all('message_received', event, rule) return self._check_rule_all("message_received", event, rule)
@log_result @log_result
def _check_rule_apply_connected(self, def _check_rule_apply_connected(self, event: PresenceReceived, rule: RuleT) -> bool:
event: PresenceReceived,
rule: RuleT
) -> bool:
return self._check_rule_all('contact_connected', event, rule) return self._check_rule_all("contact_connected", event, rule)
@log_result @log_result
def _check_rule_apply_disconnected(self, def _check_rule_apply_disconnected(
event: PresenceReceived, self, event: PresenceReceived, rule: RuleT
rule: RuleT ) -> bool:
) -> bool:
return self._check_rule_all('contact_disconnected', event, rule) return self._check_rule_all("contact_disconnected", event, rule)
@log_result @log_result
def _check_rule_apply_status_changed(self, def _check_rule_apply_status_changed(
event: PresenceReceived, self, event: PresenceReceived, rule: RuleT
rule: RuleT ) -> bool:
) -> bool:
return self._check_rule_all('contact_status_change', event, rule) return self._check_rule_all("contact_status_change", event, rule)
@log_result @log_result
def _check_rule_apply_notification(self, def _check_rule_apply_notification(self, event: Notification, rule: RuleT) -> bool:
event: Notification,
rule: RuleT
) -> bool:
# Check notification type # Check notification type
notif_type = '' notif_type = ""
if event.type == 'incoming-message': if event.type == "incoming-message":
notif_type = 'message_received' notif_type = "message_received"
# if event.type == 'pres': # if event.type == 'pres':
# # TODO: # # TODO:
# if (event.base_event.old_show < 2 and # if (event.base_event.old_show < 2 and
@@ -175,14 +178,12 @@ class Triggers(GajimPlugin):
return self._check_rule_all(notif_type, event, rule) return self._check_rule_all(notif_type, event, rule)
def _check_rule_all(self, def _check_rule_all(
notif_type: str, self, notif_type: str, event: ProcessableEventsT, rule: RuleT
event: ProcessableEventsT, ) -> bool:
rule: RuleT
) -> bool:
# Check notification type # Check notification type
if rule['event'] != notif_type: if rule["event"] != notif_type:
return False return False
# notification type is ok. Now check recipient # notification type is ok. Now check recipient
@@ -205,21 +206,17 @@ class Triggers(GajimPlugin):
return True return True
@log_result @log_result
def _check_rule_recipients(self, def _check_rule_recipients(self, event: ProcessableEventsT, rule: RuleT) -> bool:
event: ProcessableEventsT,
rule: RuleT
) -> bool:
rule_recipients = [t.strip() for t in rule['recipients'].split(',')] rule_recipients = [t.strip() for t in rule["recipients"].split(",")]
if rule['recipient_type'] == 'groupchat': if rule["recipient_type"] == "groupchat":
if event.jid in rule_recipients: if event.jid in rule_recipients:
return True return True
return False return False
if (rule['recipient_type'] == 'contact' and event.jid not in if rule["recipient_type"] == "contact" and event.jid not in rule_recipients:
rule_recipients):
return False return False
client = app.get_client(event.account) client = app.get_client(event.account)
contact = client.get_module('Contacts').get_contact(event.jid) contact = client.get_module("Contacts").get_contact(event.jid)
if contact.is_groupchat or not contact.is_in_roster: if contact.is_groupchat or not contact.is_in_roster:
return False return False
@@ -229,84 +226,74 @@ class Triggers(GajimPlugin):
if group in rule_recipients: if group in rule_recipients:
group_found = True group_found = True
break break
if rule['recipient_type'] == 'group' and not group_found: if rule["recipient_type"] == "group" and not group_found:
return False return False
return True return True
@log_result @log_result
def _check_rule_status(self, def _check_rule_status(self, event: ProcessableEventsT, rule: RuleT) -> bool:
event: ProcessableEventsT,
rule: RuleT
) -> bool:
rule_statuses = rule['status'].split() rule_statuses = rule["status"].split()
client = app.get_client(event.account) client = app.get_client(event.account)
if rule['status'] != 'all' and client.status not in rule_statuses: if rule["status"] != "all" and client.status not in rule_statuses:
return False return False
return True return True
@log_result @log_result
def _check_rule_tab_opened(self, def _check_rule_tab_opened(self, event: ProcessableEventsT, rule: RuleT) -> bool:
event: ProcessableEventsT,
rule: RuleT
) -> bool:
if rule['tab_opened'] == 'both': if rule["tab_opened"] == "both":
return True return True
tab_opened = False tab_opened = False
assert isinstance(event.jid, JID) assert isinstance(event.jid, JID)
if app.window.chat_exists(event.account, event.jid): if app.window.chat_exists(event.account, event.jid):
tab_opened = True tab_opened = True
if tab_opened and rule['tab_opened'] == 'no': if tab_opened and rule["tab_opened"] == "no":
return False return False
elif not tab_opened and rule['tab_opened'] == 'yes': elif not tab_opened and rule["tab_opened"] == "yes":
return False return False
return True return True
@log_result @log_result
def _check_rule_has_focus(self, def _check_rule_has_focus(self, event: ProcessableEventsT, rule: RuleT) -> bool:
event: ProcessableEventsT,
rule: RuleT
) -> bool:
if rule['has_focus'] == 'both': if rule["has_focus"] == "both":
return True return True
if rule['tab_opened'] == 'no': if rule["tab_opened"] == "no":
# Does not apply in this case # Does not apply in this case
return True return True
assert isinstance(event.jid, JID) assert isinstance(event.jid, JID)
chat_active = app.window.is_chat_active(event.account, event.jid) chat_active = app.window.is_chat_active(event.account, event.jid)
if chat_active and rule['has_focus'] == 'no': if chat_active and rule["has_focus"] == "no":
return False return False
elif not chat_active and rule['has_focus'] == 'yes': elif not chat_active and rule["has_focus"] == "yes":
return False return False
return True return True
def _apply_rule(self, result: RuleResult, rule: RuleT) -> None: def _apply_rule(self, result: RuleResult, rule: RuleT) -> None:
if rule['sound'] == 'no': if rule["sound"] == "no":
result.sound = False result.sound = False
result.sound_file = None result.sound_file = None
elif rule['sound'] == 'yes': elif rule["sound"] == "yes":
result.sound = False result.sound = False
result.sound_file = rule['sound_file'] result.sound_file = rule["sound_file"]
if rule['run_command']: if rule["run_command"]:
result.command = rule['command'] result.command = rule["command"]
if rule['popup'] == 'no': if rule["popup"] == "no":
result.show_notification = False result.show_notification = False
elif rule['popup'] == 'yes': elif rule["popup"] == "yes":
result.show_notification = True result.show_notification = True
def _excecute_notification_rules(self, def _excecute_notification_rules(
result: RuleResult, self, result: RuleResult, event: Notification
event: Notification ) -> bool:
) -> bool:
if result.sound is False: if result.sound is False:
event.sound = None event.sound = None
@@ -324,7 +311,7 @@ class Triggers(GajimPlugin):
if result.command is not None: if result.command is not None:
try: try:
subprocess.Popen(f'{result.command} &', shell=True).wait() subprocess.Popen(f"{result.command} &", shell=True).wait()
except Exception: except Exception:
pass pass

View File

@@ -25,14 +25,15 @@ if TYPE_CHECKING:
from .triggers import ProcessableEventsT from .triggers import ProcessableEventsT
from .triggers import RuleT from .triggers import RuleT
log = logging.getLogger('gajim.p.triggers') log = logging.getLogger("gajim.p.triggers")
def log_result(func: Callable[..., Any]) -> Callable[..., bool]: def log_result(func: Callable[..., Any]) -> Callable[..., bool]:
def wrapper(self: Any, event: ProcessableEventsT, rule: RuleT): def wrapper(self: Any, event: ProcessableEventsT, rule: RuleT):
res = func(self, event, rule) res = func(self, event, rule)
log.info(f'{event.name} -> {func.__name__} -> {res}') log.info(f"{event.name} -> {func.__name__} -> {res}")
return res return res
return wrapper return wrapper