Compare commits

..

35 commits

Author SHA1 Message Date
ogdbd3h5qze42igcv8wcrqk3 ec0abb5e24 [frontend] Add initial mastodon frontend 2023-03-19 18:36:15 +01:00
ogdbd3h5qze42igcv8wcrqk3 d897f369f7 [mastodon] Fix delete endpoint for mastodon 2023-03-19 18:36:15 +01:00
ogdbd3h5qze42igcv8wcrqk3 9c7607b7ca [frontend] Regenerate openapi for frontend due to mastodon API changes 2023-03-19 18:36:15 +01:00
missytake 0f4b25fcde [tests] Make Mastodon tests a bit more readable 2023-03-19 18:36:15 +01:00
missytake 4f96dfbee7 [mastodon] Deprecate GET mastodon route 2023-03-19 18:36:15 +01:00
missytake afd0894952 [mastodon] Return instance name in read_public API 2023-03-19 18:36:15 +01:00
missytake f0757619a7 [tests] Test mastodon_read route 2023-03-19 18:36:15 +01:00
missytake 90469a052a [tests] Replace single quotes with double quotes 2023-03-19 18:36:15 +01:00
missytake 16c325a9cb [tests] Test mastodon_delete route 2023-03-19 18:36:15 +01:00
missytake b49c4767a0 [mastodon] Remove redundant error class 2023-03-19 18:36:15 +01:00
missytake a61c48e99e [mastodon] Fix tests 2023-03-19 18:36:15 +01:00
ogdbd3h5qze42igcv8wcrqk3 dfd17aa27c [mastodon] Fix locking issue with synchronous Mastodon.py and replace last_seen with notification_dismiss 2023-03-19 18:36:15 +01:00
ogdbd3h5qze42igcv8wcrqk3 66fff6fd7d [frontend] Fix openapi-generator run for mastodon 2023-03-19 18:36:15 +01:00
missytake a548c2febc [tests] Testing the mastodon_create API endpoint 2023-03-19 18:36:15 +01:00
missytake f533efee4f [mastodon] Return 422 error for invalid input when creating mastodon bot 2023-03-19 18:36:15 +01:00
missytake cb88c24e2e [mastodon] Change mastodon_create to accept json instead of URL parameters 2023-03-19 18:36:15 +01:00
missytake 36638b1c64 [mastodon] New style: double quotes instead of single quotes 2023-03-19 18:36:15 +01:00
missytake 7fd716cecc [misc] Added pytest and pytest-aiohttp to test dependencies 2023-03-19 18:36:15 +01:00
missytake 3d482dd5f5 [mastodon] Generated openAPI routes for frontend 2023-03-19 18:36:15 +01:00
missytake fb1e88ab03 [mastodon] Moved mastodon module to new backend directory 2023-03-19 18:36:15 +01:00
missytake 5fa5a9f48e Revert "[doc] Document how to disable strict CORS checking"
This reverts commit bd17d5321b.
2023-03-19 18:36:15 +01:00
missytake 9704ed4ddf [mastodon] Working now: toot reports from mastodon, but only when the next report arrives 2023-03-19 18:36:15 +01:00
missytake 12935b79cb [mastodon] Load database references 2023-03-19 18:36:15 +01:00
missytake 37f7b98c67 [mastodon] Import mastodon API correctly 2023-03-19 18:36:12 +01:00
missytake d120d718f9 [mastodon] Added a TODO flair 2023-03-19 18:34:57 +01:00
missytake b1f8c08d25 [mastodon] Some web routes to add a Mastodon Account to Ticketfrei 2023-03-19 18:34:57 +01:00
missytake 4dc4b9cfca [mastodon] Changed MastodonAccount column: instance_id -> instance 2023-03-19 18:34:57 +01:00
missytake 07bc5a2686 [mastodon] First approach to a mastodon bot 2023-03-19 18:34:57 +01:00
missytake 3ae4a08ad5 [doc] Document how to disable strict CORS checking 2023-03-19 18:34:57 +01:00
v 767c92000b [misc] Add some type annotations 2023-03-19 01:15:37 +01:00
v 13c20ca245 [misc] Use double-quotes (black default) 2023-03-18 18:47:02 +01:00
v fd09b381a6 [misc] Add some type annotations 2023-03-18 18:42:59 +01:00
v 35eff0c416 [core] Don't read configs at the module top-level 2023-03-18 18:18:44 +01:00
ogdbd3h5qze42igcv8wcrqk3 003a10b273 [frontend] Fix openapi-generator script 2023-03-18 17:56:54 +01:00
ogdbd3h5qze42igcv8wcrqk3 dc454800cd [doc] Add documentation to update angular and openapi-generator code 2023-03-18 17:39:21 +01:00
37 changed files with 501 additions and 509 deletions

View file

@ -14,12 +14,13 @@
0. `cd backend`
1. Activate your dev environment with `source .venv/bin/activate`
2. Install with `pip install .`
3. Turn off production mode: `sudo su -c 'echo "production = 0" >> /etc/kibicara.conf'`
3. Create a config file: `echo "production = 0" > kibicara.conf`
#### Cheatsheet
- Install Kibicara with `pip install .`
- Execute Kibicara with `kibicara` (verbose: `kibicara -vvv`)
- Execute Kibicara with `kibicara -f kibicara.conf`
(verbose: `kibicara -vvv -f kibicara.conf`)
- Interact with Swagger REST-API Documentation: `http://localhost:8000/api/docs`
- Test and stylecheck with `tox`
- Fix style issues with `black -S src tests`

View file

@ -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 Christian Hagenest <c.hagenest@pm.me>
Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>

View file

@ -55,11 +55,12 @@ deps =
black
flake8
mypy
types-requests
commands =
black -S --check --diff src tests
black --check --diff src tests
flake8 src tests
# not yet
#mypy src tests
#mypy --ignore-missing-imports src tests
[testenv]
deps =

View file

@ -1,84 +1,27 @@
# 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 Martin Rey <martin.rey@mailbox.org>
#
# 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 sys import argv
from nacl.secret import SecretBox
from nacl.utils import random
from pytoml import load
config = {
'database_connection': 'sqlite:////tmp/kibicara.sqlite',
'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
'frontend_path': None, # required, path to frontend html/css/js files
'production': True,
'behind_proxy': False,
'keyfile': None, # optional for ssl
'certfile': None, # optional for ssl
# dev params
'root_url': 'http://localhost:8000', # url of backend
'cors_allow_origin': 'http://localhost:4200',
}
"""Default configuration.
The default configuration gets overwritten by a configuration file if one exists.
"""
args = None
if argv[0].endswith('kibicara'):
parser = ArgumentParser()
parser.add_argument(
'-f',
'--config',
dest='configfile',
default='/etc/kibicara.conf',
help='path to config file',
)
parser.add_argument(
'-v',
'--verbose',
action='count',
help='Raise verbosity level',
)
args = parser.parse_args()
if argv[0].endswith('kibicara_mda'):
parser = ArgumentParser()
parser.add_argument(
'-f',
'--config',
dest='configfile',
default='/etc/kibicara.conf',
help='path to config file',
)
# the MDA passes the recipient address as command line argument
parser.add_argument('recipient')
args = parser.parse_args()
if args is not None:
try:
with open(args.configfile) as configfile:
config.update(load(configfile))
except FileNotFoundError:
# run with default config
pass
config = {
"database_connection": "sqlite:////tmp/kibicara.sqlite",
"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
"frontend_path": None, # required, path to frontend html/css/js files
"production": True,
"behind_proxy": False,
"keyfile": None, # optional for ssl
"certfile": None, # optional for ssl
# dev params
"root_url": "http://localhost:8000", # url of backend
"cors_allow_origin": "http://localhost:4200",
}

View file

@ -15,7 +15,7 @@ from socket import getfqdn
logger = getLogger(__name__)
def send_email(to, subject, sender='kibicara', body=''):
def send_email(to, subject, sender="kibicara", body=""):
"""E-Mail sender.
Sends an E-Mail to a specified recipient with a body
@ -33,10 +33,10 @@ def send_email(to, subject, sender='kibicara', body=''):
body (str): The body of the e-mail
"""
msg = MIMEMultipart()
msg['From'] = 'Kibicara <{0}@{1}>'.format(sender, getfqdn())
msg['To'] = to
msg['Subject'] = '[Kibicara] {0}'.format(subject)
msg["From"] = "Kibicara <{0}@{1}>".format(sender, getfqdn())
msg["To"] = to
msg["Subject"] = "[Kibicara] {0}".format(subject)
msg.attach(MIMEText(body))
with SMTP('localhost') as smtp:
with SMTP("localhost") as smtp:
smtp.send_message(msg)

View file

@ -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 Martin Rey <martin.rey@mailbox.org>
#
@ -6,6 +6,7 @@
"""Entrypoint of Kibicara."""
from argparse import ArgumentParser
from asyncio import run as asyncio_run
from logging import DEBUG, INFO, WARNING, basicConfig, getLogger
@ -14,8 +15,9 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from hypercorn.asyncio import serve
from hypercorn.config import Config
from pytoml import load
from kibicara.config import args, config
from kibicara.config import config
from kibicara.model import Mapping
from kibicara.platformapi import Spawner
from kibicara.webapi import router
@ -31,9 +33,29 @@ class Main:
"""
def __init__(self):
asyncio_run(self.__run())
parser = ArgumentParser()
parser.add_argument(
"-f",
"--config",
dest="configfile",
default="/etc/kibicara.conf",
help="path to config file",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
help="Raise verbosity level",
)
args = parser.parse_args()
try:
with open(args.configfile) as configfile:
config.update(load(configfile))
except FileNotFoundError:
# run with default config
pass
async def __run(self):
LOGLEVELS = {
None: WARNING,
1: INFO,
@ -41,10 +63,13 @@ class Main:
}
basicConfig(
level=LOGLEVELS.get(args.verbose, DEBUG),
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())
async def __run(self):
await Spawner.init_all()
await self.__start_webserver()
@ -53,31 +78,31 @@ class Main:
async def get_response(self, path, scope):
response = await super().get_response(path, scope)
if response.status_code == 404:
response = await super().get_response('.', scope)
response = await super().get_response(".", scope)
return response
app = FastAPI()
server_config = Config()
server_config.accesslog = '-'
server_config.behind_proxy = config['behind_proxy']
server_config.keyfile = config['keyfile']
server_config.certfile = config['certfile']
if config['production']:
server_config.bind = ['0.0.0.0:8000', '[::]:8000']
server_config.accesslog = "-"
server_config.behind_proxy = config["behind_proxy"]
server_config.keyfile = config["keyfile"]
server_config.certfile = config["certfile"]
if config["production"]:
server_config.bind = ["0.0.0.0:8000", "[::]:8000"]
api = FastAPI()
api.include_router(router)
app.mount('/api', api)
if not config['production'] and config['cors_allow_origin']:
app.mount("/api", api)
if not config["production"] and config["cors_allow_origin"]:
app.add_middleware(
CORSMiddleware,
allow_origins=config['cors_allow_origin'],
allow_origins=config["cors_allow_origin"],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
allow_methods=["*"],
allow_headers=["*"],
)
if config['frontend_path'] is not None:
if config["frontend_path"] is not None:
app.mount(
'/',
app=SinglePageApplication(directory=config['frontend_path'], html=True),
"/",
app=SinglePageApplication(directory=config["frontend_path"], html=True),
)
await serve(app, server_config)

View file

@ -14,7 +14,7 @@ from kibicara.config import config
class Mapping:
database = Database(config['database_connection'])
database = Database(config["database_connection"])
metadata = MetaData()
@classmethod
@ -34,7 +34,7 @@ class Admin(Model):
passhash: Text()
class Mapping(Mapping):
table_name = 'admins'
table_name = "admins"
class Hood(Model):
@ -44,7 +44,7 @@ class Hood(Model):
email_enabled: Boolean() = True
class Mapping(Mapping):
table_name = 'hoods'
table_name = "hoods"
class AdminHoodRelation(Model):
@ -53,7 +53,7 @@ class AdminHoodRelation(Model):
hood: ForeignKey(Hood)
class Mapping(Mapping):
table_name = 'admin_hood_relations'
table_name = "admin_hood_relations"
class Trigger(Model):
@ -62,7 +62,7 @@ class Trigger(Model):
pattern: Text()
class Mapping(Mapping):
table_name = 'triggers'
table_name = "triggers"
class BadWord(Model):
@ -71,4 +71,4 @@ class BadWord(Model):
pattern: Text()
class Mapping(Mapping):
table_name = 'badwords'
table_name = "badwords"

View file

@ -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 Martin Rey <martin.rey@mailbox.org>
#
@ -29,7 +29,7 @@ class Message:
**kwargs (object, optional): Other platform-specific data.
"""
def __init__(self, text, **kwargs):
def __init__(self, text: str, **kwargs):
self.text = text
self.__dict__.update(kwargs)
@ -71,7 +71,7 @@ class Censor:
hood (Hood): A Hood Model object
"""
__instances = {}
__instances: dict[int, list["Censor"]] = {}
def __init__(self, hood):
self.hood = hood
@ -82,19 +82,19 @@ class Censor:
self.__hood_censors.append(self)
self.status = BotStatus.INSTANTIATED
def start(self):
def start(self) -> None:
"""Start the bot."""
if self.__task is None:
self.__task = create_task(self.__run())
def stop(self):
def stop(self) -> None:
"""Stop the bot."""
if self.__task is not None:
self.__task.cancel()
async def __run(self):
async def __run(self) -> None:
await self.hood.load()
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:
self.status = BotStatus.RUNNING
await self.run()
@ -104,7 +104,7 @@ class Censor:
self.__task = None
self.status = BotStatus.STOPPED
async def run(self):
async def run(self) -> None:
"""Entry point for a bot.
Note: Override this in the derived bot class.
@ -112,14 +112,14 @@ class Censor:
pass
@classmethod
async def destroy_hood(cls, hood):
async def destroy_hood(cls, hood) -> None:
"""Remove all of its database entries.
Note: Override this in the derived bot class.
"""
pass
async def publish(self, message):
async def publish(self, message: Message) -> bool:
"""Distribute a message to the bots in a hood.
Args:
@ -132,28 +132,28 @@ class Censor:
await censor._inbox.put(message)
return True
async def receive(self):
async def receive(self) -> Message:
"""Receive a message.
Returns (Message): Received message
"""
return await self._inbox.get()
async def __is_appropriate(self, message):
async def __is_appropriate(self, message: Message) -> bool:
for badword in await BadWord.objects.filter(hood=self.hood).all():
if search(badword.pattern, message.text, IGNORECASE):
logger.debug(
'Matched bad word - dropped message: {0}'.format(message.text)
"Matched bad word - dropped message: {0}".format(message.text)
)
return False
for trigger in await Trigger.objects.filter(hood=self.hood).all():
if search(trigger.pattern, message.text, IGNORECASE):
logger.debug(
'Matched trigger - passed message: {0}'.format(message.text)
"Matched trigger - passed message: {0}".format(message.text)
)
return True
logger.debug(
'Did not match any trigger - dropped message: {0}'.format(message.text)
"Did not match any trigger - dropped message: {0}".format(message.text)
)
return False
@ -177,7 +177,7 @@ class Spawner:
BotClass (Censor subclass): A Bot Class object
"""
__instances = []
__instances: list["Spawner"] = []
def __init__(self, ORMClass, BotClass):
self.ORMClass = ORMClass
@ -186,13 +186,13 @@ class Spawner:
self.__instances.append(self)
@classmethod
async def init_all(cls):
async def init_all(cls) -> None:
"""Instantiate and start a bot for every row in the corresponding ORM model."""
for spawner in cls.__instances:
await spawner._init()
@classmethod
async def destroy_hood(cls, hood):
async def destroy_hood(cls, hood) -> None:
for spawner in cls.__instances:
for pk in list(spawner.__bots):
bot = spawner.__bots[pk]
@ -201,11 +201,11 @@ class Spawner:
bot.stop()
await spawner.BotClass.destroy_hood(hood)
async def _init(self):
async def _init(self) -> None:
for item in await self.ORMClass.objects.all():
self.start(item)
def start(self, item):
def start(self, item) -> None:
"""Instantiate and start a bot with the provided ORM object.
Example:
@ -221,7 +221,7 @@ class Spawner:
if bot.enabled:
bot.start()
def stop(self, item):
def stop(self, item) -> None:
"""Stop and delete a bot.
Args:
@ -231,7 +231,7 @@ class Spawner:
if bot is not None:
bot.stop()
def get(self, item):
def get(self, item) -> Censor:
"""Get a running bot.
Args:

View file

@ -36,7 +36,7 @@ class EmailBot(Censor):
while True:
message = await self.receive()
logger.debug(
'Received message from censor ({0}): {1}'.format(
"Received message from censor ({0}): {1}".format(
self.hood.name, message.text
)
)
@ -45,19 +45,19 @@ class EmailBot(Censor):
).all():
token = to_token(email=subscriber.email, hood=self.hood.id)
body = (
'{0}\n\n--\n'
+ 'If you want to stop receiving these mails,'
+ 'follow this link: {1}/hoods/{2}/email-unsubscribe?token={3}'
).format(message.text, config['frontend_url'], self.hood.id, token)
"{0}\n\n--\n"
+ "If you want to stop receiving these mails,"
+ "follow this link: {1}/hoods/{2}/email-unsubscribe?token={3}"
).format(message.text, config["frontend_url"], self.hood.id, token)
try:
logger.debug('Trying to send: \n{0}'.format(body))
logger.debug("Trying to send: \n{0}".format(body))
email.send_email(
subscriber.email,
'Kibicara {0}'.format(self.hood.name),
"Kibicara {0}".format(self.hood.name),
body=body,
)
except (ConnectionRefusedError, SMTPException):
logger.exception('Sending email to subscriber failed.')
logger.exception("Sending email to subscriber failed.")
spawner = Spawner(Hood, EmailBot)

View file

@ -1,10 +1,11 @@
# Copyright (C) 2020 by Maike <maike@systemli.org>
# 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>
#
# SPDX-License-Identifier: 0BSD
from argparse import ArgumentParser
from asyncio import run as asyncio_run
from email.parser import BytesParser
from email.policy import default
@ -15,9 +16,10 @@ from sys import stdin
from fastapi import status
from ormantic import NoMatch
from pytoml import load
from requests import post
from kibicara.config import args, config
from kibicara.config import config
from kibicara.platforms.email.model import Email, EmailSubscribers
logger = getLogger(__name__)
@ -25,62 +27,82 @@ logger = getLogger(__name__)
class Main:
def __init__(self):
asyncio_run(self.__run())
parser = ArgumentParser()
parser.add_argument(
"-f",
"--config",
dest="configfile",
default="/etc/kibicara.conf",
help="path to config file",
)
# the MDA passes the recipient address as command line argument
parser.add_argument("recipient")
args = parser.parse_args()
try:
with open(args.configfile) as configfile:
config.update(load(configfile))
except FileNotFoundError:
# run with default config
pass
async def __run(self):
# extract email from the recipient
email_name = args.recipient.lower()
asyncio_run(self.__run(email_name))
async def __run(self, email_name):
try:
email = await Email.objects.get(name=email_name)
except NoMatch:
logger.error('No recipient with this name')
logger.error("No recipient with this name")
exit(1)
# read mail from STDIN and parse to EmailMessage object
message = BytesParser(policy=default).parsebytes(stdin.buffer.read())
sender = ''
if message.get('sender'):
sender = message.get('sender')
elif message.get('from'):
sender = message.get('from')
sender = ""
if message.get("sender"):
sender = message.get("sender")
elif message.get("from"):
sender = message.get("from")
else:
logger.error('No Sender of From header')
logger.error("No Sender of From header")
exit(1)
sender = parseaddr(sender)[1]
if not sender:
logger.error('Could not parse sender')
logger.error("Could not parse sender")
exit(1)
maybe_subscriber = await EmailSubscribers.objects.filter(email=sender).all()
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)
# extract relevant data from mail
text = sub(
r'<[^>]*>',
'',
message.get_body(preferencelist=('plain', 'html')).get_content(),
r"<[^>]*>",
"",
message.get_body(preferencelist=("plain", "html")).get_content(),
)
response = post(
'{0}/api/hoods/{1}/email/messages/'.format(
config['root_url'], email.hood.pk
"{0}/api/hoods/{1}/email/messages/".format(
config["root_url"], email.hood.pk
),
json={'text': text, 'secret': email.secret},
json={"text": text, "secret": email.secret},
)
if response.status_code == status.HTTP_201_CREATED:
exit(0)
elif response.status_code == status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS:
logger.error('Message was\'t accepted: {0}'.format(text))
logger.error("Message was't accepted: {0}".format(text))
elif response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY:
logger.error('Malformed request: {0}'.format(response.json()))
logger.error("Malformed request: {0}".format(response.json()))
elif response.status_code == status.HTTP_401_UNAUTHORIZED:
logger.error('Wrong API secret. kibicara_mda seems to be misconfigured')
logger.error("Wrong API secret. kibicara_mda seems to be misconfigured")
else:
logger.error(
'REST-API failed with response status {0}'.format(response.status_code)
"REST-API failed with response status {0}".format(response.status_code)
)
exit(1)

View file

@ -19,7 +19,7 @@ class Email(Model):
secret: Text()
class Mapping(Mapping):
table_name = 'email'
table_name = "email"
class EmailSubscribers(Model):
@ -30,4 +30,4 @@ class EmailSubscribers(Model):
email: Text(unique=True)
class Mapping(Mapping):
table_name = 'email_subscribers'
table_name = "email_subscribers"

View file

@ -29,10 +29,10 @@ logger = getLogger(__name__)
class BodyEmail(BaseModel):
name: str
@validator('name')
@validator("name")
def valid_prefix(cls, value):
if not value.startswith('kibicara-'):
raise ValueError('Recipient address didn\'t start with kibicara-')
if not value.startswith("kibicara-"):
raise ValueError("Recipient address didn't start with kibicara-")
return value
@ -79,9 +79,9 @@ router = APIRouter()
@router.get(
'/public',
"/public",
# TODO response_model
operation_id='get_emails_public',
operation_id="get_emails_public",
)
async def email_read_all_public(hood=Depends(get_hood_unauthorized)):
if hood.email_enabled:
@ -91,19 +91,19 @@ async def email_read_all_public(hood=Depends(get_hood_unauthorized)):
@router.get(
'/',
"/",
# TODO response_model
operation_id='get_emails',
operation_id="get_emails",
)
async def email_read_all(hood=Depends(get_hood)):
return await Email.objects.filter(hood=hood).select_related('hood').all()
return await Email.objects.filter(hood=hood).select_related("hood").all()
@router.post(
'/',
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model
operation_id='create_email',
operation_id="create_email",
)
async def email_create(values: BodyEmail, response: Response, hood=Depends(get_hood)):
"""Create an Email bot. Call this when creating a hood.
@ -115,27 +115,27 @@ async def email_create(values: BodyEmail, response: Response, hood=Depends(get_h
email = await Email.objects.create(
hood=hood, secret=urandom(32).hex(), **values.__dict__
)
response.headers['Location'] = str(hood.id)
response.headers["Location"] = str(hood.id)
return email
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.get(
'/status',
"/status",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id='status_email',
operation_id="status_email",
)
async def email_status(hood=Depends(get_hood)):
return {'status': spawner.get(hood).status.name}
return {"status": spawner.get(hood).status.name}
@router.post(
'/start',
"/start",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id='start_email',
operation_id="start_email",
)
async def email_start(hood=Depends(get_hood)):
await hood.update(email_enabled=True)
@ -144,10 +144,10 @@ async def email_start(hood=Depends(get_hood)):
@router.post(
'/stop',
"/stop",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id='stop_email',
operation_id="stop_email",
)
async def email_stop(hood=Depends(get_hood)):
await hood.update(email_enabled=False)
@ -156,16 +156,16 @@ async def email_stop(hood=Depends(get_hood)):
@router.get(
'/{email_id}',
"/{email_id}",
# TODO response_model
operation_id='get_email',
operation_id="get_email",
)
async def email_read(email=Depends(get_email)):
return email
@router.delete(
'/{email_id}', status_code=status.HTTP_204_NO_CONTENT, operation_id='delete_email'
"/{email_id}", status_code=status.HTTP_204_NO_CONTENT, operation_id="delete_email"
)
async def email_delete(email=Depends(get_email)):
"""Delete an Email bot.
@ -179,9 +179,9 @@ async def email_delete(email=Depends(get_email)):
@router.post(
'/subscribe/',
"/subscribe/",
status_code=status.HTTP_202_ACCEPTED,
operation_id='subscribe',
operation_id="subscribe",
response_model=BaseModel,
)
async def email_subscribe(
@ -194,8 +194,8 @@ async def email_subscribe(
:return: Returns status code 200 after sending confirmation email.
"""
token = to_token(hood=hood.id, email=subscriber.email)
confirm_link = '{0}/hoods/{1}/email-confirm?token={2}'.format(
config['frontend_url'],
confirm_link = "{0}/hoods/{1}/email-confirm?token={2}".format(
config["frontend_url"],
hood.id,
token,
)
@ -205,26 +205,26 @@ async def email_subscribe(
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
email.send_email(
subscriber.email,
'Subscribe to Kibicara {0}'.format(hood.name),
body='To confirm your subscription, follow this link: {0}'.format(
"Subscribe to Kibicara {0}".format(hood.name),
body="To confirm your subscription, follow this link: {0}".format(
confirm_link
),
)
return {}
except ConnectionRefusedError:
logger.info(token)
logger.error('Sending subscription confirmation email failed.', exc_info=True)
logger.error("Sending subscription confirmation email failed.", exc_info=True)
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY)
except SMTPException:
logger.info(token)
logger.error('Sending subscription confirmation email failed.', exc_info=True)
logger.error("Sending subscription confirmation email failed.", exc_info=True)
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY)
@router.post(
'/subscribe/confirm/{token}',
"/subscribe/confirm/{token}",
status_code=status.HTTP_201_CREATED,
operation_id='confirm_subscriber',
operation_id="confirm_subscriber",
response_model=BaseModel,
)
async def email_subscribe_confirm(token, hood=Depends(get_hood_unauthorized)):
@ -236,19 +236,19 @@ async def email_subscribe_confirm(token, hood=Depends(get_hood_unauthorized)):
"""
payload = from_token(token)
# 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)
try:
await EmailSubscribers.objects.create(hood=hood.id, email=payload['email'])
await EmailSubscribers.objects.create(hood=hood.id, email=payload["email"])
return {}
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.delete(
'/unsubscribe/{token}',
"/unsubscribe/{token}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='unsubscribe',
operation_id="unsubscribe",
)
async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)):
"""Remove a subscriber from the database when they click on an unsubscribe link.
@ -257,13 +257,13 @@ async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)):
:param hood: Hood the Email bot belongs to.
"""
try:
logger.warning('token is: {0}'.format(token))
logger.warning("token is: {0}".format(token))
payload = from_token(token)
# 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)
subscriber = await EmailSubscribers.objects.filter(
hood=payload['hood'], email=payload['email']
hood=payload["hood"], email=payload["email"]
).get()
await subscriber.delete()
return Response(status_code=status.HTTP_204_NO_CONTENT)
@ -274,28 +274,28 @@ async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)):
@router.get(
'/subscribers/',
"/subscribers/",
# TODO response_model
operation_id='get_subscribers',
operation_id="get_subscribers",
)
async def subscribers_read_all(hood=Depends(get_hood)):
return await EmailSubscribers.objects.filter(hood=hood).all()
@router.get(
'/subscribers/{subscriber_id}',
"/subscribers/{subscriber_id}",
# TODO response_model
operation_id='get_subscriber',
operation_id="get_subscriber",
)
async def subscribers_read(subscriber=Depends(get_subscriber)):
return subscriber
@router.post(
'/messages/',
"/messages/",
status_code=status.HTTP_201_CREATED,
# TODO response_model
operation_id='send_message',
operation_id="send_message",
)
async def email_message_create(
message: BodyMessage, hood=Depends(get_hood_unauthorized)
@ -310,14 +310,14 @@ async def email_message_create(
if message.secret == receiver.secret:
# pass message.text to bot.py
if await spawner.get(hood).publish(Message(message.text)):
logger.warning('Message was accepted: {0}'.format(message.text))
logger.warning("Message was accepted: {0}".format(message.text))
return {}
else:
logger.warning('Message wasn\'t accepted: {0}'.format(message.text))
logger.warning("Message wasn't accepted: {0}".format(message.text))
raise HTTPException(
status_code=status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS
)
logger.warning(
'Someone is trying to submit an email without the correct API secret'
"Someone is trying to submit an email without the correct API secret"
)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)

View file

@ -32,9 +32,9 @@ class TelegramBot(Censor):
def _create_dispatcher(self):
dp = Dispatcher(self.bot)
dp.register_message_handler(self._send_welcome, commands=['start'])
dp.register_message_handler(self._remove_user, commands=['stop'])
dp.register_message_handler(self._send_help, commands=['help'])
dp.register_message_handler(self._send_welcome, commands=["start"])
dp.register_message_handler(self._remove_user, commands=["stop"])
dp.register_message_handler(self._send_help, commands=["help"])
dp.register_message_handler(self._receive_message)
return dp
@ -42,30 +42,30 @@ class TelegramBot(Censor):
try:
self.bot = Bot(token=self.telegram_model.api_token)
self.dp = self._create_dispatcher()
logger.debug('Bot {0} starting.'.format(self.telegram_model.hood.name))
logger.debug("Bot {0} starting.".format(self.telegram_model.hood.name))
user = await self.bot.get_me()
if user.username:
await self.telegram_model.update(username=user.username)
await gather(self.dp.start_polling(), self._push())
except CancelledError:
logger.debug(
'Bot {0} received Cancellation.'.format(self.telegram_model.hood.name)
"Bot {0} received Cancellation.".format(self.telegram_model.hood.name)
)
self.dp = None
raise
except exceptions.ValidationError:
logger.debug(
'Bot {0} has invalid auth token.'.format(self.telegram_model.hood.name)
"Bot {0} has invalid auth token.".format(self.telegram_model.hood.name)
)
await self.telegram_model.update(enabled=False)
finally:
logger.debug('Bot {0} stopped.'.format(self.telegram_model.hood.name))
logger.debug("Bot {0} stopped.".format(self.telegram_model.hood.name))
async def _push(self):
while True:
message = await self.receive()
logger.debug(
'Received message from censor ({0}): {1}'.format(
"Received message from censor ({0}): {1}".format(
self.telegram_model.hood.name, message.text
)
)
@ -79,34 +79,34 @@ class TelegramBot(Censor):
await self.bot.send_message(user_id, message, disable_notification=False)
except exceptions.BotBlocked:
logger.error(
'Target [ID:{0}] ({1}): blocked by user'.format(
"Target [ID:{0}] ({1}): blocked by user".format(
user_id, self.telegram_model.hood.name
)
)
except exceptions.ChatNotFound:
logger.error(
'Target [ID:{0}] ({1}): invalid user ID'.format(
"Target [ID:{0}] ({1}): invalid user ID".format(
user_id, self.telegram_model.hood.name
)
)
except exceptions.RetryAfter as e:
logger.error(
'Target [ID:{0}] ({1}): Flood limit is exceeded.'.format(
"Target [ID:{0}] ({1}): Flood limit is exceeded.".format(
user_id, self.telegram_model.hood.name
)
+ 'Sleep {0} seconds.'.format(e.timeout)
+ "Sleep {0} seconds.".format(e.timeout)
)
await sleep(e.timeout)
return await self._send_message(user_id, message)
except exceptions.UserDeactivated:
logger.error(
'Target [ID:{0}] ({1}): user is deactivated'.format(
"Target [ID:{0}] ({1}): user is deactivated".format(
user_id, self.telegram_model.hood.name
)
)
except exceptions.TelegramAPIError:
logger.exception(
'Target [ID:{0}] ({1}): failed'.format(
"Target [ID:{0}] ({1}): failed".format(
user_id, self.telegram_model.hood.name
)
)
@ -114,14 +114,14 @@ class TelegramBot(Censor):
async def _send_welcome(self, message: types.Message):
try:
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
await TelegramUser.objects.create(
user_id=message.from_user.id, bot=self.telegram_model
)
await message.reply(self.telegram_model.welcome_message)
except IntegrityError:
await message.reply('Error: You are already registered.')
await message.reply("Error: You are already registered.")
async def _remove_user(self, message: types.Message):
try:
@ -129,19 +129,19 @@ class TelegramBot(Censor):
user_id=message.from_user.id, bot=self.telegram_model
)
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:
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):
if message.from_user.is_bot:
await message.reply('Error: Bots can\'t be helped.')
await message.reply("Error: Bots can't be helped.")
return
await message.reply('Send messages here to broadcast them to your hood')
await message.reply("Send messages here to broadcast them to your hood")
async def _receive_message(self, message: types.Message):
if not message.text:
await message.reply('Error: Only text messages are allowed.')
await message.reply("Error: Only text messages are allowed.")
return
await self.publish(Message(message.text))

View file

@ -17,7 +17,7 @@ class Telegram(Model):
enabled: Boolean() = True
class Mapping(Mapping):
table_name = 'telegrambots'
table_name = "telegrambots"
class TelegramUser(Model):
@ -27,4 +27,4 @@ class TelegramUser(Model):
bot: ForeignKey(Telegram)
class Mapping(Mapping):
table_name = 'telegramusers'
table_name = "telegramusers"

View file

@ -21,9 +21,9 @@ logger = getLogger(__name__)
class BodyTelegram(BaseModel):
api_token: str
welcome_message: str = 'Welcome!'
welcome_message: str = "Welcome!"
@validator('api_token')
@validator("api_token")
def valid_api_token(cls, value):
try:
check_token(value)
@ -48,9 +48,9 @@ telegram_callback_router = APIRouter()
@router.get(
'/public',
"/public",
# TODO response_model,
operation_id='get_telegrams_public',
operation_id="get_telegrams_public",
)
async def telegram_read_all_public(hood=Depends(get_hood_unauthorized)):
telegrambots = await Telegram.objects.filter(hood=hood).all()
@ -62,27 +62,27 @@ async def telegram_read_all_public(hood=Depends(get_hood_unauthorized)):
@router.get(
'/',
"/",
# TODO response_model,
operation_id='get_telegrams',
operation_id="get_telegrams",
)
async def telegram_read_all(hood=Depends(get_hood)):
return await Telegram.objects.filter(hood=hood).all()
@router.get(
'/{telegram_id}',
"/{telegram_id}",
# TODO response_model,
operation_id='get_telegram',
operation_id="get_telegram",
)
async def telegram_read(telegram=Depends(get_telegram)):
return telegram
@router.delete(
'/{telegram_id}',
"/{telegram_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='delete_telegram',
operation_id="delete_telegram",
)
async def telegram_delete(telegram=Depends(get_telegram)):
spawner.stop(telegram)
@ -93,10 +93,10 @@ async def telegram_delete(telegram=Depends(get_telegram)):
@router.post(
'/',
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id='create_telegram',
operation_id="create_telegram",
)
async def telegram_create(
response: Response, values: BodyTelegram, hood=Depends(get_hood)
@ -104,17 +104,17 @@ async def telegram_create(
try:
telegram = await Telegram.objects.create(hood=hood, **values.__dict__)
spawner.start(telegram)
response.headers['Location'] = str(telegram.id)
response.headers["Location"] = str(telegram.id)
return telegram
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.put(
'/{telegram_id}',
"/{telegram_id}",
status_code=status.HTTP_202_ACCEPTED,
# TODO response_model,
operation_id='update_telegram',
operation_id="update_telegram",
)
async def telegram_update(values: BodyTelegram, telegram=Depends(get_telegram)):
try:
@ -127,20 +127,20 @@ async def telegram_update(values: BodyTelegram, telegram=Depends(get_telegram)):
@router.get(
'/{telegram_id}/status',
"/{telegram_id}/status",
status_code=status.HTTP_200_OK,
# TODO response_model,
operation_id='status_telegram',
operation_id="status_telegram",
)
async def telegram_status(telegram=Depends(get_telegram)):
return {'status': spawner.get(telegram).status.name}
return {"status": spawner.get(telegram).status.name}
@router.post(
'/{telegram_id}/start',
"/{telegram_id}/start",
status_code=status.HTTP_200_OK,
# TODO response_model,
operation_id='start_telegram',
operation_id="start_telegram",
)
async def telegram_start(telegram=Depends(get_telegram)):
await telegram.update(enabled=True)
@ -149,10 +149,10 @@ async def telegram_start(telegram=Depends(get_telegram)):
@router.post(
'/{telegram_id}/stop',
"/{telegram_id}/stop",
status_code=status.HTTP_200_OK,
# TODO response_model,
operation_id='stop_telegram',
operation_id="stop_telegram",
)
async def telegram_stop(telegram=Depends(get_telegram)):
await telegram.update(enabled=False)

View file

@ -13,4 +13,4 @@ class Test(Model):
hood: ForeignKey(Hood)
class Mapping(Mapping):
table_name = 'testapi'
table_name = "testapi"

View file

@ -30,39 +30,39 @@ async def get_test(test_id: int, hood=Depends(get_hood)):
router = APIRouter()
@router.get('/')
@router.get("/")
async def test_read_all(hood=Depends(get_hood)):
return await Test.objects.filter(hood=hood).all()
@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)):
try:
test = await Test.objects.create(hood=hood)
spawner.start(test)
response.headers['Location'] = str(test.id)
response.headers["Location"] = str(test.id)
return test
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.get('/{test_id}')
@router.get("/{test_id}")
async def test_read(test=Depends(get_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)):
spawner.stop(test)
await test.delete()
@router.get('/{test_id}/messages/')
@router.get("/{test_id}/messages/")
async def test_message_read_all(test=Depends(get_test)):
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)):
await spawner.get(test).publish(Message(message.text))
return {}

View file

@ -33,52 +33,52 @@ class TwitterBot(Censor):
async def run(self):
try:
if not self.twitter_model.verified:
raise ValueError('Oauth Handshake not completed')
raise ValueError("Oauth Handshake not completed")
self.client = PeonyClient(
consumer_key=config['twitter']['consumer_key'],
consumer_secret=config['twitter']['consumer_secret'],
consumer_key=config["twitter"]["consumer_key"],
consumer_secret=config["twitter"]["consumer_secret"],
access_token=self.twitter_model.access_token,
access_token_secret=self.twitter_model.access_token_secret,
)
if self.twitter_model.mentions_since_id is None:
logger.debug('since_id is None in model, fetch newest mention id')
logger.debug("since_id is None in model, fetch newest mention id")
await self._poll_mentions()
if self.twitter_model.dms_since_id is None:
logger.debug('since_id is None in model, fetch newest dm id')
logger.debug("since_id is None in model, fetch newest dm id")
await self._poll_direct_messages()
user = await self.client.user
if user.screen_name:
await self.twitter_model.update(username=user.screen_name)
logger.debug(
'Starting Twitter bot: {0}'.format(self.twitter_model.__dict__)
"Starting Twitter bot: {0}".format(self.twitter_model.__dict__)
)
await gather(self.poll(), self.push())
except CancelledError:
logger.debug(
'Bot {0} received Cancellation.'.format(self.twitter_model.hood.name)
"Bot {0} received Cancellation.".format(self.twitter_model.hood.name)
)
except exceptions.Unauthorized:
logger.debug(
'Bot {0} has invalid auth token.'.format(self.twitter_model.hood.name)
"Bot {0} has invalid auth token.".format(self.twitter_model.hood.name)
)
await self.twitter_model.update(enabled=False)
self.enabled = self.twitter_model.enabled
except (KeyError, ValueError, exceptions.NotAuthenticated):
logger.warning('Missing consumer_keys for Twitter in your configuration.')
logger.warning("Missing consumer_keys for Twitter in your configuration.")
await self.twitter_model.update(enabled=False)
self.enabled = self.twitter_model.enabled
finally:
logger.debug('Bot {0} stopped.'.format(self.twitter_model.hood.name))
logger.debug("Bot {0} stopped.".format(self.twitter_model.hood.name))
async def poll(self):
while True:
dms = await self._poll_direct_messages()
logger.debug(
'Polled dms ({0}): {1}'.format(self.twitter_model.hood.name, str(dms))
"Polled dms ({0}): {1}".format(self.twitter_model.hood.name, str(dms))
)
mentions = await self._poll_mentions()
logger.debug(
'Polled mentions ({0}): {1}'.format(
"Polled mentions ({0}): {1}".format(
self.twitter_model.hood.name, str(mentions)
)
)
@ -135,7 +135,7 @@ class TwitterBot(Censor):
remove_indices.update(range(url.indices[0], url.indices[1] + 1))
for symbol in entities.symbols:
remove_indices.update(range(symbol.indices[0], symbol.indices[1] + 1))
filtered_text = ''
filtered_text = ""
for index, character in enumerate(text):
if index not in remove_indices:
filtered_text += character
@ -145,11 +145,11 @@ class TwitterBot(Censor):
while True:
message = await self.receive()
logger.debug(
'Received message from censor ({0}): {1}'.format(
"Received message from censor ({0}): {1}".format(
self.twitter_model.hood.name, message.text
)
)
if hasattr(message, 'twitter_mention_id'):
if hasattr(message, "twitter_mention_id"):
await self._retweet(message.twitter_mention_id)
else:
await self._post_tweet(message.text)

View file

@ -20,4 +20,4 @@ class Twitter(Model):
enabled: Boolean() = False
class Mapping(Mapping):
table_name = 'twitterbots'
table_name = "twitterbots"

View file

@ -36,9 +36,9 @@ twitter_callback_router = APIRouter()
@router.get(
'/public',
"/public",
# TODO response_model,
operation_id='get_twitters_public',
operation_id="get_twitters_public",
)
async def twitter_read_all_public(hood=Depends(get_hood_unauthorized)):
twitterbots = await Twitter.objects.filter(hood=hood).all()
@ -50,28 +50,28 @@ async def twitter_read_all_public(hood=Depends(get_hood_unauthorized)):
@router.get(
'/',
"/",
# TODO response_model,
operation_id='get_twitters',
operation_id="get_twitters",
)
async def twitter_read_all(hood=Depends(get_hood)):
return await Twitter.objects.filter(hood=hood).all()
@router.get(
'/{twitter_id}',
"/{twitter_id}",
# TODO response_model
operation_id='get_twitter',
operation_id="get_twitter",
)
async def twitter_read(twitter=Depends(get_twitter)):
return twitter
@router.delete(
'/{twitter_id}',
"/{twitter_id}",
status_code=status.HTTP_204_NO_CONTENT,
# TODO response_model
operation_id='delete_twitter',
operation_id="delete_twitter",
)
async def twitter_delete(twitter=Depends(get_twitter)):
spawner.stop(twitter)
@ -80,20 +80,20 @@ async def twitter_delete(twitter=Depends(get_twitter)):
@router.get(
'/{twitter_id}/status',
"/{twitter_id}/status",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id='status_twitter',
operation_id="status_twitter",
)
async def twitter_status(twitter=Depends(get_twitter)):
return {'status': spawner.get(twitter).status.name}
return {"status": spawner.get(twitter).status.name}
@router.post(
'/{twitter_id}/start',
"/{twitter_id}/start",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id='start_twitter',
operation_id="start_twitter",
)
async def twitter_start(twitter=Depends(get_twitter)):
await twitter.update(enabled=True)
@ -102,10 +102,10 @@ async def twitter_start(twitter=Depends(get_twitter)):
@router.post(
'/{twitter_id}/stop',
"/{twitter_id}/stop",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id='stop_twitter',
operation_id="stop_twitter",
)
async def twitter_stop(twitter=Depends(get_twitter)):
await twitter.update(enabled=False)
@ -114,10 +114,10 @@ async def twitter_stop(twitter=Depends(get_twitter)):
@router.post(
'/',
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model
operation_id='create_twitter',
operation_id="create_twitter",
)
async def twitter_create(response: Response, hood=Depends(get_hood)):
"""
@ -129,20 +129,20 @@ async def twitter_create(response: Response, hood=Depends(get_hood)):
await corpse.delete()
# Create Twitter
request_token = await get_oauth_token(
config['twitter']['consumer_key'],
config['twitter']['consumer_secret'],
callback_uri='{0}/dashboard/twitter-callback?hood={1}'.format(
config['frontend_url'], hood.id
config["twitter"]["consumer_key"],
config["twitter"]["consumer_secret"],
callback_uri="{0}/dashboard/twitter-callback?hood={1}".format(
config["frontend_url"], hood.id
),
)
if request_token['oauth_callback_confirmed'] != 'true':
if request_token["oauth_callback_confirmed"] != "true":
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
twitter = await Twitter.objects.create(
hood=hood,
access_token=request_token['oauth_token'],
access_token_secret=request_token['oauth_token_secret'],
access_token=request_token["oauth_token"],
access_token_secret=request_token["oauth_token_secret"],
)
response.headers['Location'] = str(twitter.id)
response.headers["Location"] = str(twitter.id)
return twitter
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@ -151,9 +151,9 @@ async def twitter_create(response: Response, hood=Depends(get_hood)):
@twitter_callback_router.get(
'/callback',
"/callback",
# TODO response_model
operation_id='callback_twitter',
operation_id="callback_twitter",
)
async def twitter_read_callback(
oauth_token: str, oauth_verifier: str, hood=Depends(get_hood)
@ -161,15 +161,15 @@ async def twitter_read_callback(
try:
twitter = await Twitter.objects.filter(access_token=oauth_token).get()
access_token = await get_access_token(
config['twitter']['consumer_key'],
config['twitter']['consumer_secret'],
config["twitter"]["consumer_key"],
config["twitter"]["consumer_secret"],
twitter.access_token,
twitter.access_token_secret,
oauth_verifier,
)
await twitter.update(
access_token=access_token['oauth_token'],
access_token_secret=access_token['oauth_token_secret'],
access_token=access_token["oauth_token"],
access_token_secret=access_token["oauth_token_secret"],
verified=True,
enabled=True,
)

View file

@ -24,23 +24,23 @@ from kibicara.webapi.hoods.badwords import router as badwords_router
from kibicara.webapi.hoods.triggers import router as triggers_router
router = APIRouter()
router.include_router(admin_router, prefix='/admin', tags=['admin'])
router.include_router(admin_router, prefix="/admin", tags=["admin"])
hoods_router.include_router(
triggers_router, prefix='/{hood_id}/triggers', tags=['triggers']
triggers_router, prefix="/{hood_id}/triggers", tags=["triggers"]
)
hoods_router.include_router(
badwords_router, prefix='/{hood_id}/badwords', tags=['badwords']
badwords_router, prefix="/{hood_id}/badwords", tags=["badwords"]
)
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(
telegram_router, prefix='/{hood_id}/telegram', tags=['telegram']
telegram_router, prefix="/{hood_id}/telegram", tags=["telegram"]
)
hoods_router.include_router(
twitter_router, prefix='/{hood_id}/twitter', tags=['twitter']
twitter_router, prefix="/{hood_id}/twitter", tags=["twitter"]
)
router.include_router(twitter_callback_router, prefix="/twitter", tags=["twitter"])
hoods_router.include_router(email_router, prefix="/{hood_id}/email", tags=["email"])
router.include_router(hoods_router, prefix="/hoods")
hoods_router.include_router(
mastodon_router, prefix='/{hood_id}/mastodon', tags=['mastodon']
mastodon_router, prefix="/{hood_id}/mastodon", tags=["mastodon"]
)
router.include_router(twitter_callback_router, prefix='/twitter', tags=['twitter'])
hoods_router.include_router(email_router, prefix='/{hood_id}/email', tags=['email'])
router.include_router(hoods_router, prefix='/hoods')

View file

@ -37,10 +37,10 @@ class BodyEmail(BaseModel):
class BodyPassword(BaseModel):
password: str
@validator('password')
@validator("password")
def valid_password(cls, value):
if len(value) < 8:
raise ValueError('Password is too short')
raise ValueError("Password is too short")
return value
@ -50,22 +50,22 @@ class BodyAdmin(BodyEmail, BodyPassword):
class BodyAccessToken(BaseModel):
access_token: str
token_type: str = 'bearer'
token_type: str = "bearer"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/api/admin/login')
secret_box = SecretBox(bytes.fromhex(config['secret']))
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login")
secret_box = SecretBox(bytes.fromhex(config["secret"]))
def to_token(**kwargs):
return secret_box.encrypt(dumps(kwargs), encoder=URLSafeBase64Encoder).decode(
'ascii'
"ascii"
)
def from_token(token):
return loads(
secret_box.decrypt(token.encode('ascii'), encoder=URLSafeBase64Encoder)
secret_box.decrypt(token.encode("ascii"), encoder=URLSafeBase64Encoder)
)
@ -85,8 +85,8 @@ async def get_admin(access_token=Depends(oauth2_scheme)):
except (CryptoError, ValueError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail='Invalid authentication credentials',
headers={'WWW-Authenticate': 'Bearer'},
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return admin
@ -95,10 +95,10 @@ router = APIRouter()
@router.post(
'/register/',
"/register/",
status_code=status.HTTP_202_ACCEPTED,
response_model=BaseModel,
operation_id='register',
operation_id="register",
)
async def admin_register(values: BodyAdmin):
"""Sends an email with a confirmation link.
@ -107,28 +107,28 @@ async def admin_register(values: BodyAdmin):
- **password**: Password of new hood admin
"""
register_token = to_token(**values.__dict__)
logger.debug('register_token={0}'.format(register_token))
logger.debug("register_token={0}".format(register_token))
try:
admin = await Admin.objects.filter(email=values.email).all()
if admin:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
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)
email.send_email(
to=values.email,
subject='Confirm Account',
subject="Confirm Account",
body=body,
)
except (ConnectionRefusedError, SMTPException):
logger.exception('Email sending failed')
logger.exception("Email sending failed")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY)
return {}
@router.post(
'/confirm/{register_token}',
"/confirm/{register_token}",
response_model=BodyAccessToken,
operation_id='confirm',
operation_id="confirm",
)
async def admin_confirm(register_token: str):
"""Registration confirmation and account creation.
@ -137,17 +137,17 @@ async def admin_confirm(register_token: str):
"""
try:
values = from_token(register_token)
passhash = argon2.hash(values['password'])
await Admin.objects.create(email=values['email'], passhash=passhash)
passhash = argon2.hash(values["password"])
await Admin.objects.create(email=values["email"], passhash=passhash)
return BodyAccessToken(access_token=register_token)
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.post(
'/login/',
"/login/",
response_model=BodyAccessToken,
operation_id='login',
operation_id="login",
)
async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
"""Get an access token.
@ -160,17 +160,17 @@ async def admin_login(form_data: OAuth2PasswordRequestForm = Depends()):
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Incorrect email or password',
detail="Incorrect email or password",
)
token = to_token(email=form_data.username, password=form_data.password)
return BodyAccessToken(access_token=token)
@router.post(
'/reset/',
"/reset/",
status_code=status.HTTP_202_ACCEPTED,
response_model=BaseModel,
operation_id='reset',
operation_id="reset",
)
async def admin_reset_password(values: BodyEmail):
"""Sends an email with a password reset link.
@ -179,41 +179,41 @@ async def admin_reset_password(values: BodyEmail):
- **password**: Password of new hood admin
"""
register_token = to_token(datetime=datetime.now().isoformat(), **values.__dict__)
logger.debug('register_token={0}'.format(register_token))
logger.debug("register_token={0}".format(register_token))
try:
admin = await Admin.objects.filter(email=values.email).all()
if not admin:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
body = '{0}/password-reset?token={1}'.format(
config['frontend_url'], register_token
body = "{0}/password-reset?token={1}".format(
config["frontend_url"], register_token
)
logger.debug(body)
email.send_email(
to=values.email,
subject='Reset your password',
subject="Reset your password",
body=body,
)
except (ConnectionRefusedError, SMTPException):
logger.exception('Email sending failed')
logger.exception("Email sending failed")
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY)
return {}
@router.post(
'/reset/{reset_token}',
"/reset/{reset_token}",
response_model=BodyAccessToken,
operation_id='confirm_reset',
operation_id="confirm_reset",
)
async def admin_confirm_reset(reset_token: str, values: BodyPassword):
try:
token_values = from_token(reset_token)
if (
datetime.fromisoformat(token_values['datetime']) + timedelta(hours=3)
datetime.fromisoformat(token_values["datetime"]) + timedelta(hours=3)
< datetime.now()
):
raise HTTPException(status_code=status.HTTP_410_GONE)
passhash = argon2.hash(values.password)
admins = await Admin.objects.filter(email=token_values['email']).all()
admins = await Admin.objects.filter(email=token_values["email"]).all()
if len(admins) != 1:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
await admins[0].update(passhash=passhash)
@ -225,21 +225,21 @@ async def admin_confirm_reset(reset_token: str, values: BodyPassword):
@router.get(
'/hoods/',
"/hoods/",
# TODO response_model,
operation_id='get_hoods_admin',
operation_id="get_hoods_admin",
)
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()
await AdminHoodRelation.objects.select_related("hood").filter(admin=admin).all()
)
@router.get(
'/',
"/",
# TODO response_model,
operation_id='get_admin',
operation_id="get_admin",
)
async def admin_read(admin=Depends(get_admin)):
"""Get a list of all hoods of a given admin."""
@ -250,17 +250,17 @@ async def admin_read(admin=Depends(get_admin)):
@router.delete(
'/',
"/",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='delete_admin',
operation_id="delete_admin",
)
async def admin_delete(admin=Depends(get_admin)):
hood_relations = (
await AdminHoodRelation.objects.select_related('hood').filter(admin=admin).all()
await AdminHoodRelation.objects.select_related("hood").filter(admin=admin).all()
)
for hood in hood_relations:
admins = (
await AdminHoodRelation.objects.select_related('admin')
await AdminHoodRelation.objects.select_related("admin")
.filter(hood=hood.id)
.all()
)

View file

@ -20,9 +20,9 @@ from kibicara.webapi.utils import delete_hood
class BodyHood(BaseModel):
name: str
landingpage: str = '''
landingpage: str = """
Default Landing Page
'''
"""
async def get_hood_unauthorized(hood_id: int):
@ -39,7 +39,7 @@ async def get_hood(hood=Depends(get_hood_unauthorized), admin=Depends(get_admin)
except NoMatch:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={'WWW-Authenticate': 'Bearer'},
headers={"WWW-Authenticate": "Bearer"},
)
return hood
@ -48,10 +48,10 @@ router = APIRouter()
@router.get(
'/',
"/",
# TODO response_model,
operation_id='get_hoods',
tags=['hoods'],
operation_id="get_hoods",
tags=["hoods"],
)
async def hood_read_all():
"""Get all existing hoods."""
@ -59,11 +59,11 @@ async def hood_read_all():
@router.post(
'/',
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id='create_hood',
tags=['hoods'],
operation_id="create_hood",
tags=["hoods"],
)
async def hood_create(values: BodyHood, response: Response, admin=Depends(get_admin)):
"""Creates a hood.
@ -77,19 +77,19 @@ async def hood_create(values: BodyHood, response: Response, admin=Depends(get_ad
spawner.start(hood)
# Initialize Triggers to match all
await Trigger.objects.create(hood=hood, pattern='.')
await Trigger.objects.create(hood=hood, pattern=".")
response.headers['Location'] = str(hood.id)
response.headers["Location"] = str(hood.id)
return hood
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.get(
'/{hood_id}',
"/{hood_id}",
# TODO response_model,
operation_id='get_hood',
tags=['hoods'],
operation_id="get_hood",
tags=["hoods"],
)
async def hood_read(hood=Depends(get_hood_unauthorized)):
"""Get hood with id **hood_id**."""
@ -97,10 +97,10 @@ async def hood_read(hood=Depends(get_hood_unauthorized)):
@router.put(
'/{hood_id}',
"/{hood_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='update_hood',
tags=['hoods'],
operation_id="update_hood",
tags=["hoods"],
)
async def hood_update(values: BodyHood, hood=Depends(get_hood)):
"""Updates hood with id **hood_id**.
@ -113,10 +113,10 @@ async def hood_update(values: BodyHood, hood=Depends(get_hood)):
@router.delete(
'/{hood_id}',
"/{hood_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='delete_hood',
tags=['hoods'],
operation_id="delete_hood",
tags=["hoods"],
)
async def hood_delete(hood=Depends(get_hood)):
"""Deletes hood with id **hood_id**."""

View file

@ -38,9 +38,9 @@ router = APIRouter()
@router.get(
'/',
"/",
# TODO response_model,
operation_id='get_badwords',
operation_id="get_badwords",
)
async def badword_read_all(hood=Depends(get_hood)):
"""Get all badwords of hood with id **hood_id**."""
@ -48,10 +48,10 @@ async def badword_read_all(hood=Depends(get_hood)):
@router.post(
'/',
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id='create_badword',
operation_id="create_badword",
)
async def badword_create(
values: BodyBadWord, response: Response, hood=Depends(get_hood)
@ -63,7 +63,7 @@ async def badword_create(
try:
regex_compile(values.pattern)
badword = await BadWord.objects.create(hood=hood, **values.__dict__)
response.headers['Location'] = str(badword.id)
response.headers["Location"] = str(badword.id)
return badword
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@ -72,9 +72,9 @@ async def badword_create(
@router.get(
'/{badword_id}',
"/{badword_id}",
# TODO response_model,
operation_id='get_badword',
operation_id="get_badword",
)
async def badword_read(badword=Depends(get_badword)):
"""Reads badword with id **badword_id** for hood with id **hood_id**."""
@ -82,9 +82,9 @@ async def badword_read(badword=Depends(get_badword)):
@router.put(
'/{badword_id}',
"/{badword_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='update_badword',
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**.
@ -96,9 +96,9 @@ async def badword_update(values: BodyBadWord, badword=Depends(get_badword)):
@router.delete(
'/{badword_id}',
"/{badword_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='delete_badword',
operation_id="delete_badword",
)
async def badword_delete(badword=Depends(get_badword)):
"""Deletes badword with id **badword_id** for hood with id **hood_id**."""

View file

@ -39,9 +39,9 @@ router = APIRouter()
@router.get(
'/',
"/",
# TODO response_model,
operation_id='get_triggers',
operation_id="get_triggers",
)
async def trigger_read_all(hood=Depends(get_hood)):
"""Get all triggers of hood with id **hood_id**."""
@ -49,10 +49,10 @@ async def trigger_read_all(hood=Depends(get_hood)):
@router.post(
'/',
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id='create_trigger',
operation_id="create_trigger",
)
async def trigger_create(
values: BodyTrigger, response: Response, hood=Depends(get_hood)
@ -64,7 +64,7 @@ async def trigger_create(
try:
regex_compile(values.pattern)
trigger = await Trigger.objects.create(hood=hood, **values.__dict__)
response.headers['Location'] = str(trigger.id)
response.headers["Location"] = str(trigger.id)
return trigger
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@ -73,9 +73,9 @@ async def trigger_create(
@router.get(
'/{trigger_id}',
"/{trigger_id}",
# TODO response_model,
operation_id='get_trigger',
operation_id="get_trigger",
)
async def trigger_read(trigger=Depends(get_trigger)):
"""Reads trigger with id **trigger_id** for hood with id **hood_id**."""
@ -83,9 +83,9 @@ async def trigger_read(trigger=Depends(get_trigger)):
@router.put(
'/{trigger_id}',
"/{trigger_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='update_trigger',
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**.
@ -97,9 +97,9 @@ async def trigger_update(values: BodyTrigger, trigger=Depends(get_trigger)):
@router.delete(
'/{trigger_id}',
"/{trigger_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id='delete_trigger',
operation_id="delete_trigger",
)
async def trigger_delete(trigger=Depends(get_trigger)):
"""Deletes trigger with id **trigger_id** for hood with id **hood_id**."""

View file

@ -6,10 +6,10 @@ from fastapi import status
def test_hoods_unauthorized(client):
response = client.get('/api/admin/hoods/')
response = client.get("/api/admin/hoods/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_hoods_success(client, auth_header):
response = client.get('/api/admin/hoods/', headers=auth_header)
response = client.get("/api/admin/hoods/", headers=auth_header)
assert response.status_code == status.HTTP_200_OK

View file

@ -8,75 +8,75 @@ from fastapi import status
def test_hood_read_all(client):
response = client.get('/api/hoods/')
response = client.get("/api/hoods/")
assert response.status_code == status.HTTP_200_OK
def test_hood_create_unauthorized(client, hood_id):
response = client.post('/api/hoods/')
response = client.post("/api/hoods/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_hood_read(client, hood_id):
response = client.get('/api/hoods/{0}'.format(hood_id))
response = client.get("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_200_OK
def test_hood_update_unauthorized(client, hood_id):
response = client.put('/api/hoods/{0}'.format(hood_id))
response = client.put("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_hood_delete_unauthorized(client, hood_id):
response = client.delete('/api/hoods/{0}'.format(hood_id))
response = client.delete("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_read_all_unauthorized(client, hood_id):
response = client.get('/api/hoods/{0}/triggers/'.format(hood_id))
response = client.get("/api/hoods/{0}/triggers/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_create_unauthorized(client, hood_id):
response = client.post('/api/hoods/{0}/triggers/'.format(hood_id))
response = client.post("/api/hoods/{0}/triggers/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_read_unauthorized(client, hood_id, trigger_id):
response = client.get('/api/hoods/{0}/triggers/{1}'.format(hood_id, trigger_id))
response = client.get("/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_update_unauthorized(client, hood_id, trigger_id):
response = client.put('/api/hoods/{0}/triggers/{1}'.format(hood_id, trigger_id))
response = client.put("/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_delete_unauthorized(client, hood_id, trigger_id):
response = client.delete('/api/hoods/{0}/triggers/{1}'.format(hood_id, trigger_id))
response = client.delete("/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_read_all_unauthorized(client, hood_id):
response = client.get('/api/hoods/{0}/badwords/'.format(hood_id))
response = client.get("/api/hoods/{0}/badwords/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_create_unauthorized(client, hood_id):
response = client.post('/api/hoods/{0}/badwords/'.format(hood_id))
response = client.post("/api/hoods/{0}/badwords/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_read_unauthorized(client, hood_id, badword_id):
response = client.get('/api/hoods/{0}/badwords/{1}'.format(hood_id, badword_id))
response = client.get("/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_update_unauthorized(client, hood_id, badword_id):
response = client.put('/api/hoods/{0}/badwords/{1}'.format(hood_id, badword_id))
response = client.put("/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_delete_unauthorized(client, hood_id, badword_id):
response = client.delete('/api/hoods/{0}/badwords/{1}'.format(hood_id, badword_id))
response = client.delete("/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -9,16 +9,16 @@ from fastapi import status
from pytest import fixture
@fixture(scope='function')
@fixture(scope="function")
def email_row(client, hood_id, auth_header):
response = client.post(
'/api/hoods/{0}/email/'.format(hood_id),
json={'name': 'kibicara-test'},
"/api/hoods/{0}/email/".format(hood_id),
json={"name": "kibicara-test"},
headers=auth_header,
)
assert response.status_code == status.HTTP_201_CREATED
email_id = int(response.headers['Location'])
email_id = int(response.headers["Location"])
yield response.json()
client.delete(
'/api/hoods/{0}/email/{1}'.format(hood_id, email_id), headers=auth_header
"/api/hoods/{0}/email/{1}".format(hood_id, email_id), headers=auth_header
)

View file

@ -15,49 +15,49 @@ from kibicara.webapi.admin import to_token
def test_email_subscribe_unsubscribe(client, hood_id, receive_email):
response = client.post(
'/api/hoods/{0}/email/subscribe/'.format(hood_id),
json={'email': 'test@localhost'},
"/api/hoods/{0}/email/subscribe/".format(hood_id),
json={"email": "test@localhost"},
)
assert response.status_code == status.HTTP_202_ACCEPTED
mail = receive_email()
body = mail['body']
body = mail["body"]
confirm_url = findall(
r'http[s]?://'
+ r'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+',
r"http[s]?://"
+ r"(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+",
body,
)[0]
start = len('token=')
start = len("token=")
response = client.post(
'/api/hoods/{0}/email/subscribe/confirm/{1}'.format(
"/api/hoods/{0}/email/subscribe/confirm/{1}".format(
hood_id, urlparse(confirm_url).query[start:]
)
)
assert response.status_code == status.HTTP_201_CREATED
response = client.post(
'/api/hoods/{0}/email/subscribe/confirm/{1}'.format(
"/api/hoods/{0}/email/subscribe/confirm/{1}".format(
hood_id, urlparse(confirm_url).query[start:]
)
)
assert response.status_code == status.HTTP_409_CONFLICT
token = to_token(email=mail['to'], hood=hood_id)
token = to_token(email=mail["to"], hood=hood_id)
response = client.delete(
'/api/hoods/{0}/email/unsubscribe/{1}'.format(hood_id, token)
"/api/hoods/{0}/email/unsubscribe/{1}".format(hood_id, token)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
def test_email_message(client, hood_id, trigger_id, email_row):
body = {
'text': 'test',
'author': 'test@localhost',
'secret': email_row['secret'],
"text": "test",
"author": "test@localhost",
"secret": email_row["secret"],
}
response = client.post('/api/hoods/{0}/email/messages/'.format(hood_id), json=body)
response = client.post("/api/hoods/{0}/email/messages/".format(hood_id), json=body)
assert response.status_code == status.HTTP_201_CREATED
def test_email_send_mda(trigger_id, email_row):
skip('Only works if kibicara is listening on port 8000, and only sometimes')
skip("Only works if kibicara is listening on port 8000, and only sometimes")
mail = """From test@example.com Tue Jun 16 15:33:19 2020
Return-path: <test@example.com>
Envelope-to: hood@localhost
@ -85,6 +85,6 @@ test
--AqNPlAX243a8sip3B7kXv8UKD8wuti--
"""
proc = subprocess.run(
['kibicara_mda', 'hood'], stdout=subprocess.PIPE, input=mail, encoding='ascii'
["kibicara_mda", "hood"], stdout=subprocess.PIPE, input=mail, encoding="ascii"
)
assert proc.returncode == 0

View file

@ -8,18 +8,18 @@ from fastapi import status
def test_email_create_unauthorized(client, hood_id):
response = client.post('/api/hoods/{0}/email/'.format(hood_id))
response = client.post("/api/hoods/{0}/email/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_email_delete_unauthorized(client, hood_id, email_row):
response = client.delete(
'/api/hoods/{0}/email/{1}'.format(hood_id, email_row['id'])
"/api/hoods/{0}/email/{1}".format(hood_id, email_row["id"])
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_email_message_unauthorized(client, hood_id, email_row):
body = {'text': 'test', 'author': 'author', 'secret': 'wrong'}
response = client.post('/api/hoods/{0}/email/messages/'.format(hood_id), json=body)
body = {"text": "test", "author": "author", "secret": "wrong"}
response = client.post("/api/hoods/{0}/email/messages/".format(hood_id), json=body)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -8,15 +8,15 @@ from nacl.exceptions import CryptoError
def test_email_subscribe_empty(client, hood_id):
response = client.post('/api/hoods/{0}/email/subscribe/'.format(hood_id))
response = client.post("/api/hoods/{0}/email/subscribe/".format(hood_id))
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_email_subscribe_confirm_wrong_token(client, hood_id):
try:
response = client.post(
'/api/hoods/{0}/email/subscribe/confirm/'.format(hood_id)
+ 'asdfasdfasdfasdfasdfasdfasdfasdf'
"/api/hoods/{0}/email/subscribe/confirm/".format(hood_id)
+ "asdfasdfasdfasdfasdfasdfasdfasdf"
)
assert response.status_code is not status.HTTP_201_CREATED
except CryptoError:
@ -25,25 +25,25 @@ def test_email_subscribe_confirm_wrong_token(client, hood_id):
def test_email_subscribe_confirm_wrong_hood(client):
response = client.delete(
'/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf'
"/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf"
)
assert response.json()['detail'] == 'Not Found'
assert response.json()["detail"] == "Not Found"
def test_email_message_wrong(client, hood_id, email_row):
body = {
'text': '',
'author': 'test@localhost',
'secret': email_row['secret'],
"text": "",
"author": "test@localhost",
"secret": email_row["secret"],
}
response = client.post('/api/hoods/{0}/email/messages/'.format(hood_id), json=body)
response = client.post("/api/hoods/{0}/email/messages/".format(hood_id), json=body)
assert response.status_code == status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS
def test_email_unsubscribe_wrong_token(client, hood_id):
try:
client.delete(
'/api/hoods/{0}/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf'.format(
"/api/hoods/{0}/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf".format(
hood_id
)
)
@ -53,6 +53,6 @@ def test_email_unsubscribe_wrong_token(client, hood_id):
def test_email_unsubscribe_wrong_hood(client):
response = client.delete(
'/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf'
"/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf"
)
assert response.json()['detail'] == 'Not Found'
assert response.json()["detail"] == "Not Found"

View file

@ -9,13 +9,13 @@ from kibicara.model import Hood
from kibicara.platforms.telegram.model import Telegram
@fixture(scope='function')
@fixture(scope="function")
def telegram(event_loop, hood_id, bot):
hood = event_loop.run_until_complete(Hood.objects.get(id=hood_id))
return event_loop.run_until_complete(
Telegram.objects.create(
hood=hood,
api_token=bot['api_token'],
welcome_message=bot['welcome_message'],
api_token=bot["api_token"],
welcome_message=bot["welcome_message"],
)
)

View file

@ -10,16 +10,16 @@ from kibicara.platforms import telegram
from kibicara.platforms.telegram.model import Telegram
@fixture(scope='function')
@fixture(scope="function")
def disable_spawner(monkeypatch):
class DoNothing:
def start(self, bot):
assert bot is not None
monkeypatch.setattr(telegram.webapi, 'spawner', DoNothing())
monkeypatch.setattr(telegram.webapi, "spawner", DoNothing())
@mark.parametrize('body', [{'api_token': 'string', 'welcome_message': 'string'}])
@mark.parametrize("body", [{"api_token": "string", "welcome_message": "string"}])
def test_telegram_create_bot(
event_loop,
client,
@ -32,27 +32,27 @@ def test_telegram_create_bot(
def check_token_mock(token):
return True
monkeypatch.setattr(telegram.webapi, 'check_token', check_token_mock)
monkeypatch.setattr(telegram.webapi, "check_token", check_token_mock)
response = client.post(
'/api/hoods/{0}/telegram/'.format(hood_id),
"/api/hoods/{0}/telegram/".format(hood_id),
json=body,
headers=auth_header,
)
assert response.status_code == status.HTTP_201_CREATED
bot_id = response.json()['id']
bot_id = response.json()["id"]
telegram_obj = event_loop.run_until_complete(Telegram.objects.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 (
response.json()['welcome_message']
== body['welcome_message']
response.json()["welcome_message"]
== body["welcome_message"]
== telegram_obj.welcome_message
)
assert response.json()['hood']['id'] == telegram_obj.hood.id
assert response.json()["hood"]["id"] == telegram_obj.hood.id
assert telegram_obj.enabled
@mark.parametrize('body', [{'api_token': 'string', 'welcome_message': 'string'}])
@mark.parametrize("body", [{"api_token": "string", "welcome_message": "string"}])
def test_telegram_invalid_api_token(
event_loop,
client,
@ -63,7 +63,7 @@ def test_telegram_invalid_api_token(
body,
):
response = client.post(
'/api/hoods/{0}/telegram/'.format(hood_id),
"/api/hoods/{0}/telegram/".format(hood_id),
json=body,
headers=auth_header,
)
@ -71,12 +71,12 @@ def test_telegram_invalid_api_token(
def test_telegram_create_telegram_invalid_id(client, auth_header):
response = client.post('/api/hoods/1337/telegram/', headers=auth_header)
response = client.post("/api/hoods/1337/telegram/", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.post('/api/hoods/wrong/telegram/', headers=auth_header)
response = client.post("/api/hoods/wrong/telegram/", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_telegram_create_unauthorized(client, hood_id):
response = client.post('/api/hoods/{hood_id}/telegram/')
response = client.post("/api/hoods/{hood_id}/telegram/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -10,7 +10,7 @@ from pytest import mark, raises
from kibicara.platforms.telegram.model import Telegram, TelegramUser
@mark.parametrize('bot', [{'api_token': 'apitoken123', 'welcome_message': 'msg'}])
@mark.parametrize("bot", [{"api_token": "apitoken123", "welcome_message": "msg"}])
def test_telegram_delete_bot(client, event_loop, bot, telegram, auth_header):
event_loop.run_until_complete(
TelegramUser.objects.create(user_id=1234, bot=telegram.id)
@ -19,7 +19,7 @@ def test_telegram_delete_bot(client, event_loop, bot, telegram, auth_header):
TelegramUser.objects.create(user_id=5678, bot=telegram.id)
)
response = 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,
)
assert response.status_code == status.HTTP_204_NO_CONTENT
@ -30,23 +30,23 @@ def test_telegram_delete_bot(client, event_loop, bot, telegram, auth_header):
def test_telegram_delete_bot_invalid_id(client, auth_header, hood_id):
response = client.delete('/api/hoods/1337/telegram/123', headers=auth_header)
response = client.delete("/api/hoods/1337/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.delete('/api/hoods/wrong/telegram/123', headers=auth_header)
response = client.delete("/api/hoods/wrong/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
response = client.delete(
'/api/hoods/{0}/telegram/7331'.format(hood_id), headers=auth_header
"/api/hoods/{0}/telegram/7331".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.delete(
'/api/hoods/{0}/telegram/wrong'.format(hood_id), headers=auth_header
"/api/hoods/{0}/telegram/wrong".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@mark.parametrize('bot', [{'api_token': 'apitoken123', 'welcome_message': 'msg'}])
@mark.parametrize("bot", [{"api_token": "apitoken123", "welcome_message": "msg"}])
def test_telegram_delete_bot_unauthorized(client, bot, telegram):
response = client.delete(
'/api/hoods/{0}/telegram/{1}'.format(telegram.hood.id, telegram.id)
"/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -7,36 +7,36 @@ from fastapi import status
from pytest import mark
@mark.parametrize('bot', [{'api_token': 'apitoken123', 'welcome_message': 'msg'}])
@mark.parametrize("bot", [{"api_token": "apitoken123", "welcome_message": "msg"}])
def test_telegram_get_bot(client, auth_header, event_loop, bot, telegram):
response = client.get(
'/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,
)
assert response.status_code == status.HTTP_200_OK
assert response.json()['id'] == telegram.id
assert response.json()['api_token'] == telegram.api_token
assert response.json()['welcome_message'] == telegram.welcome_message
assert response.json()["id"] == telegram.id
assert response.json()["api_token"] == telegram.api_token
assert response.json()["welcome_message"] == telegram.welcome_message
def test_telegram_get_bot_invalid_id(client, auth_header, hood_id):
response = client.get('/api/hoods/1337/telegram/123', headers=auth_header)
response = client.get("/api/hoods/1337/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.get('/api/hoods/wrong/telegram/123', headers=auth_header)
response = client.get("/api/hoods/wrong/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
response = client.get(
'/api/hoods/{0}/telegram/7331'.format(hood_id), headers=auth_header
"/api/hoods/{0}/telegram/7331".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.get(
'/api/hoods/{0}/telegram/wrong'.format(hood_id), headers=auth_header
"/api/hoods/{0}/telegram/wrong".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@mark.parametrize('bot', [{'api_token': 'apitoken456', 'welcome_message': 'msg'}])
@mark.parametrize("bot", [{"api_token": "apitoken456", "welcome_message": "msg"}])
def test_telegram_get_bot_unauthorized(client, bot, telegram):
response = client.get(
'/api/hoods/{0}/telegram/{1}'.format(telegram.hood.id, telegram.id)
"/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -14,34 +14,34 @@ def test_telegram_get_bots(client, auth_header, event_loop, hood_id):
telegram0 = event_loop.run_until_complete(
Telegram.objects.create(
hood=hood,
api_token='api_token123',
welcome_message='welcome_message123',
api_token="api_token123",
welcome_message="welcome_message123",
)
)
telegram1 = event_loop.run_until_complete(
Telegram.objects.create(
hood=hood,
api_token='api_token456',
welcome_message='welcome_message123',
api_token="api_token456",
welcome_message="welcome_message123",
)
)
response = client.get(
'/api/hoods/{0}/telegram'.format(telegram0.hood.id), headers=auth_header
"/api/hoods/{0}/telegram".format(telegram0.hood.id), headers=auth_header
)
assert response.status_code == status.HTTP_200_OK
assert response.json()[0]['id'] == telegram0.id
assert response.json()[0]['api_token'] == telegram0.api_token
assert response.json()[1]['id'] == telegram1.id
assert response.json()[1]['api_token'] == telegram1.api_token
assert response.json()[0]["id"] == telegram0.id
assert response.json()[0]["api_token"] == telegram0.api_token
assert response.json()[1]["id"] == telegram1.id
assert response.json()[1]["api_token"] == telegram1.api_token
def test_telegram_get_bots_invalid_id(client, auth_header, hood_id):
response = client.get('/api/hoods/1337/telegram', headers=auth_header)
response = client.get("/api/hoods/1337/telegram", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.get('/api/hoods/wrong/telegram', headers=auth_header)
response = client.get("/api/hoods/wrong/telegram", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_telegram_get_bots_unauthorized(client, hood_id):
response = client.get('/api/hoods/{0}/telegram'.format(hood_id))
response = client.get("/api/hoods/{0}/telegram".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -2,7 +2,7 @@
"name": "kibicara-frontend",
"version": "0.0.0",
"scripts": {
"openapi-generator": "openapi-generator generate -g typescript-angular --additional-properties=providedInRoot=true -o src/app/core/api -i http://localhost:8000/api/openapi.json",
"openapi-generator": "wget http://localhost:8000/api/openapi.json && openapi-generator generate -g typescript-angular --additional-properties=providedInRoot=true -o src/app/core/api -i openapi.json && rm openapi.json",
"ng": "ng",
"start": "ng serve",
"build": "ng build",