[doc] Add docstrings for core
This commit is contained in:
parent
a00a6b8497
commit
051dd062ac
|
@ -4,6 +4,19 @@
|
|||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
""" Configuration file and command line argument parser.
|
||||
|
||||
Gives a dictionary named `config` with configuration parsed either from
|
||||
`/etc/kibicara.conf` or from a file given by command line argument `-f`.
|
||||
If no configuration was found at all, the defaults are used.
|
||||
|
||||
Example:
|
||||
```
|
||||
from kibicara.config import config
|
||||
print(config)
|
||||
```
|
||||
"""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
from pytoml import load
|
||||
from sys import argv
|
||||
|
@ -14,6 +27,10 @@ config = {
|
|||
'frontend_path': None,
|
||||
'root_url': 'http://localhost:8000/',
|
||||
}
|
||||
""" Default configuration.
|
||||
|
||||
The default configuration gets overwritten by a configuration file if one exists.
|
||||
"""
|
||||
|
||||
if argv[0].endswith('kibicara'):
|
||||
parser = ArgumentParser()
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
""" E-Mail handling. """
|
||||
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from logging import getLogger
|
||||
|
@ -14,6 +16,22 @@ logger = getLogger(__name__)
|
|||
|
||||
|
||||
def send_email(to, subject, sender='kibicara', body=''):
|
||||
""" E-Mail sender.
|
||||
|
||||
Sends an E-Mail to a specified recipient with a body
|
||||
|
||||
Example:
|
||||
```
|
||||
from kibicara import email
|
||||
email.send_email('abc@de.fg', 'My Email subject', body='Hi this is a mail body.')
|
||||
```
|
||||
|
||||
Args:
|
||||
to (str): Recipients' e-mail address
|
||||
subject (str): The subject of the e-mail
|
||||
sender (str): optional, Sender of the e-mail
|
||||
body (str): The body of the e-mail
|
||||
"""
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = 'Kibicara <%s@%s>' % (sender, getfqdn())
|
||||
msg['To'] = to
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
""" Entrypoint of Kibicara. """
|
||||
|
||||
from asyncio import run as asyncio_run
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
@ -19,17 +21,23 @@ logger = getLogger(__name__)
|
|||
|
||||
|
||||
class Main:
|
||||
def __init__(self):
|
||||
asyncio_run(self.run())
|
||||
""" Entrypoint for Kibicara.
|
||||
|
||||
async def run(self):
|
||||
Initializes the platform bots and starts the hypercorn webserver serving the
|
||||
Kibicara application and the specified frontend on port 8000.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
asyncio_run(self.__run())
|
||||
|
||||
async def __run(self):
|
||||
basicConfig(level=DEBUG, format="%(asctime)s %(name)s %(message)s")
|
||||
getLogger('aiosqlite').setLevel(WARNING)
|
||||
Mapping.create_all()
|
||||
await Spawner.init_all()
|
||||
await self._start_webserver()
|
||||
await self.__start_webserver()
|
||||
|
||||
async def _start_webserver(self):
|
||||
async def __start_webserver(self):
|
||||
class SinglePageApplication(StaticFiles):
|
||||
async def get_response(self, path, scope):
|
||||
response = await super().get_response(path, scope)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
""" ORM Models for core. """
|
||||
|
||||
from databases import Database
|
||||
from kibicara.config import config
|
||||
from ormantic import Integer, ForeignKey, Model, Text
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
""" API classes for implementing bots for platforms. """
|
||||
|
||||
from asyncio import create_task, Queue
|
||||
from kibicara.model import BadWord, Trigger
|
||||
from logging import getLogger
|
||||
|
@ -13,47 +15,116 @@ logger = getLogger(__name__)
|
|||
|
||||
|
||||
class Message:
|
||||
"""The Message object that is send through the censor.
|
||||
|
||||
Examples:
|
||||
```
|
||||
message = Message('Message sent by a user from platform xyz', xyz_message_id=123)
|
||||
```
|
||||
|
||||
Args:
|
||||
text (str): The message text
|
||||
**kwargs (object, optional): Other platform-specific data.
|
||||
|
||||
Attributes:
|
||||
text (str): The message text
|
||||
**kwargs (object, optional): Other platform-specific data.
|
||||
"""
|
||||
|
||||
def __init__(self, text, **kwargs):
|
||||
self.text = text
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
class Censor:
|
||||
instances = {}
|
||||
""" The superclass for a platform bot.
|
||||
|
||||
The censor is the superclass for every platform bot. It distributes a message to all
|
||||
other bots from the same hood if it passes the message filter. It provides methods
|
||||
to start and stop the bot and an overwritable stub for a starting routine.
|
||||
|
||||
Examples:
|
||||
```
|
||||
class XYZPlatform(Censor):
|
||||
def __init__(self, xyz_model):
|
||||
super().__init__(xyz_model.hood)
|
||||
...
|
||||
async def run(self):
|
||||
await gather(self.poll(), self.push())
|
||||
...
|
||||
async def poll(self):
|
||||
while True:
|
||||
# XXX get text message from platform xyz
|
||||
await self.publish(Message(text))
|
||||
...
|
||||
async def push(self):
|
||||
while True:
|
||||
message = await self.receive()
|
||||
# XXX send message.text to platform xyz
|
||||
```
|
||||
|
||||
Args:
|
||||
hood (Hood): A Hood Model object
|
||||
|
||||
Attributes:
|
||||
hood (Hood): A Hood Model object
|
||||
"""
|
||||
|
||||
__instances = {}
|
||||
|
||||
def __init__(self, hood):
|
||||
self.hood = hood
|
||||
self.inbox = Queue()
|
||||
self.task = None
|
||||
self.hood_censors = self.instances.setdefault(hood.id, [])
|
||||
self.hood_censors.append(self)
|
||||
self._inbox = Queue()
|
||||
self.__task = None
|
||||
self.__hood_censors = self.__instances.setdefault(hood.id, [])
|
||||
self.__hood_censors.append(self)
|
||||
|
||||
def start(self):
|
||||
if self.task is None:
|
||||
self.task = create_task(self.__run())
|
||||
""" Start the bot.
|
||||
|
||||
Note: This will be called by a spawner, a platform bot should not call this.
|
||||
"""
|
||||
if self.__task is None:
|
||||
self.__task = create_task(self.__run())
|
||||
|
||||
def stop(self):
|
||||
if self.task is not None:
|
||||
self.task.cancel()
|
||||
self.task = None
|
||||
""" Stop the bot.
|
||||
|
||||
Note: This will be called by a spawner, a platform bot should not call this.
|
||||
"""
|
||||
if self.__task is not None:
|
||||
self.__task.cancel()
|
||||
self.__task = None
|
||||
|
||||
async def __run(self):
|
||||
await self.hood.load()
|
||||
self.task.set_name('%s %s' % (self.__class__.__name__, self.hood.name))
|
||||
self.__task.set_name('%s %s' % (self.__class__.__name__, self.hood.name))
|
||||
await self.run()
|
||||
|
||||
# override this in derived class
|
||||
async def run(self):
|
||||
""" Entry point for a bot.
|
||||
|
||||
Note: Override this in the derived bot class.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def publish(self, message):
|
||||
""" Distribute a message to the bots in a hood.
|
||||
|
||||
Args:
|
||||
message (Message): Message to distribute
|
||||
"""
|
||||
if not await self.__is_appropriate(message):
|
||||
return
|
||||
for censor in self.hood_censors:
|
||||
await censor.inbox.put(message)
|
||||
for censor in self.__hood_censors:
|
||||
await censor._inbox.put(message)
|
||||
|
||||
async def receive(self):
|
||||
return await self.inbox.get()
|
||||
""" Receive a message.
|
||||
|
||||
Returns (Message): Received message
|
||||
"""
|
||||
return await self._inbox.get()
|
||||
|
||||
async def __is_appropriate(self, message):
|
||||
for badword in await BadWord.objects.filter(hood=self.hood).all():
|
||||
|
@ -69,17 +140,40 @@ class Censor:
|
|||
|
||||
|
||||
class Spawner:
|
||||
instances = []
|
||||
""" Spawns a bot with a specific bot model.
|
||||
|
||||
Examples:
|
||||
```
|
||||
class XYZPlatform(Censor):
|
||||
# bot class
|
||||
|
||||
class XYZ(Model):
|
||||
# bot model
|
||||
|
||||
spawner = Spawner(XYZ, XYZPlatform)
|
||||
```
|
||||
|
||||
Args:
|
||||
ORMClass (ORM Model subclass): A Bot Model object
|
||||
BotClass (Censor subclass): A Bot Class object
|
||||
|
||||
Attributes:
|
||||
ORMClass (ORM Model subclass): A Hood Model object
|
||||
BotClass (Censor subclass): A Bot Class object
|
||||
"""
|
||||
|
||||
__instances = []
|
||||
|
||||
def __init__(self, ORMClass, BotClass):
|
||||
self.ORMClass = ORMClass
|
||||
self.BotClass = BotClass
|
||||
self.bots = {}
|
||||
self.instances.append(self)
|
||||
self.__bots = {}
|
||||
self.__instances.append(self)
|
||||
|
||||
@classmethod
|
||||
async def init_all(cls):
|
||||
for spawner in cls.instances:
|
||||
""" Instantiate and start a bot for every row in the corresponding ORM model. """
|
||||
for spawner in cls.__instances:
|
||||
await spawner._init()
|
||||
|
||||
async def _init(self):
|
||||
|
@ -87,13 +181,34 @@ class Spawner:
|
|||
self.start(item)
|
||||
|
||||
def start(self, item):
|
||||
bot = self.bots.setdefault(item.pk, self.BotClass(item))
|
||||
""" Instantiate and start a bot with the provided ORM object.
|
||||
|
||||
Example:
|
||||
```
|
||||
xyz = await XYZ.objects.create(hood=hood, **values.__dict__)
|
||||
spawner.start(xyz)
|
||||
```
|
||||
|
||||
Args:
|
||||
item (ORM Model object): Argument to the bot constructor
|
||||
"""
|
||||
bot = self.__bots.setdefault(item.pk, self.BotClass(item))
|
||||
bot.start()
|
||||
|
||||
def stop(self, item):
|
||||
bot = self.bots.pop(item.pk, None)
|
||||
""" Stop and delete a bot.
|
||||
|
||||
Args:
|
||||
item (ORM Model object): ORM object corresponding to bot.
|
||||
"""
|
||||
bot = self.__bots.pop(item.pk, None)
|
||||
if bot is not None:
|
||||
bot.stop()
|
||||
|
||||
def get(self, item):
|
||||
return self.bots.get(item.pk)
|
||||
""" Get a running bot.
|
||||
|
||||
Args:
|
||||
item (ORM Model object): ORM object corresponding to bot.
|
||||
"""
|
||||
return self.__bots.get(item.pk)
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
""" Routing definitions for the REST API.
|
||||
|
||||
A platform bot shall add its API router in this `__init__.py`
|
||||
file to get included into the main application.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from kibicara.platforms.test.webapi import router as test_router
|
||||
from kibicara.platforms.telegram.webapi import router as telegram_router
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
""" REST API endpoints for hood admins. """
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from kibicara import email
|
||||
|
@ -71,8 +73,14 @@ router = APIRouter()
|
|||
|
||||
@router.post('/register/', status_code=status.HTTP_202_ACCEPTED)
|
||||
async def admin_register(values: BodyAdmin):
|
||||
""" Sends an email with a confirmation link.
|
||||
|
||||
- **email**: E-Mail Address of new hood admin
|
||||
- **password**: Password of new hood admin
|
||||
"""
|
||||
register_token = to_token(**values.__dict__)
|
||||
logger.debug(f'register_token={register_token}')
|
||||
# TODO implement check to see if email already is in database
|
||||
try:
|
||||
email.send_email(
|
||||
to=values.email,
|
||||
|
@ -88,6 +96,10 @@ async def admin_register(values: BodyAdmin):
|
|||
|
||||
@router.post('/confirm/{register_token}')
|
||||
async def admin_confirm(register_token: str):
|
||||
""" Registration confirmation and account creation.
|
||||
|
||||
- **register_token**: Registration token received in email from /register
|
||||
"""
|
||||
try:
|
||||
values = from_token(register_token)
|
||||
passhash = argon2.hash(values['password'])
|
||||
|
@ -99,6 +111,11 @@ async def admin_confirm(register_token: str):
|
|||
|
||||
@router.post('/login/')
|
||||
async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
""" Get an access token.
|
||||
|
||||
- **username**: Email of a registered hood admin
|
||||
- **password**: Password of a registered hood admin
|
||||
"""
|
||||
try:
|
||||
await get_auth(form_data.username, form_data.password)
|
||||
except ValueError:
|
||||
|
@ -112,6 +129,7 @@ async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
|
|||
|
||||
@router.get('/hoods/')
|
||||
async def admin_hood_read_all(admin=Depends(get_admin)):
|
||||
""" Get a list of all hoods of a given admin. """
|
||||
return (
|
||||
await AdminHoodRelation.objects.select_related('hood').filter(admin=admin).all()
|
||||
)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
""" REST API Endpoints for managing hoods. """
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from kibicara.model import AdminHoodRelation, Hood
|
||||
from kibicara.webapi.admin import get_admin
|
||||
|
@ -38,11 +40,17 @@ router = APIRouter()
|
|||
|
||||
@router.get('/')
|
||||
async def hood_read_all():
|
||||
""" Get all existing hoods. """
|
||||
return await Hood.objects.all()
|
||||
|
||||
|
||||
@router.post('/', status_code=status.HTTP_201_CREATED)
|
||||
async def hood_create(values: BodyHood, response: Response, admin=Depends(get_admin)):
|
||||
""" Creates a hood.
|
||||
|
||||
- **name**: Name of the hood
|
||||
- **landingpage**: Markdown formatted description of the hood
|
||||
"""
|
||||
try:
|
||||
hood = await Hood.objects.create(**values.__dict__)
|
||||
await AdminHoodRelation.objects.create(admin=admin.id, hood=hood.id)
|
||||
|
@ -54,16 +62,23 @@ async def hood_create(values: BodyHood, response: Response, admin=Depends(get_ad
|
|||
|
||||
@router.get('/{hood_id}')
|
||||
async def hood_read(hood=Depends(get_hood)):
|
||||
""" Get hood with id **hood_id**. """
|
||||
return hood
|
||||
|
||||
|
||||
@router.put('/{hood_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def hood_update(values: BodyHood, hood=Depends(get_hood)):
|
||||
""" Updates hood with id **hood_id**.
|
||||
|
||||
- **name**: New name of the hood
|
||||
- **landingpage**: New Markdown formatted description of the hood
|
||||
"""
|
||||
await hood.update(**values.__dict__)
|
||||
|
||||
|
||||
@router.delete('/{hood_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def hood_delete(hood=Depends(get_hood)):
|
||||
""" Deletes hood with id **hood_id**. """
|
||||
for relation in await AdminHoodRelation.objects.filter(hood=hood).all():
|
||||
await relation.delete()
|
||||
await hood.delete()
|
||||
|
|
|
@ -3,6 +3,13 @@
|
|||
#
|
||||
# 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 fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from kibicara.model import BadWord
|
||||
from kibicara.webapi.hoods import get_hood
|
||||
|
@ -28,6 +35,7 @@ router = APIRouter()
|
|||
|
||||
@router.get('/')
|
||||
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()
|
||||
|
||||
|
||||
|
@ -35,6 +43,10 @@ async def badword_read_all(hood=Depends(get_hood)):
|
|||
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__)
|
||||
|
@ -48,14 +60,20 @@ async def badword_create(
|
|||
|
||||
@router.get('/{badword_id}')
|
||||
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)
|
||||
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__)
|
||||
|
||||
|
||||
@router.delete('/{badword_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def badword_delete(badword=Depends(get_badword)):
|
||||
""" Deletes badword with id **badword_id** for hood with id **hood_id**. """
|
||||
await badword.delete()
|
||||
|
|
|
@ -3,6 +3,14 @@
|
|||
#
|
||||
# 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 fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from kibicara.model import Trigger
|
||||
from kibicara.webapi.hoods import get_hood
|
||||
|
@ -28,6 +36,7 @@ router = APIRouter()
|
|||
|
||||
@router.get('/')
|
||||
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()
|
||||
|
||||
|
||||
|
@ -35,6 +44,10 @@ async def trigger_read_all(hood=Depends(get_hood)):
|
|||
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__)
|
||||
|
@ -48,14 +61,20 @@ async def trigger_create(
|
|||
|
||||
@router.get('/{trigger_id}')
|
||||
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)
|
||||
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__)
|
||||
|
||||
|
||||
@router.delete('/{trigger_id}', status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def trigger_delete(trigger=Depends(get_trigger)):
|
||||
""" Deletes trigger with id **trigger_id** for hood with id **hood_id**. """
|
||||
await trigger.delete()
|
||||
|
|
Loading…
Reference in a new issue