Add Cloudron packaging for Maubot
This commit is contained in:
161
maubot-src/maubot/management/api/instance_database.py
Normal file
161
maubot-src/maubot/management/api/instance_database.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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 datetime import datetime
|
||||
|
||||
from aiohttp import web
|
||||
from asyncpg import PostgresError
|
||||
import aiosqlite
|
||||
|
||||
from mautrix.util.async_db import Database
|
||||
|
||||
from ...instance import PluginInstance
|
||||
from ...lib.optionalalchemy import Engine, IntegrityError, OperationalError, asc, desc
|
||||
from .base import routes
|
||||
from .responses import resp
|
||||
|
||||
|
||||
@routes.get("/instance/{id}/database")
|
||||
async def get_database(request: web.Request) -> web.Response:
|
||||
instance_id = request.match_info["id"].lower()
|
||||
instance = await PluginInstance.get(instance_id)
|
||||
if not instance:
|
||||
return resp.instance_not_found
|
||||
elif not instance.inst_db:
|
||||
return resp.plugin_has_no_database
|
||||
return web.json_response(await instance.get_db_tables())
|
||||
|
||||
|
||||
@routes.get("/instance/{id}/database/{table}")
|
||||
async def get_table(request: web.Request) -> web.Response:
|
||||
instance_id = request.match_info["id"].lower()
|
||||
instance = await PluginInstance.get(instance_id)
|
||||
if not instance:
|
||||
return resp.instance_not_found
|
||||
elif not instance.inst_db:
|
||||
return resp.plugin_has_no_database
|
||||
tables = await instance.get_db_tables()
|
||||
try:
|
||||
table = tables[request.match_info.get("table", "")]
|
||||
except KeyError:
|
||||
return resp.table_not_found
|
||||
try:
|
||||
order = [tuple(order.split(":")) for order in request.query.getall("order")]
|
||||
order = [
|
||||
(
|
||||
(asc if sort.lower() == "asc" else desc)(table.columns[column])
|
||||
if sort
|
||||
else table.columns[column]
|
||||
)
|
||||
for column, sort in order
|
||||
]
|
||||
except KeyError:
|
||||
order = []
|
||||
limit = int(request.query.get("limit", "100"))
|
||||
if isinstance(instance.inst_db, Engine):
|
||||
return _execute_query_sqlalchemy(instance, table.select().order_by(*order).limit(limit))
|
||||
|
||||
|
||||
@routes.post("/instance/{id}/database/query")
|
||||
async def query(request: web.Request) -> web.Response:
|
||||
instance_id = request.match_info["id"].lower()
|
||||
instance = await PluginInstance.get(instance_id)
|
||||
if not instance:
|
||||
return resp.instance_not_found
|
||||
elif not instance.inst_db:
|
||||
return resp.plugin_has_no_database
|
||||
data = await request.json()
|
||||
try:
|
||||
sql_query = data["query"]
|
||||
except KeyError:
|
||||
return resp.query_missing
|
||||
rows_as_dict = data.get("rows_as_dict", False)
|
||||
if isinstance(instance.inst_db, Engine):
|
||||
return _execute_query_sqlalchemy(instance, sql_query, rows_as_dict)
|
||||
elif isinstance(instance.inst_db, Database):
|
||||
try:
|
||||
return await _execute_query_asyncpg(instance, sql_query, rows_as_dict)
|
||||
except (PostgresError, aiosqlite.Error) as e:
|
||||
return resp.sql_error(e, sql_query)
|
||||
else:
|
||||
return resp.unsupported_plugin_database
|
||||
|
||||
|
||||
def check_type(val):
|
||||
if isinstance(val, datetime):
|
||||
return val.isoformat()
|
||||
return val
|
||||
|
||||
|
||||
async def _execute_query_asyncpg(
|
||||
instance: PluginInstance, sql_query: str, rows_as_dict: bool = False
|
||||
) -> web.Response:
|
||||
data = {"ok": True, "query": sql_query}
|
||||
if sql_query.upper().startswith("SELECT"):
|
||||
res = await instance.inst_db.fetch(sql_query)
|
||||
data["rows"] = [
|
||||
(
|
||||
{key: check_type(value) for key, value in row.items()}
|
||||
if rows_as_dict
|
||||
else [check_type(value) for value in row]
|
||||
)
|
||||
for row in res
|
||||
]
|
||||
if len(res) > 0:
|
||||
# TODO can we find column names when there are no rows?
|
||||
data["columns"] = list(res[0].keys())
|
||||
else:
|
||||
res = await instance.inst_db.execute(sql_query)
|
||||
if isinstance(res, str):
|
||||
data["status_msg"] = res
|
||||
elif isinstance(res, aiosqlite.Cursor):
|
||||
data["rowcount"] = res.rowcount
|
||||
# data["inserted_primary_key"] = res.lastrowid
|
||||
else:
|
||||
data["status_msg"] = "unknown status"
|
||||
return web.json_response(data)
|
||||
|
||||
|
||||
def _execute_query_sqlalchemy(
|
||||
instance: PluginInstance, sql_query: str, rows_as_dict: bool = False
|
||||
) -> web.Response:
|
||||
assert isinstance(instance.inst_db, Engine)
|
||||
try:
|
||||
res = instance.inst_db.execute(sql_query)
|
||||
except IntegrityError as e:
|
||||
return resp.sql_integrity_error(e, sql_query)
|
||||
except OperationalError as e:
|
||||
return resp.sql_operational_error(e, sql_query)
|
||||
data = {
|
||||
"ok": True,
|
||||
"query": str(sql_query),
|
||||
}
|
||||
if res.returns_rows:
|
||||
data["rows"] = [
|
||||
(
|
||||
{key: check_type(value) for key, value in row.items()}
|
||||
if rows_as_dict
|
||||
else [check_type(value) for value in row]
|
||||
)
|
||||
for row in res
|
||||
]
|
||||
data["columns"] = res.keys()
|
||||
else:
|
||||
data["rowcount"] = res.rowcount
|
||||
if res.is_insert:
|
||||
data["inserted_primary_key"] = res.inserted_primary_key
|
||||
return web.json_response(data)
|
||||
Reference in New Issue
Block a user