Initial commit
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.venv/
|
||||||
|
session_storage/
|
||||||
|
__pycache__/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.vscode
|
||||||
|
*.json
|
||||||
|
*.sqlite
|
||||||
28
README.md
Normal file
28
README.md
Normal file
@@ -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`
|
||||||
|
|
||||||
|
**Дальнейший интерфейс ещё не придуман**
|
||||||
52
config.py
Normal file
52
config.py
Normal file
@@ -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
|
||||||
101
logic.py
Normal file
101
logic.py
Normal file
@@ -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 = "<strong>2026-matrix-down</strong><br>"
|
||||||
|
resp += "Этот бот используется для скачивания видео/аудио с хостингов, "
|
||||||
|
resp += "поддерживаемых <code>yt-dlp</code>. Просто отправьте ссылку в эту "
|
||||||
|
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 = "<strong>Сообщение начинается с <code>http://</code> или с <code>"
|
||||||
|
resp += "https://</code></strong>. Ещё не реализовано."
|
||||||
|
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 = "<strong>Сообщение заканчивается на <code> TesT</code>.</strong><br>"
|
||||||
|
resp += "Сейчас будут отправлены все файлы из директории <code>test_media"
|
||||||
|
resp += "</code>, у которых следующие расширения:<br>"
|
||||||
|
resp += f"<ul><li>Изображения: {", ".join([f"<code>{g}</code>" for g in glob_img])}</li></ul>"
|
||||||
|
resp += f"<ul><li>Видео: {", ".join([f"<code>{g}</code>" for g in glob_vid])}</li></ul>"
|
||||||
|
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"Картинка <code>{html.escape(file)}</code>",
|
||||||
|
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"Видео <code>{html.escape(file)}</code>",
|
||||||
|
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
|
||||||
60
main.py
Normal file
60
main.py
Normal file
@@ -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()
|
||||||
BIN
test_media/besplatno.gif
Normal file
BIN
test_media/besplatno.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
BIN
test_media/derevya.webp
Normal file
BIN
test_media/derevya.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
BIN
test_media/domoi_walter.mp4
Normal file
BIN
test_media/domoi_walter.mp4
Normal file
Binary file not shown.
BIN
test_media/lobok.jpg
Normal file
BIN
test_media/lobok.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
test_media/minecraft.avi
Normal file
BIN
test_media/minecraft.avi
Normal file
Binary file not shown.
BIN
test_media/pipish.mkv
Normal file
BIN
test_media/pipish.mkv
Normal file
Binary file not shown.
BIN
test_media/ptichki.mp4
Normal file
BIN
test_media/ptichki.mp4
Normal file
Binary file not shown.
BIN
test_media/sekret.png
Normal file
BIN
test_media/sekret.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 509 KiB |
BIN
test_media/utopia_show.jpg
Normal file
BIN
test_media/utopia_show.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Reference in New Issue
Block a user