commit a71d4ce5f8777ef478f5db2ecbb1129dd6a54373 Author: nikita Date: Mon Sep 7 01:25:32 2026 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..150a104 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.venv/ +session_storage/ +__pycache__/ +*.swp +*.swo +*.vscode +*.json +*.sqlite diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b61260 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# 2026-matrix-down + +Matrix бот для скачивания видео/аудио с *YouTube, Pinterest, Instagram* и других +хостингов, поддерживаемых приложением `yt-dlp`. + +Хотя этот проект в первую очередь стремится быть максимально удобным Matrix +ботом, не в последнюю очередь он существует ради того, чтобы с его помощью +тестировать библиотеку [`mab`](https://git.tyukalov.su/nikita/mab), а точнее +её возможности загрузки вложений в Matrix. + +> В системе должны быть установлены `ffmpeg` и `ffprobe`. + +## Работа с ботом + +Первый запуск сгенерирует файл `config.json` в рабочей директории. Используйте +его, чтобы ввести адрес сервера и имя пользователя. Затем при запуске бота +авторизуйтесь интерактивно. + +Бот должен быть добавлен в комнату, в которой с ним будут работать. Все +сообщения в комнате бот обрабатывает, как адресованные лично ему. + +- Если сообщение заканчивается на ` TesT` (регистр и пробел в начале важен), то + бот отправит медиафайлы из папки `test_media` +- Если сообщение начинается не с `http://` или `https://`, то бот даст справку +- Если сообщение начинается с `http://` или `https://`, то бот передаст текст + сообщения в качестве ссылки для `yt-dlp` + +**Дальнейший интерфейс ещё не придуман** \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..5b66d18 --- /dev/null +++ b/config.py @@ -0,0 +1,52 @@ +"""Module for generating and reading config""" + +import json +import aiofiles +import os +import traceback + +from mab import MatrixBotConfig + +DEFAULT_CONFIG = { + "matrix_homeserver_url": "https://matrix.domain.net", + "matrix_username_localpart": "short_username", + "storage_directory": "session_storage" +} + + + +# +# PRIVATE +# +async def write_default_config(path: str) -> None: + try: + async with aiofiles.open(path, "w") as f: + await f.write(json.dumps(DEFAULT_CONFIG, indent=4)) + except: + traceback.print_exc() + + + +# +# PUBLIC +# +async def load_config(path: str = "config.json") -> MatrixBotConfig | None: + """Load config from specified path. Creates default config if it does not exist (but still returns None).""" + # no file + if not os.path.isfile(path): + await write_default_config(path) + return None + # open and read + try: + async with aiofiles.open(path, "r") as f: + j = json.loads((await f.read()).strip()) + except: + traceback.print_exc() + return None + # parse + try: + cfg = MatrixBotConfig(**j) + return cfg + except: + traceback.print_exc() + return None \ No newline at end of file diff --git a/logic.py b/logic.py new file mode 100644 index 0000000..866c079 --- /dev/null +++ b/logic.py @@ -0,0 +1,101 @@ +"""Bot's callbacks implementation""" + +import asyncio +import glob +import html +import traceback + +from mab import MatrixBot +from mab.types import VideoFileProperties +from mab.filters.text import TextFilter, TextStartsWithFilter, TextEndsWithFilter + +from nio import MatrixRoom, RoomMessageText + +# +# PRIVATE DATA +# +_bot: MatrixBot + +# +# CALLBACKS +# +async def command_help(room: MatrixRoom, text: RoomMessageText) -> None: + """!help""" + if text.sender == _bot.get_client().user_id: + return + resp = "2026-matrix-down
" + resp += "Этот бот используется для скачивания видео/аудио с хостингов, " + resp += "поддерживаемых yt-dlp. Просто отправьте ссылку в эту " + resp += "комнату, чтобы бот предложил варианты загрузки." + await _bot.send_text_to_room(room.room_id, resp, True) + +async def command_http_https(room: MatrixRoom, text: RoomMessageText) -> None: + """The message starts with `http://` or `https://`""" + if text.sender == _bot.get_client().user_id: + return + resp = "Сообщение начинается с http:// или с " + resp += "https://. Ещё не реализовано." + await _bot.send_text_to_room(room.room_id, resp, True) + +async def command_test(room: MatrixRoom, text: RoomMessageText) -> None: + """The message ends with ` TesT` (case matters)""" + if text.sender == _bot.get_client().user_id: + return + # glob patterns to use + glob_img = ["*.png", "*.jpg", ".jpeg", "*.gif", "*.webp"] + glob_vid = ["*.mp4", "*.avi", "*.mkv"] + # TODO + glob_aud = ["*.mp3", "*.m4a", ".wav", "*.ogg", "*.flac"] + # help message + resp = "Сообщение заканчивается на TesT.
" + resp += "Сейчас будут отправлены все файлы из директории test_media" + resp += ", у которых следующие расширения:
" + resp += f"" + resp += f"" + await _bot.send_text_to_room(room.room_id, resp, is_html=True) + await asyncio.sleep(1) + # send images + for g in [f"test_media/{g}" for g in glob_img]: + imgs = glob.glob(g) + for file in imgs: + try: + await _bot.send_image_to_room( + room.room_id, + file, + caption=f"Картинка {html.escape(file)}", + is_html=True + ) + except: + await _bot.send_text_to_room(room.room_id, traceback.format_exc(), False) + await asyncio.sleep(0.5) + # send videos + for g in [f"test_media/{g}" for g in glob_vid]: + imgs = glob.glob(g) + for file in imgs: + try: + await _bot.send_video_to_room( + room.room_id, + file, + caption=f"Видео {html.escape(file)}", + is_html=True + ) + except: + await _bot.send_text_to_room(room.room_id, traceback.format_exc(), False) + await asyncio.sleep(0.5) + + + +# +# PUBLIC +# +async def initialize(bot: MatrixBot) -> None: + """This function initializes the bot. It does not start it.""" + global _bot + _bot = bot + bot.add_event_callback(command_http_https, TextStartsWithFilter(["https://", "http://"])) + bot.add_event_callback(command_test, TextEndsWithFilter(" TesT", any_case=False)) + bot.add_event_callback(command_help, TextFilter()) + +async def deinitizalize(bot: MatrixBot) -> None: + """This function deinitializes the bot. It does not stop it.""" + pass \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..061566e --- /dev/null +++ b/main.py @@ -0,0 +1,60 @@ +"""Application entry point.""" + +import asyncio +import logging +from pathlib import Path +import signal +import traceback + +from mab import MatrixBot + +import logic +import config + +def signal_callback(event: asyncio.Event) -> None: + logging.info("Signal is caught") + event.set() + +async def main() -> None: + """Application entry point.""" + # logging setup + logging.basicConfig( + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + level=logging.INFO + ) + logging.getLogger("nio").setLevel(logging.CRITICAL + 1) + logging.info("2026-matrix-down") + # setup app termination logic + stop_event = asyncio.Event() + for s in (signal.SIGINT, signal.SIGTERM): + asyncio.get_running_loop().add_signal_handler( + s, + signal_callback, + stop_event + ) + # load the config + bot_config = await config.load_config() + if bot_config is None: + logging.critical("Please edit config.json and restart the bot") + return + + # setup the bot + bot = MatrixBot(bot_config) + await logic.initialize(bot) + bot.start() + + # wait for termination + logging.info("You may use Ctrl+C to terminate the app") + await stop_event.wait() + + await bot.stop() + await logic.deinitizalize(bot) + +if __name__ == "__main__": + try: + asyncio.run(main()) + except SystemExit: + raise + except: + traceback.print_exc() \ No newline at end of file diff --git a/test_media/besplatno.gif b/test_media/besplatno.gif new file mode 100644 index 0000000..1e62832 Binary files /dev/null and b/test_media/besplatno.gif differ diff --git a/test_media/derevya.webp b/test_media/derevya.webp new file mode 100644 index 0000000..017346c Binary files /dev/null and b/test_media/derevya.webp differ diff --git a/test_media/domoi_walter.mp4 b/test_media/domoi_walter.mp4 new file mode 100644 index 0000000..81ea738 Binary files /dev/null and b/test_media/domoi_walter.mp4 differ diff --git a/test_media/lobok.jpg b/test_media/lobok.jpg new file mode 100644 index 0000000..9e024a4 Binary files /dev/null and b/test_media/lobok.jpg differ diff --git a/test_media/minecraft.avi b/test_media/minecraft.avi new file mode 100644 index 0000000..af2b9ff Binary files /dev/null and b/test_media/minecraft.avi differ diff --git a/test_media/pipish.mkv b/test_media/pipish.mkv new file mode 100644 index 0000000..9e34f11 Binary files /dev/null and b/test_media/pipish.mkv differ diff --git a/test_media/ptichki.mp4 b/test_media/ptichki.mp4 new file mode 100644 index 0000000..39fa7aa Binary files /dev/null and b/test_media/ptichki.mp4 differ diff --git a/test_media/sekret.png b/test_media/sekret.png new file mode 100644 index 0000000..08b5070 Binary files /dev/null and b/test_media/sekret.png differ diff --git a/test_media/utopia_show.jpg b/test_media/utopia_show.jpg new file mode 100644 index 0000000..000c358 Binary files /dev/null and b/test_media/utopia_show.jpg differ