Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • gajim/gajim-plugins
  • lovetox/gajim-plugins
  • ag/gajim-plugins
  • FlorianMuenchbach/gajim-plugins
  • rom1dep/gajim-plugins
  • pitchum/gajim-plugins
  • wurstsalat/gajim-plugins
  • Dicson/gajim-plugins
  • andre/gajim-plugins
  • link2xt/gajim-plugins
  • marmistrz/gajim-plugins
  • Jens/gajim-plugins
  • muelli/gajim-plugins
  • asterix/gajim-plugins
  • orhideous/gajim-plugins
  • ngvelprz/gajim-plugins
  • appleorange1/gajim-plugins
  • Martin/gajim-plugins
  • maltel/gajim-plugins
  • Seve/gajim-plugins
  • evert-mouw/gajim-plugins
  • Yuki/gajim-plugins
  • mxre/gajim-plugins
  • ValdikSS/gajim-plugins
  • SaltyBones/gajim-plugins
  • comradekingu/gajim-plugins
  • ritzmann/gajim-plugins
  • genofire/gajim-plugins
  • jjrh/gajim-plugins
  • yarmak/gajim-plugins
  • PapaTutuWawa/gajim-plugins
  • weblate/gajim-plugins
  • XutaxKamay/gajim-plugins
  • nekk/gajim-plugins
  • principis/gajim-plugins
  • cbix/gajim-plugins
  • bodqhrohro/gajim-plugins
  • airtower-luna/gajim-plugins
  • toms/gajim-plugins
  • mesonium/gajim-plugins
  • lissine/gajim-plugins
  • anviar/gajim-plugins
42 results
Show changes
Commits on Source (38)
Showing
with 697 additions and 632 deletions
from typing import Any, Iterator
from typing import Any
import os
import json
import functools
from shutil import make_archive
import json
import os
import sys
from collections.abc import Iterator
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",
}
......@@ -44,12 +45,12 @@ console = Console()
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)
ftp = FTP_TLS(FTP_URL, FTP_USER, FTP_PASS) # noqa: S321
console.print("Successfully connected to", FTP_URL)
func(ftp, *args)
ftp.quit()
console.print('Quit')
return
console.print("Quit")
return func_wrapper
......@@ -59,8 +60,8 @@ def is_manifest_valid(manifest: ManifestT) -> bool:
def download_package_index() -> ManifestT:
console.print('Download package index')
r = requests.get(PACKAGE_INDEX_URL)
console.print("Download package index")
r = requests.get(PACKAGE_INDEX_URL, timeout=30)
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')
sys.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)
image: plugins-master:latest
stages:
- test
- deploy
deploy-plugins:
stage: deploy
script:
- python3 .ci/deploy.py
test-isort:
image: gajim-test
stage: test
rules:
- changes:
- "**/*.py"
script:
- isort --version
- isort --check .
interruptible: true
test-ruff:
image: gajim-test
stage: test
rules:
- changes:
- "**/*.py"
script:
- ruff --version
- ruff check .
interruptible: true
test-black:
image: gajim-test
stage: test
rules:
- changes:
- "**/*.py"
script:
- pip install black==24.10.0
- black --version
- black . --check
interruptible: true
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.3.2
rev: v0.6.8
hooks:
- id: ruff
exclude: ".githooks/"
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.4.0
hooks:
- id: codespell
pass_filenames: false
args: ["--skip=*.svg,*.po,./acronyms_expander/acronyms.py", "-L=fpr,"]
additional_dependencies:
- tomli
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.361
rev: v1.1.382
hooks:
- id: pyright
pass_filenames: false
......@@ -27,3 +27,16 @@ repos:
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/psf/black
# The `refs/tags/<tag>:refs/tags/<tag>` is needed for black's required-version to work:
# https://github.com/psf/black/issues/2493#issuecomment-1081987650
rev: 'refs/tags/24.10.0:refs/tags/24.10.0'
hooks:
- id: black
- repo: https://github.com/fsfe/reuse-tool
rev: v5.0.2
hooks:
- id: reuse
stages: [manual]
......@@ -32,7 +32,7 @@ There are several ways to install a plugin:
*Note: Using master branch for plugins requires frequent updates of both Gajim and plugins!*
## Share / improve Plugins
## Development
You have written a new plugin or want to improve an existing one?
......@@ -41,6 +41,7 @@ First, thanks for that! Here is how to start:
- Register an account on our Gitlab [here](https://dev.gajim.org/users/sign_in)
- Tell us about your plans at [gajim@conference.gajim.org](xmpp:gajim@conference.gajim.org?join)
- Fork the Gajim-Plugins [repository](https://dev.gajim.org/gajim/gajim-plugins)
- Check `./scripts/dev_env.sh` to get a environment with dependencies installed
- When you are finished, do a merge request against the main plugins repository. You can read about how to use git [here](https://dev.gajim.org/gajim/gajim/wikis/howtogit).
- Additionally, there is a list of [plugin events](https://dev.gajim.org/gajim/gajim/wikis/development/pluginsevents) which might be helpful
......
from .acronyms_expander import AcronymsExpanderPlugin # type: ignore # noqa: F401
from .acronyms_expander import AcronymsExpanderPlugin # pyright: ignore # noqa: F401
......@@ -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)
......@@ -16,94 +16,112 @@
from __future__ import annotations
from typing import Any
from typing import cast
from typing import TYPE_CHECKING
from pathlib import Path
from gi.repository import Gtk
from gi.repository import Gdk
from gajim.common import app
from gajim.plugins.plugins_i18n import _
from gajim.gtk.widgets import GajimAppWindow
from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING:
from ..acronyms_expander import AcronymsExpanderPlugin
class ConfigDialog(Gtk.ApplicationWindow):
def __init__(self,
plugin: AcronymsExpanderPlugin,
transient: Gtk.Window
) -> None:
class ConfigBuilder(Gtk.Builder):
acronyms_store: Gtk.ListStore
box: Gtk.Box
acronyms_treeview: Gtk.TreeView
selection: Gtk.TreeSelection
acronym_renderer: Gtk.CellRendererText
sub_renderer: Gtk.CellRendererText
add_button: Gtk.Button
remove_button: Gtk.Button
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_show_menubar(False)
self.set_title(_('Acronyms Configuration'))
self.set_transient_for(transient)
self.set_default_size(400, 400)
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
self.set_modal(True)
self.set_destroy_with_parent(True)
class ConfigDialog(GajimAppWindow):
def __init__(self, plugin: AcronymsExpanderPlugin, transient: Gtk.Window) -> None:
GajimAppWindow.__init__(
self,
name="AcronymsConfigDialog",
title=_("Acronyms Configuration"),
default_width=400,
default_height=400,
transient_for=transient,
modal=True,
)
ui_path = Path(__file__).parent
self._ui = get_builder(str(ui_path.resolve() / 'config.ui'))
self._ui = cast(
ConfigBuilder, get_builder(str(ui_path.resolve() / "config.ui"))
)
self._plugin = plugin
self.add(self._ui.box)
self.set_child(self._ui.box)
self._fill_list()
self.show_all()
self._ui.connect_signals(self)
self.connect('destroy', self._on_destroy)
self._connect(self._ui.acronym_renderer, "edited", self._on_acronym_edited)
self._connect(self._ui.sub_renderer, "edited", self._on_substitute_edited)
self._connect(self._ui.add_button, "clicked", self._on_add_clicked)
self._connect(self._ui.remove_button, "clicked", self._on_remove_clicked)
self._connect(self.window, "close-request", self._on_close_request)
self.show()
def _cleanup(self) -> None:
del self._plugin
def _fill_list(self) -> None:
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)
def _on_remove_clicked(self, _button: Gtk.Button) -> None:
model, paths = self._ui.selection.get_selected_rows()
res = self._ui.selection.get_selected_rows()
if res is None:
return
model, paths = res
references: list[Gtk.TreeRowReference] = []
for path in paths:
references.append(Gtk.TreeRowReference.new(model, path))
ref = Gtk.TreeRowReference.new(model, path)
assert ref is not None
references.append(ref)
for ref in references:
iter_ = model.get_iter(ref.get_path())
path = ref.get_path()
assert path is not None
iter_ = model.get_iter(path)
self._ui.acronyms_store.remove(iter_)
def _on_destroy(self, *args: Any) -> None:
acronyms = {}
def _on_close_request(self, win: Gtk.ApplicationWindow) -> None:
acronyms: dict[str, str] = {}
for row in self._ui.acronyms_store:
acronym, substitute = row
if not acronym or not substitute:
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<requires lib="gtk" version="4.0"/>
<object class="GtkListStore" id="acronyms_store">
<columns>
<!-- column-name acronym -->
<column type="gchararray"/>
<!-- column-name substitute -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">18</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
<property name="focusable">1</property>
<property name="vexpand">1</property>
<property name="hexpand">1</property>
<property name="child">
<object class="GtkTreeView" id="acronyms_treeview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focusable">1</property>
<property name="model">acronyms_store</property>
<property name="search_column">1</property>
<child internal-child="selection">
......@@ -34,15 +26,14 @@
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="resizable">1</property>
<property name="title" translatable="yes">Acronym</property>
<property name="clickable">True</property>
<property name="sort_indicator">True</property>
<property name="clickable">1</property>
<property name="sort_indicator">1</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText">
<property name="editable">True</property>
<signal name="edited" handler="_on_acronym_edited" swapped="no"/>
<object class="GtkCellRendererText" id="acronym_renderer">
<property name="editable">1</property>
</object>
<attributes>
<attribute name="text">0</attribute>
......@@ -52,15 +43,14 @@
</child>
<child>
<object class="GtkTreeViewColumn">
<property name="resizable">True</property>
<property name="resizable">1</property>
<property name="title" translatable="yes">Substitute</property>
<property name="clickable">True</property>
<property name="sort_indicator">True</property>
<property name="clickable">1</property>
<property name="sort_indicator">1</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText">
<property name="editable">True</property>
<signal name="edited" handler="_on_substitute_edited" swapped="no"/>
<object class="GtkCellRendererText" id="sub_renderer">
<property name="editable">1</property>
</object>
<attributes>
<attribute name="text">1</attribute>
......@@ -69,55 +59,28 @@
</object>
</child>
</object>
</child>
</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkToolbar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="toolbar_style">icons</property>
<property name="icon_size">4</property>
<object class="GtkBox">
<property name="css-classes">toolbar</property>
<style>
<class name="inline-toolbar"/>
</style>
<child>
<object class="GtkToolButton">
<property name="visible">True</property>
<property name="can_focus">False</property>
<object class="GtkButton" id="add_button">
<property name="tooltip_text" translatable="yes">Add</property>
<property name="icon_name">list-add-symbolic</property>
<signal name="clicked" handler="_on_add_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton">
<property name="visible">True</property>
<property name="can_focus">False</property>
<object class="GtkButton" id="remove_button">
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="icon_name">list-remove-symbolic</property>
<signal name="clicked" handler="_on_remove_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<style>
<class name="inline-toolbar"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>
from .anti_spam import AntiSpamPlugin # type: ignore # noqa: F401
from .anti_spam import AntiSpamPlugin # pyright: ignore # noqa: F401
......@@ -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,87 @@ 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,25 @@ 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)
assert msg_from is not None
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"
from .clients_icons import ClientsIconsPlugin # type: ignore # noqa: F401
from .clients_icons import ClientsIconsPlugin # pyright: ignore # noqa: F401
This diff is collapsed.
......@@ -18,60 +18,54 @@ 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.icons import get_icon_theme
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 = _("Show client icons in the contact tooltip.")
self.config_dialog = partial(ClientsIconsConfigDialog, self)
self.gui_extension_points = {
'roster_tooltip_resource_populate': (
self._roster_tooltip_resource_populate,
None),
"contact_tooltip_resource_populate": (
self._contact_tooltip_resource_populate,
None,
),
}
self.config_default_values = {
'show_unknown_icon': (True, ''),
"show_unknown_icon": (True, ""),
}
_icon_theme = Gtk.IconTheme.get_default()
if _icon_theme is not None:
_icon_theme.append_search_path(str(Path(__file__).parent))
icon_theme = get_icon_theme()
icon_theme.add_search_path(str(Path(__file__).parent))
@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 +74,18 @@ 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
image = Gtk.Image.new_from_icon_name(icon_name)
image.set_pixel_size(16)
return image, client_name
def _roster_tooltip_resource_populate(self,
resource_box: Gtk.Box,
resource: ResourceContact
) -> None:
def _contact_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,11 +94,13 @@ 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.add(image)
client_box.add(label)
client_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
halign=Gtk.Align.START,
spacing=6,
)
client_box.append(image)
client_box.append(label)
children = resource_box.get_children()
box = cast(Gtk.Box, children[0])
box.add(client_box)
box = cast(Gtk.Box, resource_box.get_first_child())
box.append(client_box)
......@@ -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
......@@ -18,31 +18,28 @@ modules:
prefix: ${FLATPAK_DEST}
sources:
- type: archive
url: https://downloads.sourceforge.net/swig/swig-4.2.1.tar.gz
sha256: fa045354e2d048b2cddc69579e4256245d4676894858fcf0bab2290ecf59b7d8
url: https://downloads.sourceforge.net/swig/swig-4.3.0.tar.gz
sha256: f7203ef796f61af986c70c05816236cbd0d31b7aa9631e5ab53020ab7804aa9e
- name: gpgme
cleanup:
- /bin
- /include
- /lib
- /share
build-options:
config-opts:
- --enable-languages=python
env:
SWIG_LIB: "/app/plugins/openpgp/share/swig/4.2.1"
SWIG_LIB: "/app/plugins/openpgp/share/swig/4.3.0"
prepend-path: "/app/plugins/openpgp/bin:"
prefix: ${FLATPAK_DEST}
post-install:
- mkdir -p ${FLATPAK_DEST}/site-packages
- mv ${FLATPAK_DEST}/lib/python3.12/site-packages/*.egg/gpg ${FLATPAK_DEST}/site-packages/
sources:
# used version should match runtime version of gpgme
# so we don't have to ship libs
- type: archive
url: https://www.gnupg.org/ftp/gcrypt/gpgme/gpgme-1.23.2.tar.bz2
sha256: 9499e8b1f33cccb6815527a1bc16049d35a6198a6c5fae0185f2bd561bce5224
url: https://www.gnupg.org/ftp/gcrypt/gpgme/gpgme-1.24.2.tar.bz2
sha256: e11b1a0e361777e9e55f48a03d89096e2abf08c63d84b7017cfe1dce06639581
- name: gajim-openpgp
buildsystem: simple
......
from .length_notifier import LengthNotifierPlugin # type: ignore # noqa: F401
from .length_notifier import LengthNotifierPlugin # pyright: ignore # noqa: F401
......@@ -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
......@@ -85,21 +87,20 @@ class LengthNotifierPlugin(GajimPlugin):
def deactivate(self) -> None:
assert self._counter is not None
assert self._actions_box_widget is not None
self._counter.reset()
self._counter.destroy()
self._actions_box_widget.remove(self._counter)
del self._counter
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._actions_box_widget.pack_end(self._counter, False, False, 0)
self._counter = Counter(self._message_action_box.msg_textview, self.config)
self._actions_box_widget.append(self._counter)
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 +118,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.add_css_class("dim-label")
self._config = config
......@@ -134,48 +134,52 @@ class Counter(Gtk.Label):
self._color = None
self._inverted_color = None
self._provider = Gtk.CssProvider()
self._textview = message_input
self._signal_id = self._textview.connect('buffer-changed', self._update)
self._provider = None
context = self._textview.get_style_context()
context.add_provider(self._provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._signal_id = self._textview.connect("buffer-changed", self._update)
self._parse_config()
self._set_css()
self.connect('destroy', self._on_destroy)
def _on_destroy(self, _widget: Counter) -> None:
self._context.remove_class('length-warning')
assert self._signal_id is not None
if GObject.signal_handler_is_connected(
self._textview, self._signal_id):
def do_unroot(self) -> None:
if GObject.signal_handler_is_connected(self._textview, self._signal_id):
self._textview.disconnect(self._signal_id)
self._textview.get_style_context().remove_provider(self._provider)
del self._config
del self._provider
del self._textview
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 = '''
.length-warning > * {
css = """
.length-warning {
color: %s;
background-color: %s;
}
''' % (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._inverted_color,
self._color,
)
self._provider.load_from_string(css)
def _set_count(self, count: int) -> None:
self.set_label(str(count))
......@@ -196,38 +200,36 @@ 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._textview.add_css_class("length-warning")
else:
self._context.remove_class('length-warning')
self._textview.remove_css_class("length-warning")
else:
self._set_count(0)
self._context.remove_class('length-warning')
self._textview.remove_css_class("length-warning")
return False
def _jid_allowed(self, current_jid: JID) -> bool:
jids = self._config['JIDS']
if isinstance(jids, list):
# Gajim 1.0 stored this as list[str]
jids = ','.join(jids)
jids = cast(str, self._config["JIDS"])
assert isinstance(jids, str)
if not len(jids):
if not 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 +243,6 @@ class Counter(Gtk.Label):
self._update()
def reset(self) -> None:
self._context.remove_class('length-warning')
self._textview.remove_css_class("length-warning")
self._parse_config()
self._set_css()