Compare commits

4 Commits

Author SHA1 Message Date
4a947cec3d Updated mab version; added filters support 2026-09-02 18:34:56 +03:00
b28b7c583d Updated mab version; improved README 2026-09-02 14:51:27 +03:00
Nikita Tyukalov, ASUS, Linux
f8e9a528e2 Added Dockerfile and related manual 2026-08-29 20:06:53 +03:00
Nikita Tyukalov, ASUS, Linux
9266d8f32b Fixed README.md 2026-08-29 19:26:19 +03:00
6 changed files with 109 additions and 84 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
.venv/
session_storage/
__pycache__/
runtime/
*.swp
*.swo
*.vscode
*.json
*.sqlite

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.venv/ .venv/
session_storage/ session_storage/
__pycache__/ __pycache__/
runtime/
*.swp *.swp
*.swo *.swo
*.vscode *.vscode

16
Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
FROM python:3.12-slim
RUN apt-get update && apt-get install --no-install-recommends -y libolm3 libolm-dev git && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
WORKDIR /runtime
RUN chown -R 1000:1000 /usr/src /runtime
USER 1000:1000
CMD [ "python", "/usr/src/app/main.py" ]

View File

@@ -4,7 +4,30 @@
Раньше был бот с таким же функционалом, но для Telegram. Telegram больше не в Раньше был бот с таким же функционалом, но для Telegram. Telegram больше не в
почёте, и теперь у меня всё в локальном Matrix, поэтому бот тоже перенесён сюда. почёте, и теперь у меня всё в локальном Matrix, поэтому бот тоже перенесён сюда.
## Подготовка окружения ## Для запуска рекомендуется `docker`
1. [Скачайте образ](https://git.tyukalov.su/nikita/-/packages/container/2026-matrix-csonac) (измените версию на нужную вам):
```bash
sudo docker pull git.tyukalov.su/nikita/2026-matrix-csonac:0.2
```
2. Выполните первый запуск в интерактивном режиме
```bash
sudo mkdir runtime
sudo chown 1000:1000 runtime
sudo docker run -v ./runtime:/runtime -ti git.tyukalov.su/nikita/2026-matrix-csonac:0.2
```
3. В папке `runtime` появится файл `config.json`. Внесите туда нужные настройки
4. Запустите ещё раз в интерактивном режиме, чтобы авторизоваться
```bash
sudo docker run -v ./runtime:/runtime -ti git.tyukalov.su/nikita/2026-matrix-csonac:0.2
```
5. Дальше можно запускать контейнер в фоне
```bash
sudo docker run -p 127.0.0.1:4980:4980 -v ./runtime:/runtime --detach git.tyukalov.su/nikita/2026-matrix-csonac:0.2
```
> Рекомендуется запускать контейнер при помощи `docker compose`.
## Запуск без `docker`
1. Клонируйте репозиторий и перейдите в его директорию 1. Клонируйте репозиторий и перейдите в его директорию
```bash ```bash
@@ -26,17 +49,6 @@ python main.py
python main.py python main.py
``` ```
## Запуск
Для запуска приложения вы можете либо активировать `venv`, либо просто использовать полный путь до интерпретатора Python:
```bash
~/2026-matrix-downloader/.venv/bin/python main.py
```
```bash
. .venv/bin/activate
python main.py
```
## Как работает бот ## Как работает бот
Для каждой службы (канала уведомлений) создаётся своя собственная комната и в Для каждой службы (канала уведомлений) создаётся своя собственная комната и в
@@ -72,7 +84,7 @@ python main.py
``` ```
2. Использовать URL параметр `token`. Например: 2. Использовать URL параметр `token`. Например:
```plain ```plain
https://csonac.su/notify?token=f1829e94d... https://csonac.su/api/notify?token=f1829e94d...
``` ```
Рекомендуется использовать первый способ (HTTP-заголовок), так как это позволяет Рекомендуется использовать первый способ (HTTP-заголовок), так как это позволяет
избежать раскрытия токена в логах сервера и других местах, где можно посмотреть избежать раскрытия токена в логах сервера и других местах, где можно посмотреть
@@ -83,7 +95,7 @@ URL прошлых запросов.
> токен будет украден. > токен будет украден.
Доступные эндпоинты: Доступные эндпоинты:
- `GET /notify` - `GET /api/notify`
- **Описание.** Используется, чтобы отправить уведомление в канал. - **Описание.** Используется, чтобы отправить уведомление в канал.
- **Параметры запроса** - **Параметры запроса**
- `channel` - код канала, получаемый при помощи команды `!info`, - `channel` - код канала, получаемый при помощи команды `!info`,

View File

@@ -4,7 +4,7 @@ import html
import time import time
import traceback import traceback
from mab import MatrixBot from mab import MatrixBot, TextCommandFilter
from nio import MatrixRoom, RoomMessageText from nio import MatrixRoom, RoomMessageText
import util import util
@@ -17,6 +17,18 @@ from datatypes import *
_bot: MatrixBot = None # type: ignore _bot: MatrixBot = None # type: ignore
_db: Database = None # type: ignore _db: Database = None # type: ignore
_COMMANDS = {
"help": "Получить справку",
"info": "Получить сведения о комнате",
"tokens": "Получить список токенов",
"auth": "Создать новый токен",
"deauth": "Удалить существующий токен",
"name": "Задать (или удалить) имя для токена",
"ban": "Забанить IP адрес",
"unban": "Разбанить IP адрес",
"bans": "Получить список забаненных IP адресов",
}
def _generate_help_message() -> str: def _generate_help_message() -> str:
"""Generates help message in HTML markup""" """Generates help message in HTML markup"""
result = "<strong><i>Как использовать</i></strong><br><ol>" result = "<strong><i>Как использовать</i></strong><br><ol>"
@@ -27,48 +39,18 @@ def _generate_help_message() -> str:
result += "</ol>" result += "</ol>"
result += "<br><br><strong><i>Команды</i></strong>" result += "<br><br><strong><i>Команды</i></strong>"
for aliases in _COMMANDS: for key in _COMMANDS:
result += f"<br><code>!{aliases[0]}</code> - <i>{html.escape(_COMMANDS[aliases][1])}</i>" result += f"<br><code>!{key}</code> - <i>{html.escape(_COMMANDS[key])}</i>"
return result return result
#
# GENERIC EVENT HANDLERS
#
async def _on_text(room: MatrixRoom, event: RoomMessageText) -> None:
"""This callback is called when text message is received"""
if event.sender == _bot.get_client().user_id:
return
text = event.body
parts = [p for p in text.split() if p.strip()]
if len(parts) < 1:
return
cmd = parts[0].lower()
if not cmd.startswith("!"):
return
cb = None
for aliases in _COMMANDS:
for alias in aliases:
if alias == cmd.lstrip("!"):
cb = _COMMANDS[aliases][0]
break
if cb:
break
if cb is None:
await _bot.send_text_to_room(room.room_id, "<strong>Используйте <code>!help</code></strong>")
return
try:
await cb(room, parts[1:])
except:
traceback.print_exc()
# #
# COMMAND HANDLERS # COMMAND HANDLERS
# #
async def _on_cmd_help(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_help(room: MatrixRoom, event: RoomMessageText) -> None:
"""!help""" """!help"""
await _bot.send_text_to_room(room.room_id, _generate_help_message(), is_html=True) await _bot.send_text_to_room(room.room_id, _generate_help_message(), is_html=True)
async def _on_cmd_info(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_info(room: MatrixRoom, event: RoomMessageText) -> None:
"""!info""" """!info"""
# get room info (or add it) # get room info (or add it)
room_info = await _db.room_get(room.room_id) room_info = await _db.room_get(room.room_id)
@@ -87,7 +69,7 @@ async def _on_cmd_info(room: MatrixRoom, args: list[str]) -> None:
response += f"<strong>Канал:</strong> <code>{html.escape(room_info.code)}</code>" response += f"<strong>Канал:</strong> <code>{html.escape(room_info.code)}</code>"
await _bot.send_text_to_room(room.room_id, response, is_html=True) await _bot.send_text_to_room(room.room_id, response, is_html=True)
async def _on_cmd_tokens(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_tokens(room: MatrixRoom, event: RoomMessageText) -> None:
"""!info""" """!info"""
# get all tokens and check if there are none # get all tokens and check if there are none
tokens = await _db.token_get_all() tokens = await _db.token_get_all()
@@ -112,13 +94,13 @@ async def _on_cmd_tokens(room: MatrixRoom, args: list[str]) -> None:
# respond # respond
await _bot.send_text_to_room(room.room_id, response, is_html=True) await _bot.send_text_to_room(room.room_id, response, is_html=True)
async def _on_cmd_auth(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_auth(room: MatrixRoom, event: RoomMessageText) -> None:
"""!auth""" """!auth"""
# create new token # create new token
token = await _db.token_create() token = await _db.token_create()
# set the name if it is provided # set the name if it is provided
if args: if event.command_args:
token.name = " ".join(args) token.name = " ".join(event.command_args)
await _db.token_set_name(token.code, token.name) await _db.token_set_name(token.code, token.name)
# create the response # create the response
response = "<strong>Создан новый токен</strong>" response = "<strong>Создан новый токен</strong>"
@@ -131,13 +113,13 @@ async def _on_cmd_auth(room: MatrixRoom, args: list[str]) -> None:
# respond # respond
await _bot.send_text_to_room(room.room_id, response, is_html=True) await _bot.send_text_to_room(room.room_id, response, is_html=True)
async def _on_cmd_deauth(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_deauth(room: MatrixRoom, event: RoomMessageText) -> None:
"""!deauth""" """!deauth"""
# no token provided # no token provided
if len(args) != 1: if len(event.command_args) != 1:
await _bot.send_text_to_room(room.room_id, "<strong>Укажите токен, который надо удалить (должен быть ровно один аргумент)</strong>", is_html=True) await _bot.send_text_to_room(room.room_id, "<strong>Укажите токен, который надо удалить (должен быть ровно один аргумент)</strong>", is_html=True)
return return
token = args[0] token = event.command_args[0]
# check if token does not exist # check if token does not exist
if await _db.token_get(token) is None: if await _db.token_get(token) is None:
await _bot.send_text_to_room(room.room_id, "<strong>Токен не найден</strong>", is_html=True) await _bot.send_text_to_room(room.room_id, "<strong>Токен не найден</strong>", is_html=True)
@@ -148,15 +130,15 @@ async def _on_cmd_deauth(room: MatrixRoom, args: list[str]) -> None:
response = f"<strong>Удалён токен <code>{token}</code></strong>" response = f"<strong>Удалён токен <code>{token}</code></strong>"
await _bot.send_text_to_room(room.room_id, response, is_html=True) await _bot.send_text_to_room(room.room_id, response, is_html=True)
async def _on_cmd_name(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_name(room: MatrixRoom, event: RoomMessageText) -> None:
"""!name""" """!name"""
# check arguments # check arguments
if len(args) < 1: if len(event.command_args) < 1:
error = "<strong>Требуется указать как минимум токен. Если хотите убрать имя, то имя указывать не надо. Если имя нужно назначить или изменить, то после токена укажите новое имя.</strong>" error = "<strong>Требуется указать как минимум токен. Если хотите убрать имя, то имя указывать не надо. Если имя нужно назначить или изменить, то после токена укажите новое имя.</strong>"
await _bot.send_text_to_room(room.room_id, error, is_html=True) await _bot.send_text_to_room(room.room_id, error, is_html=True)
return return
token = args[0] token = event.command_args[0]
new_name = " ".join(args[1:]) new_name = " ".join(event.command_args[1:])
if not new_name.strip(): if not new_name.strip():
new_name = None new_name = None
# check if token exists # check if token exists
@@ -173,18 +155,18 @@ async def _on_cmd_name(room: MatrixRoom, args: list[str]) -> None:
# respond # respond
await _bot.send_text_to_room(room.room_id, response, is_html=True) await _bot.send_text_to_room(room.room_id, response, is_html=True)
async def _on_cmd_ban(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_ban(room: MatrixRoom, event: RoomMessageText) -> None:
"""!ban""" """!ban"""
# check arguments # check arguments
if len(args) < 2: if len(event.command_args) < 2:
error = "<strong>Формат: <code>!ban &lt;IP&gt; &lt;SECONDS&gt; [REASON]</code></strong>" error = "<strong>Формат: <code>!ban &lt;IP&gt; &lt;SECONDS&gt; [REASON]</code></strong>"
await _bot.send_text_to_room(room.room_id, error, is_html=True) await _bot.send_text_to_room(room.room_id, error, is_html=True)
return return
# get args # get event.command_args
try: try:
ip = args[0] ip = event.command_args[0]
duration = float(args[1]) duration = float(event.command_args[1])
reason = " ".join(args[2:]) if args[2:] else "Manual ban" reason = " ".join(event.command_args[2:]) if event.command_args[2:] else "Manual ban"
except: except:
error = "<strong>Возникла ошибка. Наверняка неправильно указаны секунды.</strong>" error = "<strong>Возникла ошибка. Наверняка неправильно указаны секунды.</strong>"
await _bot.send_text_to_room(room.room_id, error, is_html=True) await _bot.send_text_to_room(room.room_id, error, is_html=True)
@@ -196,21 +178,21 @@ async def _on_cmd_ban(room: MatrixRoom, args: list[str]) -> None:
except: except:
await _bot.send_text_to_room(room.room_id, "<strong>Возникла ошибка</strong>", is_html=True) await _bot.send_text_to_room(room.room_id, "<strong>Возникла ошибка</strong>", is_html=True)
async def _on_cmd_unban(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_unban(room: MatrixRoom, event: RoomMessageText) -> None:
"""!unban""" """!unban"""
# check arguments # check arguments
if len(args) != 1: if len(event.command_args) != 1:
error = "<strong>Формат: <code>!ban &lt;IP&gt;</code></strong>" error = "<strong>Формат: <code>!ban &lt;IP&gt;</code></strong>"
await _bot.send_text_to_room(room.room_id, error, is_html=True) await _bot.send_text_to_room(room.room_id, error, is_html=True)
return return
# unban # unban
try: try:
await _db.ban_delete(args[0]) await _db.ban_delete(event.command_args[0])
await _bot.send_text_to_room(room.room_id, "<strong>IP адрес разблокирован (если он был заблокирован)</strong>", is_html=True) await _bot.send_text_to_room(room.room_id, "<strong>IP адрес разблокирован (если он был заблокирован)</strong>", is_html=True)
except: except:
await _bot.send_text_to_room(room.room_id, "<strong>Возникла ошибка</strong>", is_html=True) await _bot.send_text_to_room(room.room_id, "<strong>Возникла ошибка</strong>", is_html=True)
async def _on_cmd_bans(room: MatrixRoom, args: list[str]) -> None: async def _on_cmd_bans(room: MatrixRoom, event: RoomMessageText) -> None:
"""!bans""" """!bans"""
# list # list
bans = _db.ban_get_all() bans = _db.ban_get_all()
@@ -228,18 +210,6 @@ async def _on_cmd_bans(room: MatrixRoom, args: list[str]) -> None:
await _bot.send_text_to_room(room.room_id, result, is_html=True) await _bot.send_text_to_room(room.room_id, result, is_html=True)
_COMMANDS = {
("help", "h", "?"): (_on_cmd_help, "Получить справку"),
("info", "room", "i", "r"): (_on_cmd_info, "Получить сведения о комнате"),
("tokens", "t"): (_on_cmd_tokens, "Получить список токенов"),
("auth", "create", "a", "c"): (_on_cmd_auth, "Создать новый токен"),
("deauth", "delete", "d"): (_on_cmd_deauth, "Удалить существующий токен"),
("name", "n"): (_on_cmd_name, "Задать (или удалить) имя для токена"),
("ban", "b"): (_on_cmd_ban, "Забанить IP адрес"),
("unban", "u"): (_on_cmd_unban, "Разбанить IP адрес"),
("bans", "l"): (_on_cmd_bans, "Получить список забаненных IP адресов"),
}
# #
# PUBLIC # PUBLIC
# #
@@ -248,4 +218,21 @@ def setup(bot: MatrixBot, db: Database) -> None:
global _bot, _db global _bot, _db
_db = db _db = db
_bot = bot _bot = bot
_bot.add_event_callback(_on_text, RoomMessageText) # type: ignore _bot.add_event_callback(
_on_cmd_help, TextCommandFilter(["help", "h", "?"]))
_bot.add_event_callback(
_on_cmd_info, TextCommandFilter(["info", "room", "i", "r"]))
_bot.add_event_callback(
_on_cmd_tokens, TextCommandFilter(["tokens", "t"]))
_bot.add_event_callback(
_on_cmd_auth, TextCommandFilter(["auth", "create", "a", "c"]))
_bot.add_event_callback(
_on_cmd_deauth, TextCommandFilter(["deauth", "delete", "d"]))
_bot.add_event_callback(
_on_cmd_name, TextCommandFilter(["name", "n"]))
_bot.add_event_callback(
_on_cmd_ban, TextCommandFilter(["ban", "b"]))
_bot.add_event_callback(
_on_cmd_unban, TextCommandFilter(["unban", "u"]))
_bot.add_event_callback(
_on_cmd_bans, TextCommandFilter(["bans", "l"]))

View File

@@ -34,7 +34,7 @@ idna==3.19
Jinja2==3.1.6 Jinja2==3.1.6
jsonschema==4.26.0 jsonschema==4.26.0
jsonschema-specifications==2025.9.1 jsonschema-specifications==2025.9.1
mab @ git+https://git.tyukalov.su/nikita/mab@c3046307c7bc4e65113aab62b07bad2a148b49e5 mab @ git+https://git.tyukalov.su/nikita/mab@v0.1.0
markdown-it-py==4.2.0 markdown-it-py==4.2.0
MarkupSafe==3.0.3 MarkupSafe==3.0.3
matrix-nio==0.26.0 matrix-nio==0.26.0