[misc] Migrate to TortoiseORM
This commit is contained in:
parent
767c92000b
commit
72f66896ee
|
@ -34,6 +34,7 @@ install_requires =
|
||||||
pytoml
|
pytoml
|
||||||
requests
|
requests
|
||||||
scrypt
|
scrypt
|
||||||
|
tortoise
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = src
|
where = src
|
||||||
|
@ -52,19 +53,18 @@ skip_install = True
|
||||||
deps =
|
deps =
|
||||||
black
|
black
|
||||||
flake8
|
flake8
|
||||||
mypy
|
|
||||||
types-requests
|
|
||||||
commands =
|
commands =
|
||||||
black --check --diff src tests
|
black --check --diff src tests
|
||||||
flake8 src tests
|
flake8 src tests
|
||||||
# not yet
|
|
||||||
#mypy --ignore-missing-imports src tests
|
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
deps =
|
||||||
|
mypy
|
||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
pytest-asyncio
|
||||||
|
types-requests
|
||||||
commands =
|
commands =
|
||||||
|
mypy --ignore-missing-imports src tests
|
||||||
pytest tests
|
pytest tests
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
|
|
|
@ -4,17 +4,13 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from nacl.secret import SecretBox
|
|
||||||
from nacl.utils import random
|
|
||||||
|
|
||||||
"""Default configuration.
|
"""Default configuration.
|
||||||
|
|
||||||
The default configuration gets overwritten by a configuration file if one exists.
|
The default configuration gets overwritten by a configuration file if one exists.
|
||||||
"""
|
"""
|
||||||
config = {
|
config = {
|
||||||
"database_connection": "sqlite:////tmp/kibicara.sqlite",
|
"database_connection": "sqlite:///:memory:",
|
||||||
"frontend_url": "http://localhost:4200", # url of frontend, change in prod
|
"frontend_url": "http://localhost:4200", # url of frontend, change in prod
|
||||||
"secret": random(SecretBox.KEY_SIZE).hex(), # generate with: openssl rand -hex 32
|
|
||||||
# production params
|
# production params
|
||||||
"frontend_path": None, # required, path to frontend html/css/js files
|
"frontend_path": None, # required, path to frontend html/css/js files
|
||||||
"production": True,
|
"production": True,
|
||||||
|
|
|
@ -16,9 +16,9 @@ from fastapi.staticfiles import StaticFiles
|
||||||
from hypercorn.asyncio import serve
|
from hypercorn.asyncio import serve
|
||||||
from hypercorn.config import Config
|
from hypercorn.config import Config
|
||||||
from pytoml import load
|
from pytoml import load
|
||||||
|
from tortoise import Tortoise
|
||||||
|
|
||||||
from kibicara.config import config
|
from kibicara.config import config
|
||||||
from kibicara.model import Mapping
|
|
||||||
from kibicara.platformapi import Spawner
|
from kibicara.platformapi import Spawner
|
||||||
from kibicara.webapi import router
|
from kibicara.webapi import router
|
||||||
|
|
||||||
|
@ -66,12 +66,17 @@ class Main:
|
||||||
format="%(asctime)s %(name)s %(message)s",
|
format="%(asctime)s %(name)s %(message)s",
|
||||||
)
|
)
|
||||||
getLogger("aiosqlite").setLevel(WARNING)
|
getLogger("aiosqlite").setLevel(WARNING)
|
||||||
Mapping.create_all()
|
|
||||||
asyncio_run(self.__run())
|
asyncio_run(self.__run())
|
||||||
|
|
||||||
async def __run(self):
|
async def __run(self):
|
||||||
|
await Tortoise.init(
|
||||||
|
db_url=config["database_connection"],
|
||||||
|
modules={"models": ["kibicara.model", "kibicara.platforms.test.model"]},
|
||||||
|
)
|
||||||
|
await Tortoise.generate_schemas()
|
||||||
await Spawner.init_all()
|
await Spawner.init_all()
|
||||||
await self.__start_webserver()
|
await self.__start_webserver()
|
||||||
|
await Tortoise.close_connections()
|
||||||
|
|
||||||
async def __start_webserver(self):
|
async def __start_webserver(self):
|
||||||
class SinglePageApplication(StaticFiles):
|
class SinglePageApplication(StaticFiles):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||||
#
|
#
|
||||||
|
@ -6,69 +6,52 @@
|
||||||
|
|
||||||
"""ORM Models for core."""
|
"""ORM Models for core."""
|
||||||
|
|
||||||
from databases import Database
|
from tortoise import fields
|
||||||
from ormantic import Boolean, ForeignKey, Integer, Model, Text
|
from tortoise.models import Model
|
||||||
from sqlalchemy import MetaData, create_engine
|
|
||||||
|
|
||||||
from kibicara.config import config
|
|
||||||
|
|
||||||
|
|
||||||
class Mapping:
|
|
||||||
database = Database(config["database_connection"])
|
|
||||||
metadata = MetaData()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_all(cls):
|
|
||||||
engine = create_engine(str(cls.database.url))
|
|
||||||
cls.metadata.create_all(engine)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def drop_all(cls):
|
|
||||||
engine = create_engine(str(cls.database.url))
|
|
||||||
cls.metadata.drop_all(engine)
|
|
||||||
|
|
||||||
|
|
||||||
class Admin(Model):
|
class Admin(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
email: Text(unique=True)
|
email = fields.TextField(unique=True)
|
||||||
passhash: Text()
|
passhash = fields.TextField()
|
||||||
|
hoods: fields.ManyToManyRelation["Hood"] = fields.ManyToManyField(
|
||||||
|
"models.Hood", related_name="admins", through="admin_hood_relations"
|
||||||
|
)
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "admins"
|
table = "admins"
|
||||||
|
|
||||||
|
|
||||||
class Hood(Model):
|
class Hood(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
name: Text(unique=True)
|
name = fields.TextField(unique=True)
|
||||||
landingpage: Text()
|
landingpage = fields.TextField()
|
||||||
email_enabled: Boolean() = True
|
email_enabled = fields.BooleanField(default=True)
|
||||||
|
admins: fields.ManyToManyRelation[Admin]
|
||||||
|
include_patterns: fields.ReverseRelation["IncludePattern"]
|
||||||
|
exclude_patterns: fields.ReverseRelation["ExcludePattern"]
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "hoods"
|
table = "hoods"
|
||||||
|
|
||||||
|
|
||||||
class AdminHoodRelation(Model):
|
class IncludePattern(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
admin: ForeignKey(Admin)
|
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
|
||||||
hood: ForeignKey(Hood)
|
"models.Hood", related_name="include_patterns"
|
||||||
|
)
|
||||||
|
pattern = fields.TextField()
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "admin_hood_relations"
|
table = "include_patterns"
|
||||||
|
|
||||||
|
|
||||||
class Trigger(Model):
|
class ExcludePattern(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
hood: ForeignKey(Hood)
|
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
|
||||||
pattern: Text()
|
"models.Hood", related_name="exclude_patterns"
|
||||||
|
)
|
||||||
|
pattern = fields.TextField()
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "triggers"
|
table = "exclude_patterns"
|
||||||
|
|
||||||
|
|
||||||
class BadWord(Model):
|
|
||||||
id: Integer(primary_key=True) = None
|
|
||||||
hood: ForeignKey(Hood)
|
|
||||||
pattern: Text()
|
|
||||||
|
|
||||||
class Mapping(Mapping):
|
|
||||||
table_name = "badwords"
|
|
||||||
|
|
|
@ -6,12 +6,15 @@
|
||||||
|
|
||||||
"""API classes for implementing bots for platforms."""
|
"""API classes for implementing bots for platforms."""
|
||||||
|
|
||||||
from asyncio import Queue, create_task
|
from asyncio import Queue, Task, create_task
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from re import IGNORECASE, search
|
from re import IGNORECASE, search
|
||||||
|
from typing import Generic, Optional, Type, TypeVar
|
||||||
|
|
||||||
from kibicara.model import BadWord, Trigger
|
from tortoise.models import Model
|
||||||
|
|
||||||
|
from kibicara.model import ExcludePattern, Hood, IncludePattern
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -29,7 +32,7 @@ class Message:
|
||||||
**kwargs (object, optional): Other platform-specific data.
|
**kwargs (object, optional): Other platform-specific data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, text: str, **kwargs):
|
def __init__(self, text: str, **kwargs) -> None:
|
||||||
self.text = text
|
self.text = text
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
@ -73,11 +76,11 @@ class Censor:
|
||||||
|
|
||||||
__instances: dict[int, list["Censor"]] = {}
|
__instances: dict[int, list["Censor"]] = {}
|
||||||
|
|
||||||
def __init__(self, hood):
|
def __init__(self, hood: Hood) -> None:
|
||||||
self.hood = hood
|
self.hood = hood
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
self._inbox = Queue()
|
self._inbox: Queue[Message] = Queue()
|
||||||
self.__task = None
|
self.__task: Optional[Task[None]] = None
|
||||||
self.__hood_censors = self.__instances.setdefault(hood.id, [])
|
self.__hood_censors = self.__instances.setdefault(hood.id, [])
|
||||||
self.__hood_censors.append(self)
|
self.__hood_censors.append(self)
|
||||||
self.status = BotStatus.INSTANTIATED
|
self.status = BotStatus.INSTANTIATED
|
||||||
|
@ -93,7 +96,8 @@ class Censor:
|
||||||
self.__task.cancel()
|
self.__task.cancel()
|
||||||
|
|
||||||
async def __run(self) -> None:
|
async def __run(self) -> None:
|
||||||
await self.hood.load()
|
assert self.__task is not None
|
||||||
|
await self.hood.refresh_from_db()
|
||||||
self.__task.set_name("{0} {1}".format(self.__class__.__name__, self.hood.name))
|
self.__task.set_name("{0} {1}".format(self.__class__.__name__, self.hood.name))
|
||||||
try:
|
try:
|
||||||
self.status = BotStatus.RUNNING
|
self.status = BotStatus.RUNNING
|
||||||
|
@ -112,7 +116,7 @@ class Censor:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def destroy_hood(cls, hood) -> None:
|
async def destroy_hood(cls, hood: Hood) -> None:
|
||||||
"""Remove all of its database entries.
|
"""Remove all of its database entries.
|
||||||
|
|
||||||
Note: Override this in the derived bot class.
|
Note: Override this in the derived bot class.
|
||||||
|
@ -140,25 +144,29 @@ class Censor:
|
||||||
return await self._inbox.get()
|
return await self._inbox.get()
|
||||||
|
|
||||||
async def __is_appropriate(self, message: Message) -> bool:
|
async def __is_appropriate(self, message: Message) -> bool:
|
||||||
for badword in await BadWord.objects.filter(hood=self.hood).all():
|
for exclude in await ExcludePattern.filter(hood=self.hood):
|
||||||
if search(badword.pattern, message.text, IGNORECASE):
|
if search(exclude.pattern, message.text, IGNORECASE):
|
||||||
logger.debug(
|
logger.info(
|
||||||
"Matched bad word - dropped message: {0}".format(message.text)
|
"Matched bad word - dropped message: {0}".format(message.text)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
for trigger in await Trigger.objects.filter(hood=self.hood).all():
|
for include in await IncludePattern.filter(hood=self.hood):
|
||||||
if search(trigger.pattern, message.text, IGNORECASE):
|
if search(include.pattern, message.text, IGNORECASE):
|
||||||
logger.debug(
|
logger.info(
|
||||||
"Matched trigger - passed message: {0}".format(message.text)
|
"Matched trigger - passed message: {0}".format(message.text)
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
logger.debug(
|
logger.info(
|
||||||
"Did not match any trigger - dropped message: {0}".format(message.text)
|
"Did not match any trigger - dropped message: {0}".format(message.text)
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Spawner:
|
ORMClass = TypeVar("ORMClass", bound=Model)
|
||||||
|
BotClass = TypeVar("BotClass", bound=Censor)
|
||||||
|
|
||||||
|
|
||||||
|
class Spawner(Generic[ORMClass, BotClass]):
|
||||||
"""Spawns a bot with a specific bot model.
|
"""Spawns a bot with a specific bot model.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
@ -179,10 +187,10 @@ class Spawner:
|
||||||
|
|
||||||
__instances: list["Spawner"] = []
|
__instances: list["Spawner"] = []
|
||||||
|
|
||||||
def __init__(self, ORMClass, BotClass):
|
def __init__(self, orm_class: Type[ORMClass], bot_class: Type[BotClass]) -> None:
|
||||||
self.ORMClass = ORMClass
|
self.ORMClass = orm_class
|
||||||
self.BotClass = BotClass
|
self.BotClass = bot_class
|
||||||
self.__bots = {}
|
self.__bots: dict[int, BotClass] = {}
|
||||||
self.__instances.append(self)
|
self.__instances.append(self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -192,7 +200,7 @@ class Spawner:
|
||||||
await spawner._init()
|
await spawner._init()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def destroy_hood(cls, hood) -> None:
|
async def destroy_hood(cls, hood: Hood) -> None:
|
||||||
for spawner in cls.__instances:
|
for spawner in cls.__instances:
|
||||||
for pk in list(spawner.__bots):
|
for pk in list(spawner.__bots):
|
||||||
bot = spawner.__bots[pk]
|
bot = spawner.__bots[pk]
|
||||||
|
@ -202,10 +210,10 @@ class Spawner:
|
||||||
await spawner.BotClass.destroy_hood(hood)
|
await spawner.BotClass.destroy_hood(hood)
|
||||||
|
|
||||||
async def _init(self) -> None:
|
async def _init(self) -> None:
|
||||||
for item in await self.ORMClass.objects.all():
|
async for item in self.ORMClass.all():
|
||||||
self.start(item)
|
self.start(item)
|
||||||
|
|
||||||
def start(self, item) -> None:
|
def start(self, item: ORMClass) -> None:
|
||||||
"""Instantiate and start a bot with the provided ORM object.
|
"""Instantiate and start a bot with the provided ORM object.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
@ -221,7 +229,7 @@ class Spawner:
|
||||||
if bot.enabled:
|
if bot.enabled:
|
||||||
bot.start()
|
bot.start()
|
||||||
|
|
||||||
def stop(self, item) -> None:
|
def stop(self, item: ORMClass) -> None:
|
||||||
"""Stop and delete a bot.
|
"""Stop and delete a bot.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
@ -231,10 +239,10 @@ class Spawner:
|
||||||
if bot is not None:
|
if bot is not None:
|
||||||
bot.stop()
|
bot.stop()
|
||||||
|
|
||||||
def get(self, item) -> Censor:
|
def get(self, item: ORMClass) -> BotClass:
|
||||||
"""Get a running bot.
|
"""Get a running bot.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
item (ORM Model object): ORM object corresponding to bot.
|
item (ORM Model object): ORM object corresponding to bot.
|
||||||
"""
|
"""
|
||||||
return self.__bots.get(item.pk)
|
return self.__bots[item.pk]
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from kibicara.platformapi import Censor, Spawner
|
from kibicara.platformapi import Censor, Message, Spawner
|
||||||
from kibicara.platforms.test.model import Test
|
from kibicara.platforms.test.model import Test
|
||||||
|
|
||||||
|
|
||||||
class TestBot(Censor):
|
class TestBot(Censor):
|
||||||
def __init__(self, test):
|
def __init__(self, test: Test):
|
||||||
super().__init__(test.hood)
|
super().__init__(test.hood)
|
||||||
self.messages = []
|
self.messages: list[Message] = []
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
while True:
|
while True:
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from ormantic import ForeignKey, Integer, Model
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
|
||||||
from kibicara.model import Hood, Mapping
|
from kibicara.model import Hood
|
||||||
|
|
||||||
|
|
||||||
class Test(Model):
|
class Test(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
hood: ForeignKey(Hood)
|
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
|
||||||
|
"models.Hood", related_name="platforms_test"
|
||||||
|
)
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "testapi"
|
table_name = "platforms_test"
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from sqlite3 import IntegrityError
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||||
from ormantic.exceptions import NoMatch
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
|
from kibicara.model import Hood
|
||||||
from kibicara.platformapi import Message
|
from kibicara.platformapi import Message
|
||||||
from kibicara.platforms.test.bot import spawner
|
from kibicara.platforms.test.bot import spawner
|
||||||
from kibicara.platforms.test.model import Test
|
from kibicara.platforms.test.model import Test
|
||||||
|
@ -20,10 +19,10 @@ class BodyMessage(BaseModel):
|
||||||
text: str
|
text: str
|
||||||
|
|
||||||
|
|
||||||
async def get_test(test_id: int, hood=Depends(get_hood)):
|
async def get_test(test_id: int, hood: Hood = Depends(get_hood)) -> Test:
|
||||||
try:
|
try:
|
||||||
return await Test.objects.get(id=test_id, hood=hood)
|
return await Test.get(id=test_id, hood=hood)
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,14 +30,14 @@ router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def test_read_all(hood=Depends(get_hood)):
|
async def test_read_all(hood: Hood = Depends(get_hood)):
|
||||||
return await Test.objects.filter(hood=hood).all()
|
return await Test.filter(hood=hood)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/", status_code=status.HTTP_201_CREATED)
|
@router.post("/", status_code=status.HTTP_201_CREATED)
|
||||||
async def test_create(response: Response, hood=Depends(get_hood)):
|
async def test_create(response: Response, hood: Hood = Depends(get_hood)):
|
||||||
try:
|
try:
|
||||||
test = await Test.objects.create(hood=hood)
|
test = await Test.create(hood=hood)
|
||||||
spawner.start(test)
|
spawner.start(test)
|
||||||
response.headers["Location"] = str(test.id)
|
response.headers["Location"] = str(test.id)
|
||||||
return test
|
return test
|
||||||
|
@ -47,22 +46,22 @@ async def test_create(response: Response, hood=Depends(get_hood)):
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{test_id}")
|
@router.get("/{test_id}")
|
||||||
async def test_read(test=Depends(get_test)):
|
async def test_read(test: Test = Depends(get_test)):
|
||||||
return test
|
return test
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{test_id}", status_code=status.HTTP_204_NO_CONTENT)
|
@router.delete("/{test_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def test_delete(test=Depends(get_test)):
|
async def test_delete(test: Test = Depends(get_test)):
|
||||||
spawner.stop(test)
|
spawner.stop(test)
|
||||||
await test.delete()
|
await test.delete()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{test_id}/messages/")
|
@router.get("/{test_id}/messages/")
|
||||||
async def test_message_read_all(test=Depends(get_test)):
|
async def test_message_read_all(test: Test = Depends(get_test)):
|
||||||
return spawner.get(test).messages
|
return spawner.get(test).messages
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{test_id}/messages/")
|
@router.post("/{test_id}/messages/")
|
||||||
async def test_message_create(message: BodyMessage, test=Depends(get_test)):
|
async def test_message_create(message: BodyMessage, test: Test = Depends(get_test)):
|
||||||
await spawner.get(test).publish(Message(message.text))
|
await spawner.get(test).publish(Message(message.text))
|
||||||
return {}
|
return {}
|
||||||
|
|
|
@ -11,20 +11,20 @@ from datetime import datetime, timedelta
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from pickle import dumps, loads
|
from pickle import dumps, loads
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
from sqlite3 import IntegrityError
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from nacl.encoding import URLSafeBase64Encoder
|
from nacl.encoding import URLSafeBase64Encoder
|
||||||
from nacl.exceptions import CryptoError
|
from nacl.exceptions import CryptoError
|
||||||
from nacl.secret import SecretBox
|
from nacl.secret import SecretBox
|
||||||
from ormantic.exceptions import NoMatch
|
from nacl.utils import random
|
||||||
from passlib.hash import argon2
|
from passlib.hash import argon2
|
||||||
from pydantic import BaseModel, validator
|
from pydantic import BaseModel, validator
|
||||||
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
from kibicara import email
|
from kibicara import email
|
||||||
from kibicara.config import config
|
from kibicara.config import config
|
||||||
from kibicara.model import Admin, AdminHoodRelation, Hood
|
from kibicara.model import Admin, Hood
|
||||||
from kibicara.webapi.utils import delete_hood
|
from kibicara.webapi.utils import delete_hood
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
@ -54,41 +54,42 @@ class BodyAccessToken(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login")
|
||||||
secret_box = SecretBox(bytes.fromhex(config["secret"]))
|
secret_box = SecretBox(
|
||||||
|
bytes.fromhex(str(config.get("secret", random(SecretBox.KEY_SIZE).hex())))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def to_token(**kwargs):
|
def to_token(**kwargs) -> str:
|
||||||
return secret_box.encrypt(dumps(kwargs), encoder=URLSafeBase64Encoder).decode(
|
return secret_box.encrypt(dumps(kwargs), encoder=URLSafeBase64Encoder).decode(
|
||||||
"ascii"
|
"ascii"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def from_token(token):
|
def from_token(token: str) -> dict:
|
||||||
return loads(
|
return loads(
|
||||||
secret_box.decrypt(token.encode("ascii"), encoder=URLSafeBase64Encoder)
|
secret_box.decrypt(token.encode("ascii"), encoder=URLSafeBase64Encoder)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_auth(email, password):
|
async def get_auth(email: str, password: str) -> Admin:
|
||||||
try:
|
try:
|
||||||
admin = await Admin.objects.get(email=email)
|
admin = await Admin.get(email=email)
|
||||||
if argon2.verify(password, admin.passhash):
|
if argon2.verify(password, admin.passhash):
|
||||||
return admin
|
return admin
|
||||||
raise ValueError
|
raise ValueError
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
|
||||||
|
|
||||||
async def get_admin(access_token=Depends(oauth2_scheme)):
|
async def get_admin(access_token: str = Depends(oauth2_scheme)) -> Admin:
|
||||||
try:
|
try:
|
||||||
admin = await get_auth(**from_token(access_token))
|
return await get_auth(**from_token(access_token))
|
||||||
except (CryptoError, ValueError):
|
except (CryptoError, ValueError):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid authentication credentials",
|
detail="Invalid authentication credentials",
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
return admin
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
@ -109,9 +110,9 @@ async def admin_register(values: BodyAdmin):
|
||||||
register_token = to_token(**values.__dict__)
|
register_token = to_token(**values.__dict__)
|
||||||
logger.debug("register_token={0}".format(register_token))
|
logger.debug("register_token={0}".format(register_token))
|
||||||
try:
|
try:
|
||||||
admin = await Admin.objects.filter(email=values.email).all()
|
if await Admin.exists(email=values.email):
|
||||||
if admin:
|
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
# link goes to frontend. this is not the confirm API endpoint below!
|
||||||
body = "{0}/confirm?token={1}".format(config["frontend_url"], register_token)
|
body = "{0}/confirm?token={1}".format(config["frontend_url"], register_token)
|
||||||
logger.debug(body)
|
logger.debug(body)
|
||||||
email.send_email(
|
email.send_email(
|
||||||
|
@ -138,7 +139,8 @@ async def admin_confirm(register_token: str):
|
||||||
try:
|
try:
|
||||||
values = from_token(register_token)
|
values = from_token(register_token)
|
||||||
passhash = argon2.hash(values["password"])
|
passhash = argon2.hash(values["password"])
|
||||||
await Admin.objects.create(email=values["email"], passhash=passhash)
|
await Admin.create(email=values["email"], passhash=passhash)
|
||||||
|
# XXX login and registration tokens are exchangeable. does this hurt?
|
||||||
return BodyAccessToken(access_token=register_token)
|
return BodyAccessToken(access_token=register_token)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
@ -178,14 +180,14 @@ async def admin_reset_password(values: BodyEmail):
|
||||||
- **email**: E-Mail Address of new hood admin
|
- **email**: E-Mail Address of new hood admin
|
||||||
- **password**: Password of new hood admin
|
- **password**: Password of new hood admin
|
||||||
"""
|
"""
|
||||||
register_token = to_token(datetime=datetime.now().isoformat(), **values.__dict__)
|
reset_token = to_token(datetime=datetime.now().isoformat(), **values.__dict__)
|
||||||
logger.debug("register_token={0}".format(register_token))
|
logger.debug("reset_token={0}".format(reset_token))
|
||||||
try:
|
try:
|
||||||
admin = await Admin.objects.filter(email=values.email).all()
|
if await Admin.exists(email=values.email):
|
||||||
if not admin:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
# link goes to frontend. this is not the reset API endpoint below!
|
||||||
body = "{0}/password-reset?token={1}".format(
|
body = "{0}/password-reset?token={1}".format(
|
||||||
config["frontend_url"], register_token
|
config["frontend_url"], reset_token
|
||||||
)
|
)
|
||||||
logger.debug(body)
|
logger.debug(body)
|
||||||
email.send_email(
|
email.send_email(
|
||||||
|
@ -213,11 +215,10 @@ async def admin_confirm_reset(reset_token: str, values: BodyPassword):
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=status.HTTP_410_GONE)
|
raise HTTPException(status_code=status.HTTP_410_GONE)
|
||||||
passhash = argon2.hash(values.password)
|
passhash = argon2.hash(values.password)
|
||||||
admins = await Admin.objects.filter(email=token_values["email"]).all()
|
await Admin.get(email=token_values["email"]).update(passhash=passhash)
|
||||||
if len(admins) != 1:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
||||||
await admins[0].update(passhash=passhash)
|
|
||||||
return BodyAccessToken(access_token=reset_token)
|
return BodyAccessToken(access_token=reset_token)
|
||||||
|
except DoesNotExist:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
except CryptoError:
|
except CryptoError:
|
||||||
|
@ -229,11 +230,9 @@ async def admin_confirm_reset(reset_token: str, values: BodyPassword):
|
||||||
# TODO response_model,
|
# TODO response_model,
|
||||||
operation_id="get_hoods_admin",
|
operation_id="get_hoods_admin",
|
||||||
)
|
)
|
||||||
async def admin_hood_read_all(admin=Depends(get_admin)):
|
async def admin_hood_read_all(admin: Admin = Depends(get_admin)):
|
||||||
"""Get a list of all hoods of a given admin."""
|
"""Get a list of all hoods of a given admin."""
|
||||||
return (
|
return await Hood.filter(admin=admin)
|
||||||
await AdminHoodRelation.objects.select_related("hood").filter(admin=admin).all()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
@ -241,12 +240,8 @@ async def admin_hood_read_all(admin=Depends(get_admin)):
|
||||||
# TODO response_model,
|
# TODO response_model,
|
||||||
operation_id="get_admin",
|
operation_id="get_admin",
|
||||||
)
|
)
|
||||||
async def admin_read(admin=Depends(get_admin)):
|
async def admin_read(admin: Admin = Depends(get_admin)):
|
||||||
"""Get a list of all hoods of a given admin."""
|
return BodyEmail(email=admin.email)
|
||||||
admin = await Admin.objects.filter(email=admin.email).all()
|
|
||||||
if len(admin) != 1:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
||||||
return BodyEmail(email=admin[0].email)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
|
@ -254,18 +249,10 @@ async def admin_read(admin=Depends(get_admin)):
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
operation_id="delete_admin",
|
operation_id="delete_admin",
|
||||||
)
|
)
|
||||||
async def admin_delete(admin=Depends(get_admin)):
|
async def admin_delete(admin: Admin = Depends(get_admin)):
|
||||||
hood_relations = (
|
async for hood in Hood.filter(admins__contains=admin):
|
||||||
await AdminHoodRelation.objects.select_related("hood").filter(admin=admin).all()
|
await hood.admins.remove(admin)
|
||||||
)
|
await hood.fetch_related("admins")
|
||||||
for hood in hood_relations:
|
if len(hood.admins) == 0:
|
||||||
admins = (
|
await delete_hood(hood)
|
||||||
await AdminHoodRelation.objects.select_related("admin")
|
|
||||||
.filter(hood=hood.id)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
if len(admins) == 1 and admins[0].id == admin.id:
|
|
||||||
actual_hood = await Hood.objects.filter(id=hood.id).all()
|
|
||||||
await delete_hood(actual_hood[0])
|
|
||||||
await admin.delete()
|
await admin.delete()
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||||
#
|
#
|
||||||
|
@ -6,13 +6,11 @@
|
||||||
|
|
||||||
"""REST API Endpoints for managing hoods."""
|
"""REST API Endpoints for managing hoods."""
|
||||||
|
|
||||||
from sqlite3 import IntegrityError
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||||
from ormantic.exceptions import NoMatch
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
from kibicara.model import AdminHoodRelation, Hood, Trigger
|
from kibicara.model import Admin, Hood, IncludePattern
|
||||||
from kibicara.platforms.email.bot import spawner
|
from kibicara.platforms.email.bot import spawner
|
||||||
from kibicara.webapi.admin import get_admin
|
from kibicara.webapi.admin import get_admin
|
||||||
from kibicara.webapi.utils import delete_hood
|
from kibicara.webapi.utils import delete_hood
|
||||||
|
@ -20,28 +18,24 @@ from kibicara.webapi.utils import delete_hood
|
||||||
|
|
||||||
class BodyHood(BaseModel):
|
class BodyHood(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
landingpage: str = """
|
landingpage: str = "Default Landing Page"
|
||||||
Default Landing Page
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
async def get_hood_unauthorized(hood_id: int):
|
async def get_hood_unauthorized(hood_id: int) -> Hood:
|
||||||
try:
|
try:
|
||||||
hood = await Hood.objects.get(id=hood_id)
|
return await Hood.get(id=hood_id)
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
return hood
|
|
||||||
|
|
||||||
|
|
||||||
async def get_hood(hood=Depends(get_hood_unauthorized), admin=Depends(get_admin)):
|
async def get_hood(hood_id: int, admin: Admin = Depends(get_admin)) -> Hood:
|
||||||
try:
|
try:
|
||||||
await AdminHoodRelation.objects.get(admin=admin, hood=hood)
|
return await Hood.get(id=hood_id, admins__in=[admin])
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
)
|
)
|
||||||
return hood
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
@ -55,7 +49,7 @@ router = APIRouter()
|
||||||
)
|
)
|
||||||
async def hood_read_all():
|
async def hood_read_all():
|
||||||
"""Get all existing hoods."""
|
"""Get all existing hoods."""
|
||||||
return await Hood.objects.all()
|
return await Hood.all()
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
@ -65,7 +59,9 @@ async def hood_read_all():
|
||||||
operation_id="create_hood",
|
operation_id="create_hood",
|
||||||
tags=["hoods"],
|
tags=["hoods"],
|
||||||
)
|
)
|
||||||
async def hood_create(values: BodyHood, response: Response, admin=Depends(get_admin)):
|
async def hood_create(
|
||||||
|
values: BodyHood, response: Response, admin: Admin = Depends(get_admin)
|
||||||
|
):
|
||||||
"""Creates a hood.
|
"""Creates a hood.
|
||||||
|
|
||||||
- **name**: Name of the hood
|
- **name**: Name of the hood
|
||||||
|
@ -73,11 +69,11 @@ async def hood_create(values: BodyHood, response: Response, admin=Depends(get_ad
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
hood = await Hood.objects.create(**values.__dict__)
|
hood = await Hood.objects.create(**values.__dict__)
|
||||||
await AdminHoodRelation.objects.create(admin=admin.id, hood=hood.id)
|
await admin.hoods.add(hood)
|
||||||
spawner.start(hood)
|
spawner.start(hood)
|
||||||
|
|
||||||
# Initialize Triggers to match all
|
# Initialize Triggers to match all
|
||||||
await Trigger.objects.create(hood=hood, pattern=".")
|
await IncludePattern.create(hood=hood, pattern=".")
|
||||||
|
|
||||||
response.headers["Location"] = str(hood.id)
|
response.headers["Location"] = str(hood.id)
|
||||||
return hood
|
return hood
|
||||||
|
@ -91,25 +87,24 @@ async def hood_create(values: BodyHood, response: Response, admin=Depends(get_ad
|
||||||
operation_id="get_hood",
|
operation_id="get_hood",
|
||||||
tags=["hoods"],
|
tags=["hoods"],
|
||||||
)
|
)
|
||||||
async def hood_read(hood=Depends(get_hood_unauthorized)):
|
async def hood_read(hood: Hood = Depends(get_hood_unauthorized)):
|
||||||
"""Get hood with id **hood_id**."""
|
"""Get hood with id **hood_id**."""
|
||||||
return hood
|
return hood
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
"/{hood_id}",
|
"/{hood_id}",
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
|
||||||
operation_id="update_hood",
|
operation_id="update_hood",
|
||||||
tags=["hoods"],
|
tags=["hoods"],
|
||||||
)
|
)
|
||||||
async def hood_update(values: BodyHood, hood=Depends(get_hood)):
|
async def hood_update(values: BodyHood, hood: Hood = Depends(get_hood)):
|
||||||
"""Updates hood with id **hood_id**.
|
"""Updates hood with id **hood_id**.
|
||||||
|
|
||||||
- **name**: New name of the hood
|
- **name**: New name of the hood
|
||||||
- **landingpage**: New Markdown formatted description of the hood
|
- **landingpage**: New Markdown formatted description of the hood
|
||||||
"""
|
"""
|
||||||
await hood.update(**values.__dict__)
|
await hood.update(**values.__dict__)
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
return hood
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
|
@ -121,4 +116,3 @@ async def hood_update(values: BodyHood, hood=Depends(get_hood)):
|
||||||
async def hood_delete(hood=Depends(get_hood)):
|
async def hood_delete(hood=Depends(get_hood)):
|
||||||
"""Deletes hood with id **hood_id**."""
|
"""Deletes hood with id **hood_id**."""
|
||||||
await delete_hood(hood)
|
await delete_hood(hood)
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
|
||||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: 0BSD
|
|
||||||
|
|
||||||
"""REST API endpoints for managing badwords.
|
|
||||||
|
|
||||||
Provides API endpoints for adding, removing and reading regular expressions that block a
|
|
||||||
received message to be dropped by a censor. This provides a message filter customizable
|
|
||||||
by the hood admins.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from re import compile as regex_compile
|
|
||||||
from re import error as RegexError
|
|
||||||
from sqlite3 import IntegrityError
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|
||||||
from ormantic.exceptions import NoMatch
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from kibicara.model import BadWord
|
|
||||||
from kibicara.webapi.hoods import get_hood
|
|
||||||
|
|
||||||
|
|
||||||
class BodyBadWord(BaseModel):
|
|
||||||
pattern: str
|
|
||||||
|
|
||||||
|
|
||||||
async def get_badword(badword_id: int, hood=Depends(get_hood)):
|
|
||||||
try:
|
|
||||||
return await BadWord.objects.get(id=badword_id, hood=hood)
|
|
||||||
except NoMatch:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/",
|
|
||||||
# TODO response_model,
|
|
||||||
operation_id="get_badwords",
|
|
||||||
)
|
|
||||||
async def badword_read_all(hood=Depends(get_hood)):
|
|
||||||
"""Get all badwords of hood with id **hood_id**."""
|
|
||||||
return await BadWord.objects.filter(hood=hood).all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/",
|
|
||||||
status_code=status.HTTP_201_CREATED,
|
|
||||||
# TODO response_model,
|
|
||||||
operation_id="create_badword",
|
|
||||||
)
|
|
||||||
async def badword_create(
|
|
||||||
values: BodyBadWord, response: Response, hood=Depends(get_hood)
|
|
||||||
):
|
|
||||||
"""Creates a new badword for hood with id **hood_id**.
|
|
||||||
|
|
||||||
- **pattern**: Regular expression which is used to match a badword.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
regex_compile(values.pattern)
|
|
||||||
badword = await BadWord.objects.create(hood=hood, **values.__dict__)
|
|
||||||
response.headers["Location"] = str(badword.id)
|
|
||||||
return badword
|
|
||||||
except IntegrityError:
|
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
|
||||||
except RegexError:
|
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/{badword_id}",
|
|
||||||
# TODO response_model,
|
|
||||||
operation_id="get_badword",
|
|
||||||
)
|
|
||||||
async def badword_read(badword=Depends(get_badword)):
|
|
||||||
"""Reads badword with id **badword_id** for hood with id **hood_id**."""
|
|
||||||
return badword
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
"/{badword_id}",
|
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
|
||||||
operation_id="update_badword",
|
|
||||||
)
|
|
||||||
async def badword_update(values: BodyBadWord, badword=Depends(get_badword)):
|
|
||||||
"""Updates badword with id **badword_id** for hood with id **hood_id**.
|
|
||||||
|
|
||||||
- **pattern**: Regular expression which is used to match a badword
|
|
||||||
"""
|
|
||||||
await badword.update(**values.__dict__)
|
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/{badword_id}",
|
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
|
||||||
operation_id="delete_badword",
|
|
||||||
)
|
|
||||||
async def badword_delete(badword=Depends(get_badword)):
|
|
||||||
"""Deletes badword with id **badword_id** for hood with id **hood_id**."""
|
|
||||||
await badword.delete()
|
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
116
backend/src/kibicara/webapi/hoods/exclude_patterns.py
Normal file
116
backend/src/kibicara/webapi/hoods/exclude_patterns.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
|
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
"""REST API endpoints for managing exclude_patterns.
|
||||||
|
|
||||||
|
Provides API endpoints for adding, removing and reading regular expressions that block a
|
||||||
|
received message to be dropped by a censor. This provides a message filter customizable
|
||||||
|
by the hood admins.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from re import compile as regex_compile, error as RegexError
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
|
from kibicara.model import ExcludePattern, Hood
|
||||||
|
from kibicara.webapi.hoods import get_hood
|
||||||
|
|
||||||
|
|
||||||
|
class BodyExcludePattern(BaseModel):
|
||||||
|
pattern: str
|
||||||
|
|
||||||
|
|
||||||
|
async def get_exclude_pattern(
|
||||||
|
exclude_pattern_id: int, hood: Hood = Depends(get_hood)
|
||||||
|
) -> ExcludePattern:
|
||||||
|
try:
|
||||||
|
return await ExcludePattern.get(id=exclude_pattern_id, hood=hood)
|
||||||
|
except DoesNotExist:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
# TODO response_model,
|
||||||
|
operation_id="get_exclude_patterns",
|
||||||
|
)
|
||||||
|
async def exclude_pattern_read_all(hood: Hood = Depends(get_hood)):
|
||||||
|
"""Get all exclude_patterns of hood with id **hood_id**."""
|
||||||
|
return await ExcludePattern.filter(hood=hood)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
# TODO response_model,
|
||||||
|
operation_id="create_exclude_pattern",
|
||||||
|
)
|
||||||
|
async def exclude_pattern_create(
|
||||||
|
values: BodyExcludePattern, response: Response, hood: Hood = Depends(get_hood)
|
||||||
|
):
|
||||||
|
"""Creates a new exclude_pattern for hood with id **hood_id**.
|
||||||
|
|
||||||
|
- **pattern**: Regular expression which is used to match a exclude_pattern.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
regex_compile(values.pattern)
|
||||||
|
exclude_pattern = await ExcludePattern.create(hood=hood, **values.__dict__)
|
||||||
|
response.headers["Location"] = str(exclude_pattern.id)
|
||||||
|
return exclude_pattern
|
||||||
|
except IntegrityError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
except RegexError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{exclude_pattern_id}",
|
||||||
|
# TODO response_model,
|
||||||
|
operation_id="get_exclude_pattern",
|
||||||
|
)
|
||||||
|
async def exclude_pattern_read(
|
||||||
|
exclude_pattern: ExcludePattern = Depends(get_exclude_pattern),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Reads exclude_pattern with id **exclude_pattern_id** for hood with id **hood_id**.
|
||||||
|
"""
|
||||||
|
return exclude_pattern
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/{exclude_pattern_id}",
|
||||||
|
operation_id="update_exclude_pattern",
|
||||||
|
)
|
||||||
|
async def exclude_pattern_update(
|
||||||
|
values: BodyExcludePattern,
|
||||||
|
exclude_pattern: ExcludePattern = Depends(get_exclude_pattern),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Updates exclude_pattern with id **exclude_pattern_id** for hood with id **hood_id**.
|
||||||
|
|
||||||
|
- **pattern**: Regular expression which is used to match a exclude_pattern
|
||||||
|
"""
|
||||||
|
await exclude_pattern.update(**values.__dict__)
|
||||||
|
return exclude_pattern
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{exclude_pattern_id}",
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
operation_id="delete_exclude_pattern",
|
||||||
|
)
|
||||||
|
async def exclude_pattern_delete(
|
||||||
|
exclude_pattern: ExcludePattern = Depends(get_exclude_pattern),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Deletes exclude_pattern with id **exclude_pattern_id** for hood with id **hood_id**.
|
||||||
|
"""
|
||||||
|
await exclude_pattern.delete()
|
115
backend/src/kibicara/webapi/hoods/include_patterns.py
Normal file
115
backend/src/kibicara/webapi/hoods/include_patterns.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
|
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
|
"""REST API endpoints for managing include_patterns.
|
||||||
|
|
||||||
|
Provides API endpoints for adding, removing and reading regular expressions that allow a
|
||||||
|
message to be passed through by a censor. A published message must match one of these
|
||||||
|
regular expressions otherwise it gets dropped by the censor. This provides a message
|
||||||
|
filter customizable by the hood admins.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from re import compile as regex_compile, error as RegexError
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
|
from kibicara.model import IncludePattern, Hood
|
||||||
|
from kibicara.webapi.hoods import get_hood
|
||||||
|
|
||||||
|
|
||||||
|
class BodyIncludePattern(BaseModel):
|
||||||
|
pattern: str
|
||||||
|
|
||||||
|
|
||||||
|
async def get_include_pattern(include_pattern_id: int, hood: Hood = Depends(get_hood)):
|
||||||
|
try:
|
||||||
|
return await IncludePattern.get(id=include_pattern_id, hood=hood)
|
||||||
|
except DoesNotExist:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
# TODO response_model,
|
||||||
|
operation_id="get_include_patterns",
|
||||||
|
)
|
||||||
|
async def include_pattern_read_all(hood: Hood = Depends(get_hood)):
|
||||||
|
"""Get all include_patterns of hood with id **hood_id**."""
|
||||||
|
return await IncludePattern.filter(hood=hood)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
status_code=status.HTTP_201_CREATED,
|
||||||
|
# TODO response_model,
|
||||||
|
operation_id="create_include_pattern",
|
||||||
|
)
|
||||||
|
async def include_pattern_create(
|
||||||
|
values: BodyIncludePattern, response: Response, hood: Hood = Depends(get_hood)
|
||||||
|
):
|
||||||
|
"""Creates a new include_pattern for hood with id **hood_id**.
|
||||||
|
|
||||||
|
- **pattern**: Regular expression which is used to match a include_pattern.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
regex_compile(values.pattern)
|
||||||
|
include_pattern = await IncludePattern.create(hood=hood, **values.__dict__)
|
||||||
|
response.headers["Location"] = str(include_pattern.id)
|
||||||
|
return include_pattern
|
||||||
|
except IntegrityError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
except RegexError:
|
||||||
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{include_pattern_id}",
|
||||||
|
# TODO response_model,
|
||||||
|
operation_id="get_include_pattern",
|
||||||
|
)
|
||||||
|
async def include_pattern_read(
|
||||||
|
include_pattern: IncludePattern = Depends(get_include_pattern),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Reads include_pattern with id **include_pattern_id** for hood with id **hood_id**.
|
||||||
|
"""
|
||||||
|
return include_pattern
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/{include_pattern_id}",
|
||||||
|
operation_id="update_include_pattern",
|
||||||
|
)
|
||||||
|
async def include_pattern_update(
|
||||||
|
values: BodyIncludePattern,
|
||||||
|
include_pattern: IncludePattern = Depends(get_include_pattern),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Updates include_pattern with id **include_pattern_id** for hood with id **hood_id**.
|
||||||
|
|
||||||
|
- **pattern**: Regular expression which is used to match a include_pattern
|
||||||
|
"""
|
||||||
|
await include_pattern.update(**values.__dict__)
|
||||||
|
return include_pattern
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{include_pattern_id}",
|
||||||
|
status_code=status.HTTP_204_NO_CONTENT,
|
||||||
|
operation_id="delete_include_pattern",
|
||||||
|
)
|
||||||
|
async def include_pattern_delete(
|
||||||
|
include_pattern: IncludePattern = Depends(get_include_pattern),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Deletes include_pattern with id **include_pattern_id** for hood with id **hood_id**.
|
||||||
|
"""
|
||||||
|
await include_pattern.delete()
|
|
@ -1,107 +0,0 @@
|
||||||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
|
||||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: 0BSD
|
|
||||||
|
|
||||||
"""REST API endpoints for managing triggers.
|
|
||||||
|
|
||||||
Provides API endpoints for adding, removing and reading regular expressions that allow a
|
|
||||||
message to be passed through by a censor. A published message must match one of these
|
|
||||||
regular expressions otherwise it gets dropped by the censor. This provides a message
|
|
||||||
filter customizable by the hood admins.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from re import compile as regex_compile
|
|
||||||
from re import error as RegexError
|
|
||||||
from sqlite3 import IntegrityError
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
|
||||||
from ormantic.exceptions import NoMatch
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from kibicara.model import Trigger
|
|
||||||
from kibicara.webapi.hoods import get_hood
|
|
||||||
|
|
||||||
|
|
||||||
class BodyTrigger(BaseModel):
|
|
||||||
pattern: str
|
|
||||||
|
|
||||||
|
|
||||||
async def get_trigger(trigger_id: int, hood=Depends(get_hood)):
|
|
||||||
try:
|
|
||||||
return await Trigger.objects.get(id=trigger_id, hood=hood)
|
|
||||||
except NoMatch:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/",
|
|
||||||
# TODO response_model,
|
|
||||||
operation_id="get_triggers",
|
|
||||||
)
|
|
||||||
async def trigger_read_all(hood=Depends(get_hood)):
|
|
||||||
"""Get all triggers of hood with id **hood_id**."""
|
|
||||||
return await Trigger.objects.filter(hood=hood).all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/",
|
|
||||||
status_code=status.HTTP_201_CREATED,
|
|
||||||
# TODO response_model,
|
|
||||||
operation_id="create_trigger",
|
|
||||||
)
|
|
||||||
async def trigger_create(
|
|
||||||
values: BodyTrigger, response: Response, hood=Depends(get_hood)
|
|
||||||
):
|
|
||||||
"""Creates a new trigger for hood with id **hood_id**.
|
|
||||||
|
|
||||||
- **pattern**: Regular expression which is used to match a trigger.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
regex_compile(values.pattern)
|
|
||||||
trigger = await Trigger.objects.create(hood=hood, **values.__dict__)
|
|
||||||
response.headers["Location"] = str(trigger.id)
|
|
||||||
return trigger
|
|
||||||
except IntegrityError:
|
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
|
||||||
except RegexError:
|
|
||||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/{trigger_id}",
|
|
||||||
# TODO response_model,
|
|
||||||
operation_id="get_trigger",
|
|
||||||
)
|
|
||||||
async def trigger_read(trigger=Depends(get_trigger)):
|
|
||||||
"""Reads trigger with id **trigger_id** for hood with id **hood_id**."""
|
|
||||||
return trigger
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
"/{trigger_id}",
|
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
|
||||||
operation_id="update_trigger",
|
|
||||||
)
|
|
||||||
async def trigger_update(values: BodyTrigger, trigger=Depends(get_trigger)):
|
|
||||||
"""Updates trigger with id **trigger_id** for hood with id **hood_id**.
|
|
||||||
|
|
||||||
- **pattern**: Regular expression which is used to match a trigger
|
|
||||||
"""
|
|
||||||
await trigger.update(**values.__dict__)
|
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/{trigger_id}",
|
|
||||||
status_code=status.HTTP_204_NO_CONTENT,
|
|
||||||
operation_id="delete_trigger",
|
|
||||||
)
|
|
||||||
async def trigger_delete(trigger=Depends(get_trigger)):
|
|
||||||
"""Deletes trigger with id **trigger_id** for hood with id **hood_id**."""
|
|
||||||
await trigger.delete()
|
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
|
|
@ -1,17 +1,14 @@
|
||||||
|
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from kibicara.model import AdminHoodRelation, BadWord, Trigger
|
from kibicara.model import IncludePattern, ExcludePattern
|
||||||
from kibicara.platformapi import Spawner
|
from kibicara.platformapi import Spawner
|
||||||
|
|
||||||
|
|
||||||
async def delete_hood(hood):
|
async def delete_hood(hood):
|
||||||
await Spawner.destroy_hood(hood)
|
await Spawner.destroy_hood(hood)
|
||||||
for trigger in await Trigger.objects.filter(hood=hood).all():
|
await IncludePattern.filter(hood=hood).delete()
|
||||||
await trigger.delete()
|
await ExcludePattern.filter(hood=hood).delete()
|
||||||
for badword in await BadWord.objects.filter(hood=hood).all():
|
|
||||||
await badword.delete()
|
|
||||||
for relation in await AdminHoodRelation.objects.filter(hood=hood).all():
|
|
||||||
await relation.delete()
|
|
||||||
await hood.delete()
|
await hood.delete()
|
||||||
|
|
Loading…
Reference in a new issue