diff --git a/README.md b/README.md index 2a89235..9c1543f 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,6 @@ For smaller displays you may wish to enable `single_pane_mode`: - `CTRL` + `t` or `F4` = With the Node List highlighted, send a traceroute to the selected node - `F5` = Display a node's info - `CTRL` + `f` = With the Node List highlighted, favorite the selected node -- `CTRL` + `b` = Enable/Disable Autoresponder Bot (ping / pong) - `CTRL` + `g` = With the Node List highlighted, ignore the selected node - `CTRL` + `d` = With the Channel List hightlighted, archive a chat to reduce UI clutter. Messages will be saved in the db and repopulate if you send or receive a DM from this user. - `CTRL` + `d` = With the Note List highlghted, remove a node from your nodedb. diff --git a/contact/localisations/en.ini b/contact/localisations/en.ini index b02cd30..9d803bc 100644 --- a/contact/localisations/en.ini +++ b/contact/localisations/en.ini @@ -81,7 +81,6 @@ help.traceroute, "Ctrl+T or F4 = Traceroute", "" help.node_info, "F5 = Full node info", "" help.archive_chat, "Ctrl+D = Archive chat / remove node", "" help.favorite, "Ctrl+F = Favorite", "" -help.bot_responder, "Ctrl+B = Toggle Bot Responder", "" help.ignore, "Ctrl+G = Ignore", "" help.search, "Ctrl+/ or / = Search", "" help.help, "Ctrl+K = Help", "" @@ -94,11 +93,6 @@ confirm.remove_ignored, "Remove {name} from Ignored?", "" confirm.region_unset, "Your region is UNSET. Set it now?", "" dialog.resize_title, "Resize Terminal", "" dialog.resize_body, "Please resize the terminal to at least {rows} rows.", "" -bot.status.enabled, "Enabled", "" -bot.status.disabled, "Disabled", "" -bot.dialog.title, "Bot Responder", "" -bot.dialog.body, "Bot responder is now {status}.", "" -bot.status.message, "Bot responder is now {status}.", "" [User Settings] user, "User" @@ -132,6 +126,7 @@ COLOR_CONFIG_GREEN, "Theme colors (green)", "" [app_settings.ping_bot] title, "Ping Bot", "" +enabled, "Enabled", "Enable automatic Ping Bot responses." catch_words, "Catch words", "Semicolon-separated bot trigger words." response_word, "Response word", "Bot response word." diff --git a/contact/localisations/fr.ini b/contact/localisations/fr.ini index e59762a..a7520aa 100644 --- a/contact/localisations/fr.ini +++ b/contact/localisations/fr.ini @@ -78,16 +78,10 @@ help.traceroute, "Ctrl+T ou F4 = Traceroute", "" help.node_info, "F5 = Informations complètes du nœud", "" help.archive_chat, "Ctrl+D = Archiver la discussion / supprimer le nœud", "" help.favorite, "Ctrl+F = Favori", "" -help.bot_responder, "Ctrl+B = Activer/désactiver le bot répondeur", "" help.ignore, "Ctrl+G = Ignorer", "" help.search, "Ctrl+/ ou / = Rechercher", "" help.help, "Ctrl+K = Aide", "" help.no_help, "Aucune aide disponible.", "" -bot.status.enabled, "Activé", "" -bot.status.disabled, "Désactivé", "" -bot.dialog.title, "Bot répondeur", "" -bot.dialog.body, "Le bot répondeur est maintenant {status}.", "" -bot.status.message, "Le bot répondeur est maintenant {status}.", "" [User Settings] user, "Utilisateur", "" @@ -118,6 +112,7 @@ ping_bot, "Bot Ping", "" [app_settings.ping_bot] title, "Bot Ping", "" +enabled, "Activé", "Activer les réponses automatiques du bot Ping." catch_words, "Mots déclencheurs", "Mots déclencheurs du bot séparés par des points-virgules." response_word, "Mot de réponse", "Mot de réponse du bot." diff --git a/contact/localisations/ru.ini b/contact/localisations/ru.ini index feca158..4ef94a3 100644 --- a/contact/localisations/ru.ini +++ b/contact/localisations/ru.ini @@ -78,7 +78,6 @@ help.traceroute, "Ctrl+T или F4 = Traceroute", "" help.node_info, "F5 = Полная информация об узле", "" help.archive_chat, "Ctrl+D = Архив чата / удалить узел", "" help.favorite, "Ctrl+F = Избранное", "" -help.bot_responder, "Ctrl+B = Вкл/выкл автоответчик", "" help.ignore, "Ctrl+G = Игнорировать", "" help.search, "Ctrl+/ или / = Поиск", "" help.help, "Ctrl+K = Справка", "" @@ -91,11 +90,6 @@ confirm.remove_ignored, "Убрать {name} из игнорируемых?", "" confirm.region_unset, "Ваш регион НЕ ЗАДАН. Установить сейчас?", "" dialog.resize_title, "Увеличьте окно", "" dialog.resize_body, "Пожалуйста, увеличьте окно до {rows} строк.", "" -bot.status.enabled, "Включен", "" -bot.status.disabled, "Выключен", "" -bot.dialog.title, "Автоответчик", "" -bot.dialog.body, "Автоответчик теперь {status}.", "" -bot.status.message, "Автоответчик теперь {status}.", "" [User Settings] user, "Пользователь" @@ -129,6 +123,7 @@ COLOR_CONFIG_GREEN, "Цвета темы (зеленая)", "" [app_settings.ping_bot] title, "Пинг-бот", "" +enabled, "Включен", "Включить автоматические ответы пинг-бота." catch_words, "Слова-триггеры", "Слова для активации бота, разделенные точкой с запятой." response_word, "Ответное слово", "Ответное слово бота." diff --git a/contact/message_handlers/bot_handler.py b/contact/message_handlers/bot_handler.py index 4b88225..a180d9c 100644 --- a/contact/message_handlers/bot_handler.py +++ b/contact/message_handlers/bot_handler.py @@ -5,7 +5,7 @@ import time from typing import Any, Dict import contact.ui.default_config as config -from contact.utilities.singleton import app_state, interface_state, ui_state +from contact.utilities.singleton import app_state, interface_state from contact.message_handlers.tx_handler import send_message BOT_RESPONSE_DELAY_SECONDS = 2.3 @@ -24,9 +24,15 @@ def is_bot_message(message: str) -> bool: """Return True when the incoming message should trigger an automatic response.""" return message.strip().casefold() in _get_bot_catch_words() + +def is_bot_enabled() -> bool: + """Return whether automatic responses are enabled in app settings.""" + value = getattr(config, "ping_bot_enabled", False) + return value if isinstance(value, bool) else str(value).strip().casefold() == "true" + def bot_respond(packet: Dict[str, Any], message: str, send_channel: int) -> bool: """Send a basic response when bot mode is enabled.""" - if not ui_state.bot_mode_enabled: + if not is_bot_enabled(): return False if not is_bot_message(message): @@ -70,7 +76,7 @@ def bot_respond(packet: Dict[str, Any], message: str, send_channel: int) -> bool time.sleep(BOT_RESPONSE_DELAY_SECONDS) with app_state.lock: - if not ui_state.bot_mode_enabled: + if not is_bot_enabled(): return # Use the triggering packet's ID so Meshtastic clients render this diff --git a/contact/ui/contact_ui.py b/contact/ui/contact_ui.py index 29bbe4a..4a61d7f 100644 --- a/contact/ui/contact_ui.py +++ b/contact/ui/contact_ui.py @@ -468,9 +468,6 @@ def main_ui(stdscr: curses.window) -> None: elif char == chr(6): # Ctrl + F to toggle favorite handle_ctrl_f(stdscr) - elif char == chr(2): # Ctrl + B to toggle bot responder - handle_ctrl_b(stdscr) - elif char == chr(7): # Ctrl + G to toggle ignored handle_ctlr_g(stdscr) @@ -1023,7 +1020,6 @@ def handle_ctrl_k(stdscr: curses.window) -> None: t("ui.help.node_info", default="F5 = Full node info"), t("ui.help.archive_chat", default="Ctrl+D = Archive chat / remove node"), t("ui.help.favorite", default="Ctrl+F = Favorite"), - t("ui.help.bot_responder", default="Ctrl+B = Toggle Bot Responder"), t("ui.help.ignore", default="Ctrl+G = Ignore"), t("ui.help.search", default="Ctrl+/ = Search"), t("ui.help.help", default="Ctrl+K = Help"), @@ -1035,32 +1031,6 @@ def handle_ctrl_k(stdscr: curses.window) -> None: handle_resize(stdscr, False) -def handle_ctrl_b(stdscr: curses.window) -> None: - """Handle Ctrl + B key events to toggle automatic bot responses.""" - ui_state.bot_mode_enabled = not ui_state.bot_mode_enabled - status = t("ui.bot.status.enabled", default="Enabled") if ui_state.bot_mode_enabled else t( - "ui.bot.status.disabled", default="Disabled" - ) - - curses.curs_set(0) - contact.ui.dialog.dialog( - t("ui.bot.dialog.title", default="Bot Responder"), - t("ui.bot.dialog.body", default="Bot responder is now {status}.", status=status), - ) - - if ui_state.channel_list: - channel_id = ui_state.channel_list[ui_state.selected_channel] - add_new_message( - channel_id, - f"{config.message_prefix} Info: ", - t("ui.bot.status.message", default="Bot responder is now {status}.", status=status.lower()), - ) - draw_messages_window(True) - - curses.curs_set(1) - handle_resize(stdscr, False) - - def handle_ctrl_d() -> None: if ui_state.current_window == 0: if isinstance(ui_state.channel_list[ui_state.selected_channel], int): diff --git a/contact/ui/default_config.py b/contact/ui/default_config.py index b81a776..702b080 100644 --- a/contact/ui/default_config.py +++ b/contact/ui/default_config.py @@ -243,6 +243,7 @@ def initialize_config() -> Dict[str, object]: "node_sort": "lastHeard", "theme": "dark", "ping_bot": { + "enabled": "False", "catch_words": "ping; test", "response_word": "Pong!", }, @@ -280,7 +281,7 @@ def assign_config_variables(loaded_config: Dict[str, object]) -> None: global notification_symbol, ack_implicit_str, ack_str, nak_str, ack_unknown_str global node_list_16ths, channel_list_16ths, single_pane_mode global theme, COLOR_CONFIG, language - global node_sort, notification_sound, ping_bot_catch_words, ping_bot_response_word + global node_sort, notification_sound, ping_bot_enabled, ping_bot_catch_words, ping_bot_response_word channel_list_16ths = loaded_config["channel_list_16ths"] node_list_16ths = loaded_config["node_list_16ths"] @@ -300,6 +301,7 @@ def assign_config_variables(loaded_config: Dict[str, object]) -> None: node_sort = loaded_config["node_sort"] theme = loaded_config["theme"] ping_bot = loaded_config.get("ping_bot", {}) + ping_bot_enabled = ping_bot.get("enabled", "False") ping_bot_catch_words = ping_bot.get("catch_words", "ping; test") ping_bot_response_word = ping_bot.get("response_word", "Pong!") if theme == "dark": diff --git a/contact/ui/ui_state.py b/contact/ui/ui_state.py index 1fa9ee6..514e4cc 100644 --- a/contact/ui/ui_state.py +++ b/contact/ui/ui_state.py @@ -27,8 +27,6 @@ class ChatUIState: current_window: int = 0 last_sent_time: float = 0.0 last_traceroute_time: float = 0.0 - bot_mode_enabled: bool = False - selected_index: int = 0 start_index: List[int] = field(default_factory=lambda: [0, 0, 0]) show_save_option: bool = False diff --git a/contact/ui/user_config.py b/contact/ui/user_config.py index ac35f6a..ad04a2e 100644 --- a/contact/ui/user_config.py +++ b/contact/ui/user_config.py @@ -115,6 +115,9 @@ def edit_color_pair(key: str, display_label: str, current_value: List[str]) -> L def edit_value(key: str, display_label: str, current_value: str) -> str: + if key in ("notification_sound", "single_pane_mode", "enabled"): + return get_list_input(display_label, current_value, ["True", "False"]) + w = get_effective_width() height = 10 input_width = w - 16 # Allow space for "New Value: " @@ -170,14 +173,6 @@ def edit_value(key: str, display_label: str, current_value: str) -> str: sort_options = ["lastHeard", "name", "hops"] return get_list_input(display_label, current_value, sort_options) - elif key == "notification_sound": - sound_options = ["True", "False"] - return get_list_input(display_label, current_value, sound_options) - - elif key == "single_pane_mode": - sound_options = ["True", "False"] - return get_list_input(display_label, current_value, sound_options) - # Standard Input Mode (Scrollable) edit_win.addstr(7, 2, t("ui.label.new_value", default="New Value: "), get_color("settings_default")) curses.curs_set(1) diff --git a/tests/test_bot_handler.py b/tests/test_bot_handler.py index ae73b91..67b2e45 100644 --- a/tests/test_bot_handler.py +++ b/tests/test_bot_handler.py @@ -5,7 +5,7 @@ import types from unittest import mock import contact.ui.default_config as config -from contact.utilities.singleton import interface_state, ui_state +from contact.utilities.singleton import interface_state from tests.test_support import reset_singletons @@ -36,24 +36,28 @@ class BotHandlerTests(unittest.TestCase): self.assertTrue(self.bot_handler.is_bot_message("ping")) def test_bot_response_replies_to_triggering_packet(self) -> None: - ui_state.bot_mode_enabled = True interface_state.myNodeNum = 111 packet = {"id": 900, "from": 222, "decoded": {}} - with mock.patch.object(self.bot_handler.threading, "Thread") as thread: - self.assertTrue(self.bot_handler.bot_respond(packet, "ping", 0)) - send_response = thread.call_args.kwargs["target"] - with mock.patch.object(self.bot_handler.time, "sleep"): - with mock.patch.dict( - sys.modules, - {"contact.ui.contact_ui": types.SimpleNamespace(request_ui_redraw=mock.Mock())}, - ): - send_response() + with mock.patch.object(config, "ping_bot_enabled", "True"): + with mock.patch.object(self.bot_handler.threading, "Thread") as thread: + self.assertTrue(self.bot_handler.bot_respond(packet, "ping", 0)) + send_response = thread.call_args.kwargs["target"] + with mock.patch.object(self.bot_handler.time, "sleep"): + with mock.patch.dict( + sys.modules, + {"contact.ui.contact_ui": types.SimpleNamespace(request_ui_redraw=mock.Mock())}, + ): + send_response() self.bot_handler.send_message.assert_called_once_with( "Pong!", channel=0, reply_id=900 ) + def test_bot_response_requires_enabled_app_setting(self) -> None: + with mock.patch.object(config, "ping_bot_enabled", "False"): + self.assertFalse(self.bot_handler.bot_respond({"from": 222}, "ping", 0)) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 7c029fe..f279e50 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -57,15 +57,10 @@ class I18nTests(unittest.TestCase): self.assertEqual(i18n.t("missing", default="fallback"), "fallback") self.assertEqual(parse_ini_file.call_count, 2) - def test_bot_ui_translation_keys_exist_in_all_locales(self) -> None: + def test_ping_bot_settings_translation_keys_exist_in_all_locales(self) -> None: required_keys = { - "ui.help.bot_responder", - "ui.bot.status.enabled", - "ui.bot.status.disabled", - "ui.bot.dialog.title", - "ui.bot.dialog.body", - "ui.bot.status.message", "app_settings.ping_bot", + "app_settings.ping_bot.enabled", "app_settings.ping_bot.catch_words", "app_settings.ping_bot.response_word", } diff --git a/tests/test_user_config.py b/tests/test_user_config.py new file mode 100644 index 0000000..4f232a4 --- /dev/null +++ b/tests/test_user_config.py @@ -0,0 +1,18 @@ +import unittest +from unittest import mock + +import contact.ui.default_config # Initialize config before the color helpers. +from contact.ui import user_config + + +class UserConfigTests(unittest.TestCase): + def test_ping_bot_enabled_uses_boolean_picker(self) -> None: + with mock.patch.object(user_config, "get_list_input", return_value="True") as picker: + result = user_config.edit_value("enabled", "Enabled", "False") + + self.assertEqual(result, "True") + picker.assert_called_once_with("Enabled", "False", ["True", "False"]) + + +if __name__ == "__main__": + unittest.main()