cq: Format with black and isort
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,31 +136,30 @@ 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,
|
||||
buffer_: Gtk.TextBuffer,
|
||||
start: Gtk.TextIter,
|
||||
end: Gtk.TextIter,
|
||||
substitute: str
|
||||
) -> None:
|
||||
def _replace_text(
|
||||
self,
|
||||
buffer_: Gtk.TextBuffer,
|
||||
start: Gtk.TextIter,
|
||||
end: Gtk.TextIter,
|
||||
substitute: str,
|
||||
) -> None:
|
||||
|
||||
self._replace_in_progress = True
|
||||
buffer_.delete(start, end)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
) -> None:
|
||||
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
|
||||
) -> None:
|
||||
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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'),
|
||||
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'),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config['block_subscription_requests'],
|
||||
callback=self._on_setting,
|
||||
data='block_subscription_requests'),
|
||||
Setting(SettingKind.SWITCH,
|
||||
_('Disable XHTML for Group Chats'),
|
||||
SettingType.VALUE,
|
||||
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'),
|
||||
SettingType.VALUE,
|
||||
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'),
|
||||
SettingType.VALUE,
|
||||
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'),
|
||||
SettingType.VALUE,
|
||||
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'),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config['antispam_for_conference'],
|
||||
callback=self._on_setting,
|
||||
data='antispam_for_conference',
|
||||
desc=_('Enables anti spam question for private messages '
|
||||
'in group chats')),
|
||||
]
|
||||
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"),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config["block_subscription_requests"],
|
||||
callback=self._on_setting,
|
||||
data="block_subscription_requests",
|
||||
),
|
||||
Setting(
|
||||
SettingKind.SWITCH,
|
||||
_("Disable XHTML for Group Chats"),
|
||||
SettingType.VALUE,
|
||||
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"),
|
||||
SettingType.VALUE,
|
||||
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"),
|
||||
SettingType.VALUE,
|
||||
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"),
|
||||
SettingType.VALUE,
|
||||
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"),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config["antispam_for_conference"],
|
||||
callback=self._on_setting,
|
||||
data="antispam_for_conference",
|
||||
desc=_(
|
||||
"Enables anti spam question for private messages " "in group chats"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
SettingsDialog.__init__(self,
|
||||
parent,
|
||||
_('Anti Spam Configuration'),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
'')
|
||||
SettingsDialog.__init__(
|
||||
self,
|
||||
parent,
|
||||
_("Anti Spam Configuration"),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
"",
|
||||
)
|
||||
|
||||
def _on_setting(self, value: Any, data: Any) -> None:
|
||||
self.plugin.config[data] = value
|
||||
|
||||
@@ -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',
|
||||
callback=self._subscribe_received,
|
||||
typ='subscribe',
|
||||
priority=48),
|
||||
StanzaHandler(name="message", callback=self._message_received, priority=48),
|
||||
StanzaHandler(
|
||||
name="presence",
|
||||
callback=self._subscribe_received,
|
||||
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,11 +68,9 @@ 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
|
||||
) -> None:
|
||||
def _message_received(
|
||||
self, _con: Client, _stanza: Message, properties: MessageProperties
|
||||
) -> None:
|
||||
|
||||
if properties.is_sent_carbon:
|
||||
# Another device already sent a message
|
||||
@@ -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
|
||||
) -> None:
|
||||
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"
|
||||
|
||||
@@ -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'}
|
||||
),
|
||||
'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')),
|
||||
})
|
||||
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")),
|
||||
}
|
||||
)
|
||||
# pylint: enable=too-many-lines
|
||||
|
||||
|
||||
|
||||
@@ -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,15 +60,13 @@ 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
|
||||
) -> tuple[Gtk.Image, str] | None:
|
||||
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)
|
||||
if disco_info is None:
|
||||
@@ -80,18 +75,17 @@ 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
|
||||
) -> None:
|
||||
def _roster_tooltip_resource_populate(
|
||||
self, resource_box: Gtk.Box, resource: ResourceContact
|
||||
) -> None:
|
||||
|
||||
result = self._get_image_and_client_name(resource, resource_box)
|
||||
if result is None:
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config['show_unknown_icon'],
|
||||
callback=self._on_setting,
|
||||
data='show_unknown_icon'),
|
||||
Setting(
|
||||
SettingKind.SWITCH,
|
||||
_("Show Icon for Unknown Clients"),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config["show_unknown_icon"],
|
||||
callback=self._on_setting,
|
||||
data="show_unknown_icon",
|
||||
),
|
||||
]
|
||||
|
||||
SettingsDialog.__init__(self,
|
||||
parent,
|
||||
_('Clients Icons Configuration'),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
'')
|
||||
SettingsDialog.__init__(
|
||||
self,
|
||||
parent,
|
||||
_("Clients Icons Configuration"),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
"",
|
||||
)
|
||||
|
||||
def _on_setting(self, value: Any, data: Any) -> None:
|
||||
self.plugin.config[data] = value
|
||||
|
||||
@@ -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'),
|
||||
SettingType.VALUE,
|
||||
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)},
|
||||
),
|
||||
Setting(SettingKind.COLOR,
|
||||
_('Color'),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config['WARNING_COLOR'],
|
||||
callback=self._on_setting,
|
||||
data='WARNING_COLOR',
|
||||
desc=_('Highlight color for the message input'),
|
||||
),
|
||||
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)'),
|
||||
),
|
||||
]
|
||||
Setting(
|
||||
SettingKind.SPIN,
|
||||
_("Message Length"),
|
||||
SettingType.VALUE,
|
||||
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)},
|
||||
),
|
||||
Setting(
|
||||
SettingKind.COLOR,
|
||||
_("Color"),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config["WARNING_COLOR"],
|
||||
callback=self._on_setting,
|
||||
data="WARNING_COLOR",
|
||||
desc=_("Highlight color for the message input"),
|
||||
),
|
||||
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)"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
SettingsDialog.__init__(self,
|
||||
parent,
|
||||
_('Length Notifier Configuration'),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
'')
|
||||
SettingsDialog.__init__(
|
||||
self,
|
||||
parent,
|
||||
_("Length Notifier Configuration"),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
"",
|
||||
)
|
||||
|
||||
def _on_setting(self, value: Any, data: Any) -> None:
|
||||
if isinstance(value, str):
|
||||
|
||||
@@ -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,48 +31,50 @@ 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 window’s message input if '
|
||||
'a specified message length is exceeded.')
|
||||
self.description = _(
|
||||
"Highlights the chat window’s 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
|
||||
self._actions_box_widget = None
|
||||
@@ -92,14 +94,12 @@ 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
|
||||
) -> None:
|
||||
def _message_actions_box_created(
|
||||
self, message_actions_box: MessageActionsBox, gtk_box: Gtk.Box
|
||||
) -> None:
|
||||
|
||||
self._message_action_box = message_actions_box
|
||||
self._actions_box_widget = gtk_box
|
||||
@@ -117,14 +117,13 @@ class LengthNotifierPlugin(GajimPlugin):
|
||||
|
||||
|
||||
class Counter(Gtk.Label):
|
||||
def __init__(self,
|
||||
message_input: MessageInputTextView,
|
||||
config: GajimPluginConfig
|
||||
) -> None:
|
||||
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()
|
||||
|
||||
@@ -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'),
|
||||
SettingType.VALUE,
|
||||
str(self.plugin.config['HEIGHT']),
|
||||
callback=self._on_setting,
|
||||
data='HEIGHT',
|
||||
desc=_('Size of message input in pixels'),
|
||||
props={'range_': (20, 200, 1)}),
|
||||
]
|
||||
Setting(
|
||||
SettingKind.SPIN,
|
||||
_("Height in pixels"),
|
||||
SettingType.VALUE,
|
||||
str(self.plugin.config["HEIGHT"]),
|
||||
callback=self._on_setting,
|
||||
data="HEIGHT",
|
||||
desc=_("Size of message input in pixels"),
|
||||
props={"range_": (20, 200, 1)},
|
||||
),
|
||||
]
|
||||
|
||||
SettingsDialog.__init__(self,
|
||||
parent,
|
||||
_('Message Box Size Configuration'),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
'')
|
||||
SettingsDialog.__init__(
|
||||
self,
|
||||
parent,
|
||||
_("Message Box Size Configuration"),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
"",
|
||||
)
|
||||
|
||||
def _on_setting(self, value: Any, data: Any) -> None:
|
||||
self.plugin.config[data] = value
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config['format_string'],
|
||||
callback=self._on_setting, data='format_string')
|
||||
]
|
||||
Setting(
|
||||
SettingKind.ENTRY,
|
||||
_("Format string"),
|
||||
SettingType.VALUE,
|
||||
self.plugin.config["format_string"],
|
||||
callback=self._on_setting,
|
||||
data="format_string",
|
||||
)
|
||||
]
|
||||
|
||||
SettingsDialog.__init__(self,
|
||||
parent,
|
||||
_('Now Listen Configuration'),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
'')
|
||||
SettingsDialog.__init__(
|
||||
self,
|
||||
parent,
|
||||
_("Now Listen Configuration"),
|
||||
Gtk.DialogFlags.MODAL,
|
||||
settings,
|
||||
"",
|
||||
)
|
||||
|
||||
def _on_setting(self, value: Any, data: Any) -> None:
|
||||
self.plugin.config[data] = value
|
||||
|
||||
@@ -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,29 +61,24 @@ 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
|
||||
) -> bool:
|
||||
def _on_key_press(
|
||||
self, textview: MessageInputTextView, event: Gdk.EventKey
|
||||
) -> bool:
|
||||
|
||||
# Insert text to message input box, at cursor position
|
||||
if event.keyval != Gdk.KEY_n:
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
expires=False,
|
||||
passphrase=None,
|
||||
force=False)
|
||||
result = context.create_key(
|
||||
f"xmpp:{str(self._jid)}",
|
||||
algorithm="default",
|
||||
expires=False,
|
||||
passphrase=None,
|
||||
force=False,
|
||||
)
|
||||
|
||||
log.info('Generated new key: %s', result.fpr)
|
||||
log.info("Generated new key: %s", result.fpr)
|
||||
|
||||
def get_key(self, fingerprint):
|
||||
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)
|
||||
|
||||
@@ -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'),
|
||||
recipients,
|
||||
armor=False,
|
||||
sign=self._own_fingerprint,
|
||||
always_trust=True)
|
||||
result = super().encrypt(
|
||||
str(payload).encode("utf8"),
|
||||
recipients,
|
||||
armor=False,
|
||||
sign=self._own_fingerprint,
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
key.fingerprint,
|
||||
key.active,
|
||||
key.trust,
|
||||
key.timestamp,
|
||||
key.comment)
|
||||
yield (
|
||||
self.jid,
|
||||
key.fingerprint,
|
||||
key.active,
|
||||
key.trust,
|
||||
key.timestamp,
|
||||
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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
callback=self.decrypt_message,
|
||||
ns=Namespace.OPENPGP,
|
||||
priority=9),
|
||||
StanzaHandler(
|
||||
name="message",
|
||||
callback=self.decrypt_message,
|
||||
ns=Namespace.OPENPGP,
|
||||
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,
|
||||
jid=str(remote_jid),
|
||||
message=text,
|
||||
error=error,
|
||||
time=time.time()))
|
||||
MessageNotSent(
|
||||
client=self._client,
|
||||
jid=str(remote_jid),
|
||||
message=text,
|
||||
error=error,
|
||||
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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,23 +51,21 @@ class OpenPGPPlugin(GajimPlugin):
|
||||
return
|
||||
|
||||
self.events_handlers = {
|
||||
'signed-in': (ged.PRECORE, self.signed_in),
|
||||
}
|
||||
"signed-in": (ged.PRECORE, self.signed_in),
|
||||
}
|
||||
|
||||
self.modules = [openpgp]
|
||||
|
||||
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)
|
||||
|
||||
@@ -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,
|
||||
'',
|
||||
payload,
|
||||
self._add_header_footer(signed, 'SIGNATURE')]
|
||||
)
|
||||
result = super().verify(data.encode('utf8'))
|
||||
[
|
||||
"-----BEGIN PGP SIGNED MESSAGE-----",
|
||||
"Hash: " + algo,
|
||||
"",
|
||||
payload,
|
||||
self._add_header_footer(signed, "SIGNATURE"),
|
||||
]
|
||||
)
|
||||
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
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
callback=self._message_received,
|
||||
ns=Namespace.ENCRYPTED,
|
||||
priority=9),
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._on_presence_received,
|
||||
ns=Namespace.SIGNED,
|
||||
priority=48),
|
||||
StanzaHandler(
|
||||
name="message",
|
||||
callback=self._message_received,
|
||||
ns=Namespace.ENCRYPTED,
|
||||
priority=9,
|
||||
),
|
||||
StanzaHandler(
|
||||
name="presence",
|
||||
callback=self._on_presence_received,
|
||||
ns=Namespace.SIGNED,
|
||||
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,
|
||||
jid=str(message.contact.jid),
|
||||
message=message.get_text(),
|
||||
error=_('Encryption error: %s') % error,
|
||||
time=time.time()))
|
||||
MessageNotSent(
|
||||
client=con,
|
||||
jid=str(message.contact.jid),
|
||||
message=message.get_text(),
|
||||
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, can’t sign presence')
|
||||
self._log.warning("No own key id found, can’t 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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
111
pgp/plugin.py
111
pgp/plugin.py
@@ -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)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
from .plugins_translations import PluginsTranslationsPlugin # type: ignore # noqa: F401
|
||||
from .plugins_translations import ( # type: ignore # noqa: F401
|
||||
PluginsTranslationsPlugin,
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
) -> None:
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
) -> None:
|
||||
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
|
||||
) -> None:
|
||||
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()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
DEFAULT_DATA = [
|
||||
'Hello!',
|
||||
'How are you?',
|
||||
'Good bye.',
|
||||
"Hello!",
|
||||
"How are you?",
|
||||
"Good bye.",
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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],
|
||||
cwd=REPO_DIR,
|
||||
check=True)
|
||||
for file in TRANS_DIR.glob("*.po"):
|
||||
subprocess.run(
|
||||
["msgmerge", "-U", "--sort-output", str(file), TRANS_TEMPLATE],
|
||||
cwd=REPO_DIR,
|
||||
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)],
|
||||
cwd=REPO_DIR,
|
||||
check=True)
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
) -> None:
|
||||
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
|
||||
) -> None:
|
||||
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())
|
||||
|
||||
@@ -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 Gajim’s behaviour with triggers for each contact')
|
||||
"Configure Gajim’s 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,11 +97,12 @@ class Triggers(GajimPlugin):
|
||||
check_func = self._check_rule_apply_status_changed
|
||||
self._check_all(event, check_func, self._apply_rule)
|
||||
|
||||
def _check_all(self,
|
||||
event: ProcessableEventsT,
|
||||
check_func: Callable[..., bool],
|
||||
apply_func: Callable[..., Any]
|
||||
) -> RuleResult:
|
||||
def _check_all(
|
||||
self,
|
||||
event: ProcessableEventsT,
|
||||
check_func: Callable[..., bool],
|
||||
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
|
||||
) -> bool:
|
||||
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
|
||||
) -> bool:
|
||||
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
|
||||
) -> bool:
|
||||
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
|
||||
) -> bool:
|
||||
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,84 +226,74 @@ 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
|
||||
) -> bool:
|
||||
def _excecute_notification_rules(
|
||||
self, result: RuleResult, event: Notification
|
||||
) -> bool:
|
||||
|
||||
if result.sound is False:
|
||||
event.sound = None
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user