Basic database function implemented
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ __pycache__/
|
||||
*.swp
|
||||
*.swo
|
||||
*.vscode
|
||||
*.json
|
||||
*.json
|
||||
*.sqlite
|
||||
135
bot_callbacks.py
135
bot_callbacks.py
@@ -8,22 +8,25 @@ from mab import MatrixBot
|
||||
from nio import MatrixRoom, RoomMessageText
|
||||
|
||||
import util
|
||||
from database import Database
|
||||
from datatypes import *
|
||||
|
||||
#
|
||||
# PRIVATE
|
||||
#
|
||||
_bot: MatrixBot = None # type: ignore
|
||||
_db: Database = None # type: ignore
|
||||
|
||||
def _generate_help_message() -> str:
|
||||
"""Generates help message in HTML markup"""
|
||||
result = "<b><i>Как использовать</i></b><br><ol>"
|
||||
result = "<strong><i>Как использовать</i></strong><br><ol>"
|
||||
result += "<li>Используя <code>!auth</code>, создайте токен</li>"
|
||||
result += "<li>Добавьте бота в комнату для уведомлений</li>"
|
||||
result += "<li>Выполните <code>!info</code> в комнате, чтобы узнать код канала уведомений</li>"
|
||||
result += "<li>Используя полученные токен и код канала, отправьте уведомление через веб-запрос</li>"
|
||||
result += "</ol>"
|
||||
|
||||
result += "<br><br><b><i>Команды</i></b>"
|
||||
result += "<br><br><strong><i>Команды</i></strong>"
|
||||
for aliases in _COMMANDS:
|
||||
result += f"<br><code>!{aliases[0]}</code> - <i>{html.escape(_COMMANDS[aliases][1])}</i>"
|
||||
return result
|
||||
@@ -51,7 +54,7 @@ async def _on_text(room: MatrixRoom, event: RoomMessageText) -> None:
|
||||
if cb:
|
||||
break
|
||||
if cb is None:
|
||||
await _bot.send_text_to_room(room.room_id, "<b>Используйте <code>!help</code></b>")
|
||||
await _bot.send_text_to_room(room.room_id, "<strong>Используйте <code>!help</code></strong>")
|
||||
return
|
||||
try:
|
||||
await cb(room, parts[1:])
|
||||
@@ -67,93 +70,112 @@ async def _on_cmd_help(room: MatrixRoom, args: list[str]) -> None:
|
||||
|
||||
async def _on_cmd_info(room: MatrixRoom, args: list[str]) -> None:
|
||||
"""!info"""
|
||||
response = "<b><i>Сведения о комнате</i></b><br>"
|
||||
response += f"<b>channel: <b><code>{html.escape(room.room_id)}</code>"
|
||||
# get room info (or add it)
|
||||
room_info = await _db.room_get(room.room_id)
|
||||
if room_info is None:
|
||||
room_info = await _db.room_create(room.room_id)
|
||||
# failure
|
||||
if room_info is None:
|
||||
await _bot.send_text_to_room(
|
||||
room.room_id,
|
||||
"<strong>Нет информации о комнате</strong>",
|
||||
is_html=True
|
||||
)
|
||||
return
|
||||
# respond
|
||||
response = "<strong><i>Сведения о комнате</i></strong><br>"
|
||||
response += f"<strong>Канал:</strong> <code>{html.escape(room_info.code)}</code>"
|
||||
await _bot.send_text_to_room(room.room_id, response, is_html=True)
|
||||
|
||||
async def _on_cmd_tokens(room: MatrixRoom, args: list[str]) -> None:
|
||||
"""!info"""
|
||||
TEST_LIST = {
|
||||
"t0K3n": {
|
||||
"created": time.time() - 60,
|
||||
"last_use": time.time() - 30,
|
||||
"total_uses": 534,
|
||||
"service_name": "Резервное копирование",
|
||||
},
|
||||
"T35t_t0KeN": {
|
||||
"created": time.time() - 12341,
|
||||
"last_use": time.time() - 123,
|
||||
"total_uses": 342,
|
||||
"service_name": None,
|
||||
},
|
||||
}
|
||||
|
||||
response = "<b><i>Список токенов</i></b>"
|
||||
for token in TEST_LIST:
|
||||
data = TEST_LIST[token]
|
||||
response += f"<br><b><code>{html.escape(token)}</code></b><br><ul>"
|
||||
|
||||
if data["service_name"] is not None:
|
||||
response += f"<li><b>Имя службы:</b> <code>{data["service_name"]}</code></li>"
|
||||
# get all tokens and check if there are none
|
||||
tokens = await _db.token_get_all()
|
||||
if not tokens:
|
||||
await _bot.send_text_to_room(
|
||||
room.room_id,
|
||||
"<strong>Нет токенов, используйте <code>!auth</code></strong>",
|
||||
is_html=True
|
||||
)
|
||||
return
|
||||
# create the list of tokens
|
||||
response = "<strong><i>Список токенов</i></strong>"
|
||||
for token in tokens:
|
||||
response += f"<br><strong><code>{html.escape(token.code)}</code></strong><br><ul>"
|
||||
if token.name is not None:
|
||||
response += f"<li><strong>Имя службы:</strong> <code>{token.name}</code></li>"
|
||||
else:
|
||||
response += f"<li><b>Имя службы:</b> <i>указывается в запросе</i></li>"
|
||||
response += f"<li><b>Создан:</b> <i>{util.date_to_text(data["created"])}</i></li>"
|
||||
response += f"<li><b>Последнее использование:</b> <i>{util.date_to_text(data["last_use"])}</i></li>"
|
||||
response += f"<li><b>Всего использований:</b> <i>{data["total_uses"]}</i></li>"
|
||||
|
||||
response += f"<li><strong>Имя службы:</strong> <i>указывается в запросе</i></li>"
|
||||
response += f"<li><strong>Создан:</strong> <i>{util.date_to_text(token.created_at)}</i></li>"
|
||||
response += f"<li><strong>Последнее использование:</strong> <i>{util.date_to_text(token.last_access_at)}</i></li>"
|
||||
response += "</ul>"
|
||||
# respond
|
||||
await _bot.send_text_to_room(room.room_id, response, is_html=True)
|
||||
|
||||
async def _on_cmd_auth(room: MatrixRoom, args: list[str]) -> None:
|
||||
"""!auth"""
|
||||
new_token = "inr3i4evfds"
|
||||
new_token_name = " ".join(args) if args else None
|
||||
if type(new_token_name) is str and not new_token_name.strip():
|
||||
new_token_name = None
|
||||
|
||||
response = "<b>Создан новый токен</b>"
|
||||
response += f"<br><b>Код:</b> <code>{new_token}</code>"
|
||||
response += f"<br><b>Имя сервиса:</b> "
|
||||
if new_token_name:
|
||||
response += f"<code>{html.escape(new_token_name)}</code>"
|
||||
# create new token
|
||||
token = await _db.token_create()
|
||||
# set the name if it is provided
|
||||
if args:
|
||||
token.name = " ".join(args)
|
||||
await _db.token_set_name(token.code, token.name)
|
||||
# create the response
|
||||
response = "<strong>Создан новый токен</strong>"
|
||||
response += f"<br><strong>Код:</strong> <code>{token.code}</code>"
|
||||
response += f"<br><strong>Имя сервиса:</strong> "
|
||||
if token.name:
|
||||
response += f"<code>{html.escape(token.name)}</code>"
|
||||
else:
|
||||
response += "указывается в запросе"
|
||||
# respond
|
||||
await _bot.send_text_to_room(room.room_id, response, is_html=True)
|
||||
|
||||
async def _on_cmd_deauth(room: MatrixRoom, args: list[str]) -> None:
|
||||
"""!deauth"""
|
||||
# no token provided
|
||||
if len(args) != 1:
|
||||
await _bot.send_text_to_room(room.room_id, "<b>Укажите токен, который надо удалить (должен быть ровно один аргумент)</b>", is_html=True)
|
||||
await _bot.send_text_to_room(room.room_id, "<strong>Укажите токен, который надо удалить (должен быть ровно один аргумент)</strong>", is_html=True)
|
||||
return
|
||||
token = args[0]
|
||||
# TODO
|
||||
response = f"<b>Удалён токен <code>{token}</code></b>"
|
||||
# check if token does not exist
|
||||
if await _db.token_get(token) is None:
|
||||
await _bot.send_text_to_room(room.room_id, "<strong>Токен не найден</strong>", is_html=True)
|
||||
return
|
||||
# remove the token
|
||||
await _db.token_delete(token)
|
||||
# respond
|
||||
response = f"<strong>Удалён токен <code>{token}</code></strong>"
|
||||
await _bot.send_text_to_room(room.room_id, response, is_html=True)
|
||||
|
||||
async def _on_cmd_name(room: MatrixRoom, args: list[str]) -> None:
|
||||
"""!name"""
|
||||
# check arguments
|
||||
if len(args) < 1:
|
||||
error = "<b>Требуется указать как минимум токен. Если хотите убрать имя, то имя указывать не надо. Если имя нужно назначить или изменить, то после токена укажите новое имя.</b>"
|
||||
error = "<strong>Требуется указать как минимум токен. Если хотите убрать имя, то имя указывать не надо. Если имя нужно назначить или изменить, то после токена укажите новое имя.</strong>"
|
||||
await _bot.send_text_to_room(room.room_id, error, is_html=True)
|
||||
return
|
||||
token = args[0]
|
||||
|
||||
new_name = " ".join(args[1:])
|
||||
if not new_name.strip():
|
||||
new_name = None
|
||||
|
||||
# TODO
|
||||
|
||||
# check if token exists
|
||||
if await _db.token_get(token) is None:
|
||||
await _bot.send_text_to_room(room.room_id, "<strong>Токен не существует</strong>", is_html=True)
|
||||
return
|
||||
# set new name
|
||||
await _db.token_set_name(token, new_name)
|
||||
# prepare the response
|
||||
if new_name:
|
||||
response = f"<b>Новое имя для токена <code>{token}</code>: <code>{new_name}</code></b>"
|
||||
response = f"<strong>Новое имя для токена <code>{token}</code>: <code>{new_name}</code></strong>"
|
||||
else:
|
||||
response = f"<b>Удалено имя для токена <code>{token}</code></b>"
|
||||
|
||||
response = f"<strong>Удалено имя для токена <code>{token}</code></strong>"
|
||||
# respond
|
||||
await _bot.send_text_to_room(room.room_id, response, is_html=True)
|
||||
|
||||
async def _on_cmd_leave(room: MatrixRoom, args: list[str]) -> None:
|
||||
"""!leave"""
|
||||
await _bot.send_text_to_room(room.room_id, "<b>Команда не реализована</b>", is_html=True)
|
||||
await _bot.send_text_to_room(room.room_id, "<strong>Команда не реализована</strong>", is_html=True)
|
||||
|
||||
_COMMANDS = {
|
||||
("help", "h", "?"): (_on_cmd_help, "Получить справку"),
|
||||
@@ -168,8 +190,9 @@ _COMMANDS = {
|
||||
#
|
||||
# PUBLIC
|
||||
#
|
||||
def setup(bot: MatrixBot) -> None:
|
||||
def setup(bot: MatrixBot, db: Database) -> None:
|
||||
"""Setup the callbacks"""
|
||||
global _bot
|
||||
global _bot, _db
|
||||
_db = db
|
||||
_bot = bot
|
||||
_bot.add_event_callback(_on_text, RoomMessageText) # type: ignore
|
||||
|
||||
249
database.py
249
database.py
@@ -0,0 +1,249 @@
|
||||
"""This module implements database operations."""
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
import time
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
import aiosqlite
|
||||
from aiosqlite import Connection, Row
|
||||
|
||||
from datatypes import *
|
||||
from util import get_hash
|
||||
|
||||
|
||||
class Database:
|
||||
#
|
||||
# PRIVATE
|
||||
#
|
||||
@staticmethod
|
||||
async def _setup_tables(conn: Connection) -> None:
|
||||
SETUP_SQL_SCRIPT = """
|
||||
CREATE TABLE IF NOT EXISTS rooms (
|
||||
code TEXT PRIMARY KEY,
|
||||
matrix_id TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens (
|
||||
code TEXT PRIMARY KEY,
|
||||
name TEXT,
|
||||
created_at REAL,
|
||||
last_access_at REAL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fails (
|
||||
ip TEXT PRIMARY KEY,
|
||||
timestamp INTEGER,
|
||||
score INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bans (
|
||||
ip TEXT PRIMARY KEY,
|
||||
expires_at INTEGER,
|
||||
reason TEXT
|
||||
);
|
||||
"""
|
||||
await conn.executescript(SETUP_SQL_SCRIPT)
|
||||
await conn.commit()
|
||||
|
||||
@staticmethod
|
||||
async def _select_by_anded_kwargs(conn: Connection, table_name: str, **kwargs) -> list[dict[str, Any]]:
|
||||
# prepare keys and values
|
||||
keys = tuple(k for k in kwargs)
|
||||
values = tuple(kwargs[k] for k in keys)
|
||||
# prepare the statement
|
||||
statement = f"SELECT * FROM {table_name}"
|
||||
# add `WHERE` part if there kwargs
|
||||
if keys:
|
||||
statement += " WHERE "
|
||||
statement += " AND ".join([f"{k}=?" for k in keys])
|
||||
else:
|
||||
values = None
|
||||
# execute and enumerate
|
||||
result = []
|
||||
async with conn.execute(statement, values) as cursor:
|
||||
async for row in cursor:
|
||||
entry = {}
|
||||
for k in row.keys():
|
||||
entry[k] = row[k]
|
||||
result.append(entry)
|
||||
return result
|
||||
|
||||
#
|
||||
# PUBLIC
|
||||
#
|
||||
def __init__(self, path: Path):
|
||||
self._path = path
|
||||
self._logger = logging.getLogger("database")
|
||||
|
||||
self._connection: Connection | None = None
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""Connect to the database. Returns False on failure."""
|
||||
if self._connection is not None:
|
||||
return False
|
||||
try:
|
||||
self._connection = await aiosqlite.connect(self._path)
|
||||
self._connection.row_factory = Row
|
||||
await self._setup_tables(self._connection)
|
||||
except:
|
||||
self._logger.error(traceback.format_exc())
|
||||
return False
|
||||
return True
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from the database."""
|
||||
if self._connection is None:
|
||||
return
|
||||
await self._connection.close()
|
||||
self._connection = None
|
||||
|
||||
|
||||
async def room_create(self, matrix_id: str) -> ObjectRoom | None:
|
||||
"""Create a room with specified matrix_id. Returns None if the room exists."""
|
||||
if self._connection is None:
|
||||
raise RuntimeError("Not connected to the database")
|
||||
# room exists
|
||||
if await self.room_get(matrix_id) is not None:
|
||||
return None
|
||||
# create the code
|
||||
code = None
|
||||
string_to_hash = matrix_id
|
||||
while code is None or (await self.room_get(code)) is not None:
|
||||
code = get_hash(string_to_hash)[-10:]
|
||||
string_to_hash += "A"
|
||||
# code must always start with q
|
||||
code = f"q{code}"
|
||||
# add
|
||||
try:
|
||||
statement = "INSERT INTO rooms (code, matrix_id) VALUES (?, ?)"
|
||||
await self._connection.execute(statement, (code, matrix_id))
|
||||
await self._connection.commit()
|
||||
self._logger.info(f"Added new room with code {code}")
|
||||
except:
|
||||
self._logger.error(traceback.format_exc())
|
||||
return None
|
||||
# return the added object
|
||||
return ObjectRoom(
|
||||
code=code,
|
||||
matrix_id=matrix_id
|
||||
)
|
||||
|
||||
async def room_get_all(self) -> list[ObjectRoom]:
|
||||
"""Get room by code."""
|
||||
if self._connection is None:
|
||||
raise RuntimeError("Not connected to the database")
|
||||
try:
|
||||
rooms = await self._select_by_anded_kwargs(
|
||||
self._connection,
|
||||
"rooms"
|
||||
)
|
||||
result = []
|
||||
for r in rooms:
|
||||
result.append(ObjectRoom(
|
||||
code=r["code"],
|
||||
matrix_id=r["matrix_id"]
|
||||
))
|
||||
return result
|
||||
except:
|
||||
self._logger.error(traceback.format_exc())
|
||||
return []
|
||||
|
||||
async def room_get(self, identifier: str) -> ObjectRoom | None:
|
||||
"""Get room by identifier (either `code` or `matrix_id`)."""
|
||||
if self._connection is None:
|
||||
raise RuntimeError("Not connected to the database")
|
||||
try:
|
||||
kwargs = {}
|
||||
kwargs["code" if identifier[0] == "q" else "matrix_id"] = identifier
|
||||
rooms = await self._select_by_anded_kwargs(
|
||||
self._connection,
|
||||
"rooms",
|
||||
**kwargs
|
||||
)
|
||||
if not rooms:
|
||||
self._logger.debug(f"Room `{identifier}` is not found")
|
||||
return None
|
||||
return ObjectRoom(**rooms[0])
|
||||
except:
|
||||
self._logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
|
||||
async def token_create(self) -> ObjectToken:
|
||||
"""Create a token."""
|
||||
if self._connection is None:
|
||||
raise RuntimeError("Not connected to the database")
|
||||
code = get_hash(str(time.time()).encode() + os.urandom(64))
|
||||
create_time = time.time()
|
||||
statement = """
|
||||
INSERT INTO tokens (code, name, created_at, last_access_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"""
|
||||
result = ObjectToken(
|
||||
code=code,
|
||||
name=None,
|
||||
created_at=create_time,
|
||||
last_access_at=create_time
|
||||
)
|
||||
await self._connection.execute(
|
||||
statement,
|
||||
(result.code, result.name, result.created_at, result.last_access_at)
|
||||
)
|
||||
await self._connection.commit()
|
||||
return result
|
||||
|
||||
async def token_delete(self, code: str) -> None:
|
||||
"""Delete a token."""
|
||||
if self._connection is None:
|
||||
raise RuntimeError("Not connected to the database")
|
||||
try:
|
||||
await self._connection.execute("DELETE FROM tokens WHERE code=?", (code,))
|
||||
await self._connection.commit()
|
||||
except:
|
||||
self._logger.error(traceback.format_exc())
|
||||
|
||||
async def token_get(self, code: str) -> ObjectToken | None:
|
||||
"""Get token by its code."""
|
||||
if self._connection is None:
|
||||
raise RuntimeError("Not connected to the database")
|
||||
try:
|
||||
result = await self._select_by_anded_kwargs(
|
||||
self._connection,
|
||||
"tokens",
|
||||
code=code
|
||||
)
|
||||
if not result:
|
||||
return None
|
||||
result = result[0]
|
||||
return ObjectToken(**result)
|
||||
except:
|
||||
self._logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
async def token_get_all(self) -> list[ObjectToken]:
|
||||
"""Get list of all tokens."""
|
||||
if self._connection is None:
|
||||
raise RuntimeError("Not connected to the database")
|
||||
try:
|
||||
result = await self._select_by_anded_kwargs(
|
||||
self._connection,
|
||||
"tokens"
|
||||
)
|
||||
result = [ObjectToken(**r) for r in result]
|
||||
return result
|
||||
except:
|
||||
self._logger.error(traceback.format_exc())
|
||||
return []
|
||||
|
||||
async def token_set_name(self, code: str, name: str | None) -> None:
|
||||
"""Set name for the token."""
|
||||
if self._connection is None:
|
||||
raise RuntimeError("Not connected to the database")
|
||||
try:
|
||||
statement = "UPDATE tokens SET name=? WHERE code=?"
|
||||
await self._connection.execute(statement, (name, code))
|
||||
await self._connection.commit()
|
||||
except:
|
||||
self._logger.error(traceback.format_exc())
|
||||
13
datatypes.py
13
datatypes.py
@@ -11,5 +11,14 @@ class AppConfig:
|
||||
matrix_user: str
|
||||
store_dir: str
|
||||
|
||||
class MessageType(Enum):
|
||||
TEXT = "m.text"
|
||||
@dataclass
|
||||
class ObjectRoom:
|
||||
code: str
|
||||
matrix_id: str
|
||||
|
||||
@dataclass
|
||||
class ObjectToken:
|
||||
code: str
|
||||
name: str | None
|
||||
created_at: float
|
||||
last_access_at: float
|
||||
10
main.py
10
main.py
@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
from mab import MatrixBot, MatrixBotConfig
|
||||
|
||||
from datatypes import AppConfig
|
||||
import database
|
||||
import config
|
||||
import util
|
||||
import bot_callbacks
|
||||
@@ -42,9 +43,15 @@ async def main() -> None:
|
||||
storage_directory=Path(cfg.store_dir)
|
||||
)
|
||||
bot = MatrixBot(matrix_bot_config)
|
||||
bot_callbacks.setup(bot)
|
||||
# setup the database
|
||||
db = database.Database(Path("database.sqlite"))
|
||||
# setup the callbacks
|
||||
bot_callbacks.setup(bot, db)
|
||||
|
||||
# start the app
|
||||
if not await db.connect():
|
||||
util.log_error("Can't connect to the database!")
|
||||
return
|
||||
bot.start()
|
||||
|
||||
# wait for Ctrl+C
|
||||
@@ -52,6 +59,7 @@ async def main() -> None:
|
||||
|
||||
# stop the app
|
||||
await bot.stop()
|
||||
await db.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,7 @@ aiohappyeyeballs==2.7.1
|
||||
aiohttp==3.14.3
|
||||
aiohttp_socks==0.12.0
|
||||
aiosignal==1.4.0
|
||||
aiosqlite==0.22.1
|
||||
atomicwrites==1.4.1
|
||||
attrs==26.1.0
|
||||
build==1.5.0
|
||||
|
||||
12
util.py
12
util.py
@@ -8,6 +8,7 @@ import os
|
||||
import logging
|
||||
import traceback
|
||||
import datetime
|
||||
import hashlib
|
||||
|
||||
import aioconsole
|
||||
|
||||
@@ -75,7 +76,7 @@ async def get_password() -> str | None:
|
||||
return None
|
||||
return await ainput("Matrix password: ")
|
||||
|
||||
def date_to_text(date: datetime.datetime, dow: bool = True, seconds: bool = True) -> str:
|
||||
def date_to_text(date: datetime.datetime | float, dow: bool = True, seconds: bool = True) -> str:
|
||||
''' Returns date as formatted string.
|
||||
Day of week can be added.
|
||||
Seconds can be added.
|
||||
@@ -102,9 +103,14 @@ def date_to_text(date: datetime.datetime, dow: bool = True, seconds: bool = True
|
||||
('Sat', 'Сб'),
|
||||
('Sun', 'Вс')
|
||||
]
|
||||
result = date.strftime(format_string)
|
||||
result = date.strftime(format_string) # type: ignore
|
||||
for en, ru in translate_map:
|
||||
if en in result:
|
||||
result = result.replace(en, ru)
|
||||
break
|
||||
return result
|
||||
return result
|
||||
|
||||
def get_hash(data: bytes | str) -> str:
|
||||
if type(data) is str:
|
||||
data = data.encode(errors="ignore")
|
||||
return hashlib.sha256(data).hexdigest() # type: ignore
|
||||
Reference in New Issue
Block a user