[misc] Migrate to TortoiseORM
This commit is contained in:
parent
c468543fa5
commit
65a4211bcd
|
@ -26,16 +26,15 @@ install_requires =
|
||||||
fastapi
|
fastapi
|
||||||
httpx
|
httpx
|
||||||
hypercorn
|
hypercorn
|
||||||
ormantic @ https://github.com/dl6tom/ormantic/tarball/master#egg=ormantic-0.0.32
|
Mastodon.py
|
||||||
passlib
|
passlib
|
||||||
peony-twitter[all]
|
peony-twitter[all]
|
||||||
|
pydantic[email]
|
||||||
pynacl
|
pynacl
|
||||||
python-multipart
|
python-multipart
|
||||||
pytoml
|
pytoml
|
||||||
requests
|
requests
|
||||||
scrypt
|
tortoise-orm
|
||||||
Mastodon.py
|
|
||||||
pydantic[email]
|
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = src
|
where = src
|
||||||
|
@ -54,19 +53,19 @@ 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 =
|
||||||
|
# not yet
|
||||||
|
#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,26 @@ 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.email.model",
|
||||||
|
"kibicara.platforms.mastodon.model",
|
||||||
|
"kibicara.platforms.telegram.model",
|
||||||
|
"kibicara.platforms.test.model",
|
||||||
|
"kibicara.platforms.twitter.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.CharField(64, 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.CharField(64, 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,15 +210,15 @@ 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:
|
||||||
```
|
```
|
||||||
xyz = await XYZ.objects.create(hood=hood, **values.__dict__)
|
xyz = await XYZ.create(hood=hood, **values.__dict__)
|
||||||
spawner.start(xyz)
|
spawner.start(xyz)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
|
|
@ -12,7 +12,7 @@ from kibicara import email
|
||||||
from kibicara.config import config
|
from kibicara.config import config
|
||||||
from kibicara.model import Hood
|
from kibicara.model import Hood
|
||||||
from kibicara.platformapi import Censor, Spawner
|
from kibicara.platformapi import Censor, Spawner
|
||||||
from kibicara.platforms.email.model import Email, EmailSubscribers
|
from kibicara.platforms.email.model import Email, EmailSubscriber
|
||||||
from kibicara.webapi.admin import to_token
|
from kibicara.webapi.admin import to_token
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
@ -26,9 +26,9 @@ class EmailBot(Censor):
|
||||||
@classmethod
|
@classmethod
|
||||||
async def destroy_hood(cls, hood):
|
async def destroy_hood(cls, hood):
|
||||||
"""Removes all its database entries."""
|
"""Removes all its database entries."""
|
||||||
for inbox in await Email.objects.filter(hood=hood).all():
|
for inbox in await Email.filter(hood=hood).all():
|
||||||
await inbox.delete()
|
await inbox.delete()
|
||||||
for subscriber in await EmailSubscribers.objects.filter(hood=hood).all():
|
for subscriber in await EmailSubscriber.filter(hood=hood).all():
|
||||||
await subscriber.delete()
|
await subscriber.delete()
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
|
@ -40,9 +40,7 @@ class EmailBot(Censor):
|
||||||
self.hood.name, message.text
|
self.hood.name, message.text
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for subscriber in await EmailSubscribers.objects.filter(
|
for subscriber in await EmailSubscriber.filter(hood=self.hood).all():
|
||||||
hood=self.hood
|
|
||||||
).all():
|
|
||||||
token = to_token(email=subscriber.email, hood=self.hood.id)
|
token = to_token(email=subscriber.email, hood=self.hood.id)
|
||||||
body = (
|
body = (
|
||||||
"{0}\n\n--\n"
|
"{0}\n\n--\n"
|
||||||
|
|
|
@ -20,7 +20,7 @@ from pytoml import load
|
||||||
from requests import post
|
from requests import post
|
||||||
|
|
||||||
from kibicara.config import config
|
from kibicara.config import config
|
||||||
from kibicara.platforms.email.model import Email, EmailSubscribers
|
from kibicara.platforms.email.model import Email, EmailSubscriber
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class Main:
|
||||||
|
|
||||||
async def __run(self, email_name):
|
async def __run(self, email_name):
|
||||||
try:
|
try:
|
||||||
email = await Email.objects.get(name=email_name)
|
email = await Email.get(name=email_name)
|
||||||
except NoMatch:
|
except NoMatch:
|
||||||
logger.error("No recipient with this name")
|
logger.error("No recipient with this name")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
@ -75,7 +75,7 @@ class Main:
|
||||||
logger.error("Could not parse sender")
|
logger.error("Could not parse sender")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
maybe_subscriber = await EmailSubscribers.objects.filter(email=sender).all()
|
maybe_subscriber = await EmailSubscriber.filter(email=sender).all()
|
||||||
if len(maybe_subscriber) != 1 or maybe_subscriber[0].hood.id != email.hood.id:
|
if len(maybe_subscriber) != 1 or maybe_subscriber[0].hood.id != email.hood.id:
|
||||||
logger.error("Not a subscriber")
|
logger.error("Not a subscriber")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
|
@ -1,33 +1,38 @@
|
||||||
# Copyright (C) 2020 by Maike <maike@systemli.org>
|
# Copyright (C) 2020 by Maike <maike@systemli.org>
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
# 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, Text
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
|
||||||
from kibicara.model import Hood, Mapping
|
from kibicara.model import Hood
|
||||||
|
|
||||||
|
|
||||||
class Email(Model):
|
class Email(Model):
|
||||||
"""This table is used to track the names. It also stores the token secret."""
|
"""This table is used to track the names. It also stores the token secret."""
|
||||||
|
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
hood: ForeignKey(Hood)
|
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
|
||||||
name: Text(unique=True)
|
"models.Hood", related_name="platforms_email", unique=True
|
||||||
secret: Text()
|
)
|
||||||
|
name = fields.CharField(32, unique=True)
|
||||||
|
secret = fields.TextField()
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "email"
|
table = "platforms_email"
|
||||||
|
|
||||||
|
|
||||||
class EmailSubscribers(Model):
|
class EmailSubscriber(Model):
|
||||||
"""This table stores all subscribers, who want to receive messages via email."""
|
"""This table stores all subscribers, who want to receive messages via email."""
|
||||||
|
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
hood: ForeignKey(Hood)
|
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
|
||||||
email: Text(unique=True)
|
"models.Hood", related_name="platforms_email_subscribers"
|
||||||
|
)
|
||||||
|
email = fields.CharField(64, unique=True)
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "email_subscribers"
|
table = "platforms_email_subscribers"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Copyright (C) 2020 by Maike <maike@systemli.org>
|
# Copyright (C) 2020 by Maike <maike@systemli.org>
|
||||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||||
# 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
|
||||||
|
@ -8,18 +8,18 @@
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from os import urandom
|
from os import urandom
|
||||||
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, Response, status
|
||||||
from nacl import exceptions
|
from nacl import exceptions
|
||||||
from ormantic.exceptions import NoMatch
|
|
||||||
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 Hood
|
||||||
from kibicara.platformapi import Message
|
from kibicara.platformapi import Message
|
||||||
from kibicara.platforms.email.bot import spawner
|
from kibicara.platforms.email.bot import spawner
|
||||||
from kibicara.platforms.email.model import Email, EmailSubscribers
|
from kibicara.platforms.email.model import Email, EmailSubscriber
|
||||||
from kibicara.webapi.admin import from_token, to_token
|
from kibicara.webapi.admin import from_token, to_token
|
||||||
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
|
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class BodySubscriber(BaseModel):
|
||||||
email: str
|
email: str
|
||||||
|
|
||||||
|
|
||||||
async def get_email(email_id: int, hood=Depends(get_hood)):
|
async def get_email(email_id: int, hood: Hood = Depends(get_hood)):
|
||||||
"""Get Email row by hood.
|
"""Get Email row by hood.
|
||||||
|
|
||||||
You can specify an email_id to nail it down, but it works without as well.
|
You can specify an email_id to nail it down, but it works without as well.
|
||||||
|
@ -62,16 +62,16 @@ async def get_email(email_id: int, hood=Depends(get_hood)):
|
||||||
:return: Email row of the found email bot.
|
:return: Email row of the found email bot.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return await Email.objects.get(id=email_id, hood=hood)
|
return await Email.get(id=email_id, hood=hood)
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
async def get_subscriber(subscriber_id: int, hood=Depends(get_hood)):
|
async def get_subscriber(subscriber_id: int, hood: Hood = Depends(get_hood)):
|
||||||
try:
|
try:
|
||||||
return await EmailSubscribers.objects.get(id=subscriber_id, hood=hood)
|
return await EmailSubscriber.get(id=subscriber_id, hood=hood)
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
# registers the routes, gets imported in /kibicara/webapi/__init__.py
|
# registers the routes, gets imported in /kibicara/webapi/__init__.py
|
||||||
|
@ -83,9 +83,9 @@ router = APIRouter()
|
||||||
# TODO response_model
|
# TODO response_model
|
||||||
operation_id="get_emails_public",
|
operation_id="get_emails_public",
|
||||||
)
|
)
|
||||||
async def email_read_all_public(hood=Depends(get_hood_unauthorized)):
|
async def email_read_all_public(hood: Hood = Depends(get_hood_unauthorized)):
|
||||||
if hood.email_enabled:
|
if hood.email_enabled:
|
||||||
emails = await Email.objects.filter(hood=hood).all()
|
emails = await Email.filter(hood=hood)
|
||||||
return [BodyEmailPublic(name=email.name) for email in emails]
|
return [BodyEmailPublic(name=email.name) for email in emails]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -95,8 +95,8 @@ async def email_read_all_public(hood=Depends(get_hood_unauthorized)):
|
||||||
# TODO response_model
|
# TODO response_model
|
||||||
operation_id="get_emails",
|
operation_id="get_emails",
|
||||||
)
|
)
|
||||||
async def email_read_all(hood=Depends(get_hood)):
|
async def email_read_all(hood: Hood = Depends(get_hood)):
|
||||||
return await Email.objects.filter(hood=hood).select_related("hood").all()
|
return await Email.filter(hood=hood)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
@ -112,7 +112,7 @@ async def email_create(values: BodyEmail, response: Response, hood=Depends(get_h
|
||||||
:return: Email row of the new email bot.
|
:return: Email row of the new email bot.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
email = await Email.objects.create(
|
email = await Email.create(
|
||||||
hood=hood, secret=urandom(32).hex(), **values.__dict__
|
hood=hood, secret=urandom(32).hex(), **values.__dict__
|
||||||
)
|
)
|
||||||
response.headers["Location"] = str(hood.id)
|
response.headers["Location"] = str(hood.id)
|
||||||
|
@ -200,7 +200,7 @@ async def email_subscribe(
|
||||||
token,
|
token,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
subs = await EmailSubscribers.objects.filter(email=subscriber.email).all()
|
subs = await EmailSubscriber.filter(email=subscriber.email).all()
|
||||||
if subs:
|
if subs:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
email.send_email(
|
email.send_email(
|
||||||
|
@ -239,7 +239,7 @@ async def email_subscribe_confirm(token, hood=Depends(get_hood_unauthorized)):
|
||||||
if hood.id is not payload["hood"]:
|
if hood.id is not payload["hood"]:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
try:
|
try:
|
||||||
await EmailSubscribers.objects.create(hood=hood.id, email=payload["email"])
|
await EmailSubscriber.create(hood=hood, email=payload["email"])
|
||||||
return {}
|
return {}
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
@ -262,12 +262,12 @@ async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)):
|
||||||
# If token.hood and url.hood are different, raise an error:
|
# If token.hood and url.hood are different, raise an error:
|
||||||
if hood.id is not payload["hood"]:
|
if hood.id is not payload["hood"]:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
subscriber = await EmailSubscribers.objects.filter(
|
subscriber = await EmailSubscriber.filter(
|
||||||
hood=payload["hood"], email=payload["email"]
|
hood=payload["hood"], email=payload["email"]
|
||||||
).get()
|
).get()
|
||||||
await subscriber.delete()
|
await subscriber.delete()
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
except exceptions.CryptoError:
|
except exceptions.CryptoError:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||||
|
@ -279,7 +279,7 @@ async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)):
|
||||||
operation_id="get_subscribers",
|
operation_id="get_subscribers",
|
||||||
)
|
)
|
||||||
async def subscribers_read_all(hood=Depends(get_hood)):
|
async def subscribers_read_all(hood=Depends(get_hood)):
|
||||||
return await EmailSubscribers.objects.filter(hood=hood).all()
|
return await EmailSubscriber.filter(hood=hood).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
@ -306,7 +306,7 @@ async def email_message_create(
|
||||||
:param hood: Hood the Email bot belongs to.
|
:param hood: Hood the Email bot belongs to.
|
||||||
:return: returns status code 201 if the message is accepted by the censor.
|
:return: returns status code 201 if the message is accepted by the censor.
|
||||||
"""
|
"""
|
||||||
for receiver in await Email.objects.filter(hood=hood).all():
|
for receiver in await Email.filter(hood=hood).all():
|
||||||
if message.secret == receiver.secret:
|
if message.secret == receiver.secret:
|
||||||
# pass message.text to bot.py
|
# pass message.text to bot.py
|
||||||
if await spawner.get(hood).publish(Message(message.text)):
|
if await spawner.get(hood).publish(Message(message.text)):
|
||||||
|
|
|
@ -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 asyncio import get_event_loop, sleep
|
from asyncio import gather, get_event_loop, sleep
|
||||||
from kibicara.platformapi import Censor, Spawner, Message
|
|
||||||
from kibicara.platforms.mastodon.model import MastodonAccount
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
import re
|
||||||
|
|
||||||
from mastodon import Mastodon, MastodonError
|
from mastodon import Mastodon, MastodonError
|
||||||
from asyncio import gather
|
|
||||||
import re
|
from kibicara.platformapi import Censor, Spawner, Message
|
||||||
|
from kibicara.platforms.mastodon.model import MastodonAccount
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class MastodonBot(Censor):
|
||||||
@classmethod
|
@classmethod
|
||||||
async def destroy_hood(cls, hood):
|
async def destroy_hood(cls, hood):
|
||||||
"""Removes all its database entries."""
|
"""Removes all its database entries."""
|
||||||
for mastodon in await MastodonAccount.objects.filter(hood=hood).all():
|
for mastodon in await MastodonAccount.filter(hood=hood).all():
|
||||||
await mastodon.delete()
|
await mastodon.delete()
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
|
|
|
@ -1,30 +1,36 @@
|
||||||
# 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, Text, Boolean, Model
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
|
||||||
from kibicara.model import Hood, Mapping
|
from kibicara.model import Hood
|
||||||
|
|
||||||
|
|
||||||
class MastodonInstance(Model):
|
class MastodonInstance(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
name: Text()
|
name = fields.TextField()
|
||||||
client_id: Text()
|
client_id = fields.TextField()
|
||||||
client_secret: Text()
|
client_secret = fields.TextField()
|
||||||
|
accounts: fields.ReverseRelation["MastodonAccount"]
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "mastodoninstances"
|
table = "platforms_mastodon_instances"
|
||||||
|
|
||||||
|
|
||||||
class MastodonAccount(Model):
|
class MastodonAccount(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
hood: ForeignKey(Hood)
|
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
|
||||||
instance: ForeignKey(MastodonInstance)
|
"models.Hood", related_name="platforms_mastodon"
|
||||||
access_token: Text()
|
)
|
||||||
username: Text(allow_null=True) = None
|
instance: fields.ForeignKeyRelation[MastodonInstance] = fields.ForeignKeyField(
|
||||||
enabled: Boolean() = False
|
"models.MastodonInstance", related_name="accounts"
|
||||||
|
)
|
||||||
|
access_token = fields.TextField()
|
||||||
|
username = fields.TextField(null=True)
|
||||||
|
enabled = fields.BooleanField()
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "mastodonaccounts"
|
table = "platforms_mastodon_accounts"
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
# 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>
|
||||||
|
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from asyncio import get_event_loop
|
from asyncio import get_event_loop
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||||
from ormantic.exceptions import NoMatch
|
from mastodon import Mastodon, MastodonNetworkError
|
||||||
|
from mastodon.errors import MastodonIllegalArgumentError
|
||||||
from pydantic import BaseModel, validate_email, validator
|
from pydantic import BaseModel, validate_email, validator
|
||||||
from sqlite3 import IntegrityError
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
from kibicara.config import config
|
from kibicara.config import config
|
||||||
|
from kibicara.model import Hood
|
||||||
from kibicara.platforms.mastodon.bot import spawner
|
from kibicara.platforms.mastodon.bot import spawner
|
||||||
from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
|
from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
|
||||||
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
|
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
|
||||||
|
|
||||||
from mastodon import Mastodon, MastodonNetworkError
|
|
||||||
from mastodon.errors import MastodonIllegalArgumentError
|
|
||||||
|
|
||||||
from logging import getLogger
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,10 +37,12 @@ class BodyMastodonAccount(BaseModel):
|
||||||
return validate_email(value)
|
return validate_email(value)
|
||||||
|
|
||||||
|
|
||||||
async def get_mastodon(mastodon_id, hood=Depends(get_hood)):
|
async def get_mastodon(
|
||||||
|
mastodon_id: int, hood: Hood = Depends(get_hood)
|
||||||
|
) -> MastodonAccount:
|
||||||
try:
|
try:
|
||||||
return await MastodonAccount.objects.get(id=mastodon_id, hood=hood)
|
return await MastodonAccount.get(id=mastodon_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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,16 +53,16 @@ async def get_mastodon_instance(instance_url: str) -> MastodonInstance:
|
||||||
:return the MastodonInstance ORM object
|
:return the MastodonInstance ORM object
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return await MastodonInstance.objects.get(name=instance_url)
|
return await MastodonInstance.get(name=instance_url)
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
app_name = config.get("frontend_url")
|
app_name = config.get("frontend_url")
|
||||||
client_id, client_secret = Mastodon.create_app(
|
client_id, client_secret = Mastodon.create_app(
|
||||||
app_name, api_base_url=instance_url
|
app_name, api_base_url=instance_url
|
||||||
)
|
)
|
||||||
await MastodonInstance.objects.create(
|
await MastodonInstance.create(
|
||||||
name=instance_url, client_id=client_id, client_secret=client_secret
|
name=instance_url, client_id=client_id, client_secret=client_secret
|
||||||
)
|
)
|
||||||
return await MastodonInstance.objects.get(name=instance_url)
|
return await MastodonInstance.get(name=instance_url)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
@ -73,13 +75,11 @@ twitter_callback_router = APIRouter()
|
||||||
operation_id="get_mastodons_public",
|
operation_id="get_mastodons_public",
|
||||||
)
|
)
|
||||||
async def mastodon_read_all_public(hood=Depends(get_hood_unauthorized)):
|
async def mastodon_read_all_public(hood=Depends(get_hood_unauthorized)):
|
||||||
mastodonbots = await MastodonAccount.objects.filter(hood=hood).all()
|
|
||||||
mbots = []
|
mbots = []
|
||||||
for mbot in mastodonbots:
|
async for mbot in MastodonAccount.filter(hood=hood).prefetch_related("instance"):
|
||||||
if mbot.enabled == 1 and mbot.username:
|
if mbot.enabled == 1 and mbot.username:
|
||||||
instance = await MastodonInstance.objects.get(id=mbot.instance)
|
|
||||||
mbots.append(
|
mbots.append(
|
||||||
BodyMastodonPublic(username=mbot.username, instance=instance.name)
|
BodyMastodonPublic(username=mbot.username, instance=mbot.instance.name)
|
||||||
)
|
)
|
||||||
return mbots
|
return mbots
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ async def mastodon_read_all_public(hood=Depends(get_hood_unauthorized)):
|
||||||
operation_id="get_mastodons",
|
operation_id="get_mastodons",
|
||||||
)
|
)
|
||||||
async def mastodon_read_all(hood=Depends(get_hood)):
|
async def mastodon_read_all(hood=Depends(get_hood)):
|
||||||
return await MastodonAccount.objects.filter(hood=hood).all()
|
return await MastodonAccount.filter(hood=hood).all()
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
|
@ -101,8 +101,8 @@ async def mastodon_read_all(hood=Depends(get_hood)):
|
||||||
)
|
)
|
||||||
async def mastodon_delete(mastodon=Depends(get_mastodon)):
|
async def mastodon_delete(mastodon=Depends(get_mastodon)):
|
||||||
spawner.stop(mastodon)
|
spawner.stop(mastodon)
|
||||||
await mastodon.instance.load()
|
await mastodon.fetch_related("instance")
|
||||||
object_with_instance = await MastodonAccount.objects.filter(
|
object_with_instance = await MastodonAccount.filter(
|
||||||
instance=mastodon.instance
|
instance=mastodon.instance
|
||||||
).all()
|
).all()
|
||||||
if len(object_with_instance) == 1 and object_with_instance[0] == mastodon:
|
if len(object_with_instance) == 1 and object_with_instance[0] == mastodon:
|
||||||
|
@ -172,7 +172,7 @@ async def mastodon_create(values: BodyMastodonAccount, hood=Depends(get_hood)):
|
||||||
None, account.log_in, values.email, values.password
|
None, account.log_in, values.email, values.password
|
||||||
)
|
)
|
||||||
logger.debug(f"{access_token}")
|
logger.debug(f"{access_token}")
|
||||||
mastodon = await MastodonAccount.objects.create(
|
mastodon = await MastodonAccount.create(
|
||||||
hood=hood, instance=instance, access_token=access_token, enabled=True
|
hood=hood, instance=instance, access_token=access_token, enabled=True
|
||||||
)
|
)
|
||||||
spawner.start(mastodon)
|
spawner.start(mastodon)
|
||||||
|
@ -181,4 +181,5 @@ async def mastodon_create(values: BodyMastodonAccount, hood=Depends(get_hood)):
|
||||||
logger.warning("Login to Mastodon failed.", exc_info=True)
|
logger.warning("Login to Mastodon failed.", exc_info=True)
|
||||||
raise HTTPException(status_code=status.HTTP_422_INVALID_INPUT)
|
raise HTTPException(status_code=status.HTTP_422_INVALID_INPUT)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
|
logger.warning("Login to Mastodon failed.", exc_info=True)
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
|
|
|
@ -5,13 +5,12 @@
|
||||||
|
|
||||||
from asyncio import CancelledError, gather, sleep
|
from asyncio import CancelledError, gather, sleep
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlite3 import IntegrityError
|
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher, exceptions, types
|
from aiogram import Bot, Dispatcher, exceptions, types
|
||||||
from ormantic.exceptions import NoMatch
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
from kibicara.platformapi import Censor, Message, Spawner
|
from kibicara.platformapi import Censor, Message, Spawner
|
||||||
from kibicara.platforms.telegram.model import Telegram, TelegramUser
|
from kibicara.platforms.telegram.model import Telegram, TelegramSubscriber
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
@ -25,8 +24,8 @@ class TelegramBot(Censor):
|
||||||
@classmethod
|
@classmethod
|
||||||
async def destroy_hood(cls, hood):
|
async def destroy_hood(cls, hood):
|
||||||
"""Removes all its database entries."""
|
"""Removes all its database entries."""
|
||||||
for telegram in await Telegram.objects.filter(hood=hood).all():
|
for telegram in await Telegram.filter(hood=hood).all():
|
||||||
for user in await TelegramUser.objects.filter(bot=telegram).all():
|
for user in await TelegramSubscriber.filter(bot=telegram).all():
|
||||||
await user.delete()
|
await user.delete()
|
||||||
await telegram.delete()
|
await telegram.delete()
|
||||||
|
|
||||||
|
@ -69,9 +68,7 @@ class TelegramBot(Censor):
|
||||||
self.telegram_model.hood.name, message.text
|
self.telegram_model.hood.name, message.text
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for user in await TelegramUser.objects.filter(
|
for user in await TelegramSubscriber.filter(bot=self.telegram_model).all():
|
||||||
bot=self.telegram_model
|
|
||||||
).all():
|
|
||||||
await self._send_message(user.user_id, message.text)
|
await self._send_message(user.user_id, message.text)
|
||||||
|
|
||||||
async def _send_message(self, user_id, message):
|
async def _send_message(self, user_id, message):
|
||||||
|
@ -116,7 +113,7 @@ class TelegramBot(Censor):
|
||||||
if message.from_user.is_bot:
|
if message.from_user.is_bot:
|
||||||
await message.reply("Error: Bots can not join here.")
|
await message.reply("Error: Bots can not join here.")
|
||||||
return
|
return
|
||||||
await TelegramUser.objects.create(
|
await TelegramSubscriber.create(
|
||||||
user_id=message.from_user.id, bot=self.telegram_model
|
user_id=message.from_user.id, bot=self.telegram_model
|
||||||
)
|
)
|
||||||
await message.reply(self.telegram_model.welcome_message)
|
await message.reply(self.telegram_model.welcome_message)
|
||||||
|
@ -125,12 +122,12 @@ class TelegramBot(Censor):
|
||||||
|
|
||||||
async def _remove_user(self, message: types.Message):
|
async def _remove_user(self, message: types.Message):
|
||||||
try:
|
try:
|
||||||
telegram_user = await TelegramUser.objects.get(
|
telegram_user = await TelegramSubscriber.get(
|
||||||
user_id=message.from_user.id, bot=self.telegram_model
|
user_id=message.from_user.id, bot=self.telegram_model
|
||||||
)
|
)
|
||||||
await telegram_user.delete()
|
await telegram_user.delete()
|
||||||
await message.reply("You were removed successfully from this bot.")
|
await message.reply("You were removed successfully from this bot.")
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
await message.reply("Error: You are not subscribed to this bot.")
|
await message.reply("Error: You are not subscribed to this bot.")
|
||||||
|
|
||||||
async def _send_help(self, message: types.Message):
|
async def _send_help(self, message: types.Message):
|
||||||
|
|
|
@ -1,30 +1,36 @@
|
||||||
# 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>
|
||||||
|
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from ormantic import Boolean, ForeignKey, Integer, Model, Text
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
|
||||||
from kibicara.model import Hood, Mapping
|
from kibicara.model import Hood
|
||||||
|
|
||||||
|
|
||||||
class Telegram(Model):
|
class Telegram(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
hood: ForeignKey(Hood)
|
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
|
||||||
api_token: Text(unique=True)
|
"models.Hood", related_name="platforms_telegram"
|
||||||
welcome_message: Text()
|
)
|
||||||
username: Text(allow_null=True) = None
|
api_token = fields.CharField(64, unique=True)
|
||||||
enabled: Boolean() = True
|
welcome_message = fields.TextField()
|
||||||
|
username = fields.TextField(null=True)
|
||||||
|
enabled = fields.BooleanField(default=True)
|
||||||
|
subscribers: fields.ReverseRelation["TelegramSubscriber"]
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "telegrambots"
|
table = "platforms_telegram"
|
||||||
|
|
||||||
|
|
||||||
class TelegramUser(Model):
|
class TelegramSubscriber(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
user_id: Integer(unique=True)
|
bot: fields.ForeignKeyRelation[Telegram] = fields.ForeignKeyField(
|
||||||
# TODO unique
|
"models.Telegram", related_name="subscribers"
|
||||||
bot: ForeignKey(Telegram)
|
)
|
||||||
|
user_id = fields.IntField()
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "telegramusers"
|
table = "platforms_telegram_subscribers"
|
||||||
|
|
|
@ -4,16 +4,15 @@
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from sqlite3 import IntegrityError
|
|
||||||
|
|
||||||
from aiogram import exceptions
|
from aiogram import exceptions
|
||||||
from aiogram.bot.api import check_token
|
from aiogram.bot.api import check_token
|
||||||
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, validator
|
from pydantic import BaseModel, validator
|
||||||
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
from kibicara.platforms.telegram.bot import spawner
|
from kibicara.platforms.telegram.bot import spawner
|
||||||
from kibicara.platforms.telegram.model import Telegram, TelegramUser
|
from kibicara.platforms.telegram.model import Telegram, TelegramSubscriber
|
||||||
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
|
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
@ -38,8 +37,8 @@ class BodyTelegramPublic(BaseModel):
|
||||||
|
|
||||||
async def get_telegram(telegram_id: int, hood=Depends(get_hood)):
|
async def get_telegram(telegram_id: int, hood=Depends(get_hood)):
|
||||||
try:
|
try:
|
||||||
return await Telegram.objects.get(id=telegram_id, hood=hood)
|
return await Telegram.get(id=telegram_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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,7 +52,7 @@ telegram_callback_router = APIRouter()
|
||||||
operation_id="get_telegrams_public",
|
operation_id="get_telegrams_public",
|
||||||
)
|
)
|
||||||
async def telegram_read_all_public(hood=Depends(get_hood_unauthorized)):
|
async def telegram_read_all_public(hood=Depends(get_hood_unauthorized)):
|
||||||
telegrambots = await Telegram.objects.filter(hood=hood).all()
|
telegrambots = await Telegram.filter(hood=hood).all()
|
||||||
return [
|
return [
|
||||||
BodyTelegramPublic(username=telegrambot.username)
|
BodyTelegramPublic(username=telegrambot.username)
|
||||||
for telegrambot in telegrambots
|
for telegrambot in telegrambots
|
||||||
|
@ -67,7 +66,7 @@ async def telegram_read_all_public(hood=Depends(get_hood_unauthorized)):
|
||||||
operation_id="get_telegrams",
|
operation_id="get_telegrams",
|
||||||
)
|
)
|
||||||
async def telegram_read_all(hood=Depends(get_hood)):
|
async def telegram_read_all(hood=Depends(get_hood)):
|
||||||
return await Telegram.objects.filter(hood=hood).all()
|
return await Telegram.filter(hood=hood).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
@ -86,7 +85,7 @@ async def telegram_read(telegram=Depends(get_telegram)):
|
||||||
)
|
)
|
||||||
async def telegram_delete(telegram=Depends(get_telegram)):
|
async def telegram_delete(telegram=Depends(get_telegram)):
|
||||||
spawner.stop(telegram)
|
spawner.stop(telegram)
|
||||||
for user in await TelegramUser.objects.filter(bot=telegram).all():
|
for user in await TelegramSubscriber.filter(bot=telegram).all():
|
||||||
await user.delete()
|
await user.delete()
|
||||||
await telegram.delete()
|
await telegram.delete()
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||||
|
@ -102,7 +101,7 @@ async def telegram_create(
|
||||||
response: Response, values: BodyTelegram, hood=Depends(get_hood)
|
response: Response, values: BodyTelegram, hood=Depends(get_hood)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
telegram = await Telegram.objects.create(hood=hood, **values.__dict__)
|
telegram = await Telegram.create(hood=hood, **values.__dict__)
|
||||||
spawner.start(telegram)
|
spawner.start(telegram)
|
||||||
response.headers["Location"] = str(telegram.id)
|
response.headers["Location"] = str(telegram.id)
|
||||||
return telegram
|
return telegram
|
||||||
|
|
|
@ -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 = "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 {}
|
||||||
|
|
|
@ -27,7 +27,7 @@ class TwitterBot(Censor):
|
||||||
@classmethod
|
@classmethod
|
||||||
async def destroy_hood(cls, hood):
|
async def destroy_hood(cls, hood):
|
||||||
"""Removes all its database entries."""
|
"""Removes all its database entries."""
|
||||||
for twitter in await Twitter.objects.filter(hood=hood).all():
|
for twitter in await Twitter.filter(hood=hood).all():
|
||||||
await twitter.delete()
|
await twitter.delete()
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
|
|
|
@ -1,23 +1,27 @@
|
||||||
# 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>
|
||||||
|
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from ormantic import Boolean, ForeignKey, Integer, Model, Text
|
from tortoise import fields
|
||||||
|
from tortoise.models import Model
|
||||||
|
|
||||||
from kibicara.model import Hood, Mapping
|
from kibicara.model import Hood
|
||||||
|
|
||||||
|
|
||||||
class Twitter(Model):
|
class Twitter(Model):
|
||||||
id: Integer(primary_key=True) = None
|
id = fields.IntField(pk=True)
|
||||||
hood: ForeignKey(Hood)
|
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
|
||||||
dms_since_id: Integer(allow_null=True) = None
|
"models.Hood", related_name="platforms_twitter"
|
||||||
mentions_since_id: Integer(allow_null=True) = None
|
)
|
||||||
access_token: Text()
|
dms_since_id = fields.IntField()
|
||||||
access_token_secret: Text()
|
mentions_since_id = fields.IntField()
|
||||||
username: Text(allow_null=True) = None
|
access_token = fields.TextField()
|
||||||
verified: Boolean() = False
|
access_token_secret = fields.TextField()
|
||||||
enabled: Boolean() = False
|
username = fields.TextField()
|
||||||
|
verified = fields.BooleanField(default=False)
|
||||||
|
enabled = fields.BooleanField(default=False)
|
||||||
|
|
||||||
class Mapping(Mapping):
|
class Meta:
|
||||||
table_name = "twitterbots"
|
table = "platforms_twitter"
|
||||||
|
|
|
@ -4,13 +4,12 @@
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
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 peony.exceptions import NotAuthenticated
|
from peony.exceptions import NotAuthenticated
|
||||||
from peony.oauth_dance import get_access_token, get_oauth_token
|
from peony.oauth_dance import get_access_token, get_oauth_token
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from tortoise.exceptions import DoesNotExist, IntegrityError
|
||||||
|
|
||||||
from kibicara.config import config
|
from kibicara.config import config
|
||||||
from kibicara.platforms.twitter.bot import spawner
|
from kibicara.platforms.twitter.bot import spawner
|
||||||
|
@ -26,8 +25,8 @@ class BodyTwitterPublic(BaseModel):
|
||||||
|
|
||||||
async def get_twitter(twitter_id: int, hood=Depends(get_hood)):
|
async def get_twitter(twitter_id: int, hood=Depends(get_hood)):
|
||||||
try:
|
try:
|
||||||
return await Twitter.objects.get(id=twitter_id, hood=hood)
|
return await Twitter.get(id=twitter_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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,7 +40,7 @@ twitter_callback_router = APIRouter()
|
||||||
operation_id="get_twitters_public",
|
operation_id="get_twitters_public",
|
||||||
)
|
)
|
||||||
async def twitter_read_all_public(hood=Depends(get_hood_unauthorized)):
|
async def twitter_read_all_public(hood=Depends(get_hood_unauthorized)):
|
||||||
twitterbots = await Twitter.objects.filter(hood=hood).all()
|
twitterbots = await Twitter.filter(hood=hood).all()
|
||||||
return [
|
return [
|
||||||
BodyTwitterPublic(username=twitterbot.username)
|
BodyTwitterPublic(username=twitterbot.username)
|
||||||
for twitterbot in twitterbots
|
for twitterbot in twitterbots
|
||||||
|
@ -55,7 +54,7 @@ async def twitter_read_all_public(hood=Depends(get_hood_unauthorized)):
|
||||||
operation_id="get_twitters",
|
operation_id="get_twitters",
|
||||||
)
|
)
|
||||||
async def twitter_read_all(hood=Depends(get_hood)):
|
async def twitter_read_all(hood=Depends(get_hood)):
|
||||||
return await Twitter.objects.filter(hood=hood).all()
|
return await Twitter.filter(hood=hood).all()
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
@ -125,7 +124,7 @@ async def twitter_create(response: Response, hood=Depends(get_hood)):
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Purge Twitter corpses
|
# Purge Twitter corpses
|
||||||
for corpse in await Twitter.objects.filter(hood=hood, verified=False).all():
|
for corpse in await Twitter.filter(hood=hood, verified=False).all():
|
||||||
await corpse.delete()
|
await corpse.delete()
|
||||||
# Create Twitter
|
# Create Twitter
|
||||||
request_token = await get_oauth_token(
|
request_token = await get_oauth_token(
|
||||||
|
@ -137,7 +136,7 @@ async def twitter_create(response: Response, hood=Depends(get_hood)):
|
||||||
)
|
)
|
||||||
if request_token["oauth_callback_confirmed"] != "true":
|
if request_token["oauth_callback_confirmed"] != "true":
|
||||||
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
|
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||||
twitter = await Twitter.objects.create(
|
twitter = await Twitter.create(
|
||||||
hood=hood,
|
hood=hood,
|
||||||
access_token=request_token["oauth_token"],
|
access_token=request_token["oauth_token"],
|
||||||
access_token_secret=request_token["oauth_token_secret"],
|
access_token_secret=request_token["oauth_token_secret"],
|
||||||
|
@ -159,7 +158,7 @@ async def twitter_read_callback(
|
||||||
oauth_token: str, oauth_verifier: str, hood=Depends(get_hood)
|
oauth_token: str, oauth_verifier: str, hood=Depends(get_hood)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
twitter = await Twitter.objects.filter(access_token=oauth_token).get()
|
twitter = await Twitter.filter(access_token=oauth_token).get()
|
||||||
access_token = await get_access_token(
|
access_token = await get_access_token(
|
||||||
config["twitter"]["consumer_key"],
|
config["twitter"]["consumer_key"],
|
||||||
config["twitter"]["consumer_secret"],
|
config["twitter"]["consumer_secret"],
|
||||||
|
@ -167,7 +166,7 @@ async def twitter_read_callback(
|
||||||
twitter.access_token_secret,
|
twitter.access_token_secret,
|
||||||
oauth_verifier,
|
oauth_verifier,
|
||||||
)
|
)
|
||||||
await twitter.update(
|
await Twitter.filter(id=twitter).update(
|
||||||
access_token=access_token["oauth_token"],
|
access_token=access_token["oauth_token"],
|
||||||
access_token_secret=access_token["oauth_token_secret"],
|
access_token_secret=access_token["oauth_token_secret"],
|
||||||
verified=True,
|
verified=True,
|
||||||
|
@ -177,7 +176,7 @@ async def twitter_read_callback(
|
||||||
return {}
|
return {}
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||||
except NoMatch:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||||
except (KeyError, ValueError, NotAuthenticated):
|
except (KeyError, ValueError, NotAuthenticated):
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
|
@ -20,16 +20,20 @@ from kibicara.platforms.twitter.webapi import router as twitter_router
|
||||||
from kibicara.platforms.twitter.webapi import twitter_callback_router
|
from kibicara.platforms.twitter.webapi import twitter_callback_router
|
||||||
from kibicara.webapi.admin import router as admin_router
|
from kibicara.webapi.admin import router as admin_router
|
||||||
from kibicara.webapi.hoods import router as hoods_router
|
from kibicara.webapi.hoods import router as hoods_router
|
||||||
from kibicara.webapi.hoods.badwords import router as badwords_router
|
from kibicara.webapi.hoods.exclude_patterns import router as exclude_patterns_router
|
||||||
from kibicara.webapi.hoods.triggers import router as triggers_router
|
from kibicara.webapi.hoods.include_patterns import router as include_patterns_router
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
router.include_router(admin_router, prefix="/admin", tags=["admin"])
|
router.include_router(admin_router, prefix="/admin", tags=["admin"])
|
||||||
hoods_router.include_router(
|
hoods_router.include_router(
|
||||||
triggers_router, prefix="/{hood_id}/triggers", tags=["triggers"]
|
include_patterns_router,
|
||||||
|
prefix="/{hood_id}/triggers",
|
||||||
|
tags=["include_patterns"],
|
||||||
)
|
)
|
||||||
hoods_router.include_router(
|
hoods_router.include_router(
|
||||||
badwords_router, prefix="/{hood_id}/badwords", tags=["badwords"]
|
exclude_patterns_router,
|
||||||
|
prefix="/{hood_id}/badwords",
|
||||||
|
tags=["exclude_patterns"],
|
||||||
)
|
)
|
||||||
hoods_router.include_router(test_router, prefix="/{hood_id}/test", tags=["test"])
|
hoods_router.include_router(test_router, prefix="/{hood_id}/test", tags=["test"])
|
||||||
hoods_router.include_router(
|
hoods_router.include_router(
|
||||||
|
|
|
@ -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 Christian Hagenest <c.hagenest@pm.me>
|
# Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me>
|
||||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||||
|
@ -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.filter(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(admins=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,26 @@ 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(
|
||||||
try:
|
hood: Hood = Depends(get_hood_unauthorized), admin: Admin = Depends(get_admin)
|
||||||
await AdminHoodRelation.objects.get(admin=admin, hood=hood)
|
) -> Hood:
|
||||||
except NoMatch:
|
await hood.fetch_related("admins")
|
||||||
raise HTTPException(
|
if admin in hood.admins:
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
return hood
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
raise HTTPException(
|
||||||
)
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
return hood
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
@ -55,7 +51,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,19 +61,21 @@ 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
|
||||||
- **landingpage**: Markdown formatted description of the hood
|
- **landingpage**: Markdown formatted description of the hood
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
hood = await Hood.objects.create(**values.__dict__)
|
hood = await Hood.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 +89,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.filter(id=hood).update(**values.__dict__)
|
||||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
return hood
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
|
@ -121,4 +118,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 ExcludePattern.filter(id=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 IncludePattern.filter(id=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 ExcludePattern, Hood, IncludePattern
|
||||||
from kibicara.platformapi import Spawner
|
from kibicara.platformapi import Spawner
|
||||||
|
|
||||||
|
|
||||||
async def delete_hood(hood):
|
async def delete_hood(hood: Hood) -> None:
|
||||||
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()
|
||||||
|
|
|
@ -9,27 +9,41 @@ from urllib.parse import urlparse
|
||||||
from fastapi import FastAPI, status
|
from fastapi import FastAPI, status
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
import pytest
|
import pytest
|
||||||
|
from tortoise import Tortoise
|
||||||
|
|
||||||
from kibicara import email
|
from kibicara import email
|
||||||
from kibicara.model import Mapping
|
|
||||||
from kibicara.webapi import router
|
from kibicara.webapi import router
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="session")
|
||||||
def anyio_backend():
|
def anyio_backend():
|
||||||
return "asyncio"
|
return "asyncio"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="session")
|
||||||
def client():
|
@pytest.mark.anyio
|
||||||
Mapping.drop_all()
|
async def client():
|
||||||
Mapping.create_all()
|
await Tortoise.init(
|
||||||
|
db_url="sqlite://:memory:",
|
||||||
|
modules={
|
||||||
|
"models": [
|
||||||
|
"kibicara.model",
|
||||||
|
"kibicara.platforms.email.model",
|
||||||
|
"kibicara.platforms.mastodon.model",
|
||||||
|
"kibicara.platforms.telegram.model",
|
||||||
|
"kibicara.platforms.test.model",
|
||||||
|
"kibicara.platforms.twitter.model",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await Tortoise.generate_schemas()
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.include_router(router, prefix="/api")
|
app.include_router(router, prefix="/api")
|
||||||
return AsyncClient(app=app, base_url="http://test")
|
yield AsyncClient(app=app, base_url="http://test")
|
||||||
|
await Tortoise.close_connections()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="session")
|
||||||
def monkeymodule():
|
def monkeymodule():
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
|
|
||||||
|
@ -38,7 +52,7 @@ def monkeymodule():
|
||||||
mpatch.undo()
|
mpatch.undo()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="session")
|
||||||
def receive_email(monkeymodule):
|
def receive_email(monkeymodule):
|
||||||
mailbox = []
|
mailbox = []
|
||||||
|
|
||||||
|
@ -52,7 +66,7 @@ def receive_email(monkeymodule):
|
||||||
return mock_receive_email
|
return mock_receive_email
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="session")
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def register_token(client, receive_email):
|
async def register_token(client, receive_email):
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
|
@ -62,14 +76,14 @@ async def register_token(client, receive_email):
|
||||||
return urlparse(receive_email()["body"]).query.split("=", 1)[1]
|
return urlparse(receive_email()["body"]).query.split("=", 1)[1]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="session")
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def register_confirmed(client, register_token):
|
async def register_confirmed(client, register_token):
|
||||||
response = await client.post("/api/admin/confirm/{0}".format(register_token))
|
response = await client.post("/api/admin/confirm/{0}".format(register_token))
|
||||||
assert response.status_code == status.HTTP_200_OK
|
assert response.status_code == status.HTTP_200_OK
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="session")
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def access_token(client, register_confirmed):
|
async def access_token(client, register_confirmed):
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
|
@ -79,7 +93,7 @@ async def access_token(client, register_confirmed):
|
||||||
return response.json()["access_token"]
|
return response.json()["access_token"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="session")
|
||||||
def auth_header(access_token):
|
def auth_header(access_token):
|
||||||
return {"Authorization": "Bearer {0}".format(access_token)}
|
return {"Authorization": "Bearer {0}".format(access_token)}
|
||||||
|
|
||||||
|
|
0
backend/tests/tests_mastodon/__init__.py
Normal file
0
backend/tests/tests_mastodon/__init__.py
Normal file
|
@ -12,7 +12,7 @@ from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def mastodon_instance():
|
async def mastodon_instance():
|
||||||
return await MastodonInstance.objects.create(
|
return await MastodonInstance.create(
|
||||||
name="inst4nce",
|
name="inst4nce",
|
||||||
client_id="cl13nt_id",
|
client_id="cl13nt_id",
|
||||||
client_secret="cl13nt_s3cr3t",
|
client_secret="cl13nt_s3cr3t",
|
||||||
|
@ -22,8 +22,8 @@ async def mastodon_instance():
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def mastodon_account(hood_id, mastodon_instance):
|
async def mastodon_account(hood_id, mastodon_instance):
|
||||||
hood = await Hood.objects.get(id=hood_id)
|
hood = await Hood.get(id=hood_id)
|
||||||
return await MastodonAccount.objects.create(
|
return await MastodonAccount.create(
|
||||||
hood=hood,
|
hood=hood,
|
||||||
instance=mastodon_instance,
|
instance=mastodon_instance,
|
||||||
access_token="t0k3n",
|
access_token="t0k3n",
|
||||||
|
|
|
@ -8,7 +8,7 @@ import pytest
|
||||||
from mastodon.Mastodon import Mastodon
|
from mastodon.Mastodon import Mastodon
|
||||||
|
|
||||||
from kibicara.platforms import mastodon
|
from kibicara.platforms import mastodon
|
||||||
from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
|
from kibicara.platforms.mastodon.model import MastodonAccount
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
|
@ -52,19 +52,8 @@ async def test_mastodon_create_bot(
|
||||||
print(response.json())
|
print(response.json())
|
||||||
assert response.status_code == status.HTTP_201_CREATED
|
assert response.status_code == status.HTTP_201_CREATED
|
||||||
bot_id = response.json()["id"]
|
bot_id = response.json()["id"]
|
||||||
mastodon_obj = await MastodonAccount.objects.get(id=bot_id)
|
mastodon_obj = await MastodonAccount.get(id=bot_id)
|
||||||
assert response.json()["access_token"] == mastodon_obj.access_token
|
assert response.json()["access_token"] == mastodon_obj.access_token
|
||||||
mastodon_instance = await MastodonInstance.objects.get(id=mastodon_obj.instance.id)
|
|
||||||
assert (
|
|
||||||
response.json()["instance"]["name"]
|
|
||||||
== body["instance_url"]
|
|
||||||
== mastodon_instance.name
|
|
||||||
)
|
|
||||||
assert response.json()["hood"]["id"] == mastodon_obj.hood.id
|
|
||||||
assert response.json()["instance"]["client_id"] == mastodon_instance.client_id
|
|
||||||
assert (
|
|
||||||
response.json()["instance"]["client_secret"] == mastodon_instance.client_secret
|
|
||||||
)
|
|
||||||
assert mastodon_obj.enabled
|
assert mastodon_obj.enabled
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
from ormantic.exceptions import NoMatch
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from tortoise.exceptions import DoesNotExist
|
||||||
|
|
||||||
from kibicara.platforms.mastodon.model import MastodonAccount
|
from kibicara.platforms.mastodon.model import MastodonAccount
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ async def test_mastodon_delete_bot(client, mastodon_account, auth_header):
|
||||||
headers=auth_header,
|
headers=auth_header,
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
with pytest.raises(NoMatch):
|
with pytest.raises(DoesNotExist):
|
||||||
await MastodonAccount.objects.get(id=mastodon_account.id)
|
await MastodonAccount.get(id=mastodon_account.id)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
@ -36,7 +36,7 @@ async def test_mastodon_delete_bot_invalid_id(client, auth_header, hood_id):
|
||||||
response = await client.delete(
|
response = await client.delete(
|
||||||
"/api/hoods/{0}/mastodon/wrong".format(hood_id), headers=auth_header
|
"/api/hoods/{0}/mastodon/wrong".format(hood_id), headers=auth_header
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
@ -13,7 +13,7 @@ from kibicara.platforms.mastodon.model import MastodonAccount
|
||||||
async def test_mastodon_get_bots(
|
async def test_mastodon_get_bots(
|
||||||
client, auth_header, hood_id, mastodon_account, mastodon_instance
|
client, auth_header, hood_id, mastodon_account, mastodon_instance
|
||||||
):
|
):
|
||||||
mastodon2 = await MastodonAccount.objects.create(
|
mastodon2 = await MastodonAccount.create(
|
||||||
hood=mastodon_account.hood,
|
hood=mastodon_account.hood,
|
||||||
instance=mastodon_instance,
|
instance=mastodon_instance,
|
||||||
access_token="4cc3ss",
|
access_token="4cc3ss",
|
||||||
|
|
|
@ -12,8 +12,8 @@ from kibicara.platforms.telegram.model import Telegram
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def telegram(hood_id, bot):
|
async def telegram(hood_id, bot):
|
||||||
hood = await Hood.objects.get(id=hood_id)
|
hood = await Hood.get(id=hood_id)
|
||||||
return await Telegram.objects.create(
|
return await Telegram.create(
|
||||||
hood=hood,
|
hood=hood,
|
||||||
api_token=bot["api_token"],
|
api_token=bot["api_token"],
|
||||||
welcome_message=bot["welcome_message"],
|
welcome_message=bot["welcome_message"],
|
||||||
|
|
|
@ -41,14 +41,13 @@ async def test_telegram_create_bot(
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_201_CREATED
|
assert response.status_code == status.HTTP_201_CREATED
|
||||||
bot_id = response.json()["id"]
|
bot_id = response.json()["id"]
|
||||||
telegram_obj = await Telegram.objects.get(id=bot_id)
|
telegram_obj = await Telegram.get(id=bot_id)
|
||||||
assert response.json()["api_token"] == body["api_token"] == telegram_obj.api_token
|
assert response.json()["api_token"] == body["api_token"] == telegram_obj.api_token
|
||||||
assert (
|
assert (
|
||||||
response.json()["welcome_message"]
|
response.json()["welcome_message"]
|
||||||
== body["welcome_message"]
|
== body["welcome_message"]
|
||||||
== telegram_obj.welcome_message
|
== telegram_obj.welcome_message
|
||||||
)
|
)
|
||||||
assert response.json()["hood"]["id"] == telegram_obj.hood.id
|
|
||||||
assert telegram_obj.enabled
|
assert telegram_obj.enabled
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
# SPDX-License-Identifier: 0BSD
|
# SPDX-License-Identifier: 0BSD
|
||||||
|
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
from ormantic.exceptions import NoMatch
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from tortoise.exceptions import DoesNotExist
|
||||||
|
|
||||||
from kibicara.platforms.telegram.model import Telegram, TelegramUser
|
from kibicara.platforms.telegram.model import Telegram, TelegramSubscriber
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -15,17 +15,17 @@ from kibicara.platforms.telegram.model import Telegram, TelegramUser
|
||||||
)
|
)
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_telegram_delete_bot(client, bot, telegram, auth_header):
|
async def test_telegram_delete_bot(client, bot, telegram, auth_header):
|
||||||
await TelegramUser.objects.create(user_id=1234, bot=telegram.id)
|
await TelegramSubscriber.create(user_id=1234, bot=telegram)
|
||||||
await TelegramUser.objects.create(user_id=5678, bot=telegram.id)
|
await TelegramSubscriber.create(user_id=5678, bot=telegram)
|
||||||
response = await client.delete(
|
response = await client.delete(
|
||||||
"/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id),
|
"/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id),
|
||||||
headers=auth_header,
|
headers=auth_header,
|
||||||
)
|
)
|
||||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||||
with pytest.raises(NoMatch):
|
with pytest.raises(DoesNotExist):
|
||||||
await Telegram.objects.get(id=telegram.id)
|
await Telegram.get(id=telegram.id)
|
||||||
with pytest.raises(NoMatch):
|
with pytest.raises(DoesNotExist):
|
||||||
await TelegramUser.objects.get(id=telegram.id)
|
await TelegramSubscriber.get(id=telegram.id)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
@ -12,13 +12,13 @@ from kibicara.platforms.telegram.model import Telegram
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_telegram_get_bots(client, auth_header, hood_id):
|
async def test_telegram_get_bots(client, auth_header, hood_id):
|
||||||
hood = await Hood.objects.get(id=hood_id)
|
hood = await Hood.get(id=hood_id)
|
||||||
telegram0 = await Telegram.objects.create(
|
telegram0 = await Telegram.create(
|
||||||
hood=hood,
|
hood=hood,
|
||||||
api_token="api_token123",
|
api_token="api_token123",
|
||||||
welcome_message="welcome_message123",
|
welcome_message="welcome_message123",
|
||||||
)
|
)
|
||||||
telegram1 = await Telegram.objects.create(
|
telegram1 = await Telegram.create(
|
||||||
hood=hood,
|
hood=hood,
|
||||||
api_token="api_token456",
|
api_token="api_token456",
|
||||||
welcome_message="welcome_message123",
|
welcome_message="welcome_message123",
|
||||||
|
|
Loading…
Reference in a new issue