Add Cloudron packaging for Maubot
This commit is contained in:
13
maubot-src/maubot/db/__init__.py
Normal file
13
maubot-src/maubot/db/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from mautrix.util.async_db import Database
|
||||
|
||||
from .client import Client
|
||||
from .instance import DatabaseEngine, Instance
|
||||
from .upgrade import upgrade_table
|
||||
|
||||
|
||||
def init(db: Database) -> None:
|
||||
for table in (Client, Instance):
|
||||
table.db = db
|
||||
|
||||
|
||||
__all__ = ["upgrade_table", "init", "Client", "Instance", "DatabaseEngine"]
|
||||
114
maubot-src/maubot/db/client.py
Normal file
114
maubot-src/maubot/db/client.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 Tulir Asokan
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
|
||||
from asyncpg import Record
|
||||
from attr import dataclass
|
||||
|
||||
from mautrix.client import SyncStore
|
||||
from mautrix.types import ContentURI, DeviceID, FilterID, SyncToken, UserID
|
||||
from mautrix.util.async_db import Database
|
||||
|
||||
fake_db = Database.create("") if TYPE_CHECKING else None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Client(SyncStore):
|
||||
db: ClassVar[Database] = fake_db
|
||||
|
||||
id: UserID
|
||||
homeserver: str
|
||||
access_token: str
|
||||
device_id: DeviceID
|
||||
enabled: bool
|
||||
|
||||
next_batch: SyncToken
|
||||
filter_id: FilterID
|
||||
|
||||
sync: bool
|
||||
autojoin: bool
|
||||
online: bool
|
||||
|
||||
displayname: str
|
||||
avatar_url: ContentURI
|
||||
|
||||
@classmethod
|
||||
def _from_row(cls, row: Record | None) -> Client | None:
|
||||
if row is None:
|
||||
return None
|
||||
return cls(**row)
|
||||
|
||||
_columns = (
|
||||
"id, homeserver, access_token, device_id, enabled, next_batch, filter_id, "
|
||||
"sync, autojoin, online, displayname, avatar_url"
|
||||
)
|
||||
|
||||
@property
|
||||
def _values(self):
|
||||
return (
|
||||
self.id,
|
||||
self.homeserver,
|
||||
self.access_token,
|
||||
self.device_id,
|
||||
self.enabled,
|
||||
self.next_batch,
|
||||
self.filter_id,
|
||||
self.sync,
|
||||
self.autojoin,
|
||||
self.online,
|
||||
self.displayname,
|
||||
self.avatar_url,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def all(cls) -> list[Client]:
|
||||
rows = await cls.db.fetch(f"SELECT {cls._columns} FROM client")
|
||||
return [cls._from_row(row) for row in rows]
|
||||
|
||||
@classmethod
|
||||
async def get(cls, id: str) -> Client | None:
|
||||
q = f"SELECT {cls._columns} FROM client WHERE id=$1"
|
||||
return cls._from_row(await cls.db.fetchrow(q, id))
|
||||
|
||||
async def insert(self) -> None:
|
||||
q = """
|
||||
INSERT INTO client (
|
||||
id, homeserver, access_token, device_id, enabled, next_batch, filter_id,
|
||||
sync, autojoin, online, displayname, avatar_url
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
"""
|
||||
await self.db.execute(q, *self._values)
|
||||
|
||||
async def put_next_batch(self, next_batch: SyncToken) -> None:
|
||||
await self.db.execute("UPDATE client SET next_batch=$1 WHERE id=$2", next_batch, self.id)
|
||||
self.next_batch = next_batch
|
||||
|
||||
async def get_next_batch(self) -> SyncToken:
|
||||
return self.next_batch
|
||||
|
||||
async def update(self) -> None:
|
||||
q = """
|
||||
UPDATE client SET homeserver=$2, access_token=$3, device_id=$4, enabled=$5,
|
||||
next_batch=$6, filter_id=$7, sync=$8, autojoin=$9, online=$10,
|
||||
displayname=$11, avatar_url=$12
|
||||
WHERE id=$1
|
||||
"""
|
||||
await self.db.execute(q, *self._values)
|
||||
|
||||
async def delete(self) -> None:
|
||||
await self.db.execute("DELETE FROM client WHERE id=$1", self.id)
|
||||
101
maubot-src/maubot/db/instance.py
Normal file
101
maubot-src/maubot/db/instance.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 Tulir Asokan
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from enum import Enum
|
||||
|
||||
from asyncpg import Record
|
||||
from attr import dataclass
|
||||
|
||||
from mautrix.types import UserID
|
||||
from mautrix.util.async_db import Database
|
||||
|
||||
fake_db = Database.create("") if TYPE_CHECKING else None
|
||||
|
||||
|
||||
class DatabaseEngine(Enum):
|
||||
SQLITE = "sqlite"
|
||||
POSTGRES = "postgres"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Instance:
|
||||
db: ClassVar[Database] = fake_db
|
||||
|
||||
id: str
|
||||
type: str
|
||||
enabled: bool
|
||||
primary_user: UserID
|
||||
config_str: str
|
||||
database_engine: DatabaseEngine | None
|
||||
|
||||
@property
|
||||
def database_engine_str(self) -> str | None:
|
||||
return self.database_engine.value if self.database_engine else None
|
||||
|
||||
@classmethod
|
||||
def _from_row(cls, row: Record | None) -> Instance | None:
|
||||
if row is None:
|
||||
return None
|
||||
data = {**row}
|
||||
db_engine = data.pop("database_engine", None)
|
||||
return cls(**data, database_engine=DatabaseEngine(db_engine) if db_engine else None)
|
||||
|
||||
_columns = "id, type, enabled, primary_user, config, database_engine"
|
||||
|
||||
@classmethod
|
||||
async def all(cls) -> list[Instance]:
|
||||
q = f"SELECT {cls._columns} FROM instance"
|
||||
rows = await cls.db.fetch(q)
|
||||
return [cls._from_row(row) for row in rows]
|
||||
|
||||
@classmethod
|
||||
async def get(cls, id: str) -> Instance | None:
|
||||
q = f"SELECT {cls._columns} FROM instance WHERE id=$1"
|
||||
return cls._from_row(await cls.db.fetchrow(q, id))
|
||||
|
||||
async def update_id(self, new_id: str) -> None:
|
||||
await self.db.execute("UPDATE instance SET id=$1 WHERE id=$2", new_id, self.id)
|
||||
self.id = new_id
|
||||
|
||||
@property
|
||||
def _values(self):
|
||||
return (
|
||||
self.id,
|
||||
self.type,
|
||||
self.enabled,
|
||||
self.primary_user,
|
||||
self.config_str,
|
||||
self.database_engine_str,
|
||||
)
|
||||
|
||||
async def insert(self) -> None:
|
||||
q = (
|
||||
"INSERT INTO instance (id, type, enabled, primary_user, config, database_engine) "
|
||||
"VALUES ($1, $2, $3, $4, $5, $6)"
|
||||
)
|
||||
await self.db.execute(q, *self._values)
|
||||
|
||||
async def update(self) -> None:
|
||||
q = """
|
||||
UPDATE instance SET type=$2, enabled=$3, primary_user=$4, config=$5, database_engine=$6
|
||||
WHERE id=$1
|
||||
"""
|
||||
await self.db.execute(q, *self._values)
|
||||
|
||||
async def delete(self) -> None:
|
||||
await self.db.execute("DELETE FROM instance WHERE id=$1", self.id)
|
||||
5
maubot-src/maubot/db/upgrade/__init__.py
Normal file
5
maubot-src/maubot/db/upgrade/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from mautrix.util.async_db import UpgradeTable
|
||||
|
||||
upgrade_table = UpgradeTable()
|
||||
|
||||
from . import v01_initial_revision, v02_instance_database_engine
|
||||
136
maubot-src/maubot/db/upgrade/v01_initial_revision.py
Normal file
136
maubot-src/maubot/db/upgrade/v01_initial_revision.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 Tulir Asokan
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from mautrix.util.async_db import Connection, Scheme
|
||||
|
||||
from . import upgrade_table
|
||||
|
||||
legacy_version_query = "SELECT version_num FROM alembic_version"
|
||||
last_legacy_version = "90aa88820eab"
|
||||
|
||||
|
||||
@upgrade_table.register(description="Initial asyncpg revision")
|
||||
async def upgrade_v1(conn: Connection, scheme: Scheme) -> None:
|
||||
if await conn.table_exists("alembic_version"):
|
||||
await migrate_legacy_to_v1(conn, scheme)
|
||||
else:
|
||||
return await create_v1_tables(conn)
|
||||
|
||||
|
||||
async def create_v1_tables(conn: Connection) -> None:
|
||||
await conn.execute(
|
||||
"""CREATE TABLE client (
|
||||
id TEXT PRIMARY KEY,
|
||||
homeserver TEXT NOT NULL,
|
||||
access_token TEXT NOT NULL,
|
||||
device_id TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
|
||||
next_batch TEXT NOT NULL,
|
||||
filter_id TEXT NOT NULL,
|
||||
|
||||
sync BOOLEAN NOT NULL,
|
||||
autojoin BOOLEAN NOT NULL,
|
||||
online BOOLEAN NOT NULL,
|
||||
|
||||
displayname TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL
|
||||
)"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""CREATE TABLE instance (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
primary_user TEXT NOT NULL,
|
||||
config TEXT NOT NULL,
|
||||
FOREIGN KEY (primary_user) REFERENCES client(id) ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
)"""
|
||||
)
|
||||
|
||||
|
||||
async def migrate_legacy_to_v1(conn: Connection, scheme: Scheme) -> None:
|
||||
legacy_version = await conn.fetchval(legacy_version_query)
|
||||
if legacy_version != last_legacy_version:
|
||||
raise RuntimeError(
|
||||
"Legacy database is not on last version. "
|
||||
"Please upgrade the old database with alembic or drop it completely first."
|
||||
)
|
||||
await conn.execute("ALTER TABLE plugin RENAME TO instance")
|
||||
await update_state_store(conn, scheme)
|
||||
if scheme != Scheme.SQLITE:
|
||||
await varchar_to_text(conn)
|
||||
await conn.execute("DROP TABLE alembic_version")
|
||||
|
||||
|
||||
async def update_state_store(conn: Connection, scheme: Scheme) -> None:
|
||||
# The Matrix state store already has more or less the correct schema, so set the version
|
||||
await conn.execute("CREATE TABLE mx_version (version INTEGER PRIMARY KEY)")
|
||||
await conn.execute("INSERT INTO mx_version (version) VALUES (2)")
|
||||
if scheme != Scheme.SQLITE:
|
||||
# Remove old uppercase membership type and recreate it as lowercase
|
||||
await conn.execute("ALTER TABLE mx_user_profile ALTER COLUMN membership TYPE TEXT")
|
||||
await conn.execute("DROP TYPE IF EXISTS membership")
|
||||
await conn.execute(
|
||||
"CREATE TYPE membership AS ENUM ('join', 'leave', 'invite', 'ban', 'knock')"
|
||||
)
|
||||
await conn.execute(
|
||||
"ALTER TABLE mx_user_profile ALTER COLUMN membership TYPE membership "
|
||||
"USING LOWER(membership)::membership"
|
||||
)
|
||||
else:
|
||||
# Recreate table to remove CHECK constraint and lowercase everything
|
||||
await conn.execute(
|
||||
"""CREATE TABLE new_mx_user_profile (
|
||||
room_id TEXT,
|
||||
user_id TEXT,
|
||||
membership TEXT NOT NULL
|
||||
CHECK (membership IN ('join', 'leave', 'invite', 'ban', 'knock')),
|
||||
displayname TEXT,
|
||||
avatar_url TEXT,
|
||||
PRIMARY KEY (room_id, user_id)
|
||||
)"""
|
||||
)
|
||||
await conn.execute(
|
||||
"""
|
||||
INSERT INTO new_mx_user_profile (room_id, user_id, membership, displayname, avatar_url)
|
||||
SELECT room_id, user_id, LOWER(membership), displayname, avatar_url
|
||||
FROM mx_user_profile
|
||||
"""
|
||||
)
|
||||
await conn.execute("DROP TABLE mx_user_profile")
|
||||
await conn.execute("ALTER TABLE new_mx_user_profile RENAME TO mx_user_profile")
|
||||
|
||||
|
||||
async def varchar_to_text(conn: Connection) -> None:
|
||||
columns_to_adjust = {
|
||||
"client": (
|
||||
"id",
|
||||
"homeserver",
|
||||
"device_id",
|
||||
"next_batch",
|
||||
"filter_id",
|
||||
"displayname",
|
||||
"avatar_url",
|
||||
),
|
||||
"instance": ("id", "type", "primary_user"),
|
||||
"mx_room_state": ("room_id",),
|
||||
"mx_user_profile": ("room_id", "user_id", "displayname", "avatar_url"),
|
||||
}
|
||||
for table, columns in columns_to_adjust.items():
|
||||
for column in columns:
|
||||
await conn.execute(f'ALTER TABLE "{table}" ALTER COLUMN {column} TYPE TEXT')
|
||||
25
maubot-src/maubot/db/upgrade/v02_instance_database_engine.py
Normal file
25
maubot-src/maubot/db/upgrade/v02_instance_database_engine.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# maubot - A plugin-based Matrix bot system.
|
||||
# Copyright (C) 2022 Tulir Asokan
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
from __future__ import annotations
|
||||
|
||||
from mautrix.util.async_db import Connection
|
||||
|
||||
from . import upgrade_table
|
||||
|
||||
|
||||
@upgrade_table.register(description="Store instance database engine")
|
||||
async def upgrade_v2(conn: Connection) -> None:
|
||||
await conn.execute("ALTER TABLE instance ADD COLUMN database_engine TEXT")
|
||||
Reference in New Issue
Block a user