# ---------------------------------------------------------------------------------
# ░█▀▄░▄▀▀▄░█▀▄░█▀▀▄░█▀▀▄░█▀▀▀░▄▀▀▄░░░█▀▄▀█
# ░█░░░█░░█░█░█░█▄▄▀░█▄▄█░█░▀▄░█░░█░░░█░▀░█
# ░▀▀▀░░▀▀░░▀▀░░▀░▀▀░▀░░▀░▀▀▀▀░░▀▀░░░░▀░░▒▀
# Name: MentionsTagger
# Description: Monitors and logs keyword trigger mentions across chats
# Author: @codrago_m
# ---------------------------------------------------------------------------------
# 🔒 Licensed under the GNU AGPLv3
# 🌐 https://www.gnu.org/licenses/agpl-3.0.html
# ---------------------------------------------------------------------------------
# Author: @codrago
# Commands: tagger
# scope: heroku_only
# meta developer: @codrago_m, @zetgo
# meta banner: https://raw.githubusercontent.com/coddrago/modules/refs/heads/main/banner.png
# ---------------------------------------------------------------------------------
__version__ = (1, 0)
from herokutl.custom import Message
from .. import loader, utils
from ..types import BotInlineCall
@loader.tds
class MentionsTaggerMod(loader.Module):
"""Module for monitoring and logging keyword trigger mentions across chats"""
strings = {
"name": "MentionsTagger",
"topic_description": "Системный журнал фиксации упоминаний и ключевых триггеров в чатах",
"user_label": "пользователя",
"btn_ignore_chat": "Игнорировать чат",
"btn_ignore_user": "Игнорировать пользователя",
"btn_ignore_both": "Игнорировать обоих",
"alert_chat_ignored": "Чат добавлен в список исключений",
"alert_user_ignored": "Пользователь добавлен в список исключений",
"alert_both_ignored": "Чат и пользователь добавлены в список исключений",
"status_enabled": "[MentionsTagger] Мониторинг успешно включен",
"status_disabled": "[MentionsTagger] Мониторинг выключен",
"default_notify_text": (
"❗️ "
"[Mentions Alert] Зафиксирован ключевой триггер для "
"{display_name} (@{username}). "
"Открыть контекст"
),
}
strings_en = {
"name": "MentionsTagger",
"topic_description": "System log of detected mentions and keyword triggers across chats",
"user_label": "user",
"btn_ignore_chat": "Ignore chat",
"btn_ignore_user": "Ignore user",
"btn_ignore_both": "Ignore both",
"alert_chat_ignored": "Chat added to exception list",
"alert_user_ignored": "User added to exception list",
"alert_both_ignored": "Chat and user added to exception list",
"status_enabled": "[MentionsTagger] Monitoring has been enabled",
"status_disabled": "[MentionsTagger] Monitoring has been disabled",
"default_notify_text": (
"❗️ "
"[Mentions Alert] Keyword trigger detected for "
"{display_name} (@{username}). "
"Open context"
),
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"triggers",
["codrago"],
"List of keywords/triggers to monitor",
validator=loader.validators.Series(
loader.validators.String(),
),
),
loader.ConfigValue(
"display_name",
None,
"Display name/pseudonym in logs",
validator=loader.validators.Union(
loader.validators.String(),
loader.validators.NoneType(),
),
),
loader.ConfigValue(
"custom_message",
None,
"Custom log template (placeholders: {user_id}, {display_name}, {username}, {message_link})",
validator=loader.validators.Union(
loader.validators.String(),
loader.validators.NoneType(),
),
),
loader.ConfigValue(
"ignored_chats",
[],
"IDs of chats excluded from monitoring",
validator=loader.validators.Series(
loader.validators.Integer(),
),
),
loader.ConfigValue(
"ignored_users",
[],
"IDs of users whose messages are ignored",
validator=loader.validators.Series(
loader.validators.Integer(),
),
),
)
async def client_ready(self):
self.heroku_forum = self._db.get("heroku.forums", "channel_id", 0)
self.assets_topic = await utils.asset_forum_topic(
self.client,
self.db,
self.heroku_forum,
self.strings("name"),
self.strings("topic_description"),
)
async def taggercmd(self, message: Message):
"""Toggle mentions monitoring status"""
is_active = not self.get("active", False)
self.set("active", is_active)
await utils.answer(
message,
self.strings("status_enabled" if is_active else "status_disabled"),
)
@loader.watcher()
async def watcher(self, message: Message):
if not self.get("active", False):
return
is_chat = getattr(message, "is_group", False) or getattr(message, "is_channel", False)
match is_chat:
case True:
pass
case _:
return
if (
not getattr(message, "message", "")
or message.mentioned
or message.out
or getattr(message, "via_bot_id", None) is not None
or message.sender_id == self.inline.bot_id
or message.chat_id in self.config["ignored_chats"]
or message.sender_id in self.config["ignored_users"]
):
return
msg_text_lower = message.message.lower()
if not any(trigger.lower() in msg_text_lower for trigger in self.config["triggers"]):
return
chat = getattr(message, "chat", None) or await message.get_chat()
chat_title = getattr(chat, "title", "") or ""
chat_username = getattr(chat, "username", "") or ""
if (
chat_title.lower().startswith("heroku-")
or chat_username.lower().startswith("heroku-")
):
return
username = (
self.client.heroku_me.username
or (
self.client.heroku_me.usernames[0].username
if getattr(self.client.heroku_me, "usernames", None)
else f"id{self.tg_id}"
)
)
display_name = self.config["display_name"] or self.strings("user_label")
template = self.config["custom_message"] or self.strings("default_notify_text")
text = template.format(
user_id=self.tg_id,
display_name=display_name,
username=username,
message_link=await message.link(),
)
await self.inline.bot.send_message(
self.heroku_forum,
text,
message_thread_id=self.assets_topic.id,
link_preview=False,
reply_markup=self.inline.generate_markup(
[
[
{
"text": self.strings("btn_ignore_chat"),
"callback": self._handle_ignore,
"args": ("chat", message.chat_id, message.sender_id),
"style": "primary",
"icon_custom_emoji_id": 5255961723059320920,
},
{
"text": self.strings("btn_ignore_user"),
"callback": self._handle_ignore,
"args": ("user", message.chat_id, message.sender_id),
"style": "primary",
"icon_custom_emoji_id": 5253527438675158560,
},
],
[
{
"text": self.strings("btn_ignore_both"),
"callback": self._handle_ignore,
"args": ("both", message.chat_id, message.sender_id),
"style": "primary",
"icon_custom_emoji_id": 5255831443816327915,
},
],
]
),
)
async def _handle_ignore(self, call: BotInlineCall, target: str, chat_id: int, user_id: int):
match target:
case "chat":
if chat_id not in self.config["ignored_chats"]:
self.config["ignored_chats"] = self.config["ignored_chats"] + [chat_id]
await call.answer(self.strings("alert_chat_ignored"), show_alert=True)
case "user":
if user_id not in self.config["ignored_users"]:
self.config["ignored_users"] = self.config["ignored_users"] + [user_id]
await call.answer(self.strings("alert_user_ignored"), show_alert=True)
case "both":
if chat_id not in self.config["ignored_chats"]:
self.config["ignored_chats"] = self.config["ignored_chats"] + [chat_id]
if user_id not in self.config["ignored_users"]:
self.config["ignored_users"] = self.config["ignored_users"] + [user_id]
await call.answer(self.strings("alert_both_ignored"), show_alert=True)
case _:
await call.answer()