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 (369)
Showing
with 674 additions and 330 deletions
from typing import Any
import functools
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"]
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"
REQUIRED_KEYS = {
"authors",
"description",
"homepage",
"name",
"platforms",
"requirements",
"short_name",
"version",
}
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) # noqa: S321
console.print("Successfully connected to", FTP_URL)
func(ftp, *args)
ftp.quit()
console.print("Quit")
return func_wrapper
def is_manifest_valid(manifest: ManifestT) -> bool:
manifest_keys = set(manifest.keys())
return REQUIRED_KEYS.issubset(manifest_keys)
def download_package_index() -> ManifestT:
console.print("Download package index")
r = requests.get(PACKAGE_INDEX_URL, timeout=30)
if r.status_code == 404:
return {}
r.raise_for_status()
index = r.json()
return index
def iter_manifests() -> Iterator[PackageT]:
for path in REPO_ROOT.rglob("plugin-manifest.json"):
with path.open() as f:
manifest = json.load(f)
yield manifest, path.parent
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):
sys.exit("Invalid manifest found")
short_name = manifest["short_name"]
version = manifest["version"]
try:
index["plugins"][short_name][version]
except KeyError:
packages_to_publish.append((manifest, path))
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}"
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:
name = filepath.name
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:
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:
ftp.mkd(folder)
@ftp_connection
def deploy(ftp: FTP_TLS, packages_to_publish: list[PackageT]) -> None:
ftp.cwd(REPOSITORY_FOLDER)
create_release_folder(ftp, packages_to_publish)
for manifest, path in packages_to_publish:
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"
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("..")
console.print("Deployed", package_name)
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")
else:
deploy(packages_to_publish)
...@@ -8,3 +8,7 @@ __pycache__/ ...@@ -8,3 +8,7 @@ __pycache__/
.vscode .vscode
.mypy_cache .mypy_cache
*.swp *.swp
*.sublime-workspace
*.sublime-project
build/
.venv
image: plugins-master:latest
stages: stages:
- test - 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
run-test: test-black:
image: gajim-test
stage: test stage: test
rules:
- changes:
- "**/*.py"
script: script:
- rm -rf civenv-plugins - pip install black==24.10.0
- virtualenv -p python3 --system-site-packages civenv-plugins - black --version
- . ./civenv-plugins/bin/activate - black . --check
# - pip3 install -I pylint==2.4.4 interruptible: true
# - python3 -m pylint acronyms_expander
# - python3 -m pylint anti_spam
- deactivate
- rm -rf civenv-plugins
\ No newline at end of file
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.9.7
hooks:
- id: ruff
exclude: ".githooks/"
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
pass_filenames: false
additional_dependencies:
- tomli
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.394
hooks:
- id: pyright
pass_filenames: false
additional_dependencies:
- nbxmpp @ git+https://dev.gajim.org/gajim/python-nbxmpp.git
- PyGObject-stubs @ git+https://github.com/pygobject/pygobject-stubs.git
stages: [manual]
- repo: https://github.com/pycqa/isort
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]
...@@ -4,6 +4,9 @@ In this place you will find all plugins that are written for [Gajim](https://gaj ...@@ -4,6 +4,9 @@ In this place you will find all plugins that are written for [Gajim](https://gaj
## How to install plugins ## How to install plugins
**Note:** Some plugins have external dependencies that need to be installed separately.
Check the [plugin's wiki page](https://dev.gajim.org/gajim/gajim-plugins/-/wikis/home#plugins-list) for details.
There are several ways to install a plugin: There are several ways to install a plugin:
- You can browse / download / enable / configure plugins from within Gajim via 'Gajim' > 'Plugins' menu. - You can browse / download / enable / configure plugins from within Gajim via 'Gajim' > 'Plugins' menu.
...@@ -17,18 +20,19 @@ There are several ways to install a plugin: ...@@ -17,18 +20,19 @@ There are several ways to install a plugin:
**Symlink:** `ln -s /path/to/gajim-plugins-repository/* ~/.local/share/gajim/plugins/` **Symlink:** `ln -s /path/to/gajim-plugins-repository/* ~/.local/share/gajim/plugins/`
**For each major Gajim version there is a different plugins branch:** **For each major Gajim version there is a different plugins branch. Gajim >=1.4 uses the `master` branch.**
| Version | Plugins branch | | Version | Plugins branch |
| ------- | -------------- | | ------- | -------------- |
|Gajim master|[master branch](https://dev.gajim.org/gajim/gajim-plugins/tree/master)| |Gajim master|[master branch](https://dev.gajim.org/gajim/gajim-plugins/tree/master)|
|Gajim 1.3|[1.3 branch](https://dev.gajim.org/gajim/gajim-plugins/tree/gajim_1.3)|
|Gajim 1.2|[1.2 branch](https://dev.gajim.org/gajim/gajim-plugins/tree/gajim_1.2)| |Gajim 1.2|[1.2 branch](https://dev.gajim.org/gajim/gajim-plugins/tree/gajim_1.2)|
|Gajim 1.1|[1.1 branch](https://dev.gajim.org/gajim/gajim-plugins/tree/gajim_1.1)| |Gajim 1.1|[1.1 branch](https://dev.gajim.org/gajim/gajim-plugins/tree/gajim_1.1)|
|Gajim 1.0|[1.0 branch](https://dev.gajim.org/gajim/gajim-plugins/tree/gajim_1.0)| |Gajim 1.0|[1.0 branch](https://dev.gajim.org/gajim/gajim-plugins/tree/gajim_1.0)|
*Note: Using master branch for plugins requires frequent updates of both Gajim and plugins!* *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? You have written a new plugin or want to improve an existing one?
...@@ -37,6 +41,7 @@ First, thanks for that! Here is how to start: ...@@ -37,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) - 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) - 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) - 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). - 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 - 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 from .acronyms_expander import AcronymsExpanderPlugin # pyright: ignore # noqa: F401
...@@ -15,79 +15,96 @@ ...@@ -15,79 +15,96 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Acronyms Expander. If not, see <http://www.gnu.org/licenses/>. # along with Acronyms Expander. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import json import json
import logging import logging
from pathlib import Path
from functools import partial from functools import partial
from pathlib import Path
from gi.repository import GLib from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gtk
from gajim.common import app
from gajim.common import configpaths 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 import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from acronyms_expander.acronyms import DEFAULT_DATA from acronyms_expander.acronyms import DEFAULT_DATA
from acronyms_expander.gtk.config import ConfigDialog from acronyms_expander.gtk.config import ConfigDialog
log = logging.getLogger('gajim.p.acronyms') log = logging.getLogger("gajim.p.acronyms")
class AcronymsExpanderPlugin(GajimPlugin): class AcronymsExpanderPlugin(GajimPlugin):
def init(self): def init(self) -> None:
self.description = _('Replaces acronyms (or other strings) ' self.description = _(
'with given expansions/substitutes.') "Replaces acronyms (or other strings) with given expansions/substitutes."
)
self.config_dialog = partial(ConfigDialog, self) self.config_dialog = partial(ConfigDialog, self)
self.gui_extension_points = { self.gui_extension_points = {
'chat_control_base': (self._connect, self._disconnect) "message_input": (self._connect, None),
"switch_contact": (self._on_switch_contact, None),
} }
self._invoker = ' ' self._invoker = " "
self._replace_in_progress = False self._replace_in_progress = False
self._handler_ids = {}
self._signal_id = None
self._message_input = None
self._contact = None
self.acronyms = self._load_acronyms() self.acronyms = self._load_acronyms()
@staticmethod @staticmethod
def _load_acronyms(): def _load_acronyms() -> dict[str, str]:
try: try:
data_path = Path(configpaths.get('PLUGINS_DATA')) data_path = Path(configpaths.get("PLUGINS_DATA"))
except KeyError: except KeyError:
# PLUGINS_DATA was added in 1.0.99.1 # PLUGINS_DATA was added in 1.0.99.1
return DEFAULT_DATA return DEFAULT_DATA
path = data_path / 'acronyms' / 'acronyms' path = data_path / "acronyms" / "acronyms"
if not path.exists(): if not path.exists():
return DEFAULT_DATA return DEFAULT_DATA
with path.open('r') as file: with path.open("r") as file:
acronyms = json.load(file) acronyms = json.load(file)
return acronyms return acronyms
@staticmethod @staticmethod
def _save_acronyms(acronyms): def _save_acronyms(acronyms: dict[str, str]) -> None:
try: try:
data_path = Path(configpaths.get('PLUGINS_DATA')) data_path = Path(configpaths.get("PLUGINS_DATA"))
except KeyError: except KeyError:
# PLUGINS_DATA was added in 1.0.99.1 # PLUGINS_DATA was added in 1.0.99.1
return return
path = data_path / 'acronyms' path = data_path / "acronyms"
if not path.exists(): if not path.exists():
path.mkdir(parents=True) path.mkdir(parents=True)
filepath = path / 'acronyms' filepath = path / "acronyms"
with filepath.open('w') as file: with filepath.open("w") as file:
json.dump(acronyms, file) json.dump(acronyms, file)
def set_acronyms(self, acronyms): def set_acronyms(self, acronyms: dict[str, str]) -> None:
self.acronyms = acronyms self.acronyms = acronyms
self._save_acronyms(acronyms) self._save_acronyms(acronyms)
def _on_buffer_changed(self, _textview, buffer_, contact, account): def _on_buffer_changed(self, message_input: MessageInputTextView) -> None:
if self._contact is None:
# If no chat has been activated yet
return
if self._replace_in_progress: if self._replace_in_progress:
return return
buffer_ = message_input.get_buffer()
if buffer_.get_char_count() < 2: if buffer_.get_char_count() < 2:
return return
# Get iter at cursor # Get iter at cursor
...@@ -104,54 +121,62 @@ class AcronymsExpanderPlugin(GajimPlugin): ...@@ -104,54 +121,62 @@ class AcronymsExpanderPlugin(GajimPlugin):
return return
# Get to the start of the last word # Get to the start of the last word
word_start_iter = insert_iter.copy() # word_start_iter = insert_iter.copy()
word_start_iter.backward_word_start() result = insert_iter.backward_search(
self._invoker, Gtk.TextSearchFlags.VISIBLE_ONLY, None
)
if result is None:
word_start_iter = buffer_.get_start_iter()
else:
_, word_start_iter = result
# Get last word and cut invoker # Get last word and cut invoker
last_word = word_start_iter.get_slice(insert_iter).strip() last_word = word_start_iter.get_slice(insert_iter)
if contact.is_groupchat: if isinstance(self._contact, GroupchatContact):
nick_list = app.contacts.get_nick_list(account, contact.jid) if last_word in self._contact.get_user_nicknames():
if last_word in nick_list: log.info("Groupchat participant has same nick as acronym")
log.info('Groupchat participant has same nick as acronym')
return return
if contact.is_pm_contact: if self._contact.is_pm_contact:
if last_word == contact.get_shown_name(): if last_word == self._contact.name:
log.info('Contact name equals acronym') log.info("Contact name equals acronym")
return return
substitute = self.acronyms.get(last_word) substitute = self.acronyms.get(last_word)
if substitute is None: if substitute is None:
log.debug('%s not an acronym', last_word) log.debug("%s not an acronym", last_word)
return return
# Replace GLib.idle_add(
word_end_iter = word_start_iter.copy() self._replace_text, buffer_, word_start_iter, insert_iter, substitute
word_end_iter.forward_word_end() )
GLib.idle_add(self._replace_text,
buffer_, def _replace_text(
word_start_iter, self,
word_end_iter, buffer_: Gtk.TextBuffer,
substitute) start: Gtk.TextIter,
end: Gtk.TextIter,
substitute: str,
) -> None:
def _replace_text(self, buffer_, start, end, substitute):
self._replace_in_progress = True self._replace_in_progress = True
buffer_.delete(start, end) buffer_.delete(start, end)
buffer_.insert(start, substitute) buffer_.insert(start, substitute)
self._replace_in_progress = False self._replace_in_progress = False
def _connect(self, chat_control): def _on_switch_contact(self, contact: types.ChatContactT) -> None:
textview = chat_control.msg_textview self._contact = contact
handler_id = textview.connect('text-changed',
self._on_buffer_changed, def _connect(self, message_input: MessageInputTextView) -> None:
chat_control.contact, self._message_input = message_input
chat_control.account) self._signal_id = message_input.connect(
self._handler_ids[id(textview)] = handler_id "buffer-changed", self._on_buffer_changed
)
def _disconnect(self, chat_control):
textview = chat_control.msg_textview def deactivate(self) -> None:
handler_id = self._handler_ids.get(id(textview)) assert self._message_input is not None
if handler_id is not None: assert self._signal_id is not None
textview.disconnect(handler_id) if GObject.signal_handler_is_connected(self._message_input, self._signal_id):
del self._handler_ids[id(textview)] self._message_input.disconnect(self._signal_id)
...@@ -14,74 +14,114 @@ ...@@ -14,74 +14,114 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Acronyms Expander. If not, see <http://www.gnu.org/licenses/>. # along with Acronyms Expander. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import cast
from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk
from gajim.common import app from gajim.gtk.widgets import GajimAppWindow
from gajim.plugins.plugins_i18n import _
from gajim.plugins.helpers import get_builder from gajim.plugins.helpers import get_builder
from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING:
from ..acronyms_expander import AcronymsExpanderPlugin
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
class ConfigDialog(Gtk.ApplicationWindow):
def __init__(self, plugin, transient): class ConfigDialog(GajimAppWindow):
Gtk.ApplicationWindow.__init__(self) def __init__(self, plugin: AcronymsExpanderPlugin, transient: Gtk.Window) -> None:
self.set_application(app.app)
self.set_show_menubar(False) GajimAppWindow.__init__(
self.set_title(_('Acronyms Configuration')) self,
self.set_transient_for(transient) name="AcronymsConfigDialog",
self.set_default_size(400, 400) title=_("Acronyms Configuration"),
self.set_type_hint(Gdk.WindowTypeHint.DIALOG) default_width=400,
self.set_modal(True) default_height=400,
self.set_destroy_with_parent(True) transient_for=transient,
modal=True,
)
ui_path = Path(__file__).parent ui_path = Path(__file__).parent
self._ui = get_builder(ui_path.resolve() / 'config.ui') self._ui = cast(
ConfigBuilder, get_builder(str(ui_path.resolve() / "config.ui"))
)
self._plugin = plugin self._plugin = plugin
self.add(self._ui.box) self.set_child(self._ui.box)
self._fill_list() self._fill_list()
self.show_all()
self._ui.connect_signals(self) self._connect(self._ui.acronym_renderer, "edited", self._on_acronym_edited)
self.connect('destroy', self._on_destroy) 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): def _fill_list(self) -> None:
for acronym, substitute in self._plugin.acronyms.items(): for acronym, substitute in self._plugin.acronyms.items():
self._ui.acronyms_store.append([acronym, substitute]) self._ui.acronyms_store.append([acronym, substitute])
def _on_acronym_edited(self, _renderer, path, new_text): def _on_acronym_edited(
self, _renderer: Gtk.CellRendererText, path: str, new_text: str
) -> None:
iter_ = self._ui.acronyms_store.get_iter(path) iter_ = self._ui.acronyms_store.get_iter(path)
self._ui.acronyms_store.set_value(iter_, 0, new_text) self._ui.acronyms_store.set_value(iter_, 0, new_text)
def _on_substitute_edited(self, _renderer, path, new_text): def _on_substitute_edited(
self, _renderer: Gtk.CellRendererText, path: str, new_text: str
) -> None:
iter_ = self._ui.acronyms_store.get_iter(path) iter_ = self._ui.acronyms_store.get_iter(path)
self._ui.acronyms_store.set_value(iter_, 1, new_text) self._ui.acronyms_store.set_value(iter_, 1, new_text)
def _on_add_clicked(self, _button): def _on_add_clicked(self, _button: Gtk.Button) -> None:
self._ui.acronyms_store.append(['', '']) self._ui.acronyms_store.append(["", ""])
row = self._ui.acronyms_store[-1] row = self._ui.acronyms_store[-1]
self._ui.acronyms_treeview.scroll_to_cell( self._ui.acronyms_treeview.scroll_to_cell(row.path, None, False, 0, 0)
row.path, None, False, 0, 0)
self._ui.selection.unselect_all() self._ui.selection.unselect_all()
self._ui.selection.select_path(row.path) self._ui.selection.select_path(row.path)
def _on_remove_clicked(self, _button): def _on_remove_clicked(self, _button: Gtk.Button) -> None:
model, paths = self._ui.selection.get_selected_rows() res = self._ui.selection.get_selected_rows()
references = [] if res is None:
return
model, paths = res
references: list[Gtk.TreeRowReference] = []
for path in paths: 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: 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_) self._ui.acronyms_store.remove(iter_)
def _on_destroy(self, *args): def _on_close_request(self, win: Gtk.ApplicationWindow) -> None:
acronyms = {} acronyms: dict[str, str] = {}
for row in self._ui.acronyms_store: for row in self._ui.acronyms_store:
acronym, substitute = row acronym, substitute = row
if not acronym or not substitute: if not acronym or not substitute:
......
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk" version="4.0"/>
<object class="GtkListStore" id="acronyms_store"> <object class="GtkListStore" id="acronyms_store">
<columns> <columns>
<!-- column-name acronym -->
<column type="gchararray"/> <column type="gchararray"/>
<!-- column-name substitute -->
<column type="gchararray"/> <column type="gchararray"/>
</columns> </columns>
</object> </object>
<object class="GtkBox" id="box"> <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> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="visible">True</property> <property name="focusable">1</property>
<property name="can_focus">True</property> <property name="vexpand">1</property>
<property name="vexpand">True</property> <property name="hexpand">1</property>
<property name="shadow_type">in</property> <property name="child">
<child>
<object class="GtkTreeView" id="acronyms_treeview"> <object class="GtkTreeView" id="acronyms_treeview">
<property name="visible">True</property> <property name="focusable">1</property>
<property name="can_focus">True</property>
<property name="model">acronyms_store</property> <property name="model">acronyms_store</property>
<property name="search_column">1</property> <property name="search_column">1</property>
<child internal-child="selection"> <child internal-child="selection">
...@@ -34,15 +26,14 @@ ...@@ -34,15 +26,14 @@
</child> </child>
<child> <child>
<object class="GtkTreeViewColumn"> <object class="GtkTreeViewColumn">
<property name="resizable">True</property> <property name="resizable">1</property>
<property name="title" translatable="yes">Acronym</property> <property name="title" translatable="yes">Acronym</property>
<property name="clickable">True</property> <property name="clickable">1</property>
<property name="sort_indicator">True</property> <property name="sort_indicator">1</property>
<property name="sort_column_id">0</property> <property name="sort_column_id">0</property>
<child> <child>
<object class="GtkCellRendererText"> <object class="GtkCellRendererText" id="acronym_renderer">
<property name="editable">True</property> <property name="editable">1</property>
<signal name="edited" handler="_on_acronym_edited" swapped="no"/>
</object> </object>
<attributes> <attributes>
<attribute name="text">0</attribute> <attribute name="text">0</attribute>
...@@ -52,15 +43,14 @@ ...@@ -52,15 +43,14 @@
</child> </child>
<child> <child>
<object class="GtkTreeViewColumn"> <object class="GtkTreeViewColumn">
<property name="resizable">True</property> <property name="resizable">1</property>
<property name="title" translatable="yes">Substitute</property> <property name="title" translatable="yes">Substitute</property>
<property name="clickable">True</property> <property name="clickable">1</property>
<property name="sort_indicator">True</property> <property name="sort_indicator">1</property>
<property name="sort_column_id">0</property> <property name="sort_column_id">0</property>
<child> <child>
<object class="GtkCellRendererText"> <object class="GtkCellRendererText" id="sub_renderer">
<property name="editable">True</property> <property name="editable">1</property>
<signal name="edited" handler="_on_substitute_edited" swapped="no"/>
</object> </object>
<attributes> <attributes>
<attribute name="text">1</attribute> <attribute name="text">1</attribute>
...@@ -69,55 +59,28 @@ ...@@ -69,55 +59,28 @@
</object> </object>
</child> </child>
</object> </object>
</child> </property>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkToolbar"> <object class="GtkBox">
<property name="visible">True</property> <property name="css-classes">toolbar</property>
<property name="can_focus">False</property> <style>
<property name="toolbar_style">icons</property> <class name="inline-toolbar"/>
<property name="icon_size">4</property> </style>
<child> <child>
<object class="GtkToolButton"> <object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Add</property> <property name="tooltip_text" translatable="yes">Add</property>
<property name="icon_name">list-add-symbolic</property> <property name="icon_name">list-add-symbolic</property>
<signal name="clicked" handler="_on_add_clicked" swapped="no"/>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkToolButton"> <object class="GtkButton" id="remove_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Remove</property> <property name="tooltip_text" translatable="yes">Remove</property>
<property name="icon_name">list-remove-symbolic</property> <property name="icon_name">list-remove-symbolic</property>
<signal name="clicked" handler="_on_remove_clicked" swapped="no"/>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child> </child>
<style>
<class name="inline-toolbar"/>
</style>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child> </child>
</object> </object>
</interface> </interface>
[info]
name: Acronyms Expander
short_name: acronyms_expander
version: 1.3.0
description: Replaces acronyms (or other strings) with given expansions/substitutes.
authors: Philipp Hörist <philipp@hoerist.com>
Mateusz Biliński <mateusz@bilinski.it>
homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/AcronymsExpanderPlugin
min_gajim_version: 1.2.91
max_gajim_version: 1.3.90
<?xml version="1.0" encoding="UTF-8"?>
<component type="addon">
<id>org.gajim.Gajim.Plugin.acronyms_expander</id>
<extends>org.gajim.Gajim</extends>
<name>Acronyms Expander Plugin</name>
<summary>Replace acronyms (or other strings) with given expansions/substitutes</summary>
<url type="homepage">https://gajim.org/</url>
<metadata_license>CC-BY-SA-3.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<update_contact>gajim-devel_AT_gajim.org</update_contact>
</component>
{
"authors": [
"Philipp Hörist <philipp@hoerist.com>",
"Mateusz Biliński <mateusz@bilinski.it>"
],
"description": "Replaces acronyms (or other strings) with given expansions/substitutes.",
"homepage": "https://dev.gajim.org/gajim/gajim-plugins/wikis/AcronymsExpanderPlugin",
"config_dialog": true,
"name": "Acronyms Expander",
"platforms": [
"others",
"linux",
"darwin",
"win32"
],
"requirements": [
"gajim>=2.0.0"
],
"short_name": "acronyms_expander",
"version": "1.6.0"
}
\ No newline at end of file
from .anti_spam import AntiSpamPlugin from .anti_spam import AntiSpamPlugin # pyright: ignore # noqa: F401
...@@ -11,39 +11,40 @@ ...@@ -11,39 +11,40 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
#
''' """
:author: Yann Leboulanger <asterix@lagaule.org> :author: Yann Leboulanger <asterix@lagaule.org>
:since: 16 August 2012 :since: 16 August 2012
:copyright: Copyright (2012) Yann Leboulanger <asterix@lagaule.org> :copyright: Copyright (2012) Yann Leboulanger <asterix@lagaule.org>
:license: GPLv3 :license: GPLv3
''' """
from functools import partial from functools import partial
from gajim.plugins import GajimPlugin from gajim.plugins import GajimPlugin
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
from anti_spam.modules import anti_spam
from anti_spam.config_dialog import AntiSpamConfigDialog from anti_spam.config_dialog import AntiSpamConfigDialog
from anti_spam.modules import anti_spam
class AntiSpamPlugin(GajimPlugin): class AntiSpamPlugin(GajimPlugin):
def init(self): def init(self) -> None:
self.description = _('Allows you to block various kinds of incoming ' self.description = _(
'messages (Spam, XHTML formatting, etc.)') "Allows you to block various kinds of incoming "
"messages (Spam, XHTML formatting, etc.)"
)
self.config_dialog = partial(AntiSpamConfigDialog, self) self.config_dialog = partial(AntiSpamConfigDialog, self)
self.config_default_values = { self.config_default_values = {
'disable_xhtml_muc': (False, ''), "disable_xhtml_muc": (False, ""),
'disable_xhtml_pm': (False, ''), "disable_xhtml_pm": (False, ""),
'block_subscription_requests': (False, ''), "block_subscription_requests": (False, ""),
'msgtxt_limit': (0, ''), "msgtxt_limit": (0, ""),
'msgtxt_question': ('12 x 12 = ?', ''), "msgtxt_question": ("12 x 12 = ?", ""),
'msgtxt_answer': ('', ''), "msgtxt_answer": ("", ""),
'antispam_for_conference': (False, ''), "antispam_for_conference": (False, ""),
'block_domains': ('', ''), "block_domains": ("", ""),
'whitelist': ([], ''), "whitelist": ([], ""),
} }
self.gui_extension_points = {} self.gui_extension_points = {}
self.modules = [anti_spam] self.modules = [anti_spam]
...@@ -13,87 +13,114 @@ ...@@ -13,87 +13,114 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
from typing import Any
from typing import cast
from typing import TYPE_CHECKING
from gi.repository import Gtk from gi.repository import Gtk
from gajim.gtk.settings import SettingsDialog
from gajim.gtk.const import Setting from gajim.gtk.const import Setting
from gajim.gtk.const import SettingKind from gajim.gtk.const import SettingKind
from gajim.gtk.const import SettingType from gajim.gtk.const import SettingType
from gajim.gtk.settings import SettingsDialog
from gajim.plugins.plugins_i18n import _ from gajim.plugins.plugins_i18n import _
if TYPE_CHECKING:
from .anti_spam import AntiSpamPlugin
class AntiSpamConfigDialog(SettingsDialog): class AntiSpamConfigDialog(SettingsDialog):
def __init__(self, plugin, parent): def __init__(self, plugin: AntiSpamPlugin, parent: Gtk.Window) -> None:
self.plugin = plugin self.plugin = plugin
msgtxt_limit = self.plugin.config['msgtxt_limit'] msgtxt_limit = cast(int, self.plugin.config["msgtxt_limit"])
max_length = '' if msgtxt_limit == 0 else msgtxt_limit max_length = "" if msgtxt_limit == 0 else msgtxt_limit
settings = [ settings = [
Setting(SettingKind.ENTRY, Setting(
_('Limit Message Length'), SettingKind.ENTRY,
SettingType.VALUE, _("Limit Message Length"),
max_length, SettingType.VALUE,
callback=self._on_length_setting, str(max_length),
data='msgtxt_limit', callback=self._on_length_setting,
desc=_('Limits maximum message length (leave empty to ' data="msgtxt_limit",
'disable)')), desc=_("Limits maximum message length (leave empty to disable)"),
Setting(SettingKind.SWITCH, ),
_('Deny Subscription Requests'), Setting(
SettingType.VALUE, SettingKind.SWITCH,
self.plugin.config['block_subscription_requests'], _("Deny Subscription Requests"),
callback=self._on_setting, SettingType.VALUE,
data='block_subscription_requests'), self.plugin.config["block_subscription_requests"],
Setting(SettingKind.SWITCH, callback=self._on_setting,
_('Disable XHTML for Group Chats'), data="block_subscription_requests",
SettingType.VALUE, ),
self.plugin.config['disable_xhtml_muc'], Setting(
callback=self._on_setting, SettingKind.SWITCH,
data='disable_xhtml_muc', _("Disable XHTML for Group Chats"),
desc=_('Removes XHTML formatting from group chat ' SettingType.VALUE,
'messages')), self.plugin.config["disable_xhtml_muc"],
Setting(SettingKind.SWITCH, callback=self._on_setting,
_('Disable XHTML for PMs'), data="disable_xhtml_muc",
SettingType.VALUE, desc=_("Removes XHTML formatting from group chat messages"),
self.plugin.config['disable_xhtml_pm'], ),
callback=self._on_setting, Setting(
data='disable_xhtml_pm', SettingKind.SWITCH,
desc=_('Removes XHTML formatting from private messages ' _("Disable XHTML for PMs"),
'in group chats')), SettingType.VALUE,
Setting(SettingKind.ENTRY, self.plugin.config["disable_xhtml_pm"],
_('Anti Spam Question'), callback=self._on_setting,
SettingType.VALUE, data="disable_xhtml_pm",
self.plugin.config['msgtxt_question'], desc=_("Removes XHTML formatting from private messages in group chats"),
callback=self._on_setting, ),
data='msgtxt_question', Setting(
desc=_('Question has to be answered in order to ' SettingKind.ENTRY,
'contact you')), _("Anti Spam Question"),
Setting(SettingKind.ENTRY, SettingType.VALUE,
_('Anti Spam Answer'), self.plugin.config["msgtxt_question"],
SettingType.VALUE, callback=self._on_setting,
self.plugin.config['msgtxt_answer'], data="msgtxt_question",
callback=self._on_setting, desc=_("Question has to be answered in order to contact you"),
data='msgtxt_answer', ),
desc=_('Correct answer to your Anti Spam Question ' Setting(
'(leave empty to disable question)')), SettingKind.ENTRY,
Setting(SettingKind.SWITCH, _("Anti Spam Answer"),
_('Anti Spam Question in Group Chats'), SettingType.VALUE,
SettingType.VALUE, self.plugin.config["msgtxt_answer"],
self.plugin.config['antispam_for_conference'], callback=self._on_setting,
callback=self._on_setting, data="msgtxt_answer",
data='antispam_for_conference', desc=_(
desc=_('Enables anti spam question for private messages ' "Correct answer to your Anti Spam Question "
'in group chats')), "(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'), SettingsDialog.__init__(
Gtk.DialogFlags.MODAL, settings, None) self,
parent,
_("Anti Spam Configuration"),
Gtk.DialogFlags.MODAL,
settings,
"",
)
def _on_setting(self, value, data): def _on_setting(self, value: Any, data: Any) -> None:
self.plugin.config[data] = value self.plugin.config[data] = value
def _on_length_setting(self, value, data): def _on_length_setting(self, value: str, data: str) -> None:
try: try:
self.plugin.config[data] = int(value) self.plugin.config[data] = int(value)
except Exception: except ValueError:
self.plugin.config[data] = 0 self.plugin.config[data] = 0
[info]
name: Anti Spam
short_name: anti_spam
version: 1.5.1
description: Block some incoming messages.
authors: Yann Leboulanger <asterix@lagaule.org>
Denis Fomin <fominde@gmail.com>
Ilya Kanyukov <ilya.kanukov@gmail.com>
homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/AntiSpamPlugin
min_gajim_version: 1.2.91
max_gajim_version: 1.3.90
...@@ -11,61 +11,70 @@ ...@@ -11,61 +11,70 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>. # along with Gajim. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import annotations
from typing import Any
from typing import cast
from nbxmpp import NodeProcessed from nbxmpp import NodeProcessed
from nbxmpp.protocol import JID
from nbxmpp.protocol import Message from nbxmpp.protocol import Message
from nbxmpp.protocol import Presence
from nbxmpp.structs import MessageProperties
from nbxmpp.structs import PresenceProperties
from nbxmpp.structs import StanzaHandler from nbxmpp.structs import StanzaHandler
from gajim.common import app from gajim.common import app
from gajim.common import ged from gajim.common import ged
from gajim.common.client import Client
from gajim.common.events import MessageSent
from gajim.common.modules.base import BaseModule from gajim.common.modules.base import BaseModule
# Module name # Module name
name = 'AntiSpam' name = "AntiSpam"
zeroconf = False zeroconf = False
class AntiSpam(BaseModule): class AntiSpam(BaseModule):
def __init__(self, con): def __init__(self, client: Client) -> None:
BaseModule.__init__(self, con, plugin=True) BaseModule.__init__(self, client, plugin=True)
self.handlers = [ self.handlers = [
StanzaHandler(name='message', StanzaHandler(name="message", callback=self._message_received, priority=48),
callback=self._message_received, StanzaHandler(
priority=48), name="presence",
StanzaHandler(name='presence', callback=self._subscribe_received,
callback=self._subscribe_received, typ="subscribe",
typ='subscribe', priority=48,
priority=48), ),
] ]
self.register_events([ self.register_events(
('message-sent', ged.OUT_PRECORE, self._on_message_sent), [
]) ("message-sent", ged.GUI2, self._on_message_sent),
]
)
for plugin in app.plugin_manager.plugins: for plugin in app.plugin_manager.plugins:
if plugin.short_name == 'anti_spam': if plugin.manifest.short_name == "anti_spam":
self._config = plugin.config self._config = plugin.config
self._contacted_jids = set() self._contacted_jids: set[JID] = set()
def _on_message_sent(self, event):
if event.type_ not in ('chat', 'normal'):
return
def _on_message_sent(self, event: MessageSent) -> None:
# We need self._contacted_jids in order to prevent two # We need self._contacted_jids in order to prevent two
# Anti Spam Plugins from chatting with each other. # Anti Spam Plugins from chatting with each other.
# This set contains JIDs of all outgoing chats. # This set contains JIDs of all outgoing chats.
if isinstance(event.jid, list): self._contacted_jids.add(event.jid)
for jid in event.jid:
self._contacted_jids.add(jid) def _message_received(
else: self, _con: Client, _stanza: Message, properties: MessageProperties
self._contacted_jids.add(event.jid) ) -> None:
def _message_received(self, _con, _stanza, properties):
if properties.is_sent_carbon: if properties.is_sent_carbon:
# Another device already sent a message # Another device already sent a message
assert properties.jid
self._contacted_jids.add(properties.jid) self._contacted_jids.add(properties.jid)
return return
...@@ -77,52 +86,59 @@ class AntiSpam(BaseModule): ...@@ -77,52 +86,59 @@ class AntiSpam(BaseModule):
raise NodeProcessed raise NodeProcessed
msg_from = properties.jid msg_from = properties.jid
limit = self._config['msgtxt_limit'] limit = cast(int, self._config["msgtxt_limit"])
if limit > 0 and len(msg_body) > limit: if limit > 0 and len(msg_body) > limit:
self._log.info('Discarded message from %s: message ' self._log.info(
'length exceeded' % msg_from) "Discarded message from %s: message length exceeded" % msg_from
)
raise NodeProcessed raise NodeProcessed
if self._config['disable_xhtml_muc'] and properties.type.is_groupchat: if self._config["disable_xhtml_muc"] and properties.type.is_groupchat:
properties.xhtml = None properties.xhtml = None
self._log.info('Stripped message from %s: message ' self._log.info(
'contained XHTML' % msg_from) "Stripped message from %s: message contained XHTML" % msg_from
)
if self._config['disable_xhtml_pm'] and properties.is_muc_pm: if self._config["disable_xhtml_pm"] and properties.is_muc_pm:
properties.xhtml = None properties.xhtml = None
self._log.info('Stripped message from %s: message ' self._log.info(
'contained XHTML' % msg_from) "Stripped message from %s: message contained XHTML" % msg_from
)
def _ask_question(self, properties): def _ask_question(self, properties: MessageProperties) -> bool:
answer = self._config['msgtxt_answer'] answer = cast(str, self._config["msgtxt_answer"])
if len(answer) == 0: if len(answer) == 0:
return False return False
is_muc_pm = properties.is_muc_pm is_muc_pm = properties.is_muc_pm
if is_muc_pm and not self._config['antispam_for_conference']: if is_muc_pm and not self._config["antispam_for_conference"]:
return False return False
if (properties.type.value not in ('chat', 'normal') or if properties.type.value not in ("chat", "normal") or properties.is_mam_message:
properties.is_mam_message):
return False return False
msg_from = properties.jid if is_muc_pm else properties.jid.getBare() assert properties.jid
if is_muc_pm:
msg_from = properties.jid
else:
msg_from = JID.from_string(properties.jid.bare)
if msg_from in self._contacted_jids: if msg_from in self._contacted_jids:
return False return False
# If we receive a PM or a message from an unknown user, our anti spam # If we receive a PM or a message from an unknown user, our anti spam
# question will silently be sent in the background # question will silently be sent in the background
whitelist = self._config['whitelist'] whitelist = cast(list[str], self._config["whitelist"])
if msg_from in whitelist: if str(msg_from) in whitelist:
return False return False
is_contact = app.contacts.get_contacts(self._account, msg_from) roster_item = self._client.get_module("Roster").get_item(msg_from)
if is_muc_pm or not is_contact: if is_muc_pm or roster_item is None:
if answer in properties.body.split('\n'): assert properties.body
if msg_from not in whitelist: if answer in properties.body.split("\n"):
whitelist.append(msg_from) if str(msg_from) not in whitelist:
whitelist.append(str(msg_from))
# We need to explicitly save, because 'append' does not # We need to explicitly save, because 'append' does not
# implement the __setitem__ method # implement the __setitem__ method
self._config.save() self._config.save()
...@@ -131,21 +147,26 @@ class AntiSpam(BaseModule): ...@@ -131,21 +147,26 @@ class AntiSpam(BaseModule):
return True return True
return False return False
def _send_question(self, properties, jid): def _send_question(self, properties: MessageProperties, jid: JID) -> None:
message = 'Anti Spam Question: %s' % self._config['msgtxt_question'] message = "Anti Spam Question: %s" % self._config["msgtxt_question"]
stanza = Message(to=jid, body=message, typ=properties.type.value) stanza = Message(to=jid, body=message, typ=properties.type.value)
self._con.connection.send_stanza(stanza) self._client.connection.send_stanza(stanza)
self._log.info('Anti spam question sent to %s', jid) self._log.info("Anti spam question sent to %s", jid)
def _subscribe_received(
self, _con: Client, _stanza: Presence, properties: PresenceProperties
) -> None:
def _subscribe_received(self, _con, _stanza, properties):
msg_from = properties.jid msg_from = properties.jid
block_sub = self._config['block_subscription_requests'] assert msg_from is not None
is_contact = app.contacts.get_contacts(self._account, msg_from) block_sub = self._config["block_subscription_requests"]
if block_sub and not is_contact: roster_item = self._client.get_module("Roster").get_item(msg_from)
self._con.get_module('Presence').unsubscribed(msg_from)
self._log.info('Denied subscription request from %s' % 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)
raise NodeProcessed raise NodeProcessed
def get_instance(*args, **kwargs): def get_instance(*args: Any, **kwargs: Any) -> tuple[AntiSpam, str]:
return AntiSpam(*args, **kwargs), 'AntiSpam' return AntiSpam(*args, **kwargs), "AntiSpam"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<component type="addon"> <component type="addon">
<id>org.gajim.Gajim.Plugin.url_image_preview</id> <id>org.gajim.Gajim.Plugin.anti_spam</id>
<extends>org.gajim.Gajim</extends> <extends>org.gajim.Gajim</extends>
<name>URL Image Preview Plugin</name> <name>Anti Spam Plugin</name>
<summary>Display a preview of links to images</summary> <summary>Block some incoming messages</summary>
<url type="homepage">https://gajim.org/</url> <url type="homepage">https://gajim.org/</url>
<metadata_license>CC-BY-SA-3.0</metadata_license> <metadata_license>CC-BY-SA-3.0</metadata_license>
<project_license>GPL-3.0</project_license> <project_license>GPL-3.0-only</project_license>
<update_contact>gajim-devel_AT_gajim.org</update_contact> <update_contact>gajim-devel_AT_gajim.org</update_contact>
</component> </component>
{
"authors": [
"Yann Leboulanger <asterix@lagaule.org>",
"Denis Fomin <fominde@gmail.com>",
"Ilya Kanyukov <ilya.kanukov@gmail.com>"
],
"description": "Block some incoming messages.",
"homepage": "https://dev.gajim.org/gajim/gajim-plugins/wikis/AntiSpamPlugin",
"config_dialog": true,
"name": "Anti Spam",
"platforms": [
"others",
"linux",
"darwin",
"win32"
],
"requirements": [
"gajim>=2.0.0"
],
"short_name": "anti_spam",
"version": "1.7.0"
}
\ No newline at end of file
from .plugin import AppindicatorIntegrationPlugin