Fix reusing management API responses and some other things
This commit is contained in:
parent
ec22e5eba7
commit
2736a1f47f
@ -134,6 +134,7 @@ class Client:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
self.db.delete(self.db_instance)
|
self.db.delete(self.db_instance)
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
|
@ -14,13 +14,14 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from ruamel.yaml.comments import CommentedMap
|
|
||||||
from ruamel.yaml import YAML
|
|
||||||
from asyncio import AbstractEventLoop
|
from asyncio import AbstractEventLoop
|
||||||
import logging
|
import logging
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from ruamel.yaml.comments import CommentedMap
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
from mautrix.util.config import BaseProxyConfig, RecursiveDict
|
from mautrix.util.config import BaseProxyConfig, RecursiveDict
|
||||||
from mautrix.types import UserID
|
from mautrix.types import UserID
|
||||||
|
|
||||||
@ -56,6 +57,10 @@ class PluginInstance:
|
|||||||
self.log = logging.getLogger(f"maubot.plugin.{self.id}")
|
self.log = logging.getLogger(f"maubot.plugin.{self.id}")
|
||||||
self.config = None
|
self.config = None
|
||||||
self.started = False
|
self.started = False
|
||||||
|
self.loader = None
|
||||||
|
self.client = None
|
||||||
|
self.plugin = None
|
||||||
|
self.base_cfg = None
|
||||||
self.cache[self.id] = self
|
self.cache[self.id] = self
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
@ -94,6 +99,7 @@ class PluginInstance:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
self.db.delete(self.db_instance)
|
self.db.delete(self.db_instance)
|
||||||
|
self.db.commit()
|
||||||
# TODO delete plugin db
|
# TODO delete plugin db
|
||||||
|
|
||||||
def load_config(self) -> CommentedMap:
|
def load_config(self) -> CommentedMap:
|
||||||
|
@ -192,8 +192,10 @@ class ZippedPluginLoader(PluginLoader):
|
|||||||
for module in self.modules:
|
for module in self.modules:
|
||||||
try:
|
try:
|
||||||
importer.load_module(module)
|
importer.load_module(module)
|
||||||
except ZipImportError as e:
|
except ZipImportError:
|
||||||
raise MaubotZipLoadError(f"Module {module} not found in file")
|
raise MaubotZipLoadError(f"Module {module} not found in file")
|
||||||
|
except Exception:
|
||||||
|
raise MaubotZipLoadError(f"Failed to load module {module}")
|
||||||
try:
|
try:
|
||||||
main_mod = sys.modules[self.main_module]
|
main_mod = sys.modules[self.main_module]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
@ -235,7 +237,7 @@ class ZippedPluginLoader(PluginLoader):
|
|||||||
self._importer.remove_cache()
|
self._importer.remove_cache()
|
||||||
self._importer = None
|
self._importer = None
|
||||||
self._loaded = None
|
self._loaded = None
|
||||||
os.remove(self.path)
|
self.trash(self.path, reason="delete")
|
||||||
self.id = None
|
self.id = None
|
||||||
self.path = None
|
self.path = None
|
||||||
self.version = None
|
self.version = None
|
||||||
|
@ -22,7 +22,7 @@ from mautrix.types import UserID
|
|||||||
from mautrix.util.signed_token import sign_token, verify_token
|
from mautrix.util.signed_token import sign_token, verify_token
|
||||||
|
|
||||||
from .base import routes, get_config
|
from .base import routes, get_config
|
||||||
from .responses import ErrBadAuth, ErrBodyNotJSON, ErrNoToken, ErrInvalidToken
|
from .responses import resp
|
||||||
|
|
||||||
|
|
||||||
def is_valid_token(token: str) -> bool:
|
def is_valid_token(token: str) -> bool:
|
||||||
@ -35,6 +35,7 @@ def is_valid_token(token: str) -> bool:
|
|||||||
def create_token(user: UserID) -> str:
|
def create_token(user: UserID) -> str:
|
||||||
return sign_token(get_config()["server.unshared_secret"], {
|
return sign_token(get_config()["server.unshared_secret"], {
|
||||||
"user_id": user,
|
"user_id": user,
|
||||||
|
"created_at": int(time()),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -42,17 +43,15 @@ def create_token(user: UserID) -> str:
|
|||||||
async def ping(request: web.Request) -> web.Response:
|
async def ping(request: web.Request) -> web.Response:
|
||||||
token = request.headers.get("Authorization", "")
|
token = request.headers.get("Authorization", "")
|
||||||
if not token or not token.startswith("Bearer "):
|
if not token or not token.startswith("Bearer "):
|
||||||
return ErrNoToken
|
return resp.no_token
|
||||||
|
|
||||||
data = verify_token(get_config()["server.unshared_secret"], token[len("Bearer "):])
|
data = verify_token(get_config()["server.unshared_secret"], token[len("Bearer "):])
|
||||||
if not data:
|
if not data:
|
||||||
return ErrInvalidToken
|
return resp.invalid_token
|
||||||
user = data.get("user_id", None)
|
user = data.get("user_id", None)
|
||||||
if not get_config().is_admin(user):
|
if not get_config().is_admin(user):
|
||||||
return ErrInvalidToken
|
return resp.invalid_token
|
||||||
return web.json_response({
|
return resp.pong(user)
|
||||||
"username": user,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@routes.post("/auth/login")
|
@routes.post("/auth/login")
|
||||||
@ -60,21 +59,15 @@ async def login(request: web.Request) -> web.Response:
|
|||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return ErrBodyNotJSON
|
return resp.body_not_json
|
||||||
secret = data.get("secret")
|
secret = data.get("secret")
|
||||||
if secret and get_config()["server.unshared_secret"] == secret:
|
if secret and get_config()["server.unshared_secret"] == secret:
|
||||||
user = data.get("user") or "root"
|
user = data.get("user") or "root"
|
||||||
return web.json_response({
|
return resp.logged_in(create_token(user))
|
||||||
"token": create_token(user),
|
|
||||||
"created_at": int(time()),
|
|
||||||
})
|
|
||||||
|
|
||||||
username = data.get("username")
|
username = data.get("username")
|
||||||
password = data.get("password")
|
password = data.get("password")
|
||||||
if get_config().check_password(username, password):
|
if get_config().check_password(username, password):
|
||||||
return web.json_response({
|
return resp.logged_in(create_token(username))
|
||||||
"token": create_token(username),
|
|
||||||
"created_at": int(time()),
|
|
||||||
})
|
|
||||||
|
|
||||||
return ErrBadAuth
|
return resp.bad_auth
|
||||||
|
@ -26,14 +26,12 @@ from mautrix.client import Client as MatrixClient
|
|||||||
from ...db import DBClient
|
from ...db import DBClient
|
||||||
from ...client import Client
|
from ...client import Client
|
||||||
from .base import routes
|
from .base import routes
|
||||||
from .responses import (RespDeleted, ErrClientNotFound, ErrBodyNotJSON, ErrClientInUse,
|
from .responses import resp
|
||||||
ErrBadClientAccessToken, ErrBadClientAccessDetails, ErrMXIDMismatch,
|
|
||||||
ErrUserExists)
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/clients")
|
@routes.get("/clients")
|
||||||
async def get_clients(_: web.Request) -> web.Response:
|
async def get_clients(_: web.Request) -> web.Response:
|
||||||
return web.json_response([client.to_dict() for client in Client.cache.values()])
|
return resp.found([client.to_dict() for client in Client.cache.values()])
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/client/{id}")
|
@routes.get("/client/{id}")
|
||||||
@ -41,8 +39,8 @@ async def get_client(request: web.Request) -> web.Response:
|
|||||||
user_id = request.match_info.get("id", None)
|
user_id = request.match_info.get("id", None)
|
||||||
client = Client.get(user_id, None)
|
client = Client.get(user_id, None)
|
||||||
if not client:
|
if not client:
|
||||||
return ErrClientNotFound
|
return resp.client_not_found
|
||||||
return web.json_response(client.to_dict())
|
return resp.found(client.to_dict())
|
||||||
|
|
||||||
|
|
||||||
async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response:
|
async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response:
|
||||||
@ -53,15 +51,15 @@ async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response:
|
|||||||
try:
|
try:
|
||||||
mxid = await new_client.whoami()
|
mxid = await new_client.whoami()
|
||||||
except MatrixInvalidToken:
|
except MatrixInvalidToken:
|
||||||
return ErrBadClientAccessToken
|
return resp.bad_client_access_token
|
||||||
except MatrixRequestError:
|
except MatrixRequestError:
|
||||||
return ErrBadClientAccessDetails
|
return resp.bad_client_access_details
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
existing_client = Client.get(mxid, None)
|
existing_client = Client.get(mxid, None)
|
||||||
if existing_client is not None:
|
if existing_client is not None:
|
||||||
return ErrUserExists
|
return resp.user_exists
|
||||||
elif mxid != user_id:
|
elif mxid != user_id:
|
||||||
return ErrMXIDMismatch
|
return resp.mxid_mismatch
|
||||||
db_instance = DBClient(id=mxid, homeserver=homeserver, access_token=access_token,
|
db_instance = DBClient(id=mxid, homeserver=homeserver, access_token=access_token,
|
||||||
enabled=data.get("enabled", True), next_batch=SyncToken(""),
|
enabled=data.get("enabled", True), next_batch=SyncToken(""),
|
||||||
filter_id=FilterID(""), sync=data.get("sync", True),
|
filter_id=FilterID(""), sync=data.get("sync", True),
|
||||||
@ -72,7 +70,7 @@ async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response:
|
|||||||
Client.db.add(db_instance)
|
Client.db.add(db_instance)
|
||||||
Client.db.commit()
|
Client.db.commit()
|
||||||
await client.start()
|
await client.start()
|
||||||
return web.json_response(client.to_dict())
|
return resp.created(client.to_dict())
|
||||||
|
|
||||||
|
|
||||||
async def _update_client(client: Client, data: dict) -> web.Response:
|
async def _update_client(client: Client, data: dict) -> web.Response:
|
||||||
@ -80,18 +78,18 @@ async def _update_client(client: Client, data: dict) -> web.Response:
|
|||||||
await client.update_access_details(data.get("access_token", None),
|
await client.update_access_details(data.get("access_token", None),
|
||||||
data.get("homeserver", None))
|
data.get("homeserver", None))
|
||||||
except MatrixInvalidToken:
|
except MatrixInvalidToken:
|
||||||
return ErrBadClientAccessToken
|
return resp.bad_client_access_token
|
||||||
except MatrixRequestError:
|
except MatrixRequestError:
|
||||||
return ErrBadClientAccessDetails
|
return resp.bad_client_access_details
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return ErrMXIDMismatch
|
return resp.mxid_mismatch
|
||||||
await client.update_avatar_url(data.get("avatar_url", None))
|
await client.update_avatar_url(data.get("avatar_url", None))
|
||||||
await client.update_displayname(data.get("displayname", None))
|
await client.update_displayname(data.get("displayname", None))
|
||||||
await client.update_started(data.get("started", None))
|
await client.update_started(data.get("started", None))
|
||||||
client.enabled = data.get("enabled", client.enabled)
|
client.enabled = data.get("enabled", client.enabled)
|
||||||
client.autojoin = data.get("autojoin", client.autojoin)
|
client.autojoin = data.get("autojoin", client.autojoin)
|
||||||
client.sync = data.get("sync", client.sync)
|
client.sync = data.get("sync", client.sync)
|
||||||
return web.json_response(client.to_dict(), status=HTTPStatus.CREATED)
|
return resp.updated(client.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@routes.post("/client/new")
|
@routes.post("/client/new")
|
||||||
@ -99,7 +97,7 @@ async def create_client(request: web.Request) -> web.Response:
|
|||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
return ErrBodyNotJSON
|
return resp.body_not_json
|
||||||
return await _create_client(None, data)
|
return await _create_client(None, data)
|
||||||
|
|
||||||
|
|
||||||
@ -110,7 +108,7 @@ async def update_client(request: web.Request) -> web.Response:
|
|||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
return ErrBodyNotJSON
|
return resp.body_not_json
|
||||||
if not client:
|
if not client:
|
||||||
return await _create_client(user_id, data)
|
return await _create_client(user_id, data)
|
||||||
else:
|
else:
|
||||||
@ -122,10 +120,10 @@ async def delete_client(request: web.Request) -> web.Response:
|
|||||||
user_id = request.match_info.get("id", None)
|
user_id = request.match_info.get("id", None)
|
||||||
client = Client.get(user_id, None)
|
client = Client.get(user_id, None)
|
||||||
if not client:
|
if not client:
|
||||||
return ErrClientNotFound
|
return resp.client_not_found
|
||||||
if len(client.references) > 0:
|
if len(client.references) > 0:
|
||||||
return ErrClientInUse
|
return resp.client_in_use
|
||||||
if client.started:
|
if client.started:
|
||||||
await client.stop()
|
await client.stop()
|
||||||
client.delete()
|
client.delete()
|
||||||
return RespDeleted
|
return resp.deleted
|
||||||
|
@ -23,13 +23,12 @@ from ...instance import PluginInstance
|
|||||||
from ...loader import PluginLoader
|
from ...loader import PluginLoader
|
||||||
from ...client import Client
|
from ...client import Client
|
||||||
from .base import routes
|
from .base import routes
|
||||||
from .responses import (ErrInstanceNotFound, ErrBodyNotJSON, RespDeleted, ErrPrimaryUserNotFound,
|
from .responses import resp
|
||||||
ErrPluginTypeRequired, ErrPrimaryUserRequired, ErrPluginTypeNotFound)
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/instances")
|
@routes.get("/instances")
|
||||||
async def get_instances(_: web.Request) -> web.Response:
|
async def get_instances(_: web.Request) -> web.Response:
|
||||||
return web.json_response([instance.to_dict() for instance in PluginInstance.cache.values()])
|
return resp.found([instance.to_dict() for instance in PluginInstance.cache.values()])
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/instance/{id}")
|
@routes.get("/instance/{id}")
|
||||||
@ -37,23 +36,23 @@ async def get_instance(request: web.Request) -> web.Response:
|
|||||||
instance_id = request.match_info.get("id", "").lower()
|
instance_id = request.match_info.get("id", "").lower()
|
||||||
instance = PluginInstance.get(instance_id, None)
|
instance = PluginInstance.get(instance_id, None)
|
||||||
if not instance:
|
if not instance:
|
||||||
return ErrInstanceNotFound
|
return resp.instance_not_found
|
||||||
return web.json_response(instance.to_dict())
|
return resp.found(instance.to_dict())
|
||||||
|
|
||||||
|
|
||||||
async def _create_instance(instance_id: str, data: dict) -> web.Response:
|
async def _create_instance(instance_id: str, data: dict) -> web.Response:
|
||||||
plugin_type = data.get("type", None)
|
plugin_type = data.get("type", None)
|
||||||
primary_user = data.get("primary_user", None)
|
primary_user = data.get("primary_user", None)
|
||||||
if not plugin_type:
|
if not plugin_type:
|
||||||
return ErrPluginTypeRequired
|
return resp.plugin_type_required
|
||||||
elif not primary_user:
|
elif not primary_user:
|
||||||
return ErrPrimaryUserRequired
|
return resp.primary_user_required
|
||||||
elif not Client.get(primary_user):
|
elif not Client.get(primary_user):
|
||||||
return ErrPrimaryUserNotFound
|
return resp.primary_user_not_found
|
||||||
try:
|
try:
|
||||||
PluginLoader.find(plugin_type)
|
PluginLoader.find(plugin_type)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return ErrPluginTypeNotFound
|
return resp.plugin_type_not_found
|
||||||
db_instance = DBPlugin(id=instance_id, type=plugin_type, enabled=data.get("enabled", True),
|
db_instance = DBPlugin(id=instance_id, type=plugin_type, enabled=data.get("enabled", True),
|
||||||
primary_user=primary_user, config=data.get("config", ""))
|
primary_user=primary_user, config=data.get("config", ""))
|
||||||
instance = PluginInstance(db_instance)
|
instance = PluginInstance(db_instance)
|
||||||
@ -61,18 +60,18 @@ async def _create_instance(instance_id: str, data: dict) -> web.Response:
|
|||||||
PluginInstance.db.add(db_instance)
|
PluginInstance.db.add(db_instance)
|
||||||
PluginInstance.db.commit()
|
PluginInstance.db.commit()
|
||||||
await instance.start()
|
await instance.start()
|
||||||
return web.json_response(instance.to_dict(), status=HTTPStatus.CREATED)
|
return resp.created(instance.to_dict())
|
||||||
|
|
||||||
|
|
||||||
async def _update_instance(instance: PluginInstance, data: dict) -> web.Response:
|
async def _update_instance(instance: PluginInstance, data: dict) -> web.Response:
|
||||||
if not await instance.update_primary_user(data.get("primary_user", None)):
|
if not await instance.update_primary_user(data.get("primary_user", None)):
|
||||||
return ErrPrimaryUserNotFound
|
return resp.primary_user_not_found
|
||||||
instance.update_id(data.get("id", None))
|
instance.update_id(data.get("id", None))
|
||||||
instance.update_enabled(data.get("enabled", None))
|
instance.update_enabled(data.get("enabled", None))
|
||||||
instance.update_config(data.get("config", None))
|
instance.update_config(data.get("config", None))
|
||||||
await instance.update_started(data.get("started", None))
|
await instance.update_started(data.get("started", None))
|
||||||
instance.db.commit()
|
instance.db.commit()
|
||||||
return web.json_response(instance.to_dict())
|
return resp.updated(instance.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@routes.put("/instance/{id}")
|
@routes.put("/instance/{id}")
|
||||||
@ -82,7 +81,7 @@ async def update_instance(request: web.Request) -> web.Response:
|
|||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
return ErrBodyNotJSON
|
return resp.body_not_json
|
||||||
if not instance:
|
if not instance:
|
||||||
return await _create_instance(instance_id, data)
|
return await _create_instance(instance_id, data)
|
||||||
else:
|
else:
|
||||||
@ -94,8 +93,8 @@ async def delete_instance(request: web.Request) -> web.Response:
|
|||||||
instance_id = request.match_info.get("id", "").lower()
|
instance_id = request.match_info.get("id", "").lower()
|
||||||
instance = PluginInstance.get(instance_id, None)
|
instance = PluginInstance.get(instance_id, None)
|
||||||
if not instance:
|
if not instance:
|
||||||
return ErrInstanceNotFound
|
return resp.instance_not_found
|
||||||
if instance.started:
|
if instance.started:
|
||||||
await instance.stop()
|
await instance.stop()
|
||||||
instance.delete()
|
instance.delete()
|
||||||
return RespDeleted
|
return resp.deleted
|
||||||
|
@ -14,9 +14,11 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from typing import Callable, Awaitable
|
from typing import Callable, Awaitable
|
||||||
|
import logging
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from .responses import ErrNoToken, ErrInvalidToken, ErrPathNotFound, ErrMethodNotAllowed
|
from .responses import resp
|
||||||
from .auth import is_valid_token
|
from .auth import is_valid_token
|
||||||
|
|
||||||
Handler = Callable[[web.Request], Awaitable[web.Response]]
|
Handler = Callable[[web.Request], Awaitable[web.Response]]
|
||||||
@ -28,25 +30,32 @@ async def auth(request: web.Request, handler: Handler) -> web.Response:
|
|||||||
return await handler(request)
|
return await handler(request)
|
||||||
token = request.headers.get("Authorization", "")
|
token = request.headers.get("Authorization", "")
|
||||||
if not token or not token.startswith("Bearer "):
|
if not token or not token.startswith("Bearer "):
|
||||||
return ErrNoToken
|
return resp.no_token
|
||||||
if not is_valid_token(token[len("Bearer "):]):
|
if not is_valid_token(token[len("Bearer "):]):
|
||||||
return ErrInvalidToken
|
return resp.invalid_token
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger("maubot.server")
|
||||||
|
|
||||||
|
|
||||||
@web.middleware
|
@web.middleware
|
||||||
async def error(request: web.Request, handler: Handler) -> web.Response:
|
async def error(request: web.Request, handler: Handler) -> web.Response:
|
||||||
try:
|
try:
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
except web.HTTPException as ex:
|
except web.HTTPException as ex:
|
||||||
|
print(ex)
|
||||||
if ex.status_code == 404:
|
if ex.status_code == 404:
|
||||||
return ErrPathNotFound
|
return resp.path_not_found
|
||||||
elif ex.status_code == 405:
|
elif ex.status_code == 405:
|
||||||
return ErrMethodNotAllowed
|
return resp.method_not_allowed
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"error": f"Unhandled HTTP {ex.status}",
|
"error": f"Unhandled HTTP {ex.status}",
|
||||||
"errcode": f"unhandled_http_{ex.status}",
|
"errcode": f"unhandled_http_{ex.status}",
|
||||||
}, status=ex.status)
|
}, status=ex.status)
|
||||||
|
except Exception:
|
||||||
|
log.exception("Error in handler")
|
||||||
|
return resp.internal_server_error
|
||||||
|
|
||||||
|
|
||||||
req_no = 0
|
req_no = 0
|
||||||
|
@ -23,14 +23,13 @@ import re
|
|||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError
|
from ...loader import PluginLoader, ZippedPluginLoader, MaubotZipImportError
|
||||||
from .responses import (ErrPluginNotFound, ErrPluginInUse, plugin_import_error,
|
from .responses import resp
|
||||||
plugin_reload_error, RespDeleted, RespOK, ErrUnsupportedPluginLoader)
|
|
||||||
from .base import routes, get_config
|
from .base import routes, get_config
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/plugins")
|
@routes.get("/plugins")
|
||||||
async def get_plugins(_) -> web.Response:
|
async def get_plugins(_) -> web.Response:
|
||||||
return web.json_response([plugin.to_dict() for plugin in PluginLoader.id_cache.values()])
|
return resp.found([plugin.to_dict() for plugin in PluginLoader.id_cache.values()])
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/plugin/{id}")
|
@routes.get("/plugin/{id}")
|
||||||
@ -38,8 +37,8 @@ async def get_plugin(request: web.Request) -> web.Response:
|
|||||||
plugin_id = request.match_info.get("id", None)
|
plugin_id = request.match_info.get("id", None)
|
||||||
plugin = PluginLoader.id_cache.get(plugin_id, None)
|
plugin = PluginLoader.id_cache.get(plugin_id, None)
|
||||||
if not plugin:
|
if not plugin:
|
||||||
return ErrPluginNotFound
|
return resp.plugin_not_found
|
||||||
return web.json_response(plugin.to_dict())
|
return resp.found(plugin.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@routes.delete("/plugin/{id}")
|
@routes.delete("/plugin/{id}")
|
||||||
@ -47,11 +46,11 @@ async def delete_plugin(request: web.Request) -> web.Response:
|
|||||||
plugin_id = request.match_info.get("id", None)
|
plugin_id = request.match_info.get("id", None)
|
||||||
plugin = PluginLoader.id_cache.get(plugin_id, None)
|
plugin = PluginLoader.id_cache.get(plugin_id, None)
|
||||||
if not plugin:
|
if not plugin:
|
||||||
return ErrPluginNotFound
|
return resp.plugin_not_found
|
||||||
elif len(plugin.references) > 0:
|
elif len(plugin.references) > 0:
|
||||||
return ErrPluginInUse
|
return resp.plugin_in_use
|
||||||
await plugin.delete()
|
await plugin.delete()
|
||||||
return RespDeleted
|
return resp.deleted
|
||||||
|
|
||||||
|
|
||||||
@routes.post("/plugin/{id}/reload")
|
@routes.post("/plugin/{id}/reload")
|
||||||
@ -59,15 +58,15 @@ async def reload_plugin(request: web.Request) -> web.Response:
|
|||||||
plugin_id = request.match_info.get("id", None)
|
plugin_id = request.match_info.get("id", None)
|
||||||
plugin = PluginLoader.id_cache.get(plugin_id, None)
|
plugin = PluginLoader.id_cache.get(plugin_id, None)
|
||||||
if not plugin:
|
if not plugin:
|
||||||
return ErrPluginNotFound
|
return resp.plugin_not_found
|
||||||
|
|
||||||
await plugin.stop_instances()
|
await plugin.stop_instances()
|
||||||
try:
|
try:
|
||||||
await plugin.reload()
|
await plugin.reload()
|
||||||
except MaubotZipImportError as e:
|
except MaubotZipImportError as e:
|
||||||
return plugin_reload_error(str(e), traceback.format_exc())
|
return resp.plugin_reload_error(str(e), traceback.format_exc())
|
||||||
await plugin.start_instances()
|
await plugin.start_instances()
|
||||||
return RespOK
|
return resp.ok
|
||||||
|
|
||||||
|
|
||||||
async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Response:
|
async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Response:
|
||||||
@ -78,8 +77,8 @@ async def upload_new_plugin(content: bytes, pid: str, version: str) -> web.Respo
|
|||||||
plugin = ZippedPluginLoader.get(path)
|
plugin = ZippedPluginLoader.get(path)
|
||||||
except MaubotZipImportError as e:
|
except MaubotZipImportError as e:
|
||||||
ZippedPluginLoader.trash(path)
|
ZippedPluginLoader.trash(path)
|
||||||
return plugin_import_error(str(e), traceback.format_exc())
|
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||||
return web.json_response(plugin.to_dict(), status=HTTPStatus.CREATED)
|
return resp.created(plugin.to_dict())
|
||||||
|
|
||||||
|
|
||||||
async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, new_version: str
|
async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes, new_version: str
|
||||||
@ -107,10 +106,10 @@ async def upload_replacement_plugin(plugin: ZippedPluginLoader, content: bytes,
|
|||||||
await plugin.start_instances()
|
await plugin.start_instances()
|
||||||
except MaubotZipImportError:
|
except MaubotZipImportError:
|
||||||
pass
|
pass
|
||||||
return plugin_import_error(str(e), traceback.format_exc())
|
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||||
await plugin.start_instances()
|
await plugin.start_instances()
|
||||||
ZippedPluginLoader.trash(old_path, reason="update")
|
ZippedPluginLoader.trash(old_path, reason="update")
|
||||||
return web.json_response(plugin.to_dict())
|
return resp.updated(plugin.to_dict())
|
||||||
|
|
||||||
|
|
||||||
@routes.post("/plugins/upload")
|
@routes.post("/plugins/upload")
|
||||||
@ -120,11 +119,11 @@ async def upload_plugin(request: web.Request) -> web.Response:
|
|||||||
try:
|
try:
|
||||||
pid, version = ZippedPluginLoader.verify_meta(file)
|
pid, version = ZippedPluginLoader.verify_meta(file)
|
||||||
except MaubotZipImportError as e:
|
except MaubotZipImportError as e:
|
||||||
return plugin_import_error(str(e), traceback.format_exc())
|
return resp.plugin_import_error(str(e), traceback.format_exc())
|
||||||
plugin = PluginLoader.id_cache.get(pid, None)
|
plugin = PluginLoader.id_cache.get(pid, None)
|
||||||
if not plugin:
|
if not plugin:
|
||||||
return await upload_new_plugin(content, pid, version)
|
return await upload_new_plugin(content, pid, version)
|
||||||
elif isinstance(plugin, ZippedPluginLoader):
|
elif isinstance(plugin, ZippedPluginLoader):
|
||||||
return await upload_replacement_plugin(plugin, content, version)
|
return await upload_replacement_plugin(plugin, content, version)
|
||||||
else:
|
else:
|
||||||
return ErrUnsupportedPluginLoader
|
return resp.unsupported_plugin_loader
|
||||||
|
@ -14,132 +14,211 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
ErrBodyNotJSON = web.json_response({
|
|
||||||
"error": "Request body is not JSON",
|
|
||||||
"errcode": "body_not_json",
|
|
||||||
}, status=HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
ErrPluginTypeRequired = web.json_response({
|
class _Response:
|
||||||
"error": "Plugin type is required when creating plugin instances",
|
@property
|
||||||
"errcode": "plugin_type_required",
|
def body_not_json(self) -> web.Response:
|
||||||
}, status=HTTPStatus.BAD_REQUEST)
|
return web.json_response({
|
||||||
|
"error": "Request body is not JSON",
|
||||||
|
"errcode": "body_not_json",
|
||||||
|
}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
ErrPrimaryUserRequired = web.json_response({
|
@property
|
||||||
"error": "Primary user is required when creating plugin instances",
|
def plugin_type_required(self) -> web.Response:
|
||||||
"errcode": "primary_user_required",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.BAD_REQUEST)
|
"error": "Plugin type is required when creating plugin instances",
|
||||||
|
"errcode": "plugin_type_required",
|
||||||
|
}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
ErrBadClientAccessToken = web.json_response({
|
@property
|
||||||
"error": "Invalid access token",
|
def primary_user_required(self) -> web.Response:
|
||||||
"errcode": "bad_client_access_token",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.BAD_REQUEST)
|
"error": "Primary user is required when creating plugin instances",
|
||||||
|
"errcode": "primary_user_required",
|
||||||
|
}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
ErrBadClientAccessDetails = web.json_response({
|
@property
|
||||||
"error": "Invalid homeserver or access token",
|
def bad_client_access_token(self) -> web.Response:
|
||||||
"errcode": "bad_client_access_details"
|
return web.json_response({
|
||||||
}, status=HTTPStatus.BAD_REQUEST)
|
"error": "Invalid access token",
|
||||||
|
"errcode": "bad_client_access_token",
|
||||||
|
}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
ErrMXIDMismatch = web.json_response({
|
@property
|
||||||
"error": "The Matrix user ID of the client and the user ID of the access token don't match",
|
def bad_client_access_details(self) -> web.Response:
|
||||||
"errcode": "mxid_mismatch",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.BAD_REQUEST)
|
"error": "Invalid homeserver or access token",
|
||||||
|
"errcode": "bad_client_access_details"
|
||||||
|
}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
ErrBadAuth = web.json_response({
|
@property
|
||||||
"error": "Invalid username or password",
|
def mxid_mismatch(self) -> web.Response:
|
||||||
"errcode": "invalid_auth",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.UNAUTHORIZED)
|
"error": "The Matrix user ID of the client and the user ID of the access token don't match",
|
||||||
|
"errcode": "mxid_mismatch",
|
||||||
|
}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
ErrNoToken = web.json_response({
|
@property
|
||||||
"error": "Authorization token missing",
|
def bad_auth(self) -> web.Response:
|
||||||
"errcode": "auth_token_missing",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.UNAUTHORIZED)
|
"error": "Invalid username or password",
|
||||||
|
"errcode": "invalid_auth",
|
||||||
|
}, status=HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
ErrInvalidToken = web.json_response({
|
@property
|
||||||
"error": "Invalid authorization token",
|
def no_token(self) -> web.Response:
|
||||||
"errcode": "auth_token_invalid",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.UNAUTHORIZED)
|
"error": "Authorization token missing",
|
||||||
|
"errcode": "auth_token_missing",
|
||||||
|
}, status=HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
ErrPluginNotFound = web.json_response({
|
@property
|
||||||
"error": "Plugin not found",
|
def invalid_token(self) -> web.Response:
|
||||||
"errcode": "plugin_not_found",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.NOT_FOUND)
|
"error": "Invalid authorization token",
|
||||||
|
"errcode": "auth_token_invalid",
|
||||||
|
}, status=HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
ErrClientNotFound = web.json_response({
|
@property
|
||||||
"error": "Client not found",
|
def plugin_not_found(self) -> web.Response:
|
||||||
"errcode": "client_not_found",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.NOT_FOUND)
|
"error": "Plugin not found",
|
||||||
|
"errcode": "plugin_not_found",
|
||||||
|
}, status=HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
ErrPrimaryUserNotFound = web.json_response({
|
@property
|
||||||
"error": "Client for given primary user not found",
|
def client_not_found(self) -> web.Response:
|
||||||
"errcode": "primary_user_not_found",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.NOT_FOUND)
|
"error": "Client not found",
|
||||||
|
"errcode": "client_not_found",
|
||||||
|
}, status=HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
ErrInstanceNotFound = web.json_response({
|
@property
|
||||||
"error": "Plugin instance not found",
|
def primary_user_not_found(self) -> web.Response:
|
||||||
"errcode": "instance_not_found",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.NOT_FOUND)
|
"error": "Client for given primary user not found",
|
||||||
|
"errcode": "primary_user_not_found",
|
||||||
|
}, status=HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
ErrPluginTypeNotFound = web.json_response({
|
@property
|
||||||
"error": "Given plugin type not found",
|
def instance_not_found(self) -> web.Response:
|
||||||
"errcode": "plugin_type_not_found",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.NOT_FOUND)
|
"error": "Plugin instance not found",
|
||||||
|
"errcode": "instance_not_found",
|
||||||
|
}, status=HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
ErrPathNotFound = web.json_response({
|
@property
|
||||||
"error": "Resource not found",
|
def plugin_type_not_found(self) -> web.Response:
|
||||||
"errcode": "resource_not_found",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.NOT_FOUND)
|
"error": "Given plugin type not found",
|
||||||
|
"errcode": "plugin_type_not_found",
|
||||||
|
}, status=HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
ErrMethodNotAllowed = web.json_response({
|
@property
|
||||||
"error": "Method not allowed",
|
def path_not_found(self) -> web.Response:
|
||||||
"errcode": "method_not_allowed",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.METHOD_NOT_ALLOWED)
|
"error": "Resource not found",
|
||||||
|
"errcode": "resource_not_found",
|
||||||
|
}, status=HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
ErrUserExists = web.json_response({
|
@property
|
||||||
"error": "There is already a client with the user ID of that token",
|
def method_not_allowed(self) -> web.Response:
|
||||||
"errcode": "user_exists",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.CONFLICT)
|
"error": "Method not allowed",
|
||||||
|
"errcode": "method_not_allowed",
|
||||||
|
}, status=HTTPStatus.METHOD_NOT_ALLOWED)
|
||||||
|
|
||||||
ErrPluginInUse = web.json_response({
|
@property
|
||||||
"error": "Plugin instances of this type still exist",
|
def user_exists(self) -> web.Response:
|
||||||
"errcode": "plugin_in_use",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.PRECONDITION_FAILED)
|
"error": "There is already a client with the user ID of that token",
|
||||||
|
"errcode": "user_exists",
|
||||||
|
}, status=HTTPStatus.CONFLICT)
|
||||||
|
|
||||||
ErrClientInUse = web.json_response({
|
@property
|
||||||
"error": "Plugin instances with this client as their primary user still exist",
|
def plugin_in_use(self) -> web.Response:
|
||||||
"errcode": "client_in_use",
|
return web.json_response({
|
||||||
}, status=HTTPStatus.PRECONDITION_FAILED)
|
"error": "Plugin instances of this type still exist",
|
||||||
|
"errcode": "plugin_in_use",
|
||||||
|
}, status=HTTPStatus.PRECONDITION_FAILED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_in_use(self) -> web.Response:
|
||||||
|
return web.json_response({
|
||||||
|
"error": "Plugin instances with this client as their primary user still exist",
|
||||||
|
"errcode": "client_in_use",
|
||||||
|
}, status=HTTPStatus.PRECONDITION_FAILED)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def plugin_import_error(error: str, stacktrace: str) -> web.Response:
|
||||||
|
return web.json_response({
|
||||||
|
"error": error,
|
||||||
|
"stacktrace": stacktrace,
|
||||||
|
"errcode": "plugin_invalid",
|
||||||
|
}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def plugin_reload_error(error: str, stacktrace: str) -> web.Response:
|
||||||
|
return web.json_response({
|
||||||
|
"error": error,
|
||||||
|
"stacktrace": stacktrace,
|
||||||
|
"errcode": "plugin_reload_fail",
|
||||||
|
}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def internal_server_error(self) -> web.Response:
|
||||||
|
return web.json_response({
|
||||||
|
"error": "Internal server error",
|
||||||
|
"errcode": "internal_server_error",
|
||||||
|
}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unsupported_plugin_loader(self) -> web.Response:
|
||||||
|
return web.json_response({
|
||||||
|
"error": "Existing plugin with same ID uses unsupported plugin loader",
|
||||||
|
"errcode": "unsupported_plugin_loader",
|
||||||
|
}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def not_implemented(self) -> web.Response:
|
||||||
|
return web.json_response({
|
||||||
|
"error": "Not implemented",
|
||||||
|
"errcode": "not_implemented",
|
||||||
|
}, status=HTTPStatus.NOT_IMPLEMENTED)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ok(self) -> web.Response:
|
||||||
|
return web.json_response({
|
||||||
|
"success": True,
|
||||||
|
}, status=HTTPStatus.OK)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def deleted(self) -> web.Response:
|
||||||
|
return web.Response(status=HTTPStatus.NO_CONTENT)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def found(data: dict) -> web.Response:
|
||||||
|
return web.json_response(data, status=HTTPStatus.OK)
|
||||||
|
|
||||||
|
def updated(self, data: dict) -> web.Response:
|
||||||
|
return self.found(data)
|
||||||
|
|
||||||
|
def logged_in(self, token: str) -> web.Response:
|
||||||
|
return self.found({
|
||||||
|
"token": token,
|
||||||
|
})
|
||||||
|
|
||||||
|
def pong(self, user: str) -> web.Response:
|
||||||
|
return self.found({
|
||||||
|
"username": user,
|
||||||
|
})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def created(data: dict) -> web.Response:
|
||||||
|
return web.json_response(data, status=HTTPStatus.CREATED)
|
||||||
|
|
||||||
|
|
||||||
def plugin_import_error(error: str, stacktrace: str) -> web.Response:
|
resp = _Response()
|
||||||
return web.json_response({
|
|
||||||
"error": error,
|
|
||||||
"stacktrace": stacktrace,
|
|
||||||
"errcode": "plugin_invalid",
|
|
||||||
}, status=HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
|
|
||||||
def plugin_reload_error(error: str, stacktrace: str) -> web.Response:
|
|
||||||
return web.json_response({
|
|
||||||
"error": error,
|
|
||||||
"stacktrace": stacktrace,
|
|
||||||
"errcode": "plugin_reload_fail",
|
|
||||||
}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
|
|
||||||
|
|
||||||
ErrUnsupportedPluginLoader = web.json_response({
|
|
||||||
"error": "Existing plugin with same ID uses unsupported plugin loader",
|
|
||||||
"errcode": "unsupported_plugin_loader",
|
|
||||||
}, status=HTTPStatus.BAD_REQUEST)
|
|
||||||
|
|
||||||
ErrNotImplemented = web.json_response({
|
|
||||||
"error": "Not implemented",
|
|
||||||
"errcode": "not_implemented",
|
|
||||||
}, status=HTTPStatus.NOT_IMPLEMENTED)
|
|
||||||
|
|
||||||
RespOK = web.json_response({
|
|
||||||
"success": True,
|
|
||||||
}, status=HTTPStatus.OK)
|
|
||||||
|
|
||||||
RespDeleted = web.Response(status=HTTPStatus.NO_CONTENT)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user