Added and updated repositories 2026-05-31 02:47:15

This commit is contained in:
github-actions[bot]
2026-05-31 02:47:16 +00:00
parent d279789b37
commit 811beb2b74
25 changed files with 1905 additions and 809 deletions

View File

@@ -4,4 +4,6 @@
.ruff_cache
ruff.log
ruff.log.2
ruff.toml
ruff.toml
# Heroku files
heroku/

438
archquise/q.mods/QNotes.py Normal file
View File

@@ -0,0 +1,438 @@
__version__ = (1, 1, 6)
# █▀▀▄ █▀▄▀█ █▀█ █▀▄ █▀
# ▀▀▀█ ▄ █ ▀ █ █▄█ █▄▀ ▄█
# #### Copyright (c) 2026 Archquise #####
# 💬 Contact: https://t.me/archquise
# 🔒 Licensed under the GNU AGPLv3.
# 📄 LICENSE: https://raw.githubusercontent.com/archquise/Q.Mods/main/LICENSE
# ---------------------------------------------------------------------------------
# Name: QNotes
# Description: A notes module that just works
# Author: @quise_m
# ---------------------------------------------------------------------------------
# meta developer: @quise_m
# meta banner: https://raw.githubusercontent.com/archquise/qmods_meta/main/qnotes.png
# ---------------------------------------------------------------------------------
import asyncio
import logging
import re
from datetime import date
from typing import cast
from herokutl.tl.functions.users import GetUsersRequest
from herokutl.tl.types import InputUserSelf
from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds
class QNotes(loader.Module):
"""A notes module that just works\nUsage: #notetag in any chat"""
strings = {
"name": "QNotes",
"topic_desc": "Stores your notes content\nUsage: #notetag in any chat",
"wrongargs": "<emoji document_id=5980953710157632545>❌</emoji> <b>Wrong arguments. Check command usage.</b>",
"not_exist": "There is no such note!",
"no_reply": "No reply! Reply to the message, which text will become a note.",
"already_exists": "Seems like note with the same tag already exists. Overwrite?",
"show_note_inline": "<blockquote>#{}</blockquote>\n\n<blockquote>{}</blockquote>",
"notelist": "Note list:",
"msg_not_found_inline": "Message with this note wasn't found. Probably, it was been removed. Note has been removed from the database.",
"remnote_inline": "🗑 Remove",
"close_inline": "❌ Close",
"yes": "✔️ Yes",
"no": "❌ No",
"true": "yes",
"false": "no",
"saved": "Note saved!",
"removed": "Note removed!",
"nonotes": "You don't have any notes!",
"privacy_switch": "Determines whose data will be used by the my_* placeholders\n\nTrue - the account that is issuing the note\nFalse - the account on which the userbot is running",
"note_prefix": "The prefix used to call up notes",
"placeholders": """
<b>Available placeholders</b>:
about the account on which userbot is installed:
{my_id} - ID
@{my_username} - username, tag
{my_phone} - phone number
{my_premium} - premium status (yes/no)
about reply author:
{reply_id} - ID
{reply_name} - name
{reply_surname} - surname
{reply_fullname} - full name (name + surname (if specified))
@{reply_username} - username, tag
{reply_phone} - phone number (if not hidden)
{reply_premium} - premium status (yes/no)
general:
{today} - current date
""",
}
strings_ru = {
"_cls_doc": "Модуль для заметок, который просто работает\nИспользование: #тегзаметки в любом чате",
"topic_desc": "Хранит содержимое ваших заметок\nИспользование: #тегзаметки в любом чате",
"wrongargs": "<emoji document_id=5980953710157632545>❌</emoji> <b>Неверные аргументы. Проверьте использование команды.</b>",
"no_reply": "Нет реплая! Ответьте на сообщение, текст которого станет заметкой.",
"not_exist": "Такой заметки не найдено!",
"already_exists": "Кажется, заметка с таким тегом уже существует. Перезаписать?",
"show_note_inline": "<blockquote>#{}</blockquote>\n\n<blockquote>{}</blockquote>",
"notelist": "Список заметок:",
"msg_not_found_inline": "Сообщение с этой заметкой не было найдено. Вероятно, оно было удалено. Заметка очищена из базы данных.",
"remnote_inline": "🗑 Удалить",
"close_inline": "❌ Закрыть",
"yes": "✔️ Да",
"no": "❌ Нет",
"saved": "Заметка сохранена!",
"removed": "Заметка удалена!",
"true": "да",
"false": "нет",
"nonotes": "Нет заметок!",
"privacy_switch": "Влияет на то, чьи данные будут использовать my_* плейсхолдеры\n\nTrue - аккаунта, который вызывает заметку\nFalse - аккаунта на котором стоит юзербот",
"note_prefix": "Префикс, с которым вызываются заметки",
"placeholders": """
<b>Доступные плейсхолдеры</b>:
об аккаунте, на котором стоит юзербот:
{my_id} - айди
@{my_username} - юзернейм, тег
{my_phone} - номер телефона
{my_premium} - статус премиум (да/нет)
об авторе реплая:
{reply_id} - айди
{reply_name} - имя
{reply_surname} - фамилия
{reply_fullname} - полное имя (имя + фамилия (если указана))
@{reply_username} - юзернейм, тег
{reply_phone} - номер телефона (если не скрыт)
{reply_premium} - статус премиум (да/нет)
общее:
{today} - текущая дата
""",
}
def __init__(self):
self.config = loader.ModuleConfig(
loader.ConfigValue(
"privacy_switch",
True,
lambda: self.strings["privacy_switch"],
validator=loader.validators.Boolean(), # type: ignore
),
loader.ConfigValue(
"note_prefix",
"#",
lambda: self.strings["note_prefix"],
validator=loader.validators.RegExp(r"^\S+$"), # type: ignore
),
)
async def client_ready(self, client, db): # type: ignore
self._content_channel_id = await utils.wait_for_content_channel(self._db)
self._notes_topic = await utils.asset_forum_topic(
client=self._client,
db=self._db,
peer=self._content_channel_id, # type: ignore
title="QNotes | Storage",
description=self.strings["topic_desc"],
icon_emoji_id=5272001961326049733,
)
self.my_phone = (await self._client(GetUsersRequest(id=[InputUserSelf()])))[
0
].phone
self.placeholders = {
"my_phone": self.my_phone,
"my_username": self._client.heroku_me.username,
"my_id": self.tg_id,
"my_premium": self.strings["true"]
if self._client.heroku_me.premium
else self.strings["false"],
}
self._notemap = cast(dict, self.pointer("notemap", default={}))
async def _ask_overwrite(self, message):
loop = asyncio.get_running_loop()
future = loop.create_future()
form = await self.inline.form(
self.strings["already_exists"],
message=message,
reply_markup=[
[
{
"text": self.strings["yes"],
"callback": (
lambda call, flag: (
future.set_result(flag) if not future.done() else None
)
),
"args": (True,),
},
{
"text": self.strings["no"],
"callback": (
lambda call, flag: (
future.set_result(flag) if not future.done() else None
)
),
"args": (False,),
},
]
],
)
try:
async with asyncio.timeout(15):
overwrite_answer = await future
except TimeoutError:
await form.delete() # type: ignore
return False, message
if not overwrite_answer:
await form.delete() # type: ignore
return False, form
return True, form
async def _show_note_inline(self, call, note, page=0):
async def _remnote(call, notetag, note_msg):
await note_msg.delete()
self._notemap.pop(notetag, None)
await call.edit(self.strings["removed"])
note_msg = await self._client.get_messages(
self._content_channel_id, ids=note[1]
)
if not note_msg:
self._notemap.pop(note[0], None)
await call.edit(
self.strings["msg_not_found_inline"],
reply_markup=[
{"text": "⬅️ Назад", "callback": self._list_page, "args": (page,)},
{"text": self.strings["close_inline"], "action": "close"},
],
)
return
await call.edit(
self.strings["show_note_inline"].format(note[0], note_msg.text), # type: ignore
reply_markup=[
[
{"text": "⬅️ Назад", "callback": self._list_page, "args": (page,)},
{
"text": self.strings["remnote_inline"],
"callback": _remnote,
"args": (note[0], note_msg),
},
],
[{"text": self.strings["close_inline"], "action": "close"}],
],
)
def _build_list_markup(self, page: int):
items = list(self._notemap.items())
total = -(-len(items) // 3)
page = max(0, min(page, total - 1))
rows = [
[
{
"text": notetag,
"callback": self._show_note_inline,
"args": ([notetag, msg_id], page),
}
]
for notetag, msg_id in items[page * 3 : (page + 1) * 3]
]
return (
rows
+ self.inline.build_pagination(
callback=self._list_page, # type: ignore
total_pages=total,
current_page=page + 1,
)
+ [[{"text": self.strings["close_inline"], "action": "close"}]]
)
async def _list_page(self, call, page):
await call.edit(
text=self.strings["notelist"], reply_markup=self._build_list_markup(page)
)
@loader.command(
ru_doc="Сохраняет заметку под тегом | Пример: .qnsave заметка",
en_doc="Saves note by tag | Example: .qnsave note",
)
async def qnsave(self, message) -> None:
args = utils.get_args(message)
if not args:
await utils.answer(message, self.strings["wrongargs"])
return
current_message = message
if not (reply := await message.get_reply_message()):
await utils.answer(message, self.strings["no_reply"])
return
try:
if args[0].strip() in self._notemap:
need_overwrite, msg = await self._ask_overwrite(message)
if not need_overwrite:
return
old_note_message = await self._client.get_messages(
self._content_channel_id,
ids=self._notemap[args[0].strip()],
)
old_note_message and await old_note_message.delete() # type: ignore
current_message = msg
note_message = await self._client.send_message(
self._content_channel_id,
reply.text,
reply_to=self._notes_topic.id,
file=reply.media,
)
self._notemap[args[0].strip()] = note_message.id
except Exception as e:
await utils.answer(current_message, f"Произошла ошибка: {e}")
logger.exception("Произошла ошибка при сохранении заметки!")
return
await utils.answer(current_message, self.strings["saved"])
@loader.command(
ru_doc="Удаляет заметку по тегу | Пример: .qnrem заметка",
en_doc="Removes note by tag | Example: .qnrem note",
)
async def qnrem(self, message) -> None:
args = utils.get_args(message)
if not args:
await utils.answer(message, self.strings["wrongargs"])
return
if args[0] not in self._notemap or not (
note_message := await self._client.get_messages(
self._content_channel_id,
ids=self._notemap[args[0]],
)
):
await utils.answer(message, self.strings["not_exist"])
return
await note_message.delete() # type: ignore
self._notemap.pop(args[0], None)
await utils.answer(message, self.strings["removed"])
@loader.command(
ru_doc="Выводит список всех заметок и позволяет управлять ими",
en_doc="Shows note list and allows managing them",
)
async def qnlist(self, message) -> None:
if self._notemap:
await self.inline.form(
text=self.strings["notelist"],
reply_markup=self._build_list_markup(0),
message=message,
)
return
await utils.answer(message, self.strings["nonotes"])
@loader.command(
ru_doc="Выводит список доступных плейсхолдеров",
en_doc="Displays a list of available placeholders",
)
async def qnp(self, message) -> None:
await utils.answer(message, self.strings["placeholders"])
@loader.watcher()
async def _note_watcher(self, message):
if not message.text.startswith(prefix := self.config["note_prefix"]) or not (
await self._client.dispatcher.security.check(message, self._note_watcher)
):
return
notetag = message.text.split(prefix, maxsplit=1)[1]
if notetag in self._notemap:
if not (
note_message := await self._client.get_messages(
self._content_channel_id,
ids=self._notemap[notetag],
)
):
self._notemap.pop(notetag, None)
return
notetext = note_message.text or "" # type: ignore
if re.search(r"\{\w+\}", notetext):
if (
not self.config["privacy_switch"]
or message.sender_id == self._client.heroku_me.id
):
placeholders = {**self.placeholders}
else:
message_author_entity = await self._client.get_entity(
message.sender_id
)
placeholders = {
"my_phone": (
await self._client(GetUsersRequest(id=[message.sender_id]))
)[0].phone,
"my_username": message_author_entity.username,
"my_id": message.sender_id,
"my_premium": self.strings["true"]
if message_author_entity.premium
else self.strings["false"],
}
if reply_msg := await message.get_reply_message():
reply_user = await self._client.get_entity(reply_msg.sender_id)
placeholders = {
**placeholders,
"reply_id": reply_user.id,
"reply_fullname": " ".join(
filter(None, [reply_user.first_name, reply_user.last_name])
),
"reply_name": reply_user.first_name,
"reply_surname": reply_user.last_name,
"reply_phone": (
await self._client(GetUsersRequest(id=[reply_user.id]))
)[0].phone,
"reply_username": reply_user.username,
"reply_premium": self.strings["true"]
if reply_user.premium
else self.strings["false"],
}
placeholders = placeholders | {"today": date.today()}
def replacer(match):
key = match.group(1)
if key not in placeholders or not placeholders[key]:
return match.group(0)
return utils.escape_html(str(placeholders[key]))
notetext = re.sub(r"\{(\w+)\}", replacer, notetext)
if media := note_message.media: # type: ignore
await utils.answer_file(message, media, notetext) # type: ignore
else:
await utils.answer(message, notetext)
return

View File

@@ -59,7 +59,7 @@ class FaceMod(loader.Module):
en_doc="Random kaomoji",
)
async def rfacecmd(self, message) -> None: # noqa: D102, ANN001
await utils.answer(message, self.strings("loading"))
await utils.answer(message, self.strings["loading"])
url = "https://files.archquise.ru/kaomoji.txt"
@@ -72,7 +72,7 @@ class FaceMod(loader.Module):
kaomoji = random.choice(kaomoji_list) # noqa: S311
await utils.answer(
message,
self.strings("random_face").format(kaomoji),
self.strings["random_face"].format(kaomoji),
)
else:
await utils.answer(message, self.strings("error"))
await utils.answer(message, self.strings["error"])

View File

@@ -113,24 +113,24 @@ class ShortenerMod(loader.Module):
async def shortencmd(self, message): # noqa: ANN001, ANN201
"""Shorten URL using bit.ly API."""
if self.config["token"] is None:
await utils.answer(message, self.strings("no_api"))
await utils.answer(message, self.strings["no_api"])
return
args = utils.get_args_raw(message)
if not args:
await utils.answer(message, self.strings("no_args"))
await utils.answer(message, self.strings["no_args"])
return
if not self._validate_url(args):
await utils.answer(message, self.strings("invalid_url"))
await utils.answer(message, self.strings["invalid_url"])
return
try:
short_url = await self.shorten_url(url=args, token=self.config["token"])
await utils.answer(message, self.strings("shortencmd").format(c=short_url))
await utils.answer(message, self.strings["shortencmd"].format(c=short_url))
except Exception as e:
logger.exception("Error shortening URL!")
await utils.answer(message, self.strings("api_error").format(error=str(e)))
await utils.answer(message, self.strings["api_error"].format(error=str(e)))
@loader.command(
ru_doc="Посмотреть статистику ссылки через bit.ly (ссылка без https:// | Доступно только на платных аккаунтах)",
@@ -139,22 +139,22 @@ class ShortenerMod(loader.Module):
async def statclcmd(self, message): # noqa: ANN001, ANN201
"""Get click statistics for shortened URL."""
if self.config["token"] is None:
await utils.answer(message, self.strings("no_api"))
await utils.answer(message, self.strings["no_api"])
return
args = utils.get_args_raw(message)
if not args:
await utils.answer(message, self.strings("no_args"))
await utils.answer(message, self.strings["no_args"])
return
try:
if not args.startswith("bit.ly/"):
await utils.answer(message, self.strings("invalid_url"))
await utils.answer(message, self.strings["invalid_url"])
return
clicks = await self.get_bitlink_stats(
bitlink=args, token=self.config["token"]
)
await utils.answer(message, self.strings("statclcmd").format(c=clicks))
await utils.answer(message, self.strings["statclcmd"].format(c=clicks))
except Exception as e:
logger.exception("Error getting statistics!")
await utils.answer(message, self.strings("api_error").format(error=str(e)))
await utils.answer(message, self.strings["api_error"].format(error=str(e)))