triggers/0000775000175500017550000000000014760554116012451 5ustar debacledebacletriggers/__init__.py0000664000175500017550000000007714760554116014566 0ustar debacledebaclefrom .triggers import Triggers # pyright: ignore # noqa: F401 triggers/gtk/0000775000175500017550000000000014760554116013236 5ustar debacledebacletriggers/gtk/config.py0000664000175500017550000005720114760554116015062 0ustar debacledebacle# Copyright (C) 2018 Philipp Hörist # # This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . 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 gajim.common.helpers import play_sound_file from gajim.common.util.status import get_uf_show from gajim.gtk.filechoosers import FileChooserButton from gajim.gtk.widgets import GajimAppWindow from gajim.plugins.helpers import get_builder from gajim.plugins.plugins_i18n import _ if TYPE_CHECKING: from ..triggers import Triggers EVENTS: dict[str, Any] = { "message_received": [], } RECIPIENT_TYPES = ["contact", "group", "groupchat", "all"] class ConfigBuilder(Gtk.Builder): liststore1: Gtk.ListStore liststore2: Gtk.ListStore box: Gtk.Box rules_box: Gtk.Box conditions_treeview: Gtk.TreeView up_button: Gtk.Button down_button: Gtk.Button new_button: Gtk.Button delete_button: Gtk.Button config_box: Gtk.Box event_combobox: Gtk.ComboBox recipient_type_combobox: Gtk.ComboBox recipient_list_entry: Gtk.Entry all_status_rb: Gtk.CheckButton special_status_rb: Gtk.CheckButton status_expander: Gtk.Expander online_cb: Gtk.CheckButton away_cb: Gtk.CheckButton xa_cb: Gtk.CheckButton dnd_cb: Gtk.CheckButton tab_opened_cb: Gtk.CheckButton has_focus_cb: Gtk.CheckButton not_tab_opened_cb: Gtk.CheckButton not_has_focus_cb: Gtk.CheckButton use_popup_cb: Gtk.CheckButton disable_popup_cb: Gtk.CheckButton use_sound_cb: Gtk.CheckButton sound_file_box: Gtk.Box play_button: Gtk.Button disable_sound_cb: Gtk.CheckButton run_command_cb: Gtk.CheckButton command_entry: Gtk.Entry one_shot_cb: Gtk.CheckButton class ConfigDialog(GajimAppWindow): def __init__(self, plugin: Triggers, transient: Gtk.Window) -> None: GajimAppWindow.__init__( self, name="TriggersConfigDialog", title=_("Triggers Configuration"), default_width=600, default_height=800, transient_for=transient, modal=True, ) ui_path = Path(__file__).parent self._ui = cast( ConfigBuilder, get_builder(str(ui_path.resolve() / "config.ui")) ) self._plugin = plugin self.set_child(self._ui.box) self._active_num = -1 self._config: dict[int, Any] = {} self._initialize() self._connect( self._ui.conditions_treeview, "cursor-changed", self._on_conditions_treeview_cursor_changed, ) self._connect(self._ui.up_button, "clicked", self._on_up_button_clicked) self._connect(self._ui.down_button, "clicked", self._on_down_button_clicked) self._connect(self._ui.new_button, "clicked", self._on_new_button_clicked) self._connect(self._ui.delete_button, "clicked", self._on_delete_button_clicked) self._connect( self._ui.event_combobox, "changed", self._on_event_combobox_changed ) self._connect( self._ui.recipient_type_combobox, "changed", self._on_recipient_type_combobox_changed, ) self._connect( self._ui.recipient_list_entry, "changed", self._on_recipient_list_entry_changed, ) self._connect( self._ui.all_status_rb, "toggled", self._on_status_radiobutton_toggled ) self._connect(self._ui.online_cb, "toggled", self._on_status_cb_toggled) self._connect(self._ui.away_cb, "toggled", self._on_status_cb_toggled) self._connect(self._ui.xa_cb, "toggled", self._on_status_cb_toggled) self._connect(self._ui.dnd_cb, "toggled", self._on_status_cb_toggled) self._connect(self._ui.tab_opened_cb, "toggled", self._on_tab_opened_cb_toggled) self._connect(self._ui.has_focus_cb, "toggled", self._on_has_focus_cb_toggled) self._connect( self._ui.not_tab_opened_cb, "toggled", self._on_not_tab_opened_cb_toggled ) self._connect( self._ui.not_has_focus_cb, "toggled", self._on_not_has_focus_cb_toggled ) self._connect(self._ui.use_popup_cb, "toggled", self._on_use_popup_cb_toggled) self._connect( self._ui.disable_popup_cb, "toggled", self._on_disable_popup_cb_toggled ) self._connect(self._ui.use_sound_cb, "toggled", self._on_use_sound_cb_toggled) self._connect(self._ui.play_button, "clicked", self._on_play_button_clicked) self._connect( self._ui.disable_sound_cb, "toggled", self._on_disable_sound_cb_toggled ) self._connect( self._ui.run_command_cb, "toggled", self._on_run_command_cb_toggled ) self._connect(self._ui.command_entry, "changed", self._on_command_entry_changed) self._connect(self._ui.one_shot_cb, "toggled", self._on_one_shot_cb_toggled) self._connect(self.window, "close-request", self._on_close_request) file_chooser_button = FileChooserButton() self._connect(file_chooser_button, "path-picked", self._on_sound_file_set) self._ui.sound_file_box.append(file_chooser_button) self.show() def _cleanup(self) -> None: pass def _on_close_request(self, *args: Any) -> None: for num in list(self._plugin.config.keys()): del self._plugin.config[num] for num in self._config: self._plugin.config[str(num)] = self._config[num] def _initialize(self) -> None: self._config = {} for num in self._plugin.config: self._config[int(num)] = self._plugin.config[num] model = cast(Gtk.ListStore, self._ui.conditions_treeview.get_model()) # Fill conditions_treeview num = 0 while num in self._config: iter_ = model.append((num, "")) path = model.get_path(iter_) self._ui.conditions_treeview.set_cursor(path) self._active_num = num self._initiate_rule_state() self._set_treeview_string() num += 1 # No rule selected at init time self._ui.conditions_treeview.get_selection().unselect_all() self._active_num = -1 self._ui.config_box.set_sensitive(False) self._ui.delete_button.set_sensitive(False) self._ui.down_button.set_sensitive(False) self._ui.up_button.set_sensitive(False) # filter_ = Gtk.FileFilter() # filter_.set_name(_("All Files")) # filter_.add_pattern("*") # self._ui.filechooser.add_filter(filter_) # filter_ = Gtk.FileFilter() # filter_.set_name(_("Wav Sounds")) # filter_.add_pattern("*.wav") # self._ui.filechooser.add_filter(filter_) # self._ui.filechooser.set_filter(filter_) def _initiate_rule_state(self) -> None: """ Set values for all widgets """ if self._active_num < 0: return # event value = self._config[self._active_num]["event"] legacy_values = [ "contact_connected", "contact_disconnected", "contact_status_change", ] if value and value not in legacy_values: self._ui.event_combobox.set_active(list(EVENTS.keys()).index(value)) else: self._ui.event_combobox.set_active(-1) # recipient_type value = self._config[self._active_num]["recipient_type"] if value: self._ui.recipient_type_combobox.set_active(RECIPIENT_TYPES.index(value)) else: self._ui.recipient_type_combobox.set_active(-1) # recipient value = self._config[self._active_num]["recipients"] if not value: value = "" self._ui.recipient_list_entry.set_text(value) # status value = self._config[self._active_num]["status"] if value == "all": self._ui.all_status_rb.set_active(True) else: self._ui.special_status_rb.set_active(True) values = value.split() for st in ("online", "away", "xa", "dnd"): if st in values: getattr(self._ui, f"{st}_cb").set_active(True) else: getattr(self._ui, f"{st}_cb").set_active(False) self._on_status_radiobutton_toggled(self._ui.all_status_rb) # tab_opened value = self._config[self._active_num]["tab_opened"] self._ui.tab_opened_cb.set_active(True) self._ui.not_tab_opened_cb.set_active(True) if value == "no": self._ui.tab_opened_cb.set_active(False) elif value == "yes": self._ui.not_tab_opened_cb.set_active(False) # has_focus if "has_focus" not in self._config[self._active_num]: self._config[self._active_num]["has_focus"] = "both" value = self._config[self._active_num]["has_focus"] self._ui.has_focus_cb.set_active(True) self._ui.not_has_focus_cb.set_active(True) if value == "no": self._ui.has_focus_cb.set_active(False) elif value == "yes": self._ui.not_has_focus_cb.set_active(False) # TODO # sound_file # value = self._config[self._active_num]["sound_file"] # if value is None: # self._ui.filechooser.unselect_all() # else: # self._ui.filechooser.set_filename(value) # sound, popup, auto_open, systray, roster for option in ("sound", "popup"): value = self._config[self._active_num][option] if value == "yes": getattr(self._ui, f"use_{option}_cb").set_active(True) else: getattr(self._ui, f"use_{option}_cb").set_active(False) if value == "no": getattr(self._ui, f"disable_{option}_cb").set_active(True) else: getattr(self._ui, f"disable_{option}_cb").set_active(False) # run_command value = self._config[self._active_num]["run_command"] self._ui.run_command_cb.set_active(value) # command value = self._config[self._active_num]["command"] self._ui.command_entry.set_text(value) # one shot value = self._config[self._active_num].get("one_shot", False) self._ui.one_shot_cb.set_active(value) def _set_treeview_string(self) -> None: selection = self._ui.conditions_treeview.get_selection() (model, iter_) = selection.get_selected() if not iter_: return event = "" ind = self._ui.event_combobox.get_active() if ind > -1: event_model = self._ui.event_combobox.get_model() assert event_model is not None event = event_model[ind][0] ind = self._ui.recipient_type_combobox.get_active() recipient_type = "" if ind > -1: recipient_type_model = self._ui.recipient_type_combobox.get_model() assert recipient_type_model is not None recipient_type = recipient_type_model[ind][0] recipient = "" if recipient_type != "everybody": recipient = self._ui.recipient_list_entry.get_text() if self._ui.all_status_rb.get_active(): status = "" else: status = _("and I am ") for st in ("online", "away", "xa", "dnd"): if getattr(self._ui, f"{st}_cb").get_active(): status += get_uf_show(st) + " " model[iter_][1] = _( "%(event)s (%(recipient_type)s) %(recipient)s %(status)s" ) % { "event": event, "recipient_type": recipient_type, "recipient": recipient, "status": status, } def _on_conditions_treeview_cursor_changed(self, widget: Gtk.TreeView) -> None: (model, iter_) = widget.get_selection().get_selected() if not iter_: self._active_num = -1 return self._active_num = model[iter_][0] if self._active_num == 0: self._ui.up_button.set_sensitive(False) else: self._ui.up_button.set_sensitive(True) model = widget.get_model() assert model is not None _max = model.iter_n_children(None) if self._active_num == _max - 1: self._ui.down_button.set_sensitive(False) else: self._ui.down_button.set_sensitive(True) self._initiate_rule_state() self._ui.config_box.set_sensitive(True) self._ui.delete_button.set_sensitive(True) def _on_new_button_clicked(self, _button: Gtk.Button) -> None: model = cast(Gtk.ListStore, self._ui.conditions_treeview.get_model()) assert model is not None num = model.iter_n_children(None) self._config[num] = { "event": "message_received", "recipient_type": "all", "recipients": "", "status": "all", "tab_opened": "both", "has_focus": "both", "sound": "", "sound_file": "", "popup": "", "run_command": False, "command": "", "one_shot": False, } iter_ = model.append((num, "")) path = model.get_path(iter_) self._ui.conditions_treeview.set_cursor(path) self._active_num = num self._set_treeview_string() self._ui.config_box.set_sensitive(True) def _on_delete_button_clicked(self, button: Gtk.Button) -> None: selection = self._ui.conditions_treeview.get_selection() (model, iter_) = selection.get_selected() assert isinstance(model, Gtk.ListStore) if not iter_: return # up all others iter2 = model.iter_next(iter_) num = self._active_num while iter2: num = model[iter2][0] model[iter2][0] = num - 1 self._config[num - 1] = self._config[num].copy() iter2 = model.iter_next(iter2) model.remove(iter_) del self._config[num] self._active_num = -1 button.set_sensitive(False) self._ui.up_button.set_sensitive(False) self._ui.down_button.set_sensitive(False) self._ui.config_box.set_sensitive(False) def _on_up_button_clicked(self, _button: Gtk.Button) -> None: selection = self._ui.conditions_treeview.get_selection() (model, iter_) = selection.get_selected() assert isinstance(model, Gtk.ListStore) if not iter_: return conf = self._config[self._active_num].copy() self._config[self._active_num] = self._config[self._active_num - 1] self._config[self._active_num - 1] = conf model[iter_][0] = self._active_num - 1 # get previous iter path = model.get_path(iter_) iter_ = model.get_iter((path[0] - 1,)) # type: ignore model[iter_][0] = self._active_num self._on_conditions_treeview_cursor_changed(self._ui.conditions_treeview) def _on_down_button_clicked(self, _button: Gtk.Button) -> None: selection = self._ui.conditions_treeview.get_selection() (model, iter_) = selection.get_selected() assert isinstance(model, Gtk.ListStore) if not iter_: return conf = self._config[self._active_num].copy() self._config[self._active_num] = self._config[self._active_num + 1] self._config[self._active_num + 1] = conf model[iter_][0] = self._active_num + 1 iter_ = model.iter_next(iter_) assert iter_ is not None model[iter_][0] = self._active_num self._on_conditions_treeview_cursor_changed(self._ui.conditions_treeview) def _on_event_combobox_changed(self, combo: Gtk.ComboBox) -> None: if self._active_num < 0: return active = combo.get_active() if active == -1: return event = list(EVENTS.keys())[active] self._config[self._active_num]["event"] = event for widget in EVENTS[event]: self._ui.__dict__[widget].set_sensitive(False) self._ui.__dict__[widget].set_state(False) self._set_treeview_string() def _on_recipient_type_combobox_changed(self, widget: Gtk.ComboBox) -> None: if self._active_num < 0: return recipient_type = RECIPIENT_TYPES[widget.get_active()] self._config[self._active_num]["recipient_type"] = recipient_type if recipient_type == "all": self._ui.recipient_list_entry.set_sensitive(False) else: self._ui.recipient_list_entry.set_sensitive(True) self._set_treeview_string() def _on_recipient_list_entry_changed(self, widget: Gtk.Entry) -> None: if self._active_num < 0: return recipients = widget.get_text() # TODO: do some check self._config[self._active_num]["recipients"] = recipients self._set_treeview_string() def _set_status_config(self) -> None: if self._active_num < 0: return status = "" for st in ("online", "away", "xa", "dnd"): if getattr(self._ui, f"{st}_cb").get_active(): status += st + " " if status: status = status[:-1] self._config[self._active_num]["status"] = status self._set_treeview_string() def _on_status_radiobutton_toggled(self, _widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if self._ui.all_status_rb.get_active(): self._ui.status_expander.set_expanded(False) self._config[self._active_num]["status"] = "all" # 'All status' clicked for st in ("online", "away", "xa", "dnd"): getattr(self._ui, f"{st}_cb").set_sensitive(False) else: self._ui.status_expander.set_expanded(True) self._set_status_config() # 'special status' clicked for st in ("online", "away", "xa", "dnd"): getattr(self._ui, f"{st}_cb").set_sensitive(True) self._set_treeview_string() def _on_status_cb_toggled(self, _widget: Gtk.CheckButton) -> None: if self._active_num < 0: return self._set_status_config() # tab_opened OR (not xor) not_tab_opened must be active def _on_tab_opened_cb_toggled(self, widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if widget.get_active(): self._ui.has_focus_cb.set_sensitive(True) self._ui.not_has_focus_cb.set_sensitive(True) if self._ui.not_tab_opened_cb.get_active(): self._config[self._active_num]["tab_opened"] = "both" else: self._config[self._active_num]["tab_opened"] = "yes" else: self._ui.has_focus_cb.set_sensitive(False) self._ui.not_has_focus_cb.set_sensitive(False) self._ui.not_tab_opened_cb.set_active(True) self._config[self._active_num]["tab_opened"] = "no" def _on_not_tab_opened_cb_toggled(self, widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if widget.get_active(): if self._ui.tab_opened_cb.get_active(): self._config[self._active_num]["tab_opened"] = "both" else: self._config[self._active_num]["tab_opened"] = "no" else: self._ui.tab_opened_cb.set_active(True) self._config[self._active_num]["tab_opened"] = "yes" # has_focus OR (not xor) not_has_focus must be active def _on_has_focus_cb_toggled(self, widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if widget.get_active(): if self._ui.not_has_focus_cb.get_active(): self._config[self._active_num]["has_focus"] = "both" else: self._config[self._active_num]["has_focus"] = "yes" else: self._ui.not_has_focus_cb.set_active(True) self._config[self._active_num]["has_focus"] = "no" def _on_not_has_focus_cb_toggled(self, widget: Gtk.CheckButton) -> None: if self._active_num < 0: return if widget.get_active(): if self._ui.has_focus_cb.get_active(): self._config[self._active_num]["has_focus"] = "both" else: self._config[self._active_num]["has_focus"] = "no" else: self._ui.has_focus_cb.set_active(True) self._config[self._active_num]["has_focus"] = "yes" def _on_use_it_toggled( self, widget: Gtk.CheckButton, opposite_widget: Gtk.CheckButton, option: str ) -> None: if widget.get_active(): if opposite_widget.get_active(): opposite_widget.set_active(False) self._config[self._active_num][option] = "yes" elif opposite_widget.get_active(): self._config[self._active_num][option] = "no" else: self._config[self._active_num][option] = "" def _on_disable_it_toggled( self, widget: Gtk.CheckButton, opposite_widget: Gtk.CheckButton, option: str ) -> None: if widget.get_active(): if opposite_widget.get_active(): opposite_widget.set_active(False) self._config[self._active_num][option] = "no" elif opposite_widget.get_active(): self._config[self._active_num][option] = "yes" else: self._config[self._active_num][option] = "" def _on_use_sound_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._on_use_it_toggled(widget, self._ui.disable_sound_cb, "sound") if widget.get_active(): self._ui.sound_file_box.set_sensitive(True) else: self._ui.sound_file_box.set_sensitive(False) def _on_sound_file_set(self, widget: FileChooserButton, paths: list[Path]) -> None: self._config[self._active_num]["sound_file"] = paths[0] def _on_play_button_clicked(self, _button: Gtk.Button) -> None: play_sound_file(self._config[self._active_num]["sound_file"]) def _on_disable_sound_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._on_disable_it_toggled(widget, self._ui.use_sound_cb, "sound") def _on_use_popup_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._on_use_it_toggled(widget, self._ui.disable_popup_cb, "popup") def _on_disable_popup_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._on_disable_it_toggled(widget, self._ui.use_popup_cb, "popup") def _on_run_command_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._config[self._active_num]["run_command"] = widget.get_active() if widget.get_active(): self._ui.command_entry.set_sensitive(True) else: self._ui.command_entry.set_sensitive(False) def _on_command_entry_changed(self, widget: Gtk.Entry) -> None: self._config[self._active_num]["command"] = widget.get_text() def _on_one_shot_cb_toggled(self, widget: Gtk.CheckButton) -> None: self._config[self._active_num]["one_shot"] = widget.get_active() self._ui.command_entry.set_sensitive(widget.get_active()) triggers/gtk/config.ui0000664000175500017550000007757514760554116015067 0ustar debacledebacle Contact Group Groupchat participant Everybody Receive a Message vertical 6 1 1 1 1 1 vertical 6 vertical 100 1 1 conditions_store horizontal 1 # 1 1 0 0 0 1 Condition 1 1 1 0 1 toolbar 0 Up Up 1 go-up-symbolic 0 Down Down 1 go-down-symbolic New rule New rule 1 list-add-symbolic Delete rule Delete rule 1 list-remove-symbolic 1 vertical 5 center vertical 6 center 6 Conditions 1 center 6 12 end Event 1 0 0 200 liststore2 0 1 0 end Category 1 0 1 liststore1 0 1 1 300 0 1 comma separated list 1 2 end List 0 2 center 6 6 start My status 1 All statuses 1 start 1 Certain status 1 start 1 all_status_rb 1 start 3 1 vertical 6 Online 0 1 start 1 Away 0 1 start 1 Not Available 0 1 start 1 Busy 0 1 start 1 Status 6 12 end Chat Window 0 0 end Focus 0 1 Opened 1 start 1 1 0 Has focus 1 start 1 1 Not opened 1 start 1 2 0 Does not have focus 1 start 2 1 center vertical 6 center 6 Actions 1 vertical 6 start Notifications Not_ify me with a popup 1 start 1 _Disable existing notification 1 start 1 vertical 6 start Sounds 1 center start 12 Play sound 1 start 1 12 1 media-playback-start-symbolic _Disable existing sound for this event 1 start 1 start Advanced center 6 Launch command 1 1 200 1 Command... Delete this rule once applied 1 start triggers/org.gajim.Gajim.Plugin.triggers.metainfo.xml0000664000175500017550000000072114760554116022761 0ustar debacledebacle org.gajim.Gajim.Plugin.triggers org.gajim.Gajim Triggers Plugin Configure Gajim's behaviour for each contact https://gajim.org/ CC-BY-SA-3.0 GPL-3.0-only gajim-devel_AT_gajim.org triggers/plugin-manifest.json0000664000175500017550000000073514760554116016453 0ustar debacledebacle{ "authors": [ "Yann Leboulanger " ], "description": "Configure Gajim's behaviour for each contact.", "homepage": "https://dev.gajim.org/gajim/gajim-plugins/wikis/TriggersPlugin", "config_dialog": true, "name": "Triggers", "platforms": [ "others", "linux", "darwin", "win32" ], "requirements": [ "gajim>=2.0.0" ], "short_name": "triggers", "version": "1.6.0" }triggers/triggers.png0000664000175500017550000000343714760554116015014 0ustar debacledebaclePNG  IHDR DgAMA7tEXtSoftwareAdobe ImageReadyqe<PLTECnr喼rrh5yyR1{{Ԣӑlddx1/d»@S-*zlLҫ|YV9DEu ׆8rۄXXqo:cg 2Ay{abTUƂҌ훛햖Յ- \]mcf9&gjy̡nmӗ΋kIiiuuv'::b>hLr;࡭3b ֊]ON㥭…`ܮR!*u\\llr`ADXǀ͡>:ЎY؍qS+FNc-@Z@⑔C{^`t`t'๿ټsmU_[^\MJHHBBqPPdgK)Ӯԧ0tRNSS%IDATxbO0 _ Ňn)á@lwZ^MX<~xboеM|c(v3nVP~+kN)CV|J{%|ba@L﮵$u m4*8$**os<" FN I_7w6}z]@!*SIs;ϮIdPA_#ˋb3.}ƝIލ ,k+*9Q|v;|? g0{#(p͐Qm F,=[QɨI% ?DX>N]=Z`h mv(`5W?M9/Ǐ_7BBrϛv@yO&+^Iф5K||Xm07eFDVb'H;)=mdJm/%,!ߝgjz:bMM^ڦJ?IE^.TWUU͸:I @;n `OߥIl)o_ֽ-XfGLu*hn=Z\, WAtqq!"Mں2^!K]]HHh !.=_,f֙-!QXX(!bL8oQ.7IENDB`triggers/triggers.py0000664000175500017550000002472214760554116014660 0ustar debacledebacle# Copyright (C) 2011-2017 Yann Leboulanger # Copyright (C) 2022 Philipp Hörist # # This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . from __future__ import annotations from typing import Any from typing import cast import logging import subprocess from collections.abc import Callable from functools import partial from nbxmpp.protocol import JID from gajim.common import app from gajim.common import ged from gajim.common.const import PROPAGATE_EVENT from gajim.common.const import STOP_EVENT from gajim.common.events import MessageReceived from gajim.common.events import Notification from gajim.common.events import PresenceReceived from gajim.common.helpers import play_sound_file from gajim.common.modules.contacts import BareContact from gajim.plugins import GajimPlugin from gajim.plugins.plugins_i18n import _ from triggers.gtk.config import ConfigDialog from triggers.util import log_result from triggers.util import RuleResult log = logging.getLogger("gajim.p.triggers") ProcessableEventsT = MessageReceived | Notification | PresenceReceived RuleT = dict[str, Any] class Triggers(GajimPlugin): def init(self) -> None: self.description = _( "Configure Gajim’s behaviour with triggers for each contact" ) self.config_dialog = partial(ConfigDialog, self) self.config_default_values = {} self.events_handlers = { "notification": (ged.PREGUI, self._on_notification), "message-received": (ged.PREGUI2, self._on_message_received), "gc-message-received": (ged.PREGUI2, self._on_message_received), # 'presence-received': (ged.PREGUI, self._on_presence_received), } def _on_notification(self, event: Notification) -> bool: log.info("Process %s, %s", event.name, event.type) result = self._check_all( event, self._check_rule_apply_notification, self._apply_rule ) log.info("Result: %s", result) return self._excecute_notification_rules(result, event) def _on_message_received(self, event: MessageReceived) -> bool: log.info("Process %s", event.name) message = event.message if message.text is None: log.info("Discard event because it has no message text") return PROPAGATE_EVENT result = self._check_all( event, self._check_rule_apply_msg_received, self._apply_rule ) log.info("Result: %s", result) return self._excecute_message_rules(result) def _on_presence_received(self, event: PresenceReceived) -> None: # TODO return if event.old_show < 2 and event.new_show > 1: check_func = self._check_rule_apply_connected elif event.old_show > 1 and event.new_show < 2: check_func = self._check_rule_apply_disconnected else: check_func = self._check_rule_apply_status_changed self._check_all(event, check_func, self._apply_rule) def _check_all( self, event: ProcessableEventsT, check_func: Callable[..., bool], apply_func: Callable[..., Any], ) -> RuleResult: result = RuleResult() rules_num = [int(item) for item in self.config] rules_num.sort() to_remove: list[int] = [] for num in rules_num: rule = cast(RuleT, self.config[str(num)]) if check_func(event, rule): apply_func(result, rule) if rule.get("one_shot"): to_remove.append(num) decal = 0 num = 0 while str(num) in self.config: if num + decal in to_remove: num2 = num while str(num2 + 1) in self.config: copy = self.config[str(num2 + 1)].copy() # type: ignore self.config[str(num2)] = copy num2 += 1 del self.config[str(num2)] decal += 1 else: num += 1 return result @log_result def _check_rule_apply_msg_received( self, event: MessageReceived, rule: RuleT ) -> bool: return self._check_rule_all("message_received", event, rule) @log_result def _check_rule_apply_connected(self, event: PresenceReceived, rule: RuleT) -> bool: return self._check_rule_all("contact_connected", event, rule) @log_result def _check_rule_apply_disconnected( self, event: PresenceReceived, rule: RuleT ) -> bool: return self._check_rule_all("contact_disconnected", event, rule) @log_result def _check_rule_apply_status_changed( self, event: PresenceReceived, rule: RuleT ) -> bool: return self._check_rule_all("contact_status_change", event, rule) @log_result def _check_rule_apply_notification(self, event: Notification, rule: RuleT) -> bool: # Check notification type notif_type = "" if event.type == "incoming-message": notif_type = "message_received" # if event.type == 'pres': # # TODO: # if (event.base_event.old_show < 2 and # event.base_event.new_show > 1): # notif_type = 'contact_connected' # elif (event.base_event.old_show > 1 and # event.base_event.new_show < 2): # notif_type = 'contact_disconnected' # else: # notif_type = 'contact_status_change' return self._check_rule_all(notif_type, event, rule) def _check_rule_all( self, notif_type: str, event: ProcessableEventsT, rule: RuleT ) -> bool: # Check notification type if rule["event"] != notif_type: return False # notification type is ok. Now check recipient if not self._check_rule_recipients(event, rule): return False # recipient is ok. Now check our status if not self._check_rule_status(event, rule): return False # our_status is ok. Now check opened chat window if not self._check_rule_tab_opened(event, rule): return False # tab_opened is ok. Now check opened chat window if not self._check_rule_has_focus(event, rule): # noqa: SIM103 return False # All is ok return True @log_result def _check_rule_recipients(self, event: ProcessableEventsT, rule: RuleT) -> bool: assert event.jid is not None rule_recipients = [t.strip() for t in rule["recipients"].split(",")] if rule["recipient_type"] == "groupchat": return event.jid in rule_recipients if rule["recipient_type"] == "contact" and event.jid not in rule_recipients: return False client = app.get_client(event.account) contact = client.get_module("Contacts").get_contact(event.jid) if contact.is_groupchat: return False assert isinstance(contact, BareContact) if not contact.is_in_roster: return False group_found = False for group in contact.groups: if group in rule_recipients: group_found = True break if rule["recipient_type"] == "group" and not group_found: # noqa: SIM103 return False return True @log_result def _check_rule_status(self, event: ProcessableEventsT, rule: RuleT) -> bool: rule_statuses = rule["status"].split() client = app.get_client(event.account) if ( # noqa: SIM103 rule["status"] != "all" and client.status not in rule_statuses ): return False return True @log_result def _check_rule_tab_opened(self, event: ProcessableEventsT, rule: RuleT) -> bool: if rule["tab_opened"] == "both": return True tab_opened = False assert isinstance(event.jid, JID) if app.window.chat_exists(event.account, event.jid): tab_opened = True if tab_opened and rule["tab_opened"] == "no": return False elif not tab_opened and rule["tab_opened"] == "yes": return False return True @log_result def _check_rule_has_focus(self, event: ProcessableEventsT, rule: RuleT) -> bool: if rule["has_focus"] == "both": return True if rule["tab_opened"] == "no": # Does not apply in this case return True assert isinstance(event.jid, JID) chat_active = app.window.is_chat_active(event.account, event.jid) if chat_active and rule["has_focus"] == "no": return False elif not chat_active and rule["has_focus"] == "yes": return False return True def _apply_rule(self, result: RuleResult, rule: RuleT) -> None: if rule["sound"] == "no": result.sound = False result.sound_file = None elif rule["sound"] == "yes": result.sound = False result.sound_file = rule["sound_file"] if rule["run_command"]: result.command = rule["command"] if rule["popup"] == "no": result.show_notification = False elif rule["popup"] == "yes": result.show_notification = True def _excecute_notification_rules( self, result: RuleResult, event: Notification ) -> bool: if result.sound is False: event.sound = None if result.sound_file is not None: play_sound_file(result.sound_file) if result.show_notification is False: return STOP_EVENT return PROPAGATE_EVENT def _excecute_message_rules(self, result: RuleResult) -> bool: if result.sound_file is not None: play_sound_file(result.sound_file) if result.command is not None: try: subprocess.Popen(f"{result.command} &", shell=True).wait() # noqa: S602 except Exception: pass return PROPAGATE_EVENT triggers/util.py0000664000175500017550000000251114760554116013777 0ustar debacledebacle# This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . from __future__ import annotations from typing import Any from typing import TYPE_CHECKING import logging from collections.abc import Callable from dataclasses import dataclass if TYPE_CHECKING: from .triggers import ProcessableEventsT from .triggers import RuleT log = logging.getLogger("gajim.p.triggers") def log_result(func: Callable[..., Any]) -> Callable[..., bool]: def wrapper(self: Any, event: ProcessableEventsT, rule: RuleT): res = func(self, event, rule) log.info("%s -> %s -> %s", event.name, func.__name__, res) return res return wrapper @dataclass class RuleResult: show_notification: bool | None = None command: str | None = None sound: bool | None = None sound_file: str | None = None