Compare commits

..

4 commits

81 changed files with 523 additions and 2246 deletions

View file

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

View file

@ -1,4 +1,4 @@
Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me> Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me>
Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>

View file

@ -34,8 +34,6 @@ install_requires =
pytoml pytoml
requests requests
scrypt scrypt
Mastodon.py
pydantic[email]
[options.packages.find] [options.packages.find]
where = src where = src
@ -55,12 +53,11 @@ deps =
black black
flake8 flake8
mypy mypy
types-requests
commands = commands =
black --check --diff src tests black -S --check --diff src tests
flake8 src tests flake8 src tests
# not yet # not yet
#mypy --ignore-missing-imports src tests #mypy src tests
[testenv] [testenv]
deps = deps =

View file

@ -1,27 +1,84 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
"""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.secret import SecretBox
from nacl.utils import random 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. """Default configuration.
The default configuration gets overwritten by a configuration file if one exists. The default configuration gets overwritten by a configuration file if one exists.
""" """
config = {
"database_connection": "sqlite:////tmp/kibicara.sqlite", args = None
"frontend_url": "http://localhost:4200", # url of frontend, change in prod
"secret": random(SecretBox.KEY_SIZE).hex(), # generate with: openssl rand -hex 32 if argv[0].endswith('kibicara'):
# production params parser = ArgumentParser()
"frontend_path": None, # required, path to frontend html/css/js files parser.add_argument(
"production": True, '-f',
"behind_proxy": False, '--config',
"keyfile": None, # optional for ssl dest='configfile',
"certfile": None, # optional for ssl default='/etc/kibicara.conf',
# dev params help='path to config file',
"root_url": "http://localhost:8000", # url of backend )
"cors_allow_origin": "http://localhost:4200", 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

View file

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

View file

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
@ -6,7 +6,6 @@
"""Entrypoint of Kibicara.""" """Entrypoint of Kibicara."""
from argparse import ArgumentParser
from asyncio import run as asyncio_run from asyncio import run as asyncio_run
from logging import DEBUG, INFO, WARNING, basicConfig, getLogger from logging import DEBUG, INFO, WARNING, basicConfig, getLogger
@ -15,9 +14,8 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from hypercorn.asyncio import serve from hypercorn.asyncio import serve
from hypercorn.config import Config from hypercorn.config import Config
from pytoml import load
from kibicara.config import config from kibicara.config import args, config
from kibicara.model import Mapping from kibicara.model import Mapping
from kibicara.platformapi import Spawner from kibicara.platformapi import Spawner
from kibicara.webapi import router from kibicara.webapi import router
@ -33,29 +31,9 @@ class Main:
""" """
def __init__(self): def __init__(self):
parser = ArgumentParser() asyncio_run(self.__run())
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 = { LOGLEVELS = {
None: WARNING, None: WARNING,
1: INFO, 1: INFO,
@ -63,13 +41,10 @@ class Main:
} }
basicConfig( basicConfig(
level=LOGLEVELS.get(args.verbose, DEBUG), 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() Mapping.create_all()
asyncio_run(self.__run())
async def __run(self):
await Spawner.init_all() await Spawner.init_all()
await self.__start_webserver() await self.__start_webserver()
@ -78,31 +53,31 @@ class Main:
async def get_response(self, path, scope): async def get_response(self, path, scope):
response = await super().get_response(path, scope) response = await super().get_response(path, scope)
if response.status_code == 404: if response.status_code == 404:
response = await super().get_response(".", scope) response = await super().get_response('.', scope)
return response return response
app = FastAPI() app = FastAPI()
server_config = Config() server_config = Config()
server_config.accesslog = "-" server_config.accesslog = '-'
server_config.behind_proxy = config["behind_proxy"] server_config.behind_proxy = config['behind_proxy']
server_config.keyfile = config["keyfile"] server_config.keyfile = config['keyfile']
server_config.certfile = config["certfile"] server_config.certfile = config['certfile']
if config["production"]: if config['production']:
server_config.bind = ["0.0.0.0:8000", "[::]:8000"] server_config.bind = ['0.0.0.0:8000', '[::]:8000']
api = FastAPI() api = FastAPI()
api.include_router(router) api.include_router(router)
app.mount("/api", api) app.mount('/api', api)
if not config["production"] and config["cors_allow_origin"]: if not config['production'] and config['cors_allow_origin']:
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=config["cors_allow_origin"], allow_origins=config['cors_allow_origin'],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=['*'],
allow_headers=["*"], allow_headers=['*'],
) )
if config["frontend_path"] is not None: if config['frontend_path'] is not None:
app.mount( app.mount(
"/", '/',
app=SinglePageApplication(directory=config["frontend_path"], html=True), app=SinglePageApplication(directory=config['frontend_path'], html=True),
) )
await serve(app, server_config) await serve(app, server_config)

View file

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

View file

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

View file

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

View file

@ -1,11 +1,10 @@
# Copyright (C) 2020 by Maike <maike@systemli.org> # Copyright (C) 2020 by Maike <maike@systemli.org>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from argparse import ArgumentParser
from asyncio import run as asyncio_run from asyncio import run as asyncio_run
from email.parser import BytesParser from email.parser import BytesParser
from email.policy import default from email.policy import default
@ -16,10 +15,9 @@ from sys import stdin
from fastapi import status from fastapi import status
from ormantic import NoMatch from ormantic import NoMatch
from pytoml import load
from requests import post from requests import post
from kibicara.config import config from kibicara.config import args, config
from kibicara.platforms.email.model import Email, EmailSubscribers from kibicara.platforms.email.model import Email, EmailSubscribers
logger = getLogger(__name__) logger = getLogger(__name__)
@ -27,82 +25,62 @@ logger = getLogger(__name__)
class Main: class Main:
def __init__(self): def __init__(self):
parser = ArgumentParser() asyncio_run(self.__run())
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 # extract email from the recipient
email_name = args.recipient.lower() email_name = args.recipient.lower()
asyncio_run(self.__run(email_name))
async def __run(self, email_name):
try: try:
email = await Email.objects.get(name=email_name) email = await Email.objects.get(name=email_name)
except NoMatch: except NoMatch:
logger.error("No recipient with this name") logger.error('No recipient with this name')
exit(1) exit(1)
# read mail from STDIN and parse to EmailMessage object # read mail from STDIN and parse to EmailMessage object
message = BytesParser(policy=default).parsebytes(stdin.buffer.read()) message = BytesParser(policy=default).parsebytes(stdin.buffer.read())
sender = "" sender = ''
if message.get("sender"): if message.get('sender'):
sender = message.get("sender") sender = message.get('sender')
elif message.get("from"): elif message.get('from'):
sender = message.get("from") sender = message.get('from')
else: else:
logger.error("No Sender of From header") logger.error('No Sender of From header')
exit(1) exit(1)
sender = parseaddr(sender)[1] sender = parseaddr(sender)[1]
if not sender: if not sender:
logger.error("Could not parse sender") logger.error('Could not parse sender')
exit(1) exit(1)
maybe_subscriber = await EmailSubscribers.objects.filter(email=sender).all() maybe_subscriber = await EmailSubscribers.objects.filter(email=sender).all()
if len(maybe_subscriber) != 1 or maybe_subscriber[0].hood.id != email.hood.id: if len(maybe_subscriber) != 1 or maybe_subscriber[0].hood.id != email.hood.id:
logger.error("Not a subscriber") logger.error('Not a subscriber')
exit(1) exit(1)
# extract relevant data from mail # extract relevant data from mail
text = sub( text = sub(
r"<[^>]*>", r'<[^>]*>',
"", '',
message.get_body(preferencelist=("plain", "html")).get_content(), message.get_body(preferencelist=('plain', 'html')).get_content(),
) )
response = post( response = post(
"{0}/api/hoods/{1}/email/messages/".format( '{0}/api/hoods/{1}/email/messages/'.format(
config["root_url"], email.hood.pk 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: if response.status_code == status.HTTP_201_CREATED:
exit(0) exit(0)
elif response.status_code == status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS: 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: 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: 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: else:
logger.error( 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) exit(1)

View file

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

View file

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

View file

@ -1,101 +0,0 @@
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from asyncio import get_event_loop, sleep
from kibicara.platformapi import Censor, Spawner, Message
from kibicara.platforms.mastodon.model import MastodonAccount
from logging import getLogger
from mastodon import Mastodon, MastodonError
from asyncio import gather
import re
logger = getLogger(__name__)
class MastodonBot(Censor):
def __init__(self, mastodon_account_model):
super().__init__(mastodon_account_model.hood)
self.model = mastodon_account_model
self.enabled = self.model.enabled
self.polling_interval_sec = 60
@classmethod
async def destroy_hood(cls, hood):
"""Removes all its database entries."""
for mastodon in await MastodonAccount.objects.filter(hood=hood).all():
await mastodon.delete()
async def run(self):
try:
await self.model.instance.load()
self.account = Mastodon(
client_id=self.model.instance.client_id,
client_secret=self.model.instance.client_secret,
api_base_url=self.model.instance.name,
access_token=self.model.access_token,
)
account_details = await get_event_loop().run_in_executor(
None, self.account.account_verify_credentials
)
if username := account_details.get("username"):
await self.model.update(username=username)
await gather(self.poll(), self.push())
except Exception as e:
logger.debug("Bot {0} threw an Error: {1}".format(self.model.hood.name, e))
finally:
logger.debug("Bot {0} stopped.".format(self.model.hood.name))
async def poll(self):
"""Get new mentions and DMs from Mastodon"""
while True:
try:
notifications = await get_event_loop().run_in_executor(
None, self.account.notifications
)
except MastodonError as e:
logger.warning("%s in hood %s" % (e, self.model.hood.name))
continue
for status in notifications:
try:
status_id = int(status["status"]["id"])
except KeyError:
self.account.notifications_dismiss(status["id"])
continue # ignore notifications which don't have a status
text = re.sub(r"<[^>]*>", "", status["status"]["content"])
text = re.sub(
"(?<=^|(?<=[^a-zA-Z0-9-_.]))@([A-Za-z]+[A-Za-z0-9-_]+)", "", text
)
logger.debug(
"Mastodon in %s received toot #%s: %s"
% (self.model.hood.name, status_id, text)
)
if status["status"]["visibility"] == "public":
await self.publish(Message(text, toot_id=status_id))
else:
await self.publish(Message(text))
await get_event_loop().run_in_executor(
None, self.account.notifications_dismiss, status["id"]
)
await sleep(self.polling_interval_sec)
async def push(self):
"""Push new Ticketfrei reports to Mastodon; if source is mastodon, boost it."""
while True:
message = await self.receive()
if hasattr(message, "toot_id"):
logger.debug("Boosting post %s: %s" % (message.toot_id, message.text))
await get_event_loop().run_in_executor(
None, self.account.status_reblog, message.toot_id
)
else:
logger.debug("Posting message: %s" % (message.text,))
await get_event_loop().run_in_executor(
None, self.account.status_post, message.text
)
spawner = Spawner(MastodonAccount, MastodonBot)

View file

@ -1,30 +0,0 @@
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from ormantic import ForeignKey, Integer, Text, Boolean, Model
from kibicara.model import Hood, Mapping
class MastodonInstance(Model):
id: Integer(primary_key=True) = None
name: Text()
client_id: Text()
client_secret: Text()
class Mapping(Mapping):
table_name = "mastodoninstances"
class MastodonAccount(Model):
id: Integer(primary_key=True) = None
hood: ForeignKey(Hood)
instance: ForeignKey(MastodonInstance)
access_token: Text()
username: Text(allow_null=True) = None
enabled: Boolean() = False
class Mapping(Mapping):
table_name = "mastodonaccounts"

View file

@ -1,184 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from asyncio import get_event_loop
from fastapi import APIRouter, Depends, HTTPException, Response, status
from ormantic.exceptions import NoMatch
from pydantic import BaseModel, validate_email, validator
from sqlite3 import IntegrityError
from kibicara.config import config
from kibicara.platforms.mastodon.bot import spawner
from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
from mastodon import Mastodon, MastodonNetworkError
from mastodon.errors import MastodonIllegalArgumentError
from logging import getLogger
logger = getLogger(__name__)
class BodyMastodonPublic(BaseModel):
username: str
instance: str
class BodyMastodonAccount(BaseModel):
email: str
instance_url: str
password: str
@validator("email")
def validate_email(cls, value):
return validate_email(value)
async def get_mastodon(mastodon_id, hood=Depends(get_hood)):
try:
return await MastodonAccount.objects.get(id=mastodon_id, hood=hood)
except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
async def get_mastodon_instance(instance_url: str) -> MastodonInstance:
"""Return a MastodonInstance ORM object with valid client_id and client_secret.
:param: instance_url: the API base URL of the mastodon server
:return the MastodonInstance ORM object
"""
try:
return await MastodonInstance.objects.get(name=instance_url)
except NoMatch:
app_name = config.get("frontend_url")
client_id, client_secret = Mastodon.create_app(
app_name, api_base_url=instance_url
)
await MastodonInstance.objects.create(
name=instance_url, client_id=client_id, client_secret=client_secret
)
return await MastodonInstance.objects.get(name=instance_url)
router = APIRouter()
twitter_callback_router = APIRouter()
@router.get(
"/public",
# TODO response_model,
operation_id="get_mastodons_public",
)
async def mastodon_read_all_public(hood=Depends(get_hood_unauthorized)):
mastodonbots = await MastodonAccount.objects.filter(hood=hood).all()
mbots = []
for mbot in mastodonbots:
if mbot.enabled == 1 and mbot.username:
instance = await MastodonInstance.objects.get(id=mbot.instance)
mbots.append(
BodyMastodonPublic(username=mbot.username, instance=instance.name)
)
return mbots
@router.get(
"/",
# TODO response_model,
operation_id="get_mastodons",
)
async def mastodon_read_all(hood=Depends(get_hood)):
return await MastodonAccount.objects.filter(hood=hood).all()
@router.delete(
"/{mastodon_id}",
status_code=status.HTTP_204_NO_CONTENT,
# TODO response_model
operation_id="delete_mastodon",
)
async def mastodon_delete(mastodon=Depends(get_mastodon)):
spawner.stop(mastodon)
await mastodon.instance.load()
object_with_instance = await MastodonAccount.objects.filter(
instance=mastodon.instance
).all()
if len(object_with_instance) == 1 and object_with_instance[0] == mastodon:
await mastodon.instance.delete()
await mastodon.delete()
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get(
"/{mastodon_id}/status",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id="status_mastodon",
)
async def mastodon_status(mastodon=Depends(get_mastodon)):
return {"status": spawner.get(mastodon).status.name}
@router.post(
"/{mastodon_id}/start",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id="start_mastodon",
)
async def mastodon_start(mastodon=Depends(get_mastodon)):
await mastodon.update(enabled=True)
spawner.get(mastodon).start()
return {}
@router.post(
"/{mastodon_id}/stop",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id="stop_mastodon",
)
async def mastodon_stop(mastodon=Depends(get_mastodon)):
await mastodon.update(enabled=False)
spawner.get(mastodon).stop()
return {}
@router.post(
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model
operation_id="create_mastodon",
)
async def mastodon_create(values: BodyMastodonAccount, hood=Depends(get_hood)):
"""Add a Mastodon Account to a Ticketfrei account.
open questions:
can the instance_url have different ways of writing?
:param: values: a BodyMastodonAccount object in json
:param: hood: the hood ORM object
"""
try:
instance = await get_mastodon_instance(values.instance_url)
except MastodonNetworkError:
raise HTTPException(422, "Invalid Mastodon Instance")
account = Mastodon(
instance.client_id, instance.client_secret, api_base_url=values.instance_url
)
try:
access_token = await get_event_loop().run_in_executor(
None, account.log_in, values.email, values.password
)
logger.debug(f"{access_token}")
mastodon = await MastodonAccount.objects.create(
hood=hood, instance=instance, access_token=access_token, enabled=True
)
spawner.start(mastodon)
return mastodon
except MastodonIllegalArgumentError:
logger.warning("Login to Mastodon failed.", exc_info=True)
raise HTTPException(status_code=status.HTTP_422_INVALID_INPUT)
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)

View file

@ -32,9 +32,9 @@ class TelegramBot(Censor):
def _create_dispatcher(self): def _create_dispatcher(self):
dp = Dispatcher(self.bot) dp = Dispatcher(self.bot)
dp.register_message_handler(self._send_welcome, commands=["start"]) dp.register_message_handler(self._send_welcome, commands=['start'])
dp.register_message_handler(self._remove_user, commands=["stop"]) dp.register_message_handler(self._remove_user, commands=['stop'])
dp.register_message_handler(self._send_help, commands=["help"]) dp.register_message_handler(self._send_help, commands=['help'])
dp.register_message_handler(self._receive_message) dp.register_message_handler(self._receive_message)
return dp return dp
@ -42,30 +42,30 @@ class TelegramBot(Censor):
try: try:
self.bot = Bot(token=self.telegram_model.api_token) self.bot = Bot(token=self.telegram_model.api_token)
self.dp = self._create_dispatcher() 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() user = await self.bot.get_me()
if user.username: if user.username:
await self.telegram_model.update(username=user.username) await self.telegram_model.update(username=user.username)
await gather(self.dp.start_polling(), self._push()) await gather(self.dp.start_polling(), self._push())
except CancelledError: except CancelledError:
logger.debug( 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 self.dp = None
raise raise
except exceptions.ValidationError: except exceptions.ValidationError:
logger.debug( 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) await self.telegram_model.update(enabled=False)
finally: 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): async def _push(self):
while True: while True:
message = await self.receive() message = await self.receive()
logger.debug( logger.debug(
"Received message from censor ({0}): {1}".format( 'Received message from censor ({0}): {1}'.format(
self.telegram_model.hood.name, message.text 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) await self.bot.send_message(user_id, message, disable_notification=False)
except exceptions.BotBlocked: except exceptions.BotBlocked:
logger.error( 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 user_id, self.telegram_model.hood.name
) )
) )
except exceptions.ChatNotFound: except exceptions.ChatNotFound:
logger.error( 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 user_id, self.telegram_model.hood.name
) )
) )
except exceptions.RetryAfter as e: except exceptions.RetryAfter as e:
logger.error( 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 user_id, self.telegram_model.hood.name
) )
+ "Sleep {0} seconds.".format(e.timeout) + 'Sleep {0} seconds.'.format(e.timeout)
) )
await sleep(e.timeout) await sleep(e.timeout)
return await self._send_message(user_id, message) return await self._send_message(user_id, message)
except exceptions.UserDeactivated: except exceptions.UserDeactivated:
logger.error( 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 user_id, self.telegram_model.hood.name
) )
) )
except exceptions.TelegramAPIError: except exceptions.TelegramAPIError:
logger.exception( logger.exception(
"Target [ID:{0}] ({1}): failed".format( 'Target [ID:{0}] ({1}): failed'.format(
user_id, self.telegram_model.hood.name user_id, self.telegram_model.hood.name
) )
) )
@ -114,14 +114,14 @@ class TelegramBot(Censor):
async def _send_welcome(self, message: types.Message): async def _send_welcome(self, message: types.Message):
try: try:
if message.from_user.is_bot: if message.from_user.is_bot:
await message.reply("Error: Bots can not join here.") await message.reply('Error: Bots can not join here.')
return return
await TelegramUser.objects.create( await TelegramUser.objects.create(
user_id=message.from_user.id, bot=self.telegram_model user_id=message.from_user.id, bot=self.telegram_model
) )
await message.reply(self.telegram_model.welcome_message) await message.reply(self.telegram_model.welcome_message)
except IntegrityError: 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): async def _remove_user(self, message: types.Message):
try: try:
@ -129,19 +129,19 @@ class TelegramBot(Censor):
user_id=message.from_user.id, bot=self.telegram_model user_id=message.from_user.id, bot=self.telegram_model
) )
await telegram_user.delete() await telegram_user.delete()
await message.reply("You were removed successfully from this bot.") await message.reply('You were removed successfully from this bot.')
except NoMatch: except 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): async def _send_help(self, message: types.Message):
if message.from_user.is_bot: 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 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): async def _receive_message(self, message: types.Message):
if not message.text: if not message.text:
await message.reply("Error: Only text messages are allowed.") await message.reply('Error: Only text messages are allowed.')
return return
await self.publish(Message(message.text)) await self.publish(Message(message.text))

View file

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

View file

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

View file

@ -13,4 +13,4 @@ class Test(Model):
hood: ForeignKey(Hood) hood: ForeignKey(Hood)
class Mapping(Mapping): 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 = APIRouter()
@router.get("/") @router.get('/')
async def test_read_all(hood=Depends(get_hood)): async def test_read_all(hood=Depends(get_hood)):
return await Test.objects.filter(hood=hood).all() 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)): async def test_create(response: Response, hood=Depends(get_hood)):
try: try:
test = await Test.objects.create(hood=hood) test = await Test.objects.create(hood=hood)
spawner.start(test) spawner.start(test)
response.headers["Location"] = str(test.id) response.headers['Location'] = str(test.id)
return test return test
except IntegrityError: except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.get("/{test_id}") @router.get('/{test_id}')
async def test_read(test=Depends(get_test)): async def test_read(test=Depends(get_test)):
return test return test
@router.delete("/{test_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete('/{test_id}', status_code=status.HTTP_204_NO_CONTENT)
async def test_delete(test=Depends(get_test)): async def test_delete(test=Depends(get_test)):
spawner.stop(test) spawner.stop(test)
await test.delete() await test.delete()
@router.get("/{test_id}/messages/") @router.get('/{test_id}/messages/')
async def test_message_read_all(test=Depends(get_test)): async def test_message_read_all(test=Depends(get_test)):
return spawner.get(test).messages return spawner.get(test).messages
@router.post("/{test_id}/messages/") @router.post('/{test_id}/messages/')
async def test_message_create(message: BodyMessage, test=Depends(get_test)): async def test_message_create(message: BodyMessage, test=Depends(get_test)):
await spawner.get(test).publish(Message(message.text)) await spawner.get(test).publish(Message(message.text))
return {} return {}

View file

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

View file

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

View file

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

View file

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

View file

@ -15,16 +15,16 @@ from kibicara.model import Mapping
from kibicara.webapi import router from kibicara.webapi import router
@fixture(scope="module") @fixture(scope='module')
def client(): def client():
Mapping.drop_all() Mapping.drop_all()
Mapping.create_all() Mapping.create_all()
app = FastAPI() app = FastAPI()
app.include_router(router, prefix="/api") app.include_router(router, prefix='/api')
return TestClient(app) return TestClient(app)
@fixture(scope="module") @fixture(scope='module')
def monkeymodule(): def monkeymodule():
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
@ -33,96 +33,96 @@ def monkeymodule():
mpatch.undo() mpatch.undo()
@fixture(scope="module") @fixture(scope='module')
def receive_email(monkeymodule): def receive_email(monkeymodule):
mailbox = [] mailbox = []
def mock_send_email(to, subject, sender="kibicara", body=""): def mock_send_email(to, subject, sender='kibicara', body=''):
mailbox.append(dict(to=to, subject=subject, sender=sender, body=body)) mailbox.append(dict(to=to, subject=subject, sender=sender, body=body))
def mock_receive_email(): def mock_receive_email():
return mailbox.pop() return mailbox.pop()
monkeymodule.setattr(email, "send_email", mock_send_email) monkeymodule.setattr(email, 'send_email', mock_send_email)
return mock_receive_email return mock_receive_email
@fixture(scope="module") @fixture(scope='module')
def register_token(client, receive_email): def register_token(client, receive_email):
response = client.post( response = client.post(
"/api/admin/register/", json={"email": "user", "password": "password"} '/api/admin/register/', json={'email': 'user', 'password': 'password'}
) )
assert response.status_code == status.HTTP_202_ACCEPTED assert response.status_code == status.HTTP_202_ACCEPTED
return urlparse(receive_email()["body"]).query.split("=", 1)[1] return urlparse(receive_email()['body']).query.split('=', 1)[1]
@fixture(scope="module") @fixture(scope='module')
def register_confirmed(client, register_token): def register_confirmed(client, register_token):
response = client.post("/api/admin/confirm/{0}".format(register_token)) response = client.post('/api/admin/confirm/{0}'.format(register_token))
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
@fixture(scope="module") @fixture(scope='module')
def access_token(client, register_confirmed): def access_token(client, register_confirmed):
response = client.post( response = client.post(
"/api/admin/login/", data={"username": "user", "password": "password"} '/api/admin/login/', data={'username': 'user', 'password': 'password'}
) )
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
return response.json()["access_token"] return response.json()['access_token']
@fixture(scope="module") @fixture(scope='module')
def auth_header(access_token): def auth_header(access_token):
return {"Authorization": "Bearer {0}".format(access_token)} return {'Authorization': 'Bearer {0}'.format(access_token)}
@fixture(scope="function") @fixture(scope='function')
def hood_id(client, auth_header): def hood_id(client, auth_header):
response = client.post("/api/hoods/", json={"name": "hood"}, headers=auth_header) response = client.post('/api/hoods/', json={'name': 'hood'}, headers=auth_header)
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
hood_id = int(response.headers["Location"]) hood_id = int(response.headers['Location'])
yield hood_id yield hood_id
client.delete("/api/hoods/{0}".format(hood_id), headers=auth_header) client.delete('/api/hoods/{0}'.format(hood_id), headers=auth_header)
@fixture(scope="function") @fixture(scope='function')
def trigger_id(client, hood_id, auth_header): def trigger_id(client, hood_id, auth_header):
response = client.post( response = client.post(
"/api/hoods/{0}/triggers/".format(hood_id), '/api/hoods/{0}/triggers/'.format(hood_id),
json={"pattern": "test"}, json={'pattern': 'test'},
headers=auth_header, headers=auth_header,
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
trigger_id = int(response.headers["Location"]) trigger_id = int(response.headers['Location'])
yield trigger_id yield trigger_id
client.delete( client.delete(
"/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id), headers=auth_header '/api/hoods/{0}/triggers/{1}'.format(hood_id, trigger_id), headers=auth_header
) )
@fixture(scope="function") @fixture(scope='function')
def badword_id(client, hood_id, auth_header): def badword_id(client, hood_id, auth_header):
response = client.post( response = client.post(
"/api/hoods/{0}/badwords/".format(hood_id), '/api/hoods/{0}/badwords/'.format(hood_id),
json={"pattern": ""}, json={'pattern': ''},
headers=auth_header, headers=auth_header,
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
badword_id = int(response.headers["Location"]) badword_id = int(response.headers['Location'])
yield badword_id yield badword_id
client.delete( client.delete(
"/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id), headers=auth_header '/api/hoods/{0}/badwords/{1}'.format(hood_id, badword_id), headers=auth_header
) )
@fixture(scope="function") @fixture(scope='function')
def test_id(client, hood_id, auth_header): def test_id(client, hood_id, auth_header):
response = client.post( response = client.post(
"/api/hoods/{0}/test/".format(hood_id), json={}, headers=auth_header '/api/hoods/{0}/test/'.format(hood_id), json={}, headers=auth_header
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
test_id = int(response.headers["Location"]) test_id = int(response.headers['Location'])
yield test_id yield test_id
client.delete( client.delete(
"/api/hoods/{0}/test/{1}".format(hood_id, test_id), headers=auth_header '/api/hoods/{0}/test/{1}'.format(hood_id, test_id), headers=auth_header
) )

View file

@ -6,10 +6,10 @@ from fastapi import status
def test_hoods_unauthorized(client): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_hoods_success(client, auth_header): 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 assert response.status_code == status.HTTP_200_OK

View file

@ -8,75 +8,75 @@ from fastapi import status
def test_hood_read_all(client): def test_hood_read_all(client):
response = client.get("/api/hoods/") response = client.get('/api/hoods/')
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
def test_hood_create_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_hood_read(client, hood_id): 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 assert response.status_code == status.HTTP_200_OK
def test_hood_update_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_hood_delete_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_read_all_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_create_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_read_unauthorized(client, hood_id, trigger_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_update_unauthorized(client, hood_id, trigger_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_trigger_delete_unauthorized(client, hood_id, trigger_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_read_all_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_create_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_read_unauthorized(client, hood_id, badword_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_update_unauthorized(client, hood_id, badword_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_badword_delete_unauthorized(client, hood_id, badword_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -9,16 +9,16 @@ from fastapi import status
from pytest import fixture from pytest import fixture
@fixture(scope="function") @fixture(scope='function')
def email_row(client, hood_id, auth_header): def email_row(client, hood_id, auth_header):
response = client.post( response = client.post(
"/api/hoods/{0}/email/".format(hood_id), '/api/hoods/{0}/email/'.format(hood_id),
json={"name": "kibicara-test"}, json={'name': 'kibicara-test'},
headers=auth_header, headers=auth_header,
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
email_id = int(response.headers["Location"]) email_id = int(response.headers['Location'])
yield response.json() yield response.json()
client.delete( 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): def test_email_subscribe_unsubscribe(client, hood_id, receive_email):
response = client.post( response = client.post(
"/api/hoods/{0}/email/subscribe/".format(hood_id), '/api/hoods/{0}/email/subscribe/'.format(hood_id),
json={"email": "test@localhost"}, json={'email': 'test@localhost'},
) )
assert response.status_code == status.HTTP_202_ACCEPTED assert response.status_code == status.HTTP_202_ACCEPTED
mail = receive_email() mail = receive_email()
body = mail["body"] body = mail['body']
confirm_url = findall( confirm_url = findall(
r"http[s]?://" r'http[s]?://'
+ r"(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+", + r'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+',
body, body,
)[0] )[0]
start = len("token=") start = len('token=')
response = client.post( 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:] hood_id, urlparse(confirm_url).query[start:]
) )
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
response = client.post( 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:] hood_id, urlparse(confirm_url).query[start:]
) )
) )
assert response.status_code == status.HTTP_409_CONFLICT 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( 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 assert response.status_code == status.HTTP_204_NO_CONTENT
def test_email_message(client, hood_id, trigger_id, email_row): def test_email_message(client, hood_id, trigger_id, email_row):
body = { body = {
"text": "test", 'text': 'test',
"author": "test@localhost", 'author': 'test@localhost',
"secret": email_row["secret"], '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 assert response.status_code == status.HTTP_201_CREATED
def test_email_send_mda(trigger_id, email_row): 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 mail = """From test@example.com Tue Jun 16 15:33:19 2020
Return-path: <test@example.com> Return-path: <test@example.com>
Envelope-to: hood@localhost Envelope-to: hood@localhost
@ -85,6 +85,6 @@ test
--AqNPlAX243a8sip3B7kXv8UKD8wuti-- --AqNPlAX243a8sip3B7kXv8UKD8wuti--
""" """
proc = subprocess.run( 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 assert proc.returncode == 0

View file

@ -8,18 +8,18 @@ from fastapi import status
def test_email_create_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_email_delete_unauthorized(client, hood_id, email_row): def test_email_delete_unauthorized(client, hood_id, email_row):
response = client.delete( 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_email_message_unauthorized(client, hood_id, email_row): def test_email_message_unauthorized(client, hood_id, email_row):
body = {"text": "test", "author": "author", "secret": "wrong"} body = {'text': 'test', 'author': 'author', 'secret': 'wrong'}
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_401_UNAUTHORIZED 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): 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 assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_email_subscribe_confirm_wrong_token(client, hood_id): def test_email_subscribe_confirm_wrong_token(client, hood_id):
try: try:
response = client.post( response = client.post(
"/api/hoods/{0}/email/subscribe/confirm/".format(hood_id) '/api/hoods/{0}/email/subscribe/confirm/'.format(hood_id)
+ "asdfasdfasdfasdfasdfasdfasdfasdf" + 'asdfasdfasdfasdfasdfasdfasdfasdf'
) )
assert response.status_code is not status.HTTP_201_CREATED assert response.status_code is not status.HTTP_201_CREATED
except CryptoError: except CryptoError:
@ -25,25 +25,25 @@ def test_email_subscribe_confirm_wrong_token(client, hood_id):
def test_email_subscribe_confirm_wrong_hood(client): def test_email_subscribe_confirm_wrong_hood(client):
response = client.delete( 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): def test_email_message_wrong(client, hood_id, email_row):
body = { body = {
"text": "", 'text': '',
"author": "test@localhost", 'author': 'test@localhost',
"secret": email_row["secret"], '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 assert response.status_code == status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS
def test_email_unsubscribe_wrong_token(client, hood_id): def test_email_unsubscribe_wrong_token(client, hood_id):
try: try:
client.delete( client.delete(
"/api/hoods/{0}/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf".format( '/api/hoods/{0}/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf'.format(
hood_id hood_id
) )
) )
@ -53,6 +53,6 @@ def test_email_unsubscribe_wrong_token(client, hood_id):
def test_email_unsubscribe_wrong_hood(client): def test_email_unsubscribe_wrong_hood(client):
response = client.delete( 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

@ -1,34 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from pytest import fixture
from kibicara.model import Hood
from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
@fixture(scope="function")
def mastodon_instance(event_loop):
return event_loop.run_until_complete(
MastodonInstance.objects.create(
name="inst4nce",
client_id="cl13nt_id",
client_secret="cl13nt_s3cr3t",
)
)
@fixture(scope="function")
def mastodon_account(event_loop, hood_id, mastodon_instance):
hood = event_loop.run_until_complete(Hood.objects.get(id=hood_id))
return event_loop.run_until_complete(
MastodonAccount.objects.create(
hood=hood,
instance=mastodon_instance,
access_token="t0k3n",
enabled=True,
username="us3r",
)
)

View file

@ -1,106 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
from pytest import fixture, mark
from mastodon.Mastodon import Mastodon
from kibicara.platforms import mastodon
from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
@fixture(scope="function")
def disable_spawner(monkeypatch):
class DoNothing:
def start(self, bot):
assert bot is not None
monkeypatch.setattr(mastodon.webapi, "spawner", DoNothing())
@mark.parametrize(
"body",
[
{
"instance_url": "botsin.space",
"email": "test@example.org",
"password": "string",
}
],
)
def test_mastodon_create_bot(
event_loop,
client,
disable_spawner,
hood_id,
auth_header,
monkeypatch,
body,
):
def log_in_mock(self, username, password):
return "acc3ss_t0ken"
monkeypatch.setattr(Mastodon, "log_in", log_in_mock)
response = client.post(
"/api/hoods/{0}/mastodon/".format(hood_id),
json=body,
headers=auth_header,
)
print(response.json())
assert response.status_code == status.HTTP_201_CREATED
bot_id = response.json()["id"]
mastodon_obj = event_loop.run_until_complete(MastodonAccount.objects.get(id=bot_id))
assert response.json()["access_token"] == mastodon_obj.access_token
mastodon_instance = event_loop.run_until_complete(
MastodonInstance.objects.get(id=mastodon_obj.instance.id)
)
assert (
response.json()["instance"]["name"]
== body["instance_url"]
== mastodon_instance.name
)
assert response.json()["hood"]["id"] == mastodon_obj.hood.id
assert response.json()["instance"]["client_id"] == mastodon_instance.client_id
assert (
response.json()["instance"]["client_secret"] == mastodon_instance.client_secret
)
assert mastodon_obj.enabled
@mark.parametrize(
"body",
[
{"instance_url": "botsin.space", "email": "notanemail", "password": "asdf1234"},
{"instance_url": "wrong", "email": "asdf@example.org", "password": "asdf1234"},
],
)
def test_mastodon_invalid_input(
event_loop,
client,
disable_spawner,
hood_id,
auth_header,
monkeypatch,
body,
):
response = client.post(
"/api/hoods/{0}/mastodon/".format(hood_id),
json=body,
headers=auth_header,
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_mastodon_create_mastodon_invalid_id(client, auth_header):
response = client.post("/api/hoods/1337/mastodon/", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.post("/api/hoods/wrong/mastodon/", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_mastodon_create_unauthorized(client, hood_id):
response = client.post("/api/hoods/{hood_id}/mastodon/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,48 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
from ormantic.exceptions import NoMatch
from pytest import raises
from kibicara.platforms.mastodon.model import MastodonAccount
def test_mastodon_delete_bot(client, event_loop, mastodon_account, auth_header):
response = client.delete(
"/api/hoods/{0}/mastodon/{1}".format(
mastodon_account.hood.id, mastodon_account.id
),
headers=auth_header,
)
assert response.status_code == status.HTTP_204_NO_CONTENT
with raises(NoMatch):
event_loop.run_until_complete(
MastodonAccount.objects.get(id=mastodon_account.id)
)
def test_mastodon_delete_bot_invalid_id(client, auth_header, hood_id):
response = client.delete("/api/hoods/1337/mastodon/123", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.delete("/api/hoods/wrong/mastodon/123", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
response = client.delete(
"/api/hoods/{0}/mastodon/7331".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.delete(
"/api/hoods/{0}/mastodon/wrong".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_mastodon_delete_bot_unauthorized(client, mastodon_account):
response = client.delete(
"/api/hoods/{0}/mastodon/{1}".format(
mastodon_account.hood.id, mastodon_account.id
)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,50 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
from kibicara.platforms.mastodon.model import MastodonAccount
def test_mastodon_get_bots(
client, auth_header, event_loop, hood_id, mastodon_account, mastodon_instance
):
mastodon2 = event_loop.run_until_complete(
MastodonAccount.objects.create(
hood=mastodon_account.hood,
instance=mastodon_instance,
access_token="4cc3ss",
enabled=True,
username="us4r",
)
)
response = client.get(
"/api/hoods/{0}/mastodon".format(mastodon_account.hood.id), headers=auth_header
)
assert response.status_code == status.HTTP_200_OK
assert response.json()[0]["id"] == mastodon_account.id
assert response.json()[0]["access_token"] == mastodon_account.access_token
assert response.json()[1]["id"] == mastodon2.id
assert response.json()[1]["access_token"] == mastodon2.access_token
def test_mastodon_get_bots_invalid_id(client, auth_header, hood_id):
response = client.get("/api/hoods/1337/mastodon", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.get("/api/hoods/wrong/mastodon", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_mastodon_get_bots_unauthorized(client, hood_id):
response = client.get("/api/hoods/{0}/mastodon".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_mastodon_public(client, mastodon_account, mastodon_instance, event_loop):
response = client.get(
"/api/hoods/{0}/mastodon/public".format(mastodon_account.hood.id)
)
assert response.json()[0]["username"] == mastodon_account.username
assert response.json()[0]["instance"] == mastodon_instance.name

View file

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

View file

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

View file

@ -7,36 +7,36 @@ from fastapi import status
from pytest import mark 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): def test_telegram_get_bot(client, auth_header, event_loop, bot, telegram):
response = client.get( 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, headers=auth_header,
) )
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert response.json()["id"] == telegram.id assert response.json()['id'] == telegram.id
assert response.json()["api_token"] == telegram.api_token assert response.json()['api_token'] == telegram.api_token
assert response.json()["welcome_message"] == telegram.welcome_message assert response.json()['welcome_message'] == telegram.welcome_message
def test_telegram_get_bot_invalid_id(client, auth_header, hood_id): 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 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 assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
response = client.get( 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 assert response.status_code == status.HTTP_404_NOT_FOUND
response = client.get( 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 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): def test_telegram_get_bot_unauthorized(client, bot, telegram):
response = client.get( 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 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( telegram0 = event_loop.run_until_complete(
Telegram.objects.create( Telegram.objects.create(
hood=hood, hood=hood,
api_token="api_token123", api_token='api_token123',
welcome_message="welcome_message123", welcome_message='welcome_message123',
) )
) )
telegram1 = event_loop.run_until_complete( telegram1 = event_loop.run_until_complete(
Telegram.objects.create( Telegram.objects.create(
hood=hood, hood=hood,
api_token="api_token456", api_token='api_token456',
welcome_message="welcome_message123", welcome_message='welcome_message123',
) )
) )
response = client.get( 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.status_code == status.HTTP_200_OK
assert response.json()[0]["id"] == telegram0.id assert response.json()[0]['id'] == telegram0.id
assert response.json()[0]["api_token"] == telegram0.api_token assert response.json()[0]['api_token'] == telegram0.api_token
assert response.json()[1]["id"] == telegram1.id assert response.json()[1]['id'] == telegram1.id
assert response.json()[1]["api_token"] == telegram1.api_token assert response.json()[1]['api_token'] == telegram1.api_token
def test_telegram_get_bots_invalid_id(client, auth_header, hood_id): 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 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 assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_telegram_get_bots_unauthorized(client, hood_id): 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 assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,41 +1,15 @@
# Kibicara Frontend # KibicaraFrontend
## Maintenance This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.4.
### Compatibility List
** The current compatible nodejs version is nodejs18 ** ## Development server
To check which nodejs version is required for an angular version, see [this stackoverflow post](https://stackoverflow.com/questions/60248452/is-there-a-compatibility-list-for-angular-angular-cli-and-node-js).
### Updating Angular to the newest version
To update Angular to a newer version, please refer to the [official Angular update page](https://update.angular.io/).
tldr of the update process:
0. Check which Angular version this project is currently using by looking at the version of @angular/core in the [package.json](./package.json) file.
1. Decide to which version you want to bump (e.g. 9.2 to 15.2). This depends which node version is running on the servers and which one is compatible with the angular version (see stackoverflow post above).
2. Add all existing dependencies listed on the update page e.g. `npm run-script ng add @angular/localize`
3. Bump the versions: You need to bump to every major version, so from 9.2 to 15.2 you will need to repeat these steps for 9 -> 10 -> 11 -> 12 -> 13 -> 14 -> 15
1. Version bump to the next qangular/core and @angular/cli version (e.g. here we do it from version 9 to version 10): `npx @angular/cli@10 update @angular/core@10 @angular/cli@10`
2. Version bump material: `npx @angular/cli@10 update @angular/material@10`
3. Update ngx-markdown to the next version: `npm install ngx-markdown@10 --save-dev`
4. Test if the frontend still works and fix all errors: `npm run-script ng s -o`
4. Check the official angular update page for any breaking changes and google how to fix them: `ng generate @angular/material:mdc-migration`
## Development
### Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
### Code scaffolding ## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
### Updating the openapi frontend part
The frontend uses openapi-generator to generate the calls to the backend. To regenerate this after an API change, please run: `npm run openapi-generator`.
## Build ## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.

View file

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

View file

@ -6,7 +6,6 @@ api/api.ts
api/badwords.service.ts api/badwords.service.ts
api/email.service.ts api/email.service.ts
api/hoods.service.ts api/hoods.service.ts
api/mastodon.service.ts
api/telegram.service.ts api/telegram.service.ts
api/test.service.ts api/test.service.ts
api/triggers.service.ts api/triggers.service.ts
@ -17,10 +16,9 @@ git_push.sh
index.ts index.ts
model/bodyAccessToken.ts model/bodyAccessToken.ts
model/bodyAdmin.ts model/bodyAdmin.ts
model/bodyAdminLoginAdminLoginPost.ts
model/bodyBadWord.ts model/bodyBadWord.ts
model/bodyHood.ts model/bodyHood.ts
model/bodyLogin.ts
model/bodyMastodonAccount.ts
model/bodyPassword.ts model/bodyPassword.ts
model/bodySubscriber.ts model/bodySubscriber.ts
model/bodyTelegram.ts model/bodyTelegram.ts

View file

@ -6,7 +6,6 @@ import { AdminService } from './api/admin.service';
import { BadwordsService } from './api/badwords.service'; import { BadwordsService } from './api/badwords.service';
import { EmailService } from './api/email.service'; import { EmailService } from './api/email.service';
import { HoodsService } from './api/hoods.service'; import { HoodsService } from './api/hoods.service';
import { MastodonService } from './api/mastodon.service';
import { TelegramService } from './api/telegram.service'; import { TelegramService } from './api/telegram.service';
import { TestService } from './api/test.service'; import { TestService } from './api/test.service';
import { TriggersService } from './api/triggers.service'; import { TriggersService } from './api/triggers.service';

View file

@ -33,7 +33,7 @@ import { Configuration } from '../configurat
}) })
export class AdminService { export class AdminService {
protected basePath = 'http://localhost/api'; protected basePath = 'http://localhost';
public defaultHeaders = new HttpHeaders(); public defaultHeaders = new HttpHeaders();
public configuration = new Configuration(); public configuration = new Configuration();
public encoder: HttpParameterCodec; public encoder: HttpParameterCodec;
@ -256,7 +256,7 @@ export class AdminService {
/** /**
* Admin Read * Admin Read
* Get a list of all hoods of a given admin. * Get a list of all hoods of a given admin.
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress. * @param reportProgress flag to report request and response progress.
*/ */
@ -305,7 +305,7 @@ export class AdminService {
/** /**
* Admin Hood Read All * Admin Hood Read All
* Get a list of all hoods of a given admin. * Get a list of all hoods of a given admin.
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress. * @param reportProgress flag to report request and response progress.
*/ */

View file

@ -6,12 +6,10 @@ export * from './email.service';
import { EmailService } from './email.service'; import { EmailService } from './email.service';
export * from './hoods.service'; export * from './hoods.service';
import { HoodsService } from './hoods.service'; import { HoodsService } from './hoods.service';
export * from './mastodon.service';
import { MastodonService } from './mastodon.service';
export * from './telegram.service'; export * from './telegram.service';
import { TelegramService } from './telegram.service'; import { TelegramService } from './telegram.service';
export * from './test.service'; export * from './test.service';
import { TestService } from './test.service'; import { TestService } from './test.service';
export * from './triggers.service'; export * from './triggers.service';
import { TriggersService } from './triggers.service'; import { TriggersService } from './triggers.service';
export const APIS = [AdminService, BadwordsService, EmailService, HoodsService, MastodonService, TelegramService, TestService, TriggersService]; export const APIS = [AdminService, BadwordsService, EmailService, HoodsService, TelegramService, TestService, TriggersService];

View file

@ -30,7 +30,7 @@ import { Configuration } from '../configurat
}) })
export class BadwordsService { export class BadwordsService {
protected basePath = 'http://localhost/api'; protected basePath = 'http://localhost';
public defaultHeaders = new HttpHeaders(); public defaultHeaders = new HttpHeaders();
public configuration = new Configuration(); public configuration = new Configuration();
public encoder: HttpParameterCodec; public encoder: HttpParameterCodec;
@ -154,7 +154,7 @@ export class BadwordsService {
/** /**
* Badword Delete * Badword Delete
* Deletes badword with id **badword_id** for hood with id **hood_id**. * Deletes badword with id **badword_id** for hood with id **hood_id**.
* @param badwordId * @param badwordId
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@ -211,7 +211,7 @@ export class BadwordsService {
/** /**
* Badword Read * Badword Read
* Reads badword with id **badword_id** for hood with id **hood_id**. * Reads badword with id **badword_id** for hood with id **hood_id**.
* @param badwordId * @param badwordId
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@ -268,7 +268,7 @@ export class BadwordsService {
/** /**
* Badword Read All * Badword Read All
* Get all badwords of hood with id **hood_id**. * Get all badwords of hood with id **hood_id**.
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress. * @param reportProgress flag to report request and response progress.

View file

@ -32,7 +32,7 @@ import { Configuration } from '../configurat
}) })
export class EmailService { export class EmailService {
protected basePath = 'http://localhost/api'; protected basePath = 'http://localhost';
public defaultHeaders = new HttpHeaders(); public defaultHeaders = new HttpHeaders();
public configuration = new Configuration(); public configuration = new Configuration();
public encoder: HttpParameterCodec; public encoder: HttpParameterCodec;
@ -207,7 +207,7 @@ export class EmailService {
/** /**
* Email Delete * Email Delete
* Delete an Email bot. Stops and deletes the Email bot. :param hood: Hood the Email bot belongs to. * Delete an Email bot. Stops and deletes the Email bot. :param hood: Hood the Email bot belongs to.
* @param emailId * @param emailId
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.

View file

@ -30,7 +30,7 @@ import { Configuration } from '../configurat
}) })
export class HoodsService { export class HoodsService {
protected basePath = 'http://localhost/api'; protected basePath = 'http://localhost';
public defaultHeaders = new HttpHeaders(); public defaultHeaders = new HttpHeaders();
public configuration = new Configuration(); public configuration = new Configuration();
public encoder: HttpParameterCodec; public encoder: HttpParameterCodec;
@ -150,7 +150,7 @@ export class HoodsService {
/** /**
* Hood Delete * Hood Delete
* Deletes hood with id **hood_id**. * Deletes hood with id **hood_id**.
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress. * @param reportProgress flag to report request and response progress.
@ -203,7 +203,7 @@ export class HoodsService {
/** /**
* Hood Read * Hood Read
* Get hood with id **hood_id**. * Get hood with id **hood_id**.
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress. * @param reportProgress flag to report request and response progress.
@ -249,7 +249,7 @@ export class HoodsService {
/** /**
* Hood Read All * Hood Read All
* Get all existing hoods. * Get all existing hoods.
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress. * @param reportProgress flag to report request and response progress.
*/ */

View file

@ -1,534 +0,0 @@
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/* tslint:disable:no-unused-variable member-ordering */
import { Inject, Injectable, Optional } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams,
HttpResponse, HttpEvent, HttpParameterCodec } from '@angular/common/http';
import { CustomHttpParameterCodec } from '../encoder';
import { Observable } from 'rxjs';
import { BodyMastodonAccount } from '../model/models';
import { HTTPValidationError } from '../model/models';
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
import { Configuration } from '../configuration';
@Injectable({
providedIn: 'root'
})
export class MastodonService {
protected basePath = 'http://localhost/api';
public defaultHeaders = new HttpHeaders();
public configuration = new Configuration();
public encoder: HttpParameterCodec;
constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string, @Optional() configuration: Configuration) {
if (configuration) {
this.configuration = configuration;
}
if (typeof this.configuration.basePath !== 'string') {
if (typeof basePath !== 'string') {
basePath = this.basePath;
}
this.configuration.basePath = basePath;
}
this.encoder = this.configuration.encoder || new CustomHttpParameterCodec();
}
private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams {
if (typeof value === "object" && value instanceof Date === false) {
httpParams = this.addToHttpParamsRecursive(httpParams, value);
} else {
httpParams = this.addToHttpParamsRecursive(httpParams, value, key);
}
return httpParams;
}
private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams {
if (value == null) {
return httpParams;
}
if (typeof value === "object") {
if (Array.isArray(value)) {
(value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key));
} else if (value instanceof Date) {
if (key != null) {
httpParams = httpParams.append(key,
(value as Date).toISOString().substr(0, 10));
} else {
throw Error("key may not be null if value is Date");
}
} else {
Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive(
httpParams, value[k], key != null ? `${key}.${k}` : k));
}
} else if (key != null) {
httpParams = httpParams.append(key, value);
} else {
throw Error("key may not be null if value is not object or array");
}
return httpParams;
}
/**
* Mastodon Create
* Add a Mastodon Account to a Ticketfrei account. open questions: can the instance_url have different ways of writing? :param: values: a BodyMastodonAccount object in json :param: hood: the hood ORM object
* @param hoodId
* @param bodyMastodonAccount
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public createMastodon(hoodId: number, bodyMastodonAccount: BodyMastodonAccount, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public createMastodon(hoodId: number, bodyMastodonAccount: BodyMastodonAccount, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public createMastodon(hoodId: number, bodyMastodonAccount: BodyMastodonAccount, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public createMastodon(hoodId: number, bodyMastodonAccount: BodyMastodonAccount, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling createMastodon.');
}
if (bodyMastodonAccount === null || bodyMastodonAccount === undefined) {
throw new Error('Required parameter bodyMastodonAccount was null or undefined when calling createMastodon.');
}
let headers = this.defaultHeaders;
let credential: string | undefined;
// authentication (OAuth2PasswordBearer) required
credential = this.configuration.lookupCredential('OAuth2PasswordBearer');
if (credential) {
headers = headers.set('Authorization', 'Bearer ' + credential);
}
let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
// to determine the Content-Type header
const consumes: string[] = [
'application/json'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
headers = headers.set('Content-Type', httpContentTypeSelected);
}
let responseType: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType = 'text';
}
return this.httpClient.post<any>(`${this.configuration.basePath}/api/hoods/${encodeURIComponent(String(hoodId))}/mastodon/`,
bodyMastodonAccount,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Mastodon Delete
* @param mastodonId
* @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public deleteMastodon(mastodonId: any, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public deleteMastodon(mastodonId: any, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public deleteMastodon(mastodonId: any, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public deleteMastodon(mastodonId: any, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (mastodonId === null || mastodonId === undefined) {
throw new Error('Required parameter mastodonId was null or undefined when calling deleteMastodon.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling deleteMastodon.');
}
let headers = this.defaultHeaders;
let credential: string | undefined;
// authentication (OAuth2PasswordBearer) required
credential = this.configuration.lookupCredential('OAuth2PasswordBearer');
if (credential) {
headers = headers.set('Authorization', 'Bearer ' + credential);
}
let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
let responseType: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType = 'text';
}
return this.httpClient.delete<any>(`${this.configuration.basePath}/api/hoods/${encodeURIComponent(String(hoodId))}/mastodon/${encodeURIComponent(String(mastodonId))}`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Mastodon Read
* @param mastodonId
* @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public getMastodon(mastodonId: any, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public getMastodon(mastodonId: any, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public getMastodon(mastodonId: any, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public getMastodon(mastodonId: any, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (mastodonId === null || mastodonId === undefined) {
throw new Error('Required parameter mastodonId was null or undefined when calling getMastodon.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling getMastodon.');
}
let headers = this.defaultHeaders;
let credential: string | undefined;
// authentication (OAuth2PasswordBearer) required
credential = this.configuration.lookupCredential('OAuth2PasswordBearer');
if (credential) {
headers = headers.set('Authorization', 'Bearer ' + credential);
}
let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
let responseType: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType = 'text';
}
return this.httpClient.get<any>(`${this.configuration.basePath}/api/hoods/${encodeURIComponent(String(hoodId))}/mastodon/${encodeURIComponent(String(mastodonId))}`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Mastodon Read All
* @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public getMastodons(hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public getMastodons(hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public getMastodons(hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public getMastodons(hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling getMastodons.');
}
let headers = this.defaultHeaders;
let credential: string | undefined;
// authentication (OAuth2PasswordBearer) required
credential = this.configuration.lookupCredential('OAuth2PasswordBearer');
if (credential) {
headers = headers.set('Authorization', 'Bearer ' + credential);
}
let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
let responseType: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType = 'text';
}
return this.httpClient.get<any>(`${this.configuration.basePath}/api/hoods/${encodeURIComponent(String(hoodId))}/mastodon/`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Mastodon Read All Public
* @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public getMastodonsPublic(hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public getMastodonsPublic(hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public getMastodonsPublic(hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public getMastodonsPublic(hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling getMastodonsPublic.');
}
let headers = this.defaultHeaders;
let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
let responseType: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType = 'text';
}
return this.httpClient.get<any>(`${this.configuration.basePath}/api/hoods/${encodeURIComponent(String(hoodId))}/mastodon/public`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Mastodon Start
* @param mastodonId
* @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public startMastodon(mastodonId: any, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public startMastodon(mastodonId: any, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public startMastodon(mastodonId: any, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public startMastodon(mastodonId: any, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (mastodonId === null || mastodonId === undefined) {
throw new Error('Required parameter mastodonId was null or undefined when calling startMastodon.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling startMastodon.');
}
let headers = this.defaultHeaders;
let credential: string | undefined;
// authentication (OAuth2PasswordBearer) required
credential = this.configuration.lookupCredential('OAuth2PasswordBearer');
if (credential) {
headers = headers.set('Authorization', 'Bearer ' + credential);
}
let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
let responseType: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType = 'text';
}
return this.httpClient.post<any>(`${this.configuration.basePath}/api/hoods/${encodeURIComponent(String(hoodId))}/mastodon/${encodeURIComponent(String(mastodonId))}/start`,
null,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Mastodon Status
* @param mastodonId
* @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public statusMastodon(mastodonId: any, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public statusMastodon(mastodonId: any, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public statusMastodon(mastodonId: any, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public statusMastodon(mastodonId: any, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (mastodonId === null || mastodonId === undefined) {
throw new Error('Required parameter mastodonId was null or undefined when calling statusMastodon.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling statusMastodon.');
}
let headers = this.defaultHeaders;
let credential: string | undefined;
// authentication (OAuth2PasswordBearer) required
credential = this.configuration.lookupCredential('OAuth2PasswordBearer');
if (credential) {
headers = headers.set('Authorization', 'Bearer ' + credential);
}
let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
let responseType: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType = 'text';
}
return this.httpClient.get<any>(`${this.configuration.basePath}/api/hoods/${encodeURIComponent(String(hoodId))}/mastodon/${encodeURIComponent(String(mastodonId))}/status`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Mastodon Stop
* @param mastodonId
* @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public stopMastodon(mastodonId: any, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public stopMastodon(mastodonId: any, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public stopMastodon(mastodonId: any, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public stopMastodon(mastodonId: any, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (mastodonId === null || mastodonId === undefined) {
throw new Error('Required parameter mastodonId was null or undefined when calling stopMastodon.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling stopMastodon.');
}
let headers = this.defaultHeaders;
let credential: string | undefined;
// authentication (OAuth2PasswordBearer) required
credential = this.configuration.lookupCredential('OAuth2PasswordBearer');
if (credential) {
headers = headers.set('Authorization', 'Bearer ' + credential);
}
let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}
let responseType: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType = 'text';
}
return this.httpClient.post<any>(`${this.configuration.basePath}/api/hoods/${encodeURIComponent(String(hoodId))}/mastodon/${encodeURIComponent(String(mastodonId))}/stop`,
null,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
}

View file

@ -30,7 +30,7 @@ import { Configuration } from '../configurat
}) })
export class TelegramService { export class TelegramService {
protected basePath = 'http://localhost/api'; protected basePath = 'http://localhost';
public defaultHeaders = new HttpHeaders(); public defaultHeaders = new HttpHeaders();
public configuration = new Configuration(); public configuration = new Configuration();
public encoder: HttpParameterCodec; public encoder: HttpParameterCodec;

View file

@ -30,7 +30,7 @@ import { Configuration } from '../configurat
}) })
export class TestService { export class TestService {
protected basePath = 'http://localhost/api'; protected basePath = 'http://localhost';
public defaultHeaders = new HttpHeaders(); public defaultHeaders = new HttpHeaders();
public configuration = new Configuration(); public configuration = new Configuration();
public encoder: HttpParameterCodec; public encoder: HttpParameterCodec;

View file

@ -30,7 +30,7 @@ import { Configuration } from '../configurat
}) })
export class TriggersService { export class TriggersService {
protected basePath = 'http://localhost/api'; protected basePath = 'http://localhost';
public defaultHeaders = new HttpHeaders(); public defaultHeaders = new HttpHeaders();
public configuration = new Configuration(); public configuration = new Configuration();
public encoder: HttpParameterCodec; public encoder: HttpParameterCodec;
@ -154,7 +154,7 @@ export class TriggersService {
/** /**
* Trigger Delete * Trigger Delete
* Deletes trigger with id **trigger_id** for hood with id **hood_id**. * Deletes trigger with id **trigger_id** for hood with id **hood_id**.
* @param triggerId * @param triggerId
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@ -211,7 +211,7 @@ export class TriggersService {
/** /**
* Trigger Read * Trigger Read
* Reads trigger with id **trigger_id** for hood with id **hood_id**. * Reads trigger with id **trigger_id** for hood with id **hood_id**.
* @param triggerId * @param triggerId
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
@ -268,7 +268,7 @@ export class TriggersService {
/** /**
* Trigger Read All * Trigger Read All
* Get all triggers of hood with id **hood_id**. * Get all triggers of hood with id **hood_id**.
* @param hoodId * @param hoodId
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress. * @param reportProgress flag to report request and response progress.

View file

@ -1,22 +0,0 @@
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface BodyLogin {
grant_type?: string;
username: string;
password: string;
scope?: string;
client_id?: string;
client_secret?: string;
}

View file

@ -1,19 +0,0 @@
/**
* FastAPI
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
export interface BodyMastodonAccount {
email: string;
instance_url: string;
password: string;
}

View file

@ -12,7 +12,7 @@
/** /**
* This model holds the email address of a fresh subscriber. * This model holds the email address of a fresh subscriber.
*/ */
export interface BodySubscriber { export interface BodySubscriber {
email: string; email: string;

View file

@ -12,7 +12,7 @@
/** /**
* This model shows which values are supplied by the MDA listener script. * This model shows which values are supplied by the MDA listener script.
*/ */
export interface KibicaraPlatformsEmailWebapiBodyMessage { export interface KibicaraPlatformsEmailWebapiBodyMessage {
text: string; text: string;

View file

@ -1,9 +1,8 @@
export * from './bodyAccessToken'; export * from './bodyAccessToken';
export * from './bodyAdmin'; export * from './bodyAdmin';
export * from './bodyAdminLoginAdminLoginPost';
export * from './bodyBadWord'; export * from './bodyBadWord';
export * from './bodyHood'; export * from './bodyHood';
export * from './bodyLogin';
export * from './bodyMastodonAccount';
export * from './bodyPassword'; export * from './bodyPassword';
export * from './bodySubscriber'; export * from './bodySubscriber';
export * from './bodyTelegram'; export * from './bodyTelegram';

View file

@ -12,7 +12,7 @@
export interface ValidationError { export interface ValidationError {
loc: Array<string | number>; loc: Array<string>;
msg: string; msg: string;
type: string; type: string;
} }

View file

@ -38,5 +38,4 @@
<div class="platforms-container"> <div class="platforms-container">
<app-email-settings [hoodId]="hoodId"></app-email-settings> <app-email-settings [hoodId]="hoodId"></app-email-settings>
<app-telegram-settings [hoodId]="hoodId"></app-telegram-settings> <app-telegram-settings [hoodId]="hoodId"></app-telegram-settings>
<app-mastodon-settings [hoodId]="hoodId"></app-mastodon-settings>
</div> </div>

View file

@ -1,47 +0,0 @@
<div *ngIf="mastodons$ | loading | async as mastodons">
<ng-template [ngIf]="mastodons.value">
<mat-card appearance="outlined">
<mat-card-header>
<div mat-card-avatar class="mastodon"></div>
<mat-card-title class="platform-title">
mastodon
<button mat-icon-button aria-label="How to use">
<mat-icon
matTooltip="How to send and receive hood broadcast messages with mastodon"
class="info-button"
(click)="onInfoClick()"
>info</mat-icon
>
</button>
</mat-card-title>
</mat-card-header>
<mat-card-content *ngIf="mastodons.value.length !== 0; else nomastodon">
<mat-selection-list [multiple]="false" class="list">
<a
*ngFor="let mastodon of mastodons.value"
href="https://{{mastodon.instance}}/@{{ mastodon.username }}"
routerLinkActive="router-link-active"
>
<mat-list-option>
@{{ mastodon.username }}
<mat-divider></mat-divider>
</mat-list-option>
</a>
</mat-selection-list>
</mat-card-content>
</mat-card>
<ng-template #nomastodon>
<mat-card-content>
Unfortunately your hood admin has not configured mastodon as platform
yet.
</mat-card-content>
</ng-template>
</ng-template>
<ng-template [ngIf]="mastodons.error"
><mat-icon class="warning">warning</mat-icon></ng-template
>
<ng-template [ngIf]="mastodons.loading">
<mat-spinner [diameter]="45" class="spinner"></mat-spinner>
</ng-template>
</div>

View file

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MastodonBotCardComponent } from './mastodon-bot-card.component';
describe('MastodonBotCardComponent', () => {
let component: MastodonBotCardComponent;
let fixture: ComponentFixture<MastodonBotCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MastodonBotCardComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MastodonBotCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -1,27 +0,0 @@
import { Component, OnInit, Input } from '@angular/core';
import { MastodonService } from 'src/app/core/api';
import { MastodonBotInfoDialogComponent } from './mastodon-bot-info-dialog/mastodon-bot-info-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'app-mastodon-bot-card',
templateUrl: './mastodon-bot-card.component.html',
styleUrls: ['./mastodon-bot-card.component.scss'],
})
export class MastodonBotCardComponent implements OnInit {
@Input() hoodId;
mastodons$;
constructor(
private mastodonService: MastodonService,
private dialog: MatDialog
) {}
ngOnInit(): void {
this.mastodons$ = this.mastodonService.getMastodonsPublic(this.hoodId);
}
onInfoClick() {
this.dialog.open(MastodonBotInfoDialogComponent);
}
}

View file

@ -1,54 +0,0 @@
<mat-dialog-content>
<div class="container">
<h2>How to communicate with the hood via Telegram?</h2>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title
>How to subscribe to the hood via Telegram?</mat-panel-title
>
</mat-expansion-panel-header>
<ol>
<li>
Click on the telegram bot name that is shown in the telegram card.
</li>
<li>
Start messaging the telegram bot that the link leads to by writing a
message containing only the word <strong>/start</strong>. Done!
</li>
</ol>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title
>How to send a broadcast message to the hood?</mat-panel-title
>
</mat-expansion-panel-header>
<p>
Write a direct message to the bot. This message will be broadcasted to
the other platforms.
</p>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>How to receive messages?</mat-panel-title>
</mat-expansion-panel-header>
<p>
If you subscribed to the bot, you will automatically receive the
messages of your hood from the bot.
</p>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>How to stop receiving messages?</mat-panel-title>
</mat-expansion-panel-header>
<p>
Write a message with content <strong>/stop</strong> to the bot. You
should receive a message from the bot which confirms that the
unsubscription was successful.
</p>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-dialog-content>

View file

@ -1,12 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-mastodon-bot-info-dialog',
templateUrl: './mastodon-bot-info-dialog.component.html',
styleUrls: ['./mastodon-bot-info-dialog.component.scss']
})
export class MastodonBotInfoDialogComponent implements OnInit {
constructor() {}
ngOnInit(): void {}
}

View file

@ -1,46 +0,0 @@
<h2 mat-dialog-title>Create new inbox</h2>
<mat-dialog-content>
<form class="input" [formGroup]="form">
<mat-form-field>
<mat-label>Mastodon Instance URL</mat-label>
<input matInput formControlName="instance_url" />
<mat-error
*ngIf="
form.controls.instance_url.errors &&
form.controls.instance_url.errors.required
"
>Mastodon Instance URL is required</mat-error
>
</mat-form-field>
<mat-form-field>
<mat-label>Mastodon E-Mail</mat-label>
<input matInput formControlName="email" />
<mat-error
*ngIf="
form.controls.email.errors &&
form.controls.email.errors.required
"
>Mastodon E-Mail is required</mat-error
>
</mat-form-field>
<mat-form-field>
<mat-label>Mastodon Password</mat-label>
<input matInput formControlName="password" />
<mat-error
*ngIf="
form.controls.password.errors &&
form.controls.password.errors.required
"
>Mastodon Password is required</mat-error
>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="onCancel()">Cancel</button>
<button mat-button (click)="onSubmit()" cdkFocusInitial>
Add Mastodon bot
</button>
</mat-dialog-actions>

View file

@ -1,26 +0,0 @@
.input {
display: grid;
grid-template-rows: 1fr 1fr 1fr;
width: 100%;
}
form {
margin-top: 10px;
height: 100%;
}
textarea {
height: 120px;
}
.example-image {
margin-left: 10%;
margin-right: 10%;
width: 80%;
@media screen and (max-width: 600px) {
width: 100%;
margin-left: 0%;
margin-right: 0%;
}
}

View file

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MastodonDialogComponent } from './mastodon-dialog.component';
describe('MastodonDialogComponent', () => {
let component: MastodonDialogComponent;
let fixture: ComponentFixture<MastodonDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MastodonDialogComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MastodonDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -1,79 +0,0 @@
import { Component, OnInit, Inject } from '@angular/core';
import { Validators, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MastodonService } from 'src/app/core/api';
import { MatSnackBar } from '@angular/material/snack-bar';
import { first } from 'rxjs/operators';
@Component({
selector: 'app-mastodon-dialog',
templateUrl: './mastodon-dialog.component.html',
styleUrls: ['./mastodon-dialog.component.scss'],
})
export class MastodonDialogComponent implements OnInit {
form: UntypedFormGroup;
constructor(
public dialogRef: MatDialogRef<MastodonDialogComponent>,
private formBuilder: UntypedFormBuilder,
private mastodonService: MastodonService,
private snackBar: MatSnackBar,
@Inject(MAT_DIALOG_DATA) public data
) {}
ngOnInit(): void {
this.form = this.formBuilder.group({
email: ['', Validators.required],
password: ['', Validators.required],
instance_url: ['', Validators.required],
});
if (this.data.mastodonId) {
this.mastodonService
.getMastodon(this.data.mastodonId, this.data.hoodId)
.subscribe((data) => {
this.form.controls.email.setValue(data.email);
this.form.controls.password.setValue(data.password);
this.form.controls.instance_url.setValue(data.instance_url);
});
}
}
onCancel() {
this.dialogRef.close();
}
success() {
this.dialogRef.close();
}
error() {
this.snackBar.open('Invalid API Key. Try again!', 'Close', {
duration: 2000,
});
}
onSubmit() {
if (this.form.invalid) {
return;
}
const response = {
email: this.form.controls.email.value,
instance_url: this.form.controls.instance_url.value,
password: this.form.controls.password.value
}
this.mastodonService
.createMastodon(this.data.hoodId, response)
.pipe(first())
.subscribe(
() => {
this.success();
},
() => {
this.error();
}
);
}
}

View file

@ -1,69 +0,0 @@
<mat-card appearance="outlined">
<mat-card-header>
<div mat-card-avatar class="mastodon"></div>
<mat-card-title class="platform-title">
Mastodon
<button mat-icon-button aria-label="How to use">
<mat-icon
matTooltip="How to add an mastodon bot to your hood"
class="info-button"
(click)="onInfoClick()"
>info</mat-icon
>
</button>
</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-list *ngIf="mastodons$ | loading | async as mastodons">
<ng-template [ngIf]="mastodons.value">
<mat-list-item *ngIf="mastodons.value.length === 0">
<button class="add-button" mat-button (click)="onCreate()">
<div class="in-add-button">
<mat-icon>add</mat-icon>
<span> Add a platform connection!</span>
</div>
</button>
<mat-divider></mat-divider>
</mat-list-item>
<mat-list-item *ngFor="let mastodon of mastodons.value">
<div class="entry">
@{{ mastodon.username }}
<mat-slide-toggle
[checked]="mastodon.enabled === 1"
(change)="onChange(mastodon)"
></mat-slide-toggle>
<button
mat-icon-button
[matMenuTriggerFor]="menu"
aria-label="Example icon-button with a menu"
>
<mat-icon>more_vert</mat-icon>
</button>
</div>
<mat-divider></mat-divider>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="onEdit(mastodon.id)">
<mat-icon>edit</mat-icon>
<span>Edit</span>
</button>
<button mat-menu-item (click)="onDelete(mastodon.id)">
<mat-icon>delete</mat-icon>
<span>Delete</span>
</button>
<button mat-menu-item (click)="onCreate()">
<mat-icon>add</mat-icon>
<span>Add another</span>
</button>
</mat-menu>
</mat-list-item>
</ng-template>
<ng-template [ngIf]="mastodons.error"
><mat-icon class="warning">warning</mat-icon></ng-template
>
<ng-template [ngIf]="mastodons.loading">
<mat-spinner [diameter]="45" class="spinner"></mat-spinner>
</ng-template>
</mat-list>
</mat-card-content>
</mat-card>

View file

@ -1,23 +0,0 @@
.entry {
display: grid;
grid-template-columns: 4fr 40px 20px;
width: 100%;
align-items: center;
}
.platform-title {
display: grid;
grid-template-columns: 1fr 40px;
width: 100%;
align-items: center;
}
.platform-heading {
align-self: flex-end;
}
.mastodon {
background-image: url("../../../../assets/mastodon.png");
background-size: cover;
}

View file

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MastodonSettingsComponent } from './mastodon-settings.component';
describe('MastodonSettingsComponent', () => {
let component: MastodonSettingsComponent;
let fixture: ComponentFixture<MastodonSettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MastodonSettingsComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MastodonSettingsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -1,102 +0,0 @@
import { Component, OnInit, Input } from '@angular/core';
import { MastodonService } from 'src/app/core/api';
import { Observable } from 'rxjs';
import { MastodonBotInfoDialogComponent } from '../mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { MastodonDialogComponent } from './mastodon-dialog/mastodon-dialog.component';
import { YesNoDialogComponent } from 'src/app/shared/yes-no-dialog/yes-no-dialog.component';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-mastodon-settings',
templateUrl: './mastodon-settings.component.html',
styleUrls: ['./mastodon-settings.component.scss'],
})
export class MastodonSettingsComponent implements OnInit {
@Input() hoodId;
mastodons$: Observable<Array<any>>;
constructor(
private mastodonService: MastodonService,
public dialog: MatDialog,
private snackBar: MatSnackBar
) {}
ngOnInit(): void {
this.reload();
}
private reload() {
this.mastodons$ = this.mastodonService.getMastodons(this.hoodId);
}
onInfoClick() {
this.dialog.open(MastodonBotInfoDialogComponent);
}
onDelete(mastodonId) {
const dialogRef = this.dialog.open(YesNoDialogComponent, {
data: {
title: 'Warning',
content:
'This will also delete the list of subscribers of the mastodon bot.',
},
});
dialogRef.afterClosed().subscribe((response) => {
if (response) {
this.mastodonService
.deleteMastodon(mastodonId, this.hoodId)
.subscribe(() => {
this.reload();
});
}
});
}
onCreate() {
const dialogRef = this.dialog.open(MastodonDialogComponent, {
data: { hoodId: this.hoodId },
});
dialogRef.afterClosed().subscribe(() => {
this.reload();
});
}
onEdit(mastodonId) {
const dialogRef = this.dialog.open(MastodonDialogComponent, {
data: { hoodId: this.hoodId, mastodonId },
});
dialogRef.afterClosed().subscribe(() => {
this.reload();
});
}
onChange(mastodon) {
if (mastodon.enabled === 0) {
this.mastodonService.startMastodon(mastodon.id, this.hoodId).subscribe(
() => {},
(error) => {
this.snackBar.open('Could not start. Check your settings.', 'Close', {
duration: 2000,
});
}
);
} else if (mastodon.enabled === 1) {
this.mastodonService.stopMastodon(mastodon.id, this.hoodId).subscribe(
() => {},
(error) => {
this.snackBar.open('Could not stop. Check your settings.', 'Close', {
duration: 2000,
});
}
);
}
// TODO yeah i know this is bad, implement disabling/enabling
setTimeout(() => {
this.reload();
}, 100);
}
}

View file

@ -2,5 +2,4 @@
<div class="container"> <div class="container">
<app-telegram-bot-card [hoodId]="hoodId"></app-telegram-bot-card> <app-telegram-bot-card [hoodId]="hoodId"></app-telegram-bot-card>
<app-email-bot-card [hoodId]="hoodId"></app-email-bot-card> <app-email-bot-card [hoodId]="hoodId"></app-email-bot-card>
<app-mastodon-bot-card [hoodId]="hoodId"></app-mastodon-bot-card>
</div> </div>

View file

@ -14,10 +14,6 @@ import { EmailBotInfoDialogComponent } from './email/email-bot-card/email-bot-in
import { TelegramBotInfoDialogComponent } from './telegram/telegram-bot-card/telegram-bot-info-dialog/telegram-bot-info-dialog.component'; import { TelegramBotInfoDialogComponent } from './telegram/telegram-bot-card/telegram-bot-info-dialog/telegram-bot-info-dialog.component';
import { EmailConfirmationComponent } from './email/email-confirmation/email-confirmation.component'; import { EmailConfirmationComponent } from './email/email-confirmation/email-confirmation.component';
import { EmailUnsubscribeComponent } from './email/email-unsubscribe/email-unsubscribe.component'; import { EmailUnsubscribeComponent } from './email/email-unsubscribe/email-unsubscribe.component';
import { MastodonBotCardComponent } from './mastodon/mastodon-bot-card/mastodon-bot-card.component';
import { MastodonSettingsComponent } from './mastodon/mastodon-settings/mastodon-settings.component';
import { MastodonDialogComponent } from './mastodon/mastodon-settings/mastodon-dialog/mastodon-dialog.component';
import { MastodonBotInfoDialogComponent } from './mastodon/mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -34,15 +30,10 @@ import { MastodonBotInfoDialogComponent } from './mastodon/mastodon-bot-card/mas
TelegramBotInfoDialogComponent, TelegramBotInfoDialogComponent,
EmailConfirmationComponent, EmailConfirmationComponent,
EmailUnsubscribeComponent, EmailUnsubscribeComponent,
MastodonBotCardComponent,
MastodonSettingsComponent,
MastodonDialogComponent,
MastodonBotInfoDialogComponent
], ],
imports: [CommonModule, SharedModule], imports: [CommonModule, SharedModule],
exports: [ exports: [
TelegramSettingsComponent, TelegramSettingsComponent,
MastodonSettingsComponent,
EmailSettingsComponent, EmailSettingsComponent,
PlatformsInfoPageComponent, PlatformsInfoPageComponent,
], ],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -6,4 +6,4 @@ ln -sf ../../git-hooks/commit-msg .git/hooks/commit-msg
# create virtualenv # create virtualenv
virtualenv -p $(which python3.10) backend/.venv virtualenv -p $(which python3.10) backend/.venv
backend/.venv/bin/pip install tox black pytest pytest-aiohttp backend/.venv/bin/pip install tox black