"""Processes incoming events and dispatches them to appropriate handlers"""
# Friendly Telegram (telegram userbot)
# Copyright (C) 2018-2022 The Authors
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program 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 Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see {utils.escape_html(message.message)}
failed"
" due to RPC (Telegram) error:"
f" {utils.escape_html(str(exc))}
"
)
txt = (
self._client.loader.lookup("translations")
.strings("rpc_error")
.format(
utils.escape_html(message.message),
utils.escape_html(str(exc)),
)
)
else:
if not self._db.get(main.__name__, "inlinelogs", True):
txt = (
"{utils.escape_html(message.message)}
"
" failed!"
)
else:
exc = "\n".join(traceback.format_exc().splitlines()[1:])
txt = (
"{utils.escape_html(message.message)}
"
" failed!\n\n🧾 Logs:\n
{utils.escape_html(exc)}
'
)
with contextlib.suppress(Exception):
await (message.edit if message.out else message.reply)(txt)
async def watcher_exc(self, *_):
logger.exception("Error running watcher", extra={"stack": inspect.stack()})
async def _handle_tags(
self,
event: typing.Union[events.NewMessage, events.MessageDeleted],
func: callable,
) -> bool:
return bool(await self._handle_tags_ext(event, func))
async def _handle_tags_ext(
self,
event: typing.Union[events.NewMessage, events.MessageDeleted],
func: callable,
) -> str:
"""
Handle tags.
:param event: The event to handle.
:param func: The function to handle.
:return: The reason for the tag to fail.
"""
m = event if isinstance(event, Message) else getattr(event, "message", event)
reverse_mapping = {
"out": lambda: getattr(m, "out", True),
"in": lambda: not getattr(m, "out", True),
"only_messages": lambda: isinstance(m, Message),
"editable": (
lambda: not getattr(m, "out", False)
and not getattr(m, "fwd_from", False)
and not getattr(m, "sticker", False)
and not getattr(m, "via_bot_id", False)
),
"no_media": lambda: (
not isinstance(m, Message) or not getattr(m, "media", False)
),
"only_media": lambda: isinstance(m, Message) and getattr(m, "media", False),
"only_photos": lambda: utils.mime_type(m).startswith("image/"),
"only_videos": lambda: utils.mime_type(m).startswith("video/"),
"only_audios": lambda: utils.mime_type(m).startswith("audio/"),
"only_stickers": lambda: getattr(m, "sticker", False),
"only_docs": lambda: getattr(m, "document", False),
"only_inline": lambda: getattr(m, "via_bot_id", False),
"only_channels": lambda: (
getattr(m, "is_channel", False) and not getattr(m, "is_group", False)
),
"no_channels": lambda: not getattr(m, "is_channel", False),
"no_groups": (
lambda: not getattr(m, "is_group", False)
or getattr(m, "private", False)
or getattr(m, "is_channel", False)
),
"only_groups": (
lambda: getattr(m, "is_group", False)
or not getattr(m, "private", False)
and not getattr(m, "is_channel", False)
),
"no_pm": lambda: not getattr(m, "private", False),
"only_pm": lambda: getattr(m, "private", False),
"no_inline": lambda: not getattr(m, "via_bot_id", False),
"no_stickers": lambda: not getattr(m, "sticker", False),
"no_docs": lambda: not getattr(m, "document", False),
"no_audios": lambda: not utils.mime_type(m).startswith("audio/"),
"no_videos": lambda: not utils.mime_type(m).startswith("video/"),
"no_photos": lambda: not utils.mime_type(m).startswith("image/"),
"no_forwards": lambda: not getattr(m, "fwd_from", False),
"no_reply": lambda: not getattr(m, "reply_to_msg_id", False),
"only_forwards": lambda: getattr(m, "fwd_from", False),
"only_reply": lambda: getattr(m, "reply_to_msg_id", False),
"mention": lambda: getattr(m, "mentioned", False),
"no_mention": lambda: not getattr(m, "mentioned", False),
"startswith": lambda: (
isinstance(m, Message) and m.raw_text.startswith(func.startswith)
),
"endswith": lambda: (
isinstance(m, Message) and m.raw_text.endswith(func.endswith)
),
"contains": lambda: isinstance(m, Message) and func.contains in m.raw_text,
"filter": lambda: callable(func.filter) and func.filter(m),
"from_id": lambda: getattr(m, "sender_id", None) == func.from_id,
"chat_id": lambda: utils.get_chat_id(m)
== (
func.chat_id
if not str(func.chat_id).startswith("-100")
else int(str(func.chat_id)[4:])
),
"regex": lambda: (
isinstance(m, Message) and re.search(func.regex, m.raw_text)
),
}
return (
"no_commands"
if getattr(func, "no_commands", False)
and await self._handle_command(event, watcher=True)
else (
"only_commands"
if getattr(func, "only_commands", False)
and not await self._handle_command(event, watcher=True)
else next(
(
tag
for tag in ALL_TAGS
if getattr(func, tag, False)
and tag in reverse_mapping
and not reverse_mapping[tag]()
),
None,
)
)
)
async def handle_incoming(
self,
event: typing.Union[events.NewMessage, events.MessageDeleted],
):
"""Handle all incoming messages"""
message = utils.censor(getattr(event, "message", event))
blacklist_chats = self._db.get(main.__name__, "blacklist_chats", [])
whitelist_chats = self._db.get(main.__name__, "whitelist_chats", [])
whitelist_modules = self._db.get(main.__name__, "whitelist_modules", [])
# ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
# It's not recommended to remove the security check below (external_bl)
# If you attempt to bypass this protection, you will be banned from the chat
# The protection from using userbots is multi-layer and this is one of the layers
# If you bypass it, the next (external) layer will trigger and you will be banned
# ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
if (
(chat_id := utils.get_chat_id(message)) in self._external_bl
or chat_id in blacklist_chats
or (whitelist_chats and chat_id not in whitelist_chats)
):
logger.debug("Message is blacklisted")
return
for func in self._modules.watchers:
bl = self._db.get(main.__name__, "disabled_watchers", {})
modname = str(func.__self__.__class__.strings["name"])
if (
modname in bl
and isinstance(message, Message)
and (
"*" in bl[modname]
or chat_id in bl[modname]
or "only_chats" in bl[modname]
and message.is_private
or "only_pm" in bl[modname]
and not message.is_private
or "out" in bl[modname]
and not message.out
or "in" in bl[modname]
and message.out
)
or f"{str(chat_id)}.{func.__self__.__module__}" in blacklist_chats
or whitelist_modules
and f"{str(chat_id)}.{func.__self__.__module__}"
not in whitelist_modules
or await self._handle_tags(event, func)
):
logger.debug(
"Ignored watcher of module %s because of %s",
modname,
await self._handle_tags_ext(event, func),
)
continue
# Avoid weird AttributeErrors in weird dochub modules by settings placeholder
# of attributes
for placeholder in {"text", "raw_text", "out"}:
try:
if not hasattr(message, placeholder):
setattr(message, placeholder, "")
except UnicodeDecodeError:
pass
# Run watcher via ensure_future so in case user has a lot
# of watchers with long actions, they can run simultaneously
asyncio.ensure_future(
self.future_dispatcher(
func,
message,
self.watcher_exc,
)
)
async def future_dispatcher(
self,
func: callable,
message: Message,
exception_handler: callable,
*args,
):
# Will be used to determine, which client caused logging messages
# parsed via inspect.stack()
_hikka_client_id_logging_tag = copy.copy(self.client.tg_id) # noqa: F841
try:
await func(message)
except Exception as e:
await exception_handler(e, message, *args)
async def _external_bl_reload_loop(self):
while True:
with contextlib.suppress(Exception):
self._external_bl = (
await utils.run_sync(
requests.get,
"https://ubguard.dan.tatar/blacklist.json",
)
).json()["blacklist"]
await asyncio.sleep(60)