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

View File

@@ -19,8 +19,8 @@ from __future__ import annotations
import json
import logging
from pathlib import Path
from functools import partial
from pathlib import Path
from gi.repository import GLib
from gi.repository import GObject
@@ -29,28 +29,27 @@ from gi.repository import Gtk
from gajim.common import configpaths
from gajim.common import types
from gajim.common.modules.contacts import GroupchatContact
from gajim.gtk.message_input import MessageInputTextView
from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _
from acronyms_expander.acronyms import DEFAULT_DATA
from acronyms_expander.gtk.config import ConfigDialog
log = logging.getLogger('gajim.p.acronyms')
log = logging.getLogger("gajim.p.acronyms")
class AcronymsExpanderPlugin(GajimPlugin):
def init(self) -> None:
self.description = _('Replaces acronyms (or other strings) '
'with given expansions/substitutes.')
self.description = _(
"Replaces acronyms (or other strings) " "with given expansions/substitutes."
)
self.config_dialog = partial(ConfigDialog, self)
self.gui_extension_points = {
'message_input': (self._connect, None),
'switch_contact': (self._on_switch_contact, None)
"message_input": (self._connect, None),
"switch_contact": (self._on_switch_contact, None),
}
self._invoker = ' '
self._invoker = " "
self._replace_in_progress = False
self._signal_id = None
@@ -62,42 +61,40 @@ class AcronymsExpanderPlugin(GajimPlugin):
@staticmethod
def _load_acronyms() -> dict[str, str]:
try:
data_path = Path(configpaths.get('PLUGINS_DATA'))
data_path = Path(configpaths.get("PLUGINS_DATA"))
except KeyError:
# PLUGINS_DATA was added in 1.0.99.1
return DEFAULT_DATA
path = data_path / 'acronyms' / 'acronyms'
path = data_path / "acronyms" / "acronyms"
if not path.exists():
return DEFAULT_DATA
with path.open('r') as file:
with path.open("r") as file:
acronyms = json.load(file)
return acronyms
@staticmethod
def _save_acronyms(acronyms: dict[str, str]) -> None:
try:
data_path = Path(configpaths.get('PLUGINS_DATA'))
data_path = Path(configpaths.get("PLUGINS_DATA"))
except KeyError:
# PLUGINS_DATA was added in 1.0.99.1
return
path = data_path / 'acronyms'
path = data_path / "acronyms"
if not path.exists():
path.mkdir(parents=True)
filepath = path / 'acronyms'
with filepath.open('w') as file:
filepath = path / "acronyms"
with filepath.open("w") as file:
json.dump(acronyms, file)
def set_acronyms(self, acronyms: dict[str, str]) -> None:
self.acronyms = acronyms
self._save_acronyms(acronyms)
def _on_buffer_changed(self,
message_input: MessageInputTextView
) -> None:
def _on_buffer_changed(self, message_input: MessageInputTextView) -> None:
if self._contact is None:
# If no chat has been activated yet
@@ -126,9 +123,8 @@ class AcronymsExpanderPlugin(GajimPlugin):
# Get to the start of the last word
# word_start_iter = insert_iter.copy()
result = insert_iter.backward_search(
self._invoker,
Gtk.TextSearchFlags.VISIBLE_ONLY,
None)
self._invoker, Gtk.TextSearchFlags.VISIBLE_ONLY, None
)
if result is None:
word_start_iter = buffer_.get_start_iter()
@@ -140,30 +136,29 @@ class AcronymsExpanderPlugin(GajimPlugin):
if isinstance(self._contact, GroupchatContact):
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
if self._contact.is_pm_contact:
if last_word == self._contact.name:
log.info('Contact name equals acronym')
log.info("Contact name equals acronym")
return
substitute = self.acronyms.get(last_word)
if substitute is None:
log.debug('%s not an acronym', last_word)
log.debug("%s not an acronym", last_word)
return
GLib.idle_add(self._replace_text,
buffer_,
word_start_iter,
insert_iter,
substitute)
GLib.idle_add(
self._replace_text, buffer_, word_start_iter, insert_iter, substitute
)
def _replace_text(self,
def _replace_text(
self,
buffer_: Gtk.TextBuffer,
start: Gtk.TextIter,
end: Gtk.TextIter,
substitute: str
substitute: str,
) -> None:
self._replace_in_progress = True
@@ -176,11 +171,12 @@ class AcronymsExpanderPlugin(GajimPlugin):
def _connect(self, message_input: MessageInputTextView) -> None:
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:
assert self._message_input is not None
assert self._signal_id is not None
if GObject.signal_handler_is_connected(
self._message_input, self._signal_id):
if GObject.signal_handler_is_connected(self._message_input, 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 gajim.gtk.widgets import GajimAppWindow
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING:
from ..acronyms_expander import AcronymsExpanderPlugin
class ConfigDialog(GajimAppWindow):
def __init__(self,
plugin: AcronymsExpanderPlugin,
transient: Gtk.Window
) -> None:
def __init__(self, plugin: AcronymsExpanderPlugin, transient: Gtk.Window) -> None:
GajimAppWindow.__init__(
self,
name="AcronymsConfigDialog",
title=_('Acronyms Configuration'),
title=_("Acronyms Configuration"),
default_width=400,
default_height=400,
transient_for=transient,
@@ -48,7 +44,7 @@ class ConfigDialog(GajimAppWindow):
)
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
@@ -71,29 +67,24 @@ class ConfigDialog(GajimAppWindow):
for acronym, substitute in self._plugin.acronyms.items():
self._ui.acronyms_store.append([acronym, substitute])
def _on_acronym_edited(self,
_renderer: Gtk.CellRendererText,
path: str,
new_text: str
def _on_acronym_edited(
self, _renderer: Gtk.CellRendererText, path: str, new_text: str
) -> None:
iter_ = self._ui.acronyms_store.get_iter(path)
self._ui.acronyms_store.set_value(iter_, 0, new_text)
def _on_substitute_edited(self,
_renderer: Gtk.CellRendererText,
path: str,
new_text: str
def _on_substitute_edited(
self, _renderer: Gtk.CellRendererText, path: str, new_text: str
) -> None:
iter_ = self._ui.acronyms_store.get_iter(path)
self._ui.acronyms_store.set_value(iter_, 1, new_text)
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]
self._ui.acronyms_treeview.scroll_to_cell(
row.path, None, False, 0, 0)
self._ui.acronyms_treeview.scroll_to_cell(row.path, None, False, 0, 0)
self._ui.selection.unselect_all()
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
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
'''
"""
:author: Yann Leboulanger <asterix@lagaule.org>
:since: 16 August 2012
:copyright: Copyright (2012) Yann Leboulanger <asterix@lagaule.org>
:license: GPLv3
'''
"""
from functools import partial
from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _
from anti_spam.modules import anti_spam
from anti_spam.config_dialog import AntiSpamConfigDialog
from anti_spam.modules import anti_spam
class AntiSpamPlugin(GajimPlugin):
def init(self) -> None:
self.description = _('Allows you to block various kinds of incoming '
'messages (Spam, XHTML formatting, etc.)')
self.description = _(
"Allows you to block various kinds of incoming "
"messages (Spam, XHTML formatting, etc.)"
)
self.config_dialog = partial(AntiSpamConfigDialog, self)
self.config_default_values = {
'disable_xhtml_muc': (False, ''),
'disable_xhtml_pm': (False, ''),
'block_subscription_requests': (False, ''),
'msgtxt_limit': (0, ''),
'msgtxt_question': ('12 x 12 = ?', ''),
'msgtxt_answer': ('', ''),
'antispam_for_conference': (False, ''),
'block_domains': ('', ''),
'whitelist': ([], ''),
"disable_xhtml_muc": (False, ""),
"disable_xhtml_pm": (False, ""),
"block_subscription_requests": (False, ""),
"msgtxt_limit": (0, ""),
"msgtxt_question": ("12 x 12 = ?", ""),
"msgtxt_answer": ("", ""),
"antispam_for_conference": (False, ""),
"block_domains": ("", ""),
"whitelist": ([], ""),
}
self.gui_extension_points = {}
self.modules = [anti_spam]

View File

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

View File

@@ -32,7 +32,7 @@ from gajim.common.events import MessageSent
from gajim.common.modules.base import BaseModule
# Module name
name = 'AntiSpam'
name = "AntiSpam"
zeroconf = False
@@ -41,21 +41,23 @@ class AntiSpam(BaseModule):
BaseModule.__init__(self, client, plugin=True)
self.handlers = [
StanzaHandler(name='message',
callback=self._message_received,
priority=48),
StanzaHandler(name='presence',
StanzaHandler(name="message", callback=self._message_received, priority=48),
StanzaHandler(
name="presence",
callback=self._subscribe_received,
typ='subscribe',
priority=48),
typ="subscribe",
priority=48,
),
]
self.register_events([
('message-sent', ged.GUI2, self._on_message_sent),
])
self.register_events(
[
("message-sent", ged.GUI2, self._on_message_sent),
]
)
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._contacted_jids: set[JID] = set()
@@ -66,10 +68,8 @@ class AntiSpam(BaseModule):
# This set contains JIDs of all outgoing chats.
self._contacted_jids.add(event.jid)
def _message_received(self,
_con: Client,
_stanza: Message,
properties: MessageProperties
def _message_received(
self, _con: Client, _stanza: Message, properties: MessageProperties
) -> None:
if properties.is_sent_carbon:
@@ -86,33 +86,35 @@ class AntiSpam(BaseModule):
raise NodeProcessed
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:
self._log.info('Discarded message from %s: message '
'length exceeded' % msg_from)
self._log.info(
"Discarded message from %s: message " "length exceeded" % msg_from
)
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
self._log.info('Stripped message from %s: message '
'contained XHTML' % msg_from)
self._log.info(
"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
self._log.info('Stripped message from %s: message '
'contained XHTML' % msg_from)
self._log.info(
"Stripped message from %s: message " "contained XHTML" % msg_from
)
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:
return False
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
if (properties.type.value not in ('chat', 'normal') or
properties.is_mam_message):
if properties.type.value not in ("chat", "normal") or properties.is_mam_message:
return False
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
# 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:
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:
assert properties.body
if answer in properties.body.split('\n'):
if answer in properties.body.split("\n"):
if str(msg_from) not in whitelist:
whitelist.append(str(msg_from))
# We need to explicitly save, because 'append' does not
@@ -146,26 +148,24 @@ class AntiSpam(BaseModule):
return False
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)
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,
_con: Client,
_stanza: Presence,
properties: PresenceProperties
def _subscribe_received(
self, _con: Client, _stanza: Presence, properties: PresenceProperties
) -> None:
msg_from = properties.jid
block_sub = self._config['block_subscription_requests']
roster_item = self._client.get_module('Roster').get_item(msg_from)
block_sub = self._config["block_subscription_requests"]
roster_item = self._client.get_module("Roster").get_item(msg_from)
if block_sub and roster_item is None:
self._client.get_module('Presence').unsubscribed(msg_from)
self._log.info('Denied subscription request from %s' % msg_from)
self._client.get_module("Presence").unsubscribed(msg_from)
self._log.info("Denied subscription request from %s" % msg_from)
raise NodeProcessed
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:
return []
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()
return alts
@@ -48,23 +48,23 @@ class ClientsDict(UserDict[str, ClientData]):
def get_client_data(self, name: str, node: str) -> tuple[str, str]:
client_data = self.get(node)
if client_data is None:
return _('Unknown'), 'xmpp-client-unknown'
return _("Unknown"), "xmpp-client-unknown"
if client_data.variations is None:
assert client_data.default is not None
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)
for var in variations:
try:
return var, f'xmpp-client-{client_data.variations[var]}'
return var, f"xmpp-client-{client_data.variations[var]}"
except KeyError:
pass
assert client_data.default is not None
client_name, icon_name = client_data.default
return client_name, f'xmpp-client-{icon_name}'
return client_name, f"xmpp-client-{icon_name}"
# ClientData(
@@ -73,137 +73,159 @@ class ClientsDict(UserDict[str, ClientData]):
# )
# pylint: disable=too-many-lines
CLIENTS = ClientsDict({
'http://gajim.org': ClientData(('Gajim', 'gajim')),
'https://gajim.org': ClientData(('Gajim', 'gajim')),
'http://conversations.im': ClientData(
default=('Conversations', 'conversations'),
variations={'Conversations Legacy': 'conversations-legacy'}
CLIENTS = ClientsDict(
{
"http://gajim.org": ClientData(("Gajim", "gajim")),
"https://gajim.org": ClientData(("Gajim", "gajim")),
"http://conversations.im": ClientData(
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://monocles.de': ClientData(('monocles chat', 'monocles-chat')),
'http://pidgin.im/': ClientData(('Pidgin', 'pidgin')),
'https://poez.io': ClientData(('Poezio', 'poezio')),
'https://yaxim.org/': ClientData(('yaxim', 'yaxim')),
'https://yaxim.org/bruno/': ClientData(('Bruno', 'bruno')),
'http://mcabber.com/caps': ClientData(('MCabber', 'mcabber')),
'http://psi-plus.com': ClientData(('Psi+', 'psiplus')),
'https://psi-plus.com': ClientData(('Psi+', 'psiplus')),
'https://dino.im': ClientData(('Dino', 'dino')),
'http://monal.im/': ClientData(('Monal', 'monal')),
'http://slixmpp.com/ver/1.2.4': ClientData(('Bot', 'bot')),
'http://slixmpp.com/ver/1.3.0': ClientData(('Bot', 'bot')),
'https://www.xabber.com/': ClientData(('Xabber', 'xabber')),
'http://www.profanity.im': ClientData(('Profanity', 'profanity')),
'http://swift.im': ClientData(('Swift', 'swift')),
'https://salut-a-toi.org': ClientData(('Salut à Toi', 'sat')),
'https://conversejs.org': ClientData(('Converse', 'conversejs')),
'http://bitlbee.org/xmpp/caps': ClientData(('BitlBee', 'bitlbee')),
'http://tkabber.jabber.ru/': ClientData(('Tkabber', 'tkabber')),
'http://miranda-ng.org/caps': ClientData(('Miranda NG', 'miranda_ng')),
'http://www.adium.im/': ClientData(('Adium', 'adium')),
'http://www.adiumx.com/caps': ClientData(('Adium', 'adium')),
'http://www.adiumx.com': ClientData(('Adium', 'adium')),
'http://aqq.eu/': ClientData(('Aqq', 'aqq')),
'http://www.asterisk.org/xmpp/client/caps': ClientData(('Asterisk', 'asterisk')),
'http://ayttm.souceforge.net/caps': ClientData(('Ayttm', 'ayttm')),
'http://www.barobin.com/caps': ClientData(('Bayanicq', 'bayanicq')),
'http://simpleapps.ru/caps#blacksmith': ClientData(('Blacksmith', 'bot')),
'http://blacksmith-2.googlecode.com/svn/': ClientData(('Blacksmith-2', 'bot')),
'http://coccinella.sourceforge.net/protocol/caps': ClientData(('Coccinella', 'coccinella')),
'http://digsby.com/caps': ClientData(('Digsby', 'digsby')),
'http://emacs-jabber.sourceforge.net': ClientData(('Emacs Jabber Client', 'emacs')),
'http://emess.eqx.su/caps': ClientData(('Emess', 'emess')),
'http://live.gnome.org/empathy/caps': ClientData(('Empathy', 'telepathy.freedesktop.org')),
'http://eqo.com/': ClientData(('Eqo', 'libpurple')),
'http://exodus.jabberstudio.org/caps': ClientData(('Exodus', 'exodus')),
'http://fatal-bot.spb.ru/caps': ClientData(('Fatal-bot', 'bot')),
'http://svn.posix.ru/fatal-bot/trunk': ClientData(('Fatal-bot', 'bot')),
'http://isida.googlecode.com': ClientData(('Isida', 'isida-bot')),
'http://isida-bot.com': ClientData(('Isida', 'isida-bot')),
'http://jabga.ru': ClientData(('Fin jabber', 'fin')),
'http://chat.freize.org/caps': ClientData(('Freize', 'freize')),
'http://gabber.sourceforge.net': ClientData(('Gabber', 'gabber')),
'http://glu.net/': ClientData(('Glu', 'glu')),
'http://mail.google.com/xmpp/client/caps': ClientData(('GMail', 'google.com')),
'http://www.android.com/gtalk/client/caps': ClientData(('GTalk', 'talk.google.com')),
'talk.google.com': ClientData(('GTalk', 'talk.google.com')),
'http://talkgadget.google.com/client/caps': ClientData(('GTalk', 'google')),
'http://talk.google.com/xmpp/bot/caps': ClientData(('GTalk', 'google')),
'http://aspro.users.ru/historian-bot/': ClientData(('Historian-bot', 'bot')),
'http://www.apple.com/ichat/caps': ClientData(('IChat', 'ichat')),
'http://instantbird.com/': ClientData(('Instantbird', 'instantbird')),
'http://j-tmb.ru/caps': ClientData(('J-tmb', 'bot')),
'http://jabbroid.akuz.de': ClientData(('Jabbroid', 'android')),
'http://jabbroid.akuz.de/caps': ClientData(('Jabbroid', 'android')),
'http://dev.jabbim.cz/jabbim/caps': ClientData(('Jabbim', 'jabbim')),
'http://jabbrik.ru/caps': ClientData(('Jabbrik', 'bot')),
'http://jabrvista.net.ru': ClientData(('Jabvista', 'bot')),
'http://jajc.jrudevels.org/caps': ClientData(('JAJC', 'jajc')),
'http://qabber.ru/jame-bot': ClientData(('Jame-bot', 'bot')),
'https://www.jappix.com/': ClientData(('Jappix', 'jappix')),
'http://japyt.googlecode.com': ClientData(('Japyt', 'japyt')),
'http://jasmineicq.ru/caps': ClientData(('Jasmine', 'jasmine')),
'http://jimm.net.ru/caps': ClientData(('Jimm', 'jimm-aspro')),
'http://jitsi.org': ClientData(('Jitsi', 'jitsi')),
'http://jtalk.ustyugov.net/caps': ClientData(('Jtalk', 'jtalk')),
'http://pjc.googlecode.com/caps': ClientData(('Jubo', 'jubo')),
'http://juick.com/caps': ClientData(('Juick', 'juick')),
'http://kopete.kde.org/jabber/caps': ClientData(('Kopete', 'kopete')),
'http://bluendo.com/protocol/caps': ClientData(('Lampiro', 'lampiro')),
'http://lytgeygen.ru/caps': ClientData(('Lytgeygen', 'bot')),
'http://agent.mail.ru/caps': ClientData(('Mailruagent', 'mailruagent')),
'http://agent.mail.ru/': ClientData(('Mailruagent', 'mailruagent')),
'http://tomclaw.com/mandarin_im/caps': ClientData(('Mandarin', 'mandarin')),
'http://mchat.mgslab.com/': ClientData(('Mchat', 'mchat')),
'https://www.meebo.com/': ClientData(('Meebo', 'meebo')),
'http://megafonvolga.ru/': ClientData(('Megafon', 'megafon')),
'http://miranda-im.org/caps': ClientData(('Miranda', 'miranda')),
'https://movim.eu/': ClientData(('Movim', 'movim')),
'http://moxl.movim.eu/': ClientData(('Movim', 'movim')),
'nimbuzz:caps': ClientData(('Nimbuzz', 'nimbuzz')),
'http://nimbuzz.com/caps': ClientData(('Nimbuzz', 'nimbuzz')),
'http://home.gna.org/': ClientData(('Omnipresence', 'omnipresence')),
'http://oneteam.im/caps': ClientData(('OneTeam', 'oneteamiphone')),
'http://www.process-one.net/en/solutions/oneteam_iphone/': ClientData(('OneTeam-IPhone', 'oneteamiphone')),
'rss@isida-bot.com': ClientData(('Osiris', 'osiris')),
'http://chat.ovi.com/caps': ClientData(('Ovi-chat', 'ovi-chat')),
'http://opensource.palm.com/packages.html': ClientData(('Palm', 'palm')),
'http://palringo.com/caps': ClientData(('Palringo', 'palringo')),
'http://pandion.im/': ClientData(('Pandion', 'pandion')),
'http://pigeon.vpro.ru/caps': ClientData(('Pigeon', 'pigeon')),
'psto@psto.net': ClientData(('Psto', 'psto')),
'http://qq-im.com/caps': ClientData(('QQ', 'qq')),
'http://qq.com/caps': ClientData(('QQ', 'qq')),
'http://2010.qip.ru/caps': ClientData(('Qip', 'qip')),
'http://qip.ru/caps': ClientData(('Qip', 'qip')),
'http://qip.ru/caps?QIP': ClientData(('Qip', 'qip')),
'http://pda.qip.ru/caps': ClientData(('Qip-PDA', 'qippda')),
'http://qutim.org': ClientData(('QutIM', 'qutim')),
'http://qutim.org/': ClientData(('QutIM', 'qutim')),
'http://apps.radio-t.com/caps': ClientData(('Radio-t', 'radio-t')),
'http://sim-im.org/caps': ClientData(('Sim', 'sim')),
'http://www.lonelycatgames.com/slick/caps': ClientData(('Slick', 'slick')),
'http://snapi-bot.googlecode.com/caps': ClientData(('Snapi-bot', 'bot')),
'http://www.igniterealtime.org/project/spark/caps': ClientData(('Spark', 'spark')),
'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')),
})
"http://jabber.pix-art.de": ClientData(("Pix-Art Messenger", "pixart")),
"http://blabber.im": ClientData(("blabber.im", "blabber")),
"http://monocles.de": ClientData(("monocles chat", "monocles-chat")),
"http://pidgin.im/": ClientData(("Pidgin", "pidgin")),
"https://poez.io": ClientData(("Poezio", "poezio")),
"https://yaxim.org/": ClientData(("yaxim", "yaxim")),
"https://yaxim.org/bruno/": ClientData(("Bruno", "bruno")),
"http://mcabber.com/caps": ClientData(("MCabber", "mcabber")),
"http://psi-plus.com": ClientData(("Psi+", "psiplus")),
"https://psi-plus.com": ClientData(("Psi+", "psiplus")),
"https://dino.im": ClientData(("Dino", "dino")),
"http://monal.im/": ClientData(("Monal", "monal")),
"http://slixmpp.com/ver/1.2.4": ClientData(("Bot", "bot")),
"http://slixmpp.com/ver/1.3.0": ClientData(("Bot", "bot")),
"https://www.xabber.com/": ClientData(("Xabber", "xabber")),
"http://www.profanity.im": ClientData(("Profanity", "profanity")),
"http://swift.im": ClientData(("Swift", "swift")),
"https://salut-a-toi.org": ClientData(("Salut à Toi", "sat")),
"https://conversejs.org": ClientData(("Converse", "conversejs")),
"http://bitlbee.org/xmpp/caps": ClientData(("BitlBee", "bitlbee")),
"http://tkabber.jabber.ru/": ClientData(("Tkabber", "tkabber")),
"http://miranda-ng.org/caps": ClientData(("Miranda NG", "miranda_ng")),
"http://www.adium.im/": ClientData(("Adium", "adium")),
"http://www.adiumx.com/caps": ClientData(("Adium", "adium")),
"http://www.adiumx.com": ClientData(("Adium", "adium")),
"http://aqq.eu/": ClientData(("Aqq", "aqq")),
"http://www.asterisk.org/xmpp/client/caps": ClientData(
("Asterisk", "asterisk")
),
"http://ayttm.souceforge.net/caps": ClientData(("Ayttm", "ayttm")),
"http://www.barobin.com/caps": ClientData(("Bayanicq", "bayanicq")),
"http://simpleapps.ru/caps#blacksmith": ClientData(("Blacksmith", "bot")),
"http://blacksmith-2.googlecode.com/svn/": ClientData(("Blacksmith-2", "bot")),
"http://coccinella.sourceforge.net/protocol/caps": ClientData(
("Coccinella", "coccinella")
),
"http://digsby.com/caps": ClientData(("Digsby", "digsby")),
"http://emacs-jabber.sourceforge.net": ClientData(
("Emacs Jabber Client", "emacs")
),
"http://emess.eqx.su/caps": ClientData(("Emess", "emess")),
"http://live.gnome.org/empathy/caps": ClientData(
("Empathy", "telepathy.freedesktop.org")
),
"http://eqo.com/": ClientData(("Eqo", "libpurple")),
"http://exodus.jabberstudio.org/caps": ClientData(("Exodus", "exodus")),
"http://fatal-bot.spb.ru/caps": ClientData(("Fatal-bot", "bot")),
"http://svn.posix.ru/fatal-bot/trunk": ClientData(("Fatal-bot", "bot")),
"http://isida.googlecode.com": ClientData(("Isida", "isida-bot")),
"http://isida-bot.com": ClientData(("Isida", "isida-bot")),
"http://jabga.ru": ClientData(("Fin jabber", "fin")),
"http://chat.freize.org/caps": ClientData(("Freize", "freize")),
"http://gabber.sourceforge.net": ClientData(("Gabber", "gabber")),
"http://glu.net/": ClientData(("Glu", "glu")),
"http://mail.google.com/xmpp/client/caps": ClientData(("GMail", "google.com")),
"http://www.android.com/gtalk/client/caps": ClientData(
("GTalk", "talk.google.com")
),
"talk.google.com": ClientData(("GTalk", "talk.google.com")),
"http://talkgadget.google.com/client/caps": ClientData(("GTalk", "google")),
"http://talk.google.com/xmpp/bot/caps": ClientData(("GTalk", "google")),
"http://aspro.users.ru/historian-bot/": ClientData(("Historian-bot", "bot")),
"http://www.apple.com/ichat/caps": ClientData(("IChat", "ichat")),
"http://instantbird.com/": ClientData(("Instantbird", "instantbird")),
"http://j-tmb.ru/caps": ClientData(("J-tmb", "bot")),
"http://jabbroid.akuz.de": ClientData(("Jabbroid", "android")),
"http://jabbroid.akuz.de/caps": ClientData(("Jabbroid", "android")),
"http://dev.jabbim.cz/jabbim/caps": ClientData(("Jabbim", "jabbim")),
"http://jabbrik.ru/caps": ClientData(("Jabbrik", "bot")),
"http://jabrvista.net.ru": ClientData(("Jabvista", "bot")),
"http://jajc.jrudevels.org/caps": ClientData(("JAJC", "jajc")),
"http://qabber.ru/jame-bot": ClientData(("Jame-bot", "bot")),
"https://www.jappix.com/": ClientData(("Jappix", "jappix")),
"http://japyt.googlecode.com": ClientData(("Japyt", "japyt")),
"http://jasmineicq.ru/caps": ClientData(("Jasmine", "jasmine")),
"http://jimm.net.ru/caps": ClientData(("Jimm", "jimm-aspro")),
"http://jitsi.org": ClientData(("Jitsi", "jitsi")),
"http://jtalk.ustyugov.net/caps": ClientData(("Jtalk", "jtalk")),
"http://pjc.googlecode.com/caps": ClientData(("Jubo", "jubo")),
"http://juick.com/caps": ClientData(("Juick", "juick")),
"http://kopete.kde.org/jabber/caps": ClientData(("Kopete", "kopete")),
"http://bluendo.com/protocol/caps": ClientData(("Lampiro", "lampiro")),
"http://lytgeygen.ru/caps": ClientData(("Lytgeygen", "bot")),
"http://agent.mail.ru/caps": ClientData(("Mailruagent", "mailruagent")),
"http://agent.mail.ru/": ClientData(("Mailruagent", "mailruagent")),
"http://tomclaw.com/mandarin_im/caps": ClientData(("Mandarin", "mandarin")),
"http://mchat.mgslab.com/": ClientData(("Mchat", "mchat")),
"https://www.meebo.com/": ClientData(("Meebo", "meebo")),
"http://megafonvolga.ru/": ClientData(("Megafon", "megafon")),
"http://miranda-im.org/caps": ClientData(("Miranda", "miranda")),
"https://movim.eu/": ClientData(("Movim", "movim")),
"http://moxl.movim.eu/": ClientData(("Movim", "movim")),
"nimbuzz:caps": ClientData(("Nimbuzz", "nimbuzz")),
"http://nimbuzz.com/caps": ClientData(("Nimbuzz", "nimbuzz")),
"http://home.gna.org/": ClientData(("Omnipresence", "omnipresence")),
"http://oneteam.im/caps": ClientData(("OneTeam", "oneteamiphone")),
"http://www.process-one.net/en/solutions/oneteam_iphone/": ClientData(
("OneTeam-IPhone", "oneteamiphone")
),
"rss@isida-bot.com": ClientData(("Osiris", "osiris")),
"http://chat.ovi.com/caps": ClientData(("Ovi-chat", "ovi-chat")),
"http://opensource.palm.com/packages.html": ClientData(("Palm", "palm")),
"http://palringo.com/caps": ClientData(("Palringo", "palringo")),
"http://pandion.im/": ClientData(("Pandion", "pandion")),
"http://pigeon.vpro.ru/caps": ClientData(("Pigeon", "pigeon")),
"psto@psto.net": ClientData(("Psto", "psto")),
"http://qq-im.com/caps": ClientData(("QQ", "qq")),
"http://qq.com/caps": ClientData(("QQ", "qq")),
"http://2010.qip.ru/caps": ClientData(("Qip", "qip")),
"http://qip.ru/caps": ClientData(("Qip", "qip")),
"http://qip.ru/caps?QIP": ClientData(("Qip", "qip")),
"http://pda.qip.ru/caps": ClientData(("Qip-PDA", "qippda")),
"http://qutim.org": ClientData(("QutIM", "qutim")),
"http://qutim.org/": ClientData(("QutIM", "qutim")),
"http://apps.radio-t.com/caps": ClientData(("Radio-t", "radio-t")),
"http://sim-im.org/caps": ClientData(("Sim", "sim")),
"http://www.lonelycatgames.com/slick/caps": ClientData(("Slick", "slick")),
"http://snapi-bot.googlecode.com/caps": ClientData(("Snapi-bot", "bot")),
"http://www.igniterealtime.org/project/spark/caps": ClientData(
("Spark", "spark")
),
"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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,15 +16,14 @@
import logging
from nbxmpp.protocol import JID
import gpg
from gpg.results import ImportResult
from nbxmpp.protocol import JID
from openpgp.backend.util import parse_uid
from openpgp.modules.util import DecryptionFailed
log = logging.getLogger('gajim.p.openpgp.gpgme')
log = logging.getLogger("gajim.p.openpgp.gpgme")
class KeyringItem:
@@ -73,31 +72,33 @@ class GPGME:
def __init__(self, jid, gnuhome):
self._jid = jid
self._context_args = {
'home_dir': str(gnuhome),
'offline': True,
'armor': False,
"home_dir": str(gnuhome),
"offline": True,
"armor": False,
}
def generate_key(self):
with gpg.Context(**self._context_args) as context:
result = context.create_key(f'xmpp:{str(self._jid)}',
algorithm='default',
result = context.create_key(
f"xmpp:{str(self._jid)}",
algorithm="default",
expires=False,
passphrase=None,
force=False)
force=False,
)
log.info('Generated new key: %s', result.fpr)
log.info("Generated new key: %s", result.fpr)
def get_key(self, fingerprint):
with gpg.Context(**self._context_args) as context:
try:
key = context.get_key(fingerprint)
except gpg.errors.KeyNotFound as error:
log.warning('key not found: %s', error.keystr)
log.warning("key not found: %s", error.keystr)
return
except Exception as error:
log.warning('get_key() error: %s', error)
log.warning("get_key() error: %s", error)
return
return key
@@ -121,7 +122,7 @@ class GPGME:
for key in context.keylist():
keyring_item = KeyringItem(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)
continue
@@ -157,12 +158,12 @@ class GPGME:
recipients.append(key)
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:
result = context.encrypt(str(plaintext).encode(),
recipients,
always_trust=True)
result = context.encrypt(
str(plaintext).encode(), recipients, always_trust=True
)
ciphertext, result, _sign_result = result
return ciphertext, None
@@ -172,7 +173,7 @@ class GPGME:
try:
result = context.decrypt(ciphertext)
except Exception as error:
raise DecryptionFailed('Decryption failed: %s' % error)
raise DecryptionFailed("Decryption failed: %s" % error)
plaintext, result, verify_result = result
plaintext = plaintext.decode()
@@ -181,16 +182,16 @@ class GPGME:
if not fingerprints or len(fingerprints) > 1:
log.error(result)
log.error(verify_result)
raise DecryptionFailed('Verification failed')
raise DecryptionFailed("Verification failed")
return plaintext, fingerprints[0]
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:
result = context.key_import(data)
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)
return
@@ -198,7 +199,7 @@ class GPGME:
key = self.get_key(fingerprint)
item = KeyringItem(key)
if not item.is_valid(jid):
log.warning('Invalid key found')
log.warning("Invalid key found")
log.warning(key)
self.delete_key(item.fingerprint)
return
@@ -206,7 +207,7 @@ class GPGME:
return item
def delete_key(self, fingerprint):
log.info('Delete Key: %s', fingerprint)
log.info("Delete Key: %s", fingerprint)
key = self.get_key(fingerprint)
with gpg.Context(**self._context_args) as context:
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.modules.util import DecryptionFailed
log = logging.getLogger('gajim.p.openpgp.pygnupg')
log = logging.getLogger("gajim.p.openpgp.pygnupg")
if log.getEffectiveLevel() == logging.DEBUG:
log = logging.getLogger('gnupg')
log = logging.getLogger("gnupg")
log.addHandler(logging.StreamHandler())
log.setLevel(logging.DEBUG)
@@ -50,10 +49,10 @@ class KeyringItem:
@property
def keyid(self) -> str:
return self._key['keyid']
return self._key["keyid"]
def _get_uid(self) -> str | None:
for uid in self._key['uids']:
for uid in self._key["uids"]:
try:
return parse_uid(uid)
except Exception:
@@ -61,7 +60,7 @@ class KeyringItem:
@property
def fingerprint(self):
return self._key['fingerprint']
return self._key["fingerprint"]
@property
def uid(self):
@@ -79,28 +78,28 @@ class KeyringItem:
class PythonGnuPG(gnupg.GPG):
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._own_fingerprint = None
@staticmethod
def _get_key_params(jid):
'''
"""
Generate --gen-key input
'''
"""
params = {
'Key-Type': 'RSA',
'Key-Length': 2048,
'Name-Real': 'xmpp:%s' % jid,
"Key-Type": "RSA",
"Key-Length": 2048,
"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()):
out += '%s: %s\n' % (key, val)
out += '%no-protection\n'
out += '%commit\n'
out += "%s: %s\n" % (key, val)
out += "%no-protection\n"
out += "%commit\n"
return out
def generate_key(self):
@@ -108,18 +107,20 @@ class PythonGnuPG(gnupg.GPG):
def encrypt(self, payload, keys):
recipients = [key.fingerprint for key in keys]
log.info('encrypt to:')
log.info("encrypt to:")
for fingerprint in recipients:
log.info(fingerprint)
result = super().encrypt(str(payload).encode('utf8'),
result = super().encrypt(
str(payload).encode("utf8"),
recipients,
armor=False,
sign=self._own_fingerprint,
always_trust=True)
always_trust=True,
)
if result.ok:
error = ''
error = ""
else:
error = result.status
@@ -130,7 +131,7 @@ class PythonGnuPG(gnupg.GPG):
if not result.ok:
raise DecryptionFailed(result.status)
return result.data.decode('utf8'), result.fingerprint
return result.data.decode("utf8"), result.fingerprint
def get_key(self, fingerprint):
return super().list_keys(keys=[fingerprint])
@@ -141,7 +142,7 @@ class PythonGnuPG(gnupg.GPG):
for key in result:
item = KeyringItem(key)
if not item.is_xmpp_key:
log.warning('Invalid key found, deleting key')
log.warning("Invalid key found, deleting key")
log.warning(key)
self.delete_key(item.fingerprint)
continue
@@ -149,17 +150,17 @@ class PythonGnuPG(gnupg.GPG):
return keys
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)
if not result:
log.error('Could not import key')
log.error("Could not import key")
log.error(result)
return
key = self.get_key(result.results[0]['fingerprint'])
key = self.get_key(result.results[0]["fingerprint"])
item = KeyringItem(key[0])
if not item.is_valid(jid):
log.warning('Invalid key found, deleting key')
log.warning("Invalid key found, deleting key")
log.warning(key)
self.delete_key(item.fingerprint)
return
@@ -172,17 +173,16 @@ class PythonGnuPG(gnupg.GPG):
return None, None
if len(result) > 1:
log.error('More than one secret key found')
log.error("More than one secret key found")
return None, None
self._own_fingerprint = result[0]['fingerprint']
return self._own_fingerprint, int(result[0]['date'])
self._own_fingerprint = result[0]["fingerprint"]
return self._own_fingerprint, int(result[0]["date"])
def export_key(self, fingerprint):
key = super().export_keys(
fingerprint, secret=False, armor=False, minimal=True)
key = super().export_keys(fingerprint, secret=False, armor=False, minimal=True)
return key
def delete_key(self, fingerprint):
log.info('Delete Key: %s', fingerprint)
log.info("Delete Key: %s", fingerprint)
super().delete_keys(fingerprint)

View File

@@ -14,13 +14,13 @@
# 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/>.
import sqlite3
import logging
import sqlite3
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 (
jid TEXT,
fingerprint TEXT,
@@ -29,13 +29,14 @@ TABLE_LAYOUT = '''
timestamp INTEGER,
comment TEXT
);
CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);'''
CREATE UNIQUE INDEX jid_fingerprint ON contacts (jid, fingerprint);"""
class Storage:
def __init__(self, folder_path):
self._con = sqlite3.connect(str(folder_path / 'contacts.db'),
detect_types=sqlite3.PARSE_COLNAMES)
self._con = sqlite3.connect(
str(folder_path / "contacts.db"), detect_types=sqlite3.PARSE_COLNAMES
)
self._con.row_factory = self._namedtuple_factory
self._create_database()
@@ -51,11 +52,11 @@ class Storage:
return named_row
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):
if not self._user_version():
log.info('Create contacts.db')
log.info("Create contacts.db")
self._execute_query(TABLE_LAYOUT)
def _execute_query(self, query):
@@ -64,41 +65,43 @@ class Storage:
%s
PRAGMA user_version=1;
END TRANSACTION;
""" % (query)
""" % (
query
)
self._con.executescript(transaction)
def _migrate_database(self):
pass
def load_contacts(self):
sql = '''SELECT jid as "jid [jid]",
sql = """SELECT jid as "jid [jid]",
fingerprint,
active,
trust,
timestamp,
comment
FROM contacts'''
FROM contacts"""
return self._con.execute(sql).fetchall()
def save_contact(self, db_values):
sql = '''REPLACE INTO
sql = """REPLACE INTO
contacts(jid, fingerprint, active, trust, timestamp, comment)
VALUES(?, ?, ?, ?, ?, ?)'''
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.commit()
def set_trust(self, jid, fingerprint, trust):
sql = 'UPDATE contacts SET trust = ? WHERE jid = ? AND fingerprint = ?'
log.info('Set Trust: %s %s %s', trust, jid, fingerprint)
sql = "UPDATE contacts SET trust = ? WHERE jid = ? AND fingerprint = ?"
log.info("Set Trust: %s %s %s", trust, jid, fingerprint)
self._con.execute(sql, (trust, jid, fingerprint))
self._con.commit()
def delete_key(self, jid, fingerprint):
sql = 'DELETE from contacts WHERE jid = ? AND fingerprint = ?'
log.info('Delete Key: %s %s', jid, fingerprint)
sql = "DELETE from contacts WHERE jid = ? AND fingerprint = ?"
log.info("Delete Key: %s %s", jid, fingerprint)
self._con.execute(sql, (jid, fingerprint))
self._con.commit()

View File

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

View File

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

View File

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

View File

@@ -14,24 +14,24 @@
# 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/>.
import logging
import sys
import time
import logging
from pathlib import Path
from nbxmpp.namespaces import Namespace
from nbxmpp import Node
from nbxmpp import StanzaMalformed
from nbxmpp.errors import MalformedStanzaError
from nbxmpp.errors import StanzaError
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 MessageProperties
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 configpaths
@@ -40,22 +40,22 @@ from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import event_node
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.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
else:
from openpgp.backend.gpgme import GPGME as PGPBackend
log = logging.getLogger('gajim.p.openpgp')
log = logging.getLogger("gajim.p.openpgp")
# Module name
@@ -65,24 +65,26 @@ zeroconf = False
class OpenPGP(BaseModule):
_nbxmpp_extends = 'OpenPGP'
_nbxmpp_extends = "OpenPGP"
_nbxmpp_methods = [
'set_keylist',
'request_keylist',
'set_public_key',
'request_public_key',
'set_secret_key',
'request_secret_key',
"set_keylist",
"request_keylist",
"set_public_key",
"request_public_key",
"set_secret_key",
"request_secret_key",
]
def __init__(self, client):
BaseModule.__init__(self, client)
self.handlers = [
StanzaHandler(name='message',
StanzaHandler(
name="message",
callback=self.decrypt_message,
ns=Namespace.OPENPGP,
priority=9),
priority=9,
),
]
self._register_pubsub_handler(self._keylist_notification_received)
@@ -90,7 +92,7 @@ class OpenPGP(BaseModule):
self.own_jid = self._client.get_own_jid()
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():
path.mkdir(mode=0o700, parents=True)
@@ -98,7 +100,7 @@ class OpenPGP(BaseModule):
self._storage = Storage(path)
self._contacts = PGPContacts(self._pgp, self._storage)
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
def secret_key_available(self):
@@ -112,27 +114,22 @@ class OpenPGP(BaseModule):
self._pgp.generate_key()
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)
self._nbxmpp('OpenPGP').set_public_key(
key, self._fingerprint, self._date)
self._nbxmpp("OpenPGP").set_public_key(key, self._fingerprint, self._date)
def request_public_key(self, jid, fingerprint):
log.info('%s => Request public key %s - %s',
self._account, fingerprint, jid)
self._nbxmpp('OpenPGP').request_public_key(
jid,
fingerprint,
callback=self._public_key_received,
user_data=fingerprint)
log.info("%s => Request public key %s - %s", self._account, fingerprint, jid)
self._nbxmpp("OpenPGP").request_public_key(
jid, fingerprint, callback=self._public_key_received, user_data=fingerprint
)
def _public_key_received(self, task):
fingerprint = task.get_user_data()
try:
result = task.finish()
except (StanzaError, MalformedStanzaError) as error:
log.error('%s => Public Key not found: %s',
self._account, error)
log.error("%s => Public Key not found: %s", self._account, error)
return
imported_key = self._pgp.import_key(result.key, result.jid)
@@ -142,8 +139,8 @@ class OpenPGP(BaseModule):
def set_keylist(self, keylist=None):
if keylist is None:
keylist = [PGPKeyMetadata(None, self._fingerprint, self._date)]
log.info('%s => Publish keylist', self._account)
self._nbxmpp('OpenPGP').set_keylist(keylist)
log.info("%s => Publish keylist", self._account)
self._nbxmpp("OpenPGP").set_keylist(keylist)
@event_node(Namespace.OPENPGP_PK)
def _keylist_notification_received(self, _con, _stanza, properties):
@@ -157,46 +154,43 @@ class OpenPGP(BaseModule):
def request_keylist(self, jid=None):
if jid is None:
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(
jid,
callback=self._keylist_received,
user_data=jid)
self._nbxmpp("OpenPGP").request_keylist(
jid, callback=self._keylist_received, user_data=jid
)
def _keylist_received(self, task):
jid = task.get_user_data()
try:
keylist = task.finish()
except (StanzaError, MalformedStanzaError) as error:
log.error('%s => Keylist query failed: %s',
self._account, error)
log.error("%s => Keylist query failed: %s", self._account, error)
if self.own_jid.bare_match(jid) and self._fingerprint is not None:
self.set_keylist()
return
log.info('Keylist received from %s', jid)
log.info("Keylist received from %s", jid)
self._process_keylist(keylist, jid)
def _process_keylist(self, keylist, from_jid):
if not keylist:
log.warning('%s => Empty keylist received from %s',
self._account, from_jid)
log.warning("%s => Empty keylist received from %s", self._account, from_jid)
self._contacts.process_keylist(self.own_jid, keylist)
if self.own_jid.bare_match(from_jid) and self._fingerprint is not None:
self.set_keylist()
return
if self.own_jid.bare_match(from_jid):
log.info('Received own keylist')
log.info("Received own keylist")
for key in keylist:
log.info(key.fingerprint)
for key in keylist:
# Check if own fingerprint is published
if key.fingerprint == self._fingerprint:
log.info('Own key found in keys list')
log.info("Own key found in keys list")
return
log.info('Own key not published')
log.info("Own key not published")
if self._fingerprint is not None:
keylist.append(Key(self._fingerprint, self._date))
self.set_keylist(keylist)
@@ -228,31 +222,29 @@ class OpenPGP(BaseModule):
try:
payload, recipients, _timestamp = parse_signcrypt(signcrypt)
except StanzaMalformed as error:
log.warning('Decryption failed: %s', error)
log.warning("Decryption failed: %s", error)
log.warning(payload)
return
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)
return
keys = self._contacts.get_keys(remote_jid)
fingerprints = [key.fingerprint for key in keys]
if fingerprint not in fingerprints:
log.warning('Invalid fingerprint on message: %s', fingerprint)
log.warning('Expected: %s', fingerprints)
log.warning("Invalid fingerprint on message: %s", fingerprint)
log.warning("Expected: %s", fingerprints)
return
log.info('Received OpenPGP message from: %s', properties.jid)
log.info("Received OpenPGP message from: %s", properties.jid)
prepare_stanza(stanza, payload)
trust = self._contacts.get_trust(remote_jid, fingerprint)
properties.encrypted = EncryptionData(
protocol=ENCRYPTION_NAME,
key=fingerprint,
trust=trust
protocol=ENCRYPTION_NAME, key=fingerprint, trust=trust
)
raise StanzaDecrypted
@@ -262,39 +254,38 @@ class OpenPGP(BaseModule):
keys = self._contacts.get_keys(remote_jid)
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
keys += self._contacts.get_keys(self.own_jid)
keys += [Key(self._fingerprint, None)]
payload = create_signcrypt_node(message.get_stanza(),
[remote_jid],
NOT_ENCRYPTED_TAGS)
payload = create_signcrypt_node(
message.get_stanza(), [remote_jid], NOT_ENCRYPTED_TAGS
)
encrypted_payload, error = self._pgp.encrypt(payload, keys)
if error:
log.error('Error: %s', error)
text = message.get_text(with_fallback=False) or ''
log.error("Error: %s", error)
text = message.get_text(with_fallback=False) or ""
app.ged.raise_event(
MessageNotSent(client=self._client,
MessageNotSent(
client=self._client,
jid=str(remote_jid),
message=text,
error=error,
time=time.time()))
time=time.time(),
)
)
return
create_message_stanza(
message.get_stanza(),
encrypted_payload,
bool(message.get_text())
message.get_stanza(), encrypted_payload, bool(message.get_text())
)
message.set_encryption(
EncryptionData(
protocol=ENCRYPTION_NAME,
key='Unknown',
trust=Trust.VERIFIED
protocol=ENCRYPTION_NAME, key="Unknown", trust=Trust.VERIFIED
)
)
@@ -302,12 +293,12 @@ class OpenPGP(BaseModule):
@staticmethod
def print_msg_to_log(stanza):
""" Prints a stanza in a fancy way to the log """
log.debug('-'*15)
stanzastr = '\n' + stanza.__str__(fancy=True)
"""Prints a stanza in a fancy way to the log"""
log.debug("-" * 15)
stanzastr = "\n" + stanza.__str__(fancy=True)
stanzastr = stanzastr[0:-1]
log.debug(stanzastr)
log.debug('-'*15)
log.debug("-" * 15)
def get_keys(self, jid=None, only_trusted=True):
if jid is None:
@@ -324,4 +315,4 @@ class OpenPGP(BaseModule):
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
# along with OpenPGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
from enum import IntEnum
from collections import namedtuple
from enum import IntEnum
from nbxmpp.namespaces import Namespace
ENCRYPTION_NAME = 'OpenPGP'
ENCRYPTION_NAME = "OpenPGP"
NOT_ENCRYPTED_TAGS = [
('no-store', Namespace.HINTS),
('store', Namespace.HINTS),
('no-copy', Namespace.HINTS),
('no-permanent-store', Namespace.HINTS),
('origin-id', Namespace.SID),
('thread', None)
("no-store", Namespace.HINTS),
("store", Namespace.HINTS),
("no-copy", Namespace.HINTS),
("no-permanent-store", Namespace.HINTS),
("origin-id", Namespace.SID),
("thread", None),
]
Key = namedtuple('Key', 'fingerprint date')
Key = namedtuple("Key", "fingerprint date")
class Trust(IntEnum):
@@ -42,8 +41,8 @@ class Trust(IntEnum):
def prepare_stanza(stanza, payload):
delete_nodes(stanza, 'openpgp', Namespace.OPENPGP)
delete_nodes(stanza, 'body')
delete_nodes(stanza, "openpgp", Namespace.OPENPGP)
delete_nodes(stanza, "body")
nodes = [(node.getName(), node.getNamespace()) for node in payload]
for name, namespace in nodes:
@@ -56,7 +55,7 @@ def prepare_stanza(stanza, payload):
def delete_nodes(stanza, name, namespace=None):
attrs = None
if namespace is not None:
attrs = {'xmlns': Namespace.OPENPGP}
attrs = {"xmlns": Namespace.OPENPGP}
nodes = stanza.getTags(name, attrs)
for node in nodes:
stanza.delChild(node)

View File

@@ -17,22 +17,21 @@
import logging
from pathlib import Path
from gi.repository import Gtk
from gi.repository import Gdk
from nbxmpp.namespaces import Namespace
from gi.repository import Gtk
from nbxmpp import JID
from nbxmpp.namespaces import Namespace
from gajim.common import app
from gajim.common import ged
from gajim.common import configpaths
from gajim.common import ged
from gajim.common.const import CSSPriority
from gajim.gtk.dialogs import SimpleDialog
from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _
from openpgp.modules.util import ENCRYPTION_NAME
try:
from openpgp.modules import openpgp
except (ImportError, OSError) as e:
@@ -40,7 +39,7 @@ except (ImportError, OSError) as e:
else:
ERROR_MSG = None
log = logging.getLogger('gajim.p.openpgp')
log = logging.getLogger("gajim.p.openpgp")
class OpenPGPPlugin(GajimPlugin):
@@ -52,7 +51,7 @@ class OpenPGPPlugin(GajimPlugin):
return
self.events_handlers = {
'signed-in': (ged.PRECORE, self.signed_in),
"signed-in": (ged.PRECORE, self.signed_in),
}
self.modules = [openpgp]
@@ -60,14 +59,12 @@ class OpenPGPPlugin(GajimPlugin):
self.encryption_name = ENCRYPTION_NAME
self.config_dialog = None
self.gui_extension_points = {
'encrypt' + self.encryption_name: (self._encrypt_message, None),
'send_message' + self.encryption_name: (
self._before_sendmessage, None),
'encryption_dialog' + self.encryption_name: (
self.on_encryption_button_clicked, None),
'encryption_state' + self.encryption_name: (
self.encryption_state, None),
'update_caps': (self._update_caps, None),
"encrypt" + self.encryption_name: (self._encrypt_message, None),
"send_message" + self.encryption_name: (self._before_sendmessage, None),
"encryption_dialog"
+ self.encryption_name: (self.on_encryption_button_clicked, None),
"encryption_state" + self.encryption_name: (self.encryption_state, None),
"update_caps": (self._update_caps, None),
}
self.connections = {}
@@ -80,74 +77,78 @@ class OpenPGPPlugin(GajimPlugin):
self._load_css()
def _load_css(self):
path = Path(__file__).parent / 'gtk' / 'style.css'
path = Path(__file__).parent / "gtk" / "style.css"
try:
with path.open('r') as f:
with path.open("r") as f:
css = f.read()
except Exception as exc:
log.error('Error loading css: %s', exc)
log.error("Error loading css: %s", exc)
return
try:
provider = Gtk.CssProvider()
provider.load_from_data(bytes(css.encode('utf-8')))
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
provider,
CSSPriority.DEFAULT_THEME)
provider.load_from_data(bytes(css.encode("utf-8")))
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(), provider, CSSPriority.DEFAULT_THEME
)
except Exception:
log.exception('Error loading application css')
log.exception("Error loading application css")
@staticmethod
def _create_paths():
keyring_path = Path(configpaths.get('MY_DATA')) / 'openpgp'
keyring_path = Path(configpaths.get("MY_DATA")) / "openpgp"
if not keyring_path.exists():
keyring_path.mkdir()
def signed_in(self, event):
client = app.get_client(event.account)
if client.get_module('OpenPGP').secret_key_available:
log.info('%s => Publish keylist and public key after sign in',
event.account)
client.get_module('OpenPGP').request_keylist()
client.get_module('OpenPGP').set_public_key()
if client.get_module("OpenPGP").secret_key_available:
log.info(
"%s => Publish keylist and public key after sign in", event.account
)
client.get_module("OpenPGP").request_keylist()
client.get_module("OpenPGP").set_public_key()
def activate(self):
for account in app.settings.get_active_accounts():
client = app.get_client(account)
client.get_module('Caps').update_caps()
client.get_module("Caps").update_caps()
if app.account_is_connected(account):
if client.get_module('OpenPGP').secret_key_available:
log.info('%s => Publish keylist and public key '
'after plugin activation', account)
client.get_module('OpenPGP').request_keylist()
client.get_module('OpenPGP').set_public_key()
if client.get_module("OpenPGP").secret_key_available:
log.info(
"%s => Publish keylist and public key "
"after plugin activation",
account,
)
client.get_module("OpenPGP").request_keylist()
client.get_module("OpenPGP").set_public_key()
def deactivate(self):
pass
@staticmethod
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):
account = chat_control.account
jid = chat_control.contact.jid
client = app.get_client(account)
if client.get_module('OpenPGP').secret_key_available:
keys = client.get_module('OpenPGP').get_keys(
jid, only_trusted=False)
if client.get_module("OpenPGP").secret_key_available:
keys = client.get_module("OpenPGP").get_keys(jid, only_trusted=False)
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
from openpgp.gtk.wizard import KeyWizard
KeyWizard(self, account, chat_control)
return False
@staticmethod
def encryption_state(_chat_control, state):
state['authenticated'] = True
state['visible'] = True
state["authenticated"] = True
state["visible"] = True
@staticmethod
def on_encryption_button_clicked(chat_control):
@@ -155,6 +156,7 @@ class OpenPGPPlugin(GajimPlugin):
jid = chat_control.contact.jid
from openpgp.gtk.key import KeyDialog
KeyDialog(account, jid, app.window)
def _before_sendmessage(self, chat_control):
@@ -162,20 +164,21 @@ class OpenPGPPlugin(GajimPlugin):
jid = chat_control.contact.jid
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
KeyWizard(self, account, chat_control)
return
keys = client.get_module('OpenPGP').get_keys(jid)
keys = client.get_module("OpenPGP").get_keys(jid)
if not keys:
SimpleDialog(
_('Not Trusted'),
_('There was no trusted and active key found'))
_("Not Trusted"), _("There was no trusted and active key found")
)
chat_control.sendmessage = False
@staticmethod
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
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
# along with PGP Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
import os
import logging
import os
from functools import lru_cache
import gnupg
@@ -30,56 +30,51 @@ from gajim.common.util.classes import Singleton
from pgp.exceptions import SignError
logger = logging.getLogger('gajim.p.pgplegacy')
logger = logging.getLogger("gajim.p.pgplegacy")
if logger.getEffectiveLevel() == logging.DEBUG:
logger = logging.getLogger('gnupg')
logger = logging.getLogger("gnupg")
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
class PGP(gnupg.GPG, metaclass=Singleton):
def __init__(self, binary, encoding=None):
super().__init__(gpgbinary=binary,
use_agent=True)
super().__init__(gpgbinary=binary, use_agent=True)
if encoding is not None:
self.encoding = encoding
self.decode_errors = 'replace'
self.decode_errors = "replace"
def encrypt(self, payload, recipients, always_trust=False):
if not always_trust:
# check that we'll be able to encrypt
result = self.get_key(recipients[0])
for key in result:
if key['trust'] not in ('f', 'u'):
return '', 'NOT_TRUSTED ' + key['keyid'][-8:]
if key["trust"] not in ("f", "u"):
return "", "NOT_TRUSTED " + key["keyid"][-8:]
result = super().encrypt(
payload.encode('utf8'),
recipients,
always_trust=always_trust)
payload.encode("utf8"), recipients, always_trust=always_trust
)
if result.ok:
error = ''
error = ""
else:
error = result.status
return self._strip_header_footer(str(result)), error
def decrypt(self, payload):
data = self._add_header_footer(payload, 'MESSAGE')
result = super().decrypt(data.encode('utf8'))
data = self._add_header_footer(payload, "MESSAGE")
result = super().decrypt(data.encode("utf8"))
return result.data.decode('utf8')
return result.data.decode("utf8")
@lru_cache(maxsize=8)
def sign(self, payload, key_id):
if payload is None:
payload = ''
result = super().sign(payload.encode('utf8'),
keyid=key_id,
detach=True)
payload = ""
result = super().sign(payload.encode("utf8"), keyid=key_id, detach=True)
if result.fingerprint:
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
if payload is None:
payload = ''
payload = ""
hash_algorithms = ['SHA512', 'SHA384', 'SHA256',
'SHA224', 'SHA1', 'RIPEMD160']
hash_algorithms = ["SHA512", "SHA384", "SHA256", "SHA224", "SHA1", "RIPEMD160"]
for algo in hash_algorithms:
data = os.linesep.join(
['-----BEGIN PGP SIGNED MESSAGE-----',
'Hash: ' + algo,
'',
[
"-----BEGIN PGP SIGNED MESSAGE-----",
"Hash: " + algo,
"",
payload,
self._add_header_footer(signed, 'SIGNATURE')]
self._add_header_footer(signed, "SIGNATURE"),
]
)
result = super().verify(data.encode('utf8'))
result = super().verify(data.encode("utf8"))
if result.valid:
return result.fingerprint
@@ -116,7 +112,7 @@ class PGP(gnupg.GPG, metaclass=Singleton):
for key in result:
# 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
@staticmethod
@@ -125,19 +121,19 @@ class PGP(gnupg.GPG, metaclass=Singleton):
Remove header and footer from data
"""
if not data:
return ''
return ""
lines = data.splitlines()
while lines[0] != '':
while lines[0] != "":
lines.remove(lines[0])
while lines[0] == '':
while lines[0] == "":
lines.remove(lines[0])
i = 0
for line in lines:
if line:
if line[0] == '-':
if line[0] == "-":
break
i = i+1
line = '\n'.join(lines[0:i])
i = i + 1
line = "\n".join(lines[0:i])
return line
@staticmethod

View File

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

View File

@@ -16,12 +16,11 @@
from pathlib import Path
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import Gtk
from gajim.common import app
from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
@@ -33,14 +32,14 @@ class PGPConfigDialog(Gtk.ApplicationWindow):
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_show_menubar(False)
self.set_title(_('PGP Configuration'))
self.set_title(_("PGP Configuration"))
self.set_transient_for(parent)
self.set_resizable(True)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.set_destroy_with_parent(True)
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)
@@ -50,9 +49,7 @@ class PGPConfigDialog(Gtk.ApplicationWindow):
for account in app.settings.get_active_accounts():
page = Page(plugin, account)
self._ui.stack.add_titled(page,
account,
app.get_account_label(account))
self._ui.stack.add_titled(page, account, app.get_account_label(account))
self.show_all()
@@ -64,11 +61,11 @@ class Page(Gtk.Box):
self._client = app.get_client(account)
self._plugin = plugin
self._label = Gtk.Label()
self._button = Gtk.Button(label=_('Assign Key'))
self._button.get_style_context().add_class('suggested-action')
self._button = Gtk.Button(label=_("Assign Key"))
self._button.get_style_context().add_class("suggested-action")
self._button.set_halign(Gtk.Align.CENTER)
self._button.set_margin_top(18)
self._button.connect('clicked', self._on_assign)
self._button.connect("clicked", self._on_assign)
self._load_key()
self.add(self._label)
@@ -76,34 +73,34 @@ class Page(Gtk.Box):
self.show_all()
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)
dialog = ChooseGPGKeyDialog(secret_keys, self.get_toplevel())
dialog.connect('response', self._on_response)
dialog.connect("response", self._on_response)
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:
self._set_key(None)
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):
if response != Gtk.ResponseType.OK:
return
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)
else:
self._client.get_module('PGPLegacy').set_own_key_data(
dialog.selected_key)
self._client.get_module("PGPLegacy").set_own_key_data(dialog.selected_key)
self._set_key(dialog.selected_key)
def _set_key(self, key_data):
if key_data is None:
self._label.set_text(_('No key assigned'))
self._label.set_text(_("No key assigned"))
else:
key_id, key_user = key_data
self._label.set_markup('<b><tt>%s</tt> %s</b>' % \
(key_id, GLib.markup_escape_text(key_user)))
self._label.set_markup(
"<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 gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gtk
from gajim.common import app
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
class KeyDialog(Gtk.Dialog):
def __init__(self, plugin, account, jid, transient):
super().__init__(title=_('Assign key for %s') % jid,
destroy_with_parent=True)
super().__init__(title=_("Assign key for %s") % jid, destroy_with_parent=True)
self.set_transient_for(transient)
self.set_resizable(True)
@@ -39,11 +38,11 @@ class KeyDialog(Gtk.Dialog):
self._label = Gtk.Label()
self._assign_button = Gtk.Button(label=_('Assign Key'))
self._assign_button.get_style_context().add_class('suggested-action')
self._assign_button = Gtk.Button(label=_("Assign Key"))
self._assign_button.get_style_context().add_class("suggested-action")
self._assign_button.set_halign(Gtk.Align.CENTER)
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.set_border_width(18)
@@ -57,13 +56,12 @@ class KeyDialog(Gtk.Dialog):
self.show_all()
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.connect('response', self._on_response)
dialog.connect("response", self._on_response)
def _load_key(self):
key_data = self._client.get_module('PGPLegacy').get_contact_key_data(
self._jid)
key_data = self._client.get_module("PGPLegacy").get_contact_key_data(self._jid)
if key_data is None:
self._set_key(None)
else:
@@ -74,42 +72,43 @@ class KeyDialog(Gtk.Dialog):
return
if dialog.selected_key is None:
self._client.get_module('PGPLegacy').set_contact_key_data(
self._jid, None)
self._client.get_module("PGPLegacy").set_contact_key_data(self._jid, None)
self._set_key(None)
else:
self._client.get_module('PGPLegacy').set_contact_key_data(
self._jid, dialog.selected_key)
self._client.get_module("PGPLegacy").set_contact_key_data(
self._jid, dialog.selected_key
)
self._set_key(dialog.selected_key)
def _set_key(self, key_data):
if key_data is None:
self._label.set_text(_('No key assigned'))
self._label.set_text(_("No key assigned"))
else:
key_id, key_user = key_data
self._label.set_markup('<b><tt>%s</tt> %s</b>' % \
(key_id, GLib.markup_escape_text(key_user)))
self._label.set_markup(
"<b><tt>%s</tt> %s</b>" % (key_id, GLib.markup_escape_text(key_user))
)
class ChooseGPGKeyDialog(Gtk.Dialog):
def __init__(self, secret_keys, transient_for):
Gtk.Dialog.__init__(self,
title=_('Assign PGP Key'),
transient_for=transient_for)
Gtk.Dialog.__init__(
self, title=_("Assign PGP Key"), transient_for=transient_for
)
secret_keys[_('None')] = _('None')
secret_keys[_("None")] = _("None")
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
self.set_resizable(True)
self.set_default_size(500, 300)
self.add_button(_('Cancel'), Gtk.ResponseType.CANCEL)
self.add_button(_('OK'), Gtk.ResponseType.OK)
self.add_button(_("Cancel"), Gtk.ResponseType.CANCEL)
self.add_button(_("OK"), Gtk.ResponseType.OK)
self._selected_key = None
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
@@ -124,7 +123,7 @@ class ChooseGPGKeyDialog(Gtk.Dialog):
self._ui.connect_signals(self)
self.connect_after('response', self._on_response)
self.connect_after("response", self._on_response)
self.show_all()
@@ -136,9 +135,9 @@ class ChooseGPGKeyDialog(Gtk.Dialog):
def _sort(model, iter1, iter2, _data):
value1 = model[iter1][1]
value2 = model[iter2][1]
if value1 == _('None'):
if value1 == _("None"):
return -1
if value2 == _('None'):
if value2 == _("None"):
return 1
if value1 < value2:
return -1
@@ -154,7 +153,7 @@ class ChooseGPGKeyDialog(Gtk.Dialog):
self._selected_key = None
else:
key_id, key_user = model[iter_][0], model[iter_][1]
if key_id == _('None'):
if key_id == _("None"):
self._selected_key = None
else:
self._selected_key = key_id, key_user

View File

@@ -14,7 +14,8 @@
from __future__ import annotations
from typing import Any, Callable
from typing import Any
from typing import Callable
from dataclasses import dataclass
from dataclasses import field
@@ -24,12 +25,12 @@ from gajim.common.events import ApplicationEvent
@dataclass
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_no: Callable[..., Any]
@dataclass
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

View File

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

View File

@@ -14,29 +14,29 @@
# 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/>.
import logging
import os
import sys
import logging
from functools import partial
from packaging.version import Version as V
from gajim.common import app
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.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.gtk.config import PGPConfigDialog
from pgp.gtk.key import KeyDialog
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
try:
@@ -51,29 +51,29 @@ else:
# on a much lower version number than gnupg
# Also we need at least python-gnupg 0.3.8
v_gnupg = gnupg.__version__
if V(v_gnupg) < V('0.3.8') or V(v_gnupg) > V('1.0.0'):
log.error('We need python-gnupg >= 0.3.8')
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")
ERROR = True
ERROR_MSG = None
BINARY = find_gpg()
log.info('Found GPG executable: %s', BINARY)
log.info("Found GPG executable: %s", BINARY)
if BINARY is None or ERROR:
if os.name == 'nt':
ERROR_MSG = _('Please install GnuPG / Gpg4win')
if os.name == "nt":
ERROR_MSG = _("Please install GnuPG / Gpg4win")
else:
ERROR_MSG = _('Please install python-gnupg and gnupg')
ERROR_MSG = _("Please install python-gnupg and gnupg")
else:
from pgp.modules import pgp_legacy
from pgp.backend.python_gnupg import PGP
from pgp.modules import pgp_legacy
class PGPPlugin(GajimPlugin):
def init(self):
# 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:
self.activatable = False
self.config_dialog = None
@@ -84,30 +84,26 @@ class PGPPlugin(GajimPlugin):
self.encryption_name = ENCRYPTION_NAME
self.allow_zeroconf = True
self.gui_extension_points = {
'encrypt' + ENCRYPTION_NAME: (self._encrypt_message, None),
'send_message' + ENCRYPTION_NAME: (
self._before_sendmessage, None),
'encryption_dialog' + ENCRYPTION_NAME: (
self._on_encryption_dialog, None),
'encryption_state' + ENCRYPTION_NAME: (
self._encryption_state, None),
'send-presence': (self._on_send_presence, None),
"encrypt" + ENCRYPTION_NAME: (self._encrypt_message, None),
"send_message" + ENCRYPTION_NAME: (self._before_sendmessage, None),
"encryption_dialog" + ENCRYPTION_NAME: (self._on_encryption_dialog, None),
"encryption_state" + ENCRYPTION_NAME: (self._encryption_state, None),
"send-presence": (self._on_send_presence, None),
}
self.modules = [pgp_legacy]
self.events_handlers = {
'pgp-not-trusted': (ged.PRECORE, self._on_not_trusted),
'pgp-file-encryption-error': (ged.PRECORE,
self._on_file_encryption_error),
"pgp-not-trusted": (ged.PRECORE, self._on_not_trusted),
"pgp-file-encryption-error": (ged.PRECORE, 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)
@staticmethod
def get_pgp_module(account):
return app.get_client(account).get_module('PGPLegacy')
return app.get_client(account).get_module("PGPLegacy")
def activate(self):
pass
@@ -121,8 +117,8 @@ class PGPPlugin(GajimPlugin):
@staticmethod
def _encryption_state(_chat_control, state):
state['visible'] = True
state['authenticated'] = True
state["visible"] = True
state["authenticated"] = True
def _on_encryption_dialog(self, chat_control):
account = chat_control.account
@@ -137,17 +133,20 @@ class PGPPlugin(GajimPlugin):
@staticmethod
def _on_not_trusted(event):
ConfirmationCheckDialog(
_('Untrusted PGP key'),
_('The PGP key used to encrypt this chat is not '
'trusted. Do you really want to encrypt this '
'message?'),
_('_Do not ask me again'),
[DialogButton.make('Cancel',
text=_('_No'),
callback=event.on_no),
DialogButton.make('OK',
text=_('_Encrypt Anyway'),
callback=event.on_yes)]).show()
_("Untrusted PGP key"),
_(
"The PGP key used to encrypt this chat is not "
"trusted. Do you really want to encrypt this "
"message?"
),
_("_Do not ask me again"),
[
DialogButton.make("Cancel", text=_("_No"), callback=event.on_no),
DialogButton.make(
"OK", text=_("_Encrypt Anyway"), callback=event.on_yes
),
],
).show()
@staticmethod
def _before_sendmessage(chat_control):
@@ -156,24 +155,30 @@ class PGPPlugin(GajimPlugin):
client = app.get_client(account)
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:
SimpleDialog(
_('PGP Key mismatch'),
_('The contact\'s key (%s) <b>does not match</b> the key '
'assigned in Gajim.') % announced_key_id)
_("PGP Key mismatch"),
_(
"The contact's key (%s) <b>does not match</b> the key "
"assigned in Gajim."
)
% announced_key_id,
)
chat_control.sendmessage = False
return
if not valid:
SimpleDialog(
_('No OpenPGP key assigned'),
_('No OpenPGP key is assigned to this contact.'))
_("No OpenPGP key assigned"),
_("No OpenPGP key is assigned to this contact."),
)
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(
_('No OpenPGP key assigned'),
_('No OpenPGP key is assigned to your account.'))
_("No OpenPGP key assigned"),
_("No OpenPGP key is assigned to your account."),
)
chat_control.sendmessage = False
def _encrypt_message(self, conn, event, callback):
@@ -185,4 +190,4 @@ class PGPPlugin(GajimPlugin):
@staticmethod
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 gajim.common import configpaths
from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _
log = logging.getLogger('gajim.p.plugins_translations')
log = logging.getLogger("gajim.p.plugins_translations")
class PluginsTranslationsPlugin(GajimPlugin):
def init(self) -> None:
self.description = _('This plugin contains translations for other '
'Gajim plugins. Please restart Gajim after '
'enabling this plugin.')
self.description = _(
"This plugin contains translations for other "
"Gajim plugins. Please restart Gajim after "
"enabling this plugin."
)
self.config_dialog = None
self.config_default_values = {'last_version': ('0', '')}
self.locale_dir = Path(configpaths.get('PLUGINS_USER')) / 'locale'
self.config_default_values = {"last_version": ("0", "")}
self.locale_dir = Path(configpaths.get("PLUGINS_USER")) / "locale"
def activate(self) -> None:
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
files = glob(self.__path__ + '/*.mo')
files = glob(self.__path__ + "/*.mo")
self._remove_translations()
self.locale_dir.mkdir()
locales = [
os.path.splitext(os.path.basename(name))[0] for name in files
]
log.info('Installing new translations...')
locales = [os.path.splitext(os.path.basename(name))[0] for name in files]
log.info("Installing new translations...")
for locale in locales:
dst = self.locale_dir / locale / 'LC_MESSAGES'
dst = self.locale_dir / locale / "LC_MESSAGES"
dst.mkdir(parents=True)
shutil.copy2(os.path.join(self.__path__, '%s.mo' % locale),
dst / 'gajim_plugins.mo')
shutil.copy2(
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:
log.info('Removing old translations...')
log.info("Removing old translations...")
if self.locale_dir.exists():
shutil.rmtree(str(self.locale_dir))
def deactivate(self) -> None:
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 gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Gtk
from gajim.common import app
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING:
from ..plugin import QuickRepliesPlugin
class ConfigDialog(Gtk.ApplicationWindow):
def __init__(self,
plugin: QuickRepliesPlugin,
transient: Gtk.Window
) -> None:
def __init__(self, plugin: QuickRepliesPlugin, transient: Gtk.Window) -> None:
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_show_menubar(False)
self.set_title(_('Quick Replies Configuration'))
self.set_title(_("Quick Replies Configuration"))
self.set_transient_for(transient)
self.set_default_size(400, 400)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
@@ -50,7 +46,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
self.set_destroy_with_parent(True)
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
@@ -60,26 +56,23 @@ class ConfigDialog(Gtk.ApplicationWindow):
self.show_all()
self._ui.connect_signals(self)
self.connect('destroy', self._on_destroy)
self.connect("destroy", self._on_destroy)
def _fill_list(self) -> None:
for reply in self._plugin.quick_replies:
self._ui.replies_store.append([reply])
def _on_reply_edited(self,
_renderer: Gtk.CellRendererText,
path: str,
new_text: str
def _on_reply_edited(
self, _renderer: Gtk.CellRendererText, path: str, new_text: str
) -> None:
iter_ = self._ui.replies_store.get_iter(path)
self._ui.replies_store.set_value(iter_, 0, new_text)
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]
self._ui.replies_treeview.scroll_to_cell(
row.path, None, False, 0, 0)
self._ui.replies_treeview.scroll_to_cell(row.path, None, False, 0, 0)
self._ui.selection.unselect_all()
self._ui.selection.select_path(row.path)
@@ -96,7 +89,7 @@ class ConfigDialog(Gtk.ApplicationWindow):
def _on_destroy(self, *args: Any) -> None:
replies: list[str] = []
for row in self._ui.replies_store:
if row[0] == '':
if row[0] == "":
continue
replies.append(row[0])
self._plugin.set_quick_replies(replies)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,14 +25,15 @@ if TYPE_CHECKING:
from .triggers import ProcessableEventsT
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 wrapper(self: Any, event: ProcessableEventsT, rule: RuleT):
res = func(self, event, rule)
log.info(f'{event.name} -> {func.__name__} -> {res}')
log.info(f"{event.name} -> {func.__name__} -> {res}")
return res
return wrapper