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__/
.vscode
.mypy_cache
*.swp
*.sublime-workspace
*.sublime-project
build/
.venv
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
run-test:
test-black:
image: gajim-test
stage: test
rules:
- changes:
- "**/*.py"
script:
- rm -rf civenv-plugins
- virtualenv -p python3 --system-site-packages civenv-plugins
- . ./civenv-plugins/bin/activate
# - pip3 install -I pylint==2.4.4
# - python3 -m pylint acronyms_expander
# - python3 -m pylint anti_spam
- deactivate
- rm -rf civenv-plugins
\ No newline at end of file
- pip install black==24.10.0
- black --version
- black . --check
interruptible: true
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
## 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:
- 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:
**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 |
| ------- | -------------- |
|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.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)|
*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?
......@@ -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)
- 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
from .acronyms_expander import AcronymsExpanderPlugin # pyright: ignore # noqa: F401
......@@ -15,79 +15,96 @@
# You should have received a copy of the GNU General Public License
# along with Acronyms Expander. If not, see <http://www.gnu.org/licenses/>.
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
from gi.repository import Gtk
from gajim.common import app
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):
self.description = _('Replaces acronyms (or other strings) '
'with given expansions/substitutes.')
def init(self) -> None:
self.description = _(
"Replaces acronyms (or other strings) with given expansions/substitutes."
)
self.config_dialog = partial(ConfigDialog, self)
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._handler_ids = {}
self._signal_id = None
self._message_input = None
self._contact = None
self.acronyms = self._load_acronyms()
@staticmethod
def _load_acronyms():
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):
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):
def set_acronyms(self, acronyms: dict[str, str]) -> None:
self.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:
return
buffer_ = message_input.get_buffer()
if buffer_.get_char_count() < 2:
return
# Get iter at cursor
......@@ -104,54 +121,62 @@ class AcronymsExpanderPlugin(GajimPlugin):
return
# Get to the start of the last word
word_start_iter = insert_iter.copy()
word_start_iter.backward_word_start()
# word_start_iter = insert_iter.copy()
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
last_word = word_start_iter.get_slice(insert_iter).strip()
last_word = word_start_iter.get_slice(insert_iter)
if contact.is_groupchat:
nick_list = app.contacts.get_nick_list(account, contact.jid)
if last_word in nick_list:
log.info('Groupchat participant has same nick as acronym')
if isinstance(self._contact, GroupchatContact):
if last_word in self._contact.get_user_nicknames():
log.info("Groupchat participant has same nick as acronym")
return
if contact.is_pm_contact:
if last_word == contact.get_shown_name():
log.info('Contact name equals acronym')
if self._contact.is_pm_contact:
if last_word == self._contact.name:
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
# Replace
word_end_iter = word_start_iter.copy()
word_end_iter.forward_word_end()
GLib.idle_add(self._replace_text,
buffer_,
word_start_iter,
word_end_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_, start, end, substitute):
self._replace_in_progress = True
buffer_.delete(start, end)
buffer_.insert(start, substitute)
self._replace_in_progress = False
def _connect(self, chat_control):
textview = chat_control.msg_textview
handler_id = textview.connect('text-changed',
self._on_buffer_changed,
chat_control.contact,
chat_control.account)
self._handler_ids[id(textview)] = handler_id
def _disconnect(self, chat_control):
textview = chat_control.msg_textview
handler_id = self._handler_ids.get(id(textview))
if handler_id is not None:
textview.disconnect(handler_id)
del self._handler_ids[id(textview)]
def _on_switch_contact(self, contact: types.ChatContactT) -> None:
self._contact = contact
def _connect(self, message_input: MessageInputTextView) -> None:
self._message_input = message_input
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):
self._message_input.disconnect(self._signal_id)
......@@ -14,74 +14,114 @@
# You should have received a copy of the GNU General Public License
# 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 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 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):
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(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):
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, 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)
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)
self._ui.acronyms_store.set_value(iter_, 1, new_text)
def _on_add_clicked(self, _button):
self._ui.acronyms_store.append(['', ''])
def _on_add_clicked(self, _button: Gtk.Button) -> None:
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):
model, paths = self._ui.selection.get_selected_rows()
references = []
def _on_remove_clicked(self, _button: Gtk.Button) -> None:
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):
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>
[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 @@
#
# 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):
self.description = _('Allows you to block various kinds of incoming '
'messages (Spam, XHTML formatting, etc.)')
def init(self) -> None:
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]
......@@ -13,87 +13,114 @@
# You should have received a copy of the GNU General Public License
# 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 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 .anti_spam import AntiSpamPlugin
class AntiSpamConfigDialog(SettingsDialog):
def __init__(self, plugin, parent):
def __init__(self, plugin: AntiSpamPlugin, parent: Gtk.Window) -> None:
self.plugin = plugin
msgtxt_limit = 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,
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, None)
SettingsDialog.__init__(
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
def _on_length_setting(self, value, data):
def _on_length_setting(self, value: str, data: str) -> None:
try:
self.plugin.config[data] = int(value)
except Exception:
except ValueError:
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 @@
#
# You should have received a copy of the GNU General Public License
# 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.protocol import JID
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 gajim.common import app
from gajim.common import ged
from gajim.common.client import Client
from gajim.common.events import MessageSent
from gajim.common.modules.base import BaseModule
# Module name
name = 'AntiSpam'
name = "AntiSpam"
zeroconf = False
class AntiSpam(BaseModule):
def __init__(self, con):
BaseModule.__init__(self, con, plugin=True)
def __init__(self, client: Client) -> None:
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.OUT_PRECORE, self._on_message_sent),
])
self.register_events(
[
("message-sent", ged.GUI2, self._on_message_sent),
]
)
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._contacted_jids = set()
def _on_message_sent(self, event):
if event.type_ not in ('chat', 'normal'):
return
self._contacted_jids: set[JID] = set()
def _on_message_sent(self, event: MessageSent) -> None:
# We need self._contacted_jids in order to prevent two
# Anti Spam Plugins from chatting with each other.
# This set contains JIDs of all outgoing chats.
if isinstance(event.jid, list):
for jid in event.jid:
self._contacted_jids.add(jid)
else:
self._contacted_jids.add(event.jid)
self._contacted_jids.add(event.jid)
def _message_received(
self, _con: Client, _stanza: Message, properties: MessageProperties
) -> None:
def _message_received(self, _con, _stanza, properties):
if properties.is_sent_carbon:
# Another device already sent a message
assert properties.jid
self._contacted_jids.add(properties.jid)
return
......@@ -77,52 +86,59 @@ class AntiSpam(BaseModule):
raise NodeProcessed
msg_from = properties.jid
limit = 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):
answer = self._config['msgtxt_answer']
def _ask_question(self, properties: MessageProperties) -> bool:
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
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:
return False
# If we receive a PM or a message from an unknown user, our anti spam
# question will silently be sent in the background
whitelist = self._config['whitelist']
if msg_from in whitelist:
whitelist = cast(list[str], self._config["whitelist"])
if str(msg_from) in whitelist:
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 answer in properties.body.split('\n'):
if msg_from not in whitelist:
whitelist.append(msg_from)
if is_muc_pm or roster_item is None:
assert properties.body
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
# implement the __setitem__ method
self._config.save()
......@@ -131,21 +147,26 @@ class AntiSpam(BaseModule):
return True
return False
def _send_question(self, properties, jid):
message = 'Anti Spam Question: %s' % self._config['msgtxt_question']
def _send_question(self, properties: MessageProperties, jid: JID) -> None:
message = "Anti Spam Question: %s" % self._config["msgtxt_question"]
stanza = Message(to=jid, body=message, typ=properties.type.value)
self._con.connection.send_stanza(stanza)
self._log.info('Anti spam question sent to %s', jid)
self._client.connection.send_stanza(stanza)
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
block_sub = self._config['block_subscription_requests']
is_contact = app.contacts.get_contacts(self._account, msg_from)
if block_sub and not is_contact:
self._con.get_module('Presence').unsubscribed(msg_from)
self._log.info('Denied subscription request from %s' % 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)
raise NodeProcessed
def get_instance(*args, **kwargs):
return AntiSpam(*args, **kwargs), 'AntiSpam'
def get_instance(*args: Any, **kwargs: Any) -> tuple[AntiSpam, str]:
return AntiSpam(*args, **kwargs), "AntiSpam"
<?xml version="1.0" encoding="UTF-8"?>
<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>
<name>URL Image Preview Plugin</name>
<summary>Display a preview of links to images</summary>
<name>Anti Spam Plugin</name>
<summary>Block some incoming messages</summary>
<url type="homepage">https://gajim.org/</url>
<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>
</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