Compare commits

..

11 commits

421 changed files with 35901 additions and 32343 deletions

View file

@ -3,39 +3,39 @@
## Setup Development Environment ## Setup Development Environment
### General
1. Install `python>=3.10` and development packages
(`apt install python3-dev g++` on Ubuntu)
2. Run `./setup.sh`
### Backend ### Backend
0. `cd backend` 1. Install `python>=3.8`
1. Activate your dev environment with `source .venv/bin/activate` 2. Create a virtual environment with `python3 -m venv .venv`
2. Install with `pip install .` 3. Activate your dev environment with `source .venv/bin/activate`
3. Create a config file: `echo "production = 0" > kibicara.conf` 4. Update pip packages with `pip install -U pip setuptools wheel`
5. Install with `pip install .`
6. Install development dependencies with `pip install tox black`
7. Add git-hook to run test and stylecheck before commmit with
`ln -s ../../git-hooks/pre-commit .git/hooks/pre-commit`
8. Add git-hook to check commmit message format with
`ln -s ../../git-hooks/commit-msg .git/hooks/commit-msg`
9. 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 kibicara tests`
### Frontend ### Frontend
1. Install node.js (e.g. via 1. Install node.js (e.g. via
[nvm](https://github.com/nvm-sh/nvm#installation-and-update)) [nvm](https://github.com/nvm-sh/nvm#installation-and-update))
2. `cd frontend` 2. `cd kibicara-frontend`
3. Install the dependencies with `npm i` 3. Install the dependencies with `npm i`
4. Install Angular with `npm i @angular/cli` 4. Install Angular with `npm i -g @angular/cli`
5. Turn off production mode if you have not already (see above in backend). 5. Turn off production mode if you have not already (see above in backend).
6. Start the backend in a different terminal 6. Start the backend in a different terminal
7. To serve and open the application, run `node_modules/@angular/cli/bin/ng.js s -o`. 7. To serve and open the application, run `ng s -o`. The application will open
The application will open under [http://127.0.0.1:4200](http://127.0.0.1:4200). under [http://127.0.0.1:4200](http://127.0.0.1:4200).
### Creating an account ### Creating an account
@ -60,9 +60,14 @@ email address and register via frontend or manually at `http://localhost:8000/ap
## Contribution Guidelines ## Contribution Guidelines
### Branches ### Branches
- **Master:** The master branch tracks the last stable release.
- Releases will be done using release tags.
- Force push and pushes without group consent are disallowed.
- There never should be a merge commit from development into master!
- **Development:** The development branch is used to add new features. - **Development:** The development branch is used to add new features.
- Only rebase of feature branches is allowed. - Only rebase of feature branches is allowed.
- On Release a release tag will be created - On Release the development branch will be rebased onto master and a release
tag will be created on master
- **Feature-Branches:** - **Feature-Branches:**
- A feature branch will be used to develop a feature. - A feature branch will be used to develop a feature.
- It belongs to one developer only and force push is allowed. - It belongs to one developer only and force push is allowed.
@ -128,29 +133,15 @@ development team.
## How to implement a new Platform/Social Network ## How to implement a new Platform/Social Network
For transferring messages, Kibicara supports a range of platforms/social ### tl;dr
networks, e.g. Mastodon, E-Mail, and Telegram - but more can be added easily.
This guide explains what you need to do to add another platform, e.g. Matrix or
XMPP.
### Overview: 1. Implement the following modules in `platforms/<your-platform>/`:
1. Implement the backend modules in `platforms/<your-platform>/`:
- `bot.py` - `bot.py`
- `model.py` - `model.py`
- `webapi.py` - `webapi.py`
2. Import your bot in `kibicara/webapi/__init__.py`. 2. Import your bot in `kibicara/webapi/__init__.py`.
3. Generate the FastAPI boilerplate code
4. Generate the angular boilerplate code
5. Copy-paste frontend components from other bots into the angular boilerplate
and adjust them to your needs
At the bottom you can find a checklist what your pull request needs to be ### Explanation
merged into kibicara.
### Step by step
#### Implement the backend modules
In `kibicara/platforms/<your-platform>/bot.py`, you write the functions through In `kibicara/platforms/<your-platform>/bot.py`, you write the functions through
which the platform asks the social network for new messages, and publishes which the platform asks the social network for new messages, and publishes
@ -166,90 +157,14 @@ You will probably need to store the following things:
* platform-specific settings * platform-specific settings
* anything else your platform needs * anything else your platform needs
In `kibicara/platforms/<your-platform>/webapi.py`, you can define REST API In `kibicara/platforms/<your-platform>/webapi.py`, you can define HTTP routes.
routes. You will need them to: You will need them to:
* let admins authenticate to the social network in the kibicara web interface * let admins authenticate to the social network in the kibicara web interface
* update platform-specific settings * update platform-specific settings
#### Import your bot into the kibicara REST API
To run the platform, you need to import the bot in To run the platform, you need to import the bot in
`kibicara/webapi/__init__.py`. You can see how the other platforms did it. `kibicara/webapi/__init__.py`.
#### Generate the FastAPI boilerplate code
Whenever you changed the REST API in the backend, you need to re-generate the
FastAPI boilerplate code:
1. Start backend with `kibicara > /dev/null 2>&1 &`
2. Go to the frontend directory: `cd frontend`
3. Use this command to download the openapi.json from the backend and
generate the boilerplate: `npm run openapi-generator`
4. (Now you can stop the backend again, e.g. with `pkill kibicara`)
5. Append `/api` to all relevant URLs:
`find src/app/core/api/ -type f -exec sed -i "s#{this.configuration.basePath}#{this.configuration.basePath}/api#g" {} +`
6. Check if everything is okay (e.g. all api calls need the `/api` prefix)
#### Generate the Angular boilerplate code
##### Generate the platform-specific "public facing" page
Generate boilerplate "card" for myplatform:
```
ng generate component platforms/myplatform/myplatform-bot-card
```
Generate boilerplate for the popup that shows the users the guide on how to use
the bot:
```
ng generate component platforms/myplatform/myplatform-bot-card/myplatform-bot-info-dialog
```
##### Generate the platform-specific "settings" page
Generate bot card for settings page:
```
ng generate component platforms/myplatform/myplatform-settings
```
Generate popup that will show when you click on "Add" to add the myplatform
credentials (e.g. login data, api tokens etc):
```
ng generate component platforms/myplatform/myplatform-settings/mastodon-dialog
```
If something does not work, try to check `platforms.module.ts` and check if the
module was imported there. Every module needs to be imported there
#### Adjust the Angular code for your specific platform
Every frontend part for a bot has a similar structure. Basically copy the
content of the other files e.g. the telegram bot into the generated boilerplate
above and search and replace all occurrences with `myplatform`. You can see
the UI with `ng s -o`, it will auto-update on code change.
A component in angular has 3-4 files, only these ones ending with
`*.component.ts` (typescript) and `*.component.html`(html) are important for
us. Basically the typescript controls what is shown in the html. Please correct
every error that stops the angular page build or shows up on the page while you
go, otherwise this can become a mess.
With that in mind, first write the logic to call the /create endpoint:
- `src/app/platforms/myplatform/myplatform-settings/myplatform-dialog/myplatform-dialog.component.ts`:
implement the form to take the user inputs and the onSubmit() function
- `src/app/platforms/myplatform/myplatform-settings/myplatform-dialog/myplatform-dialog.component.html`:
implement the html skeleton that takes the form from the user
Then, fix up the public user facing page:
- `src/app/platforms/myplatform/myplatform-bot-card/myplatform-bot-info-dialog/myplatform-bot-info-dialog.component.html`
Finally, check the other typescript and html pages and adjust e.g. the tutorial
text for the users.
### Acceptance criteria for bots (Checklist) ### Acceptance criteria for bots (Checklist)
@ -287,11 +202,3 @@ A bot should have at least this functionality:
- e.g. Twitter via posts or retweets - e.g. Twitter via posts or retweets
- e.g. Telegram via direct message from the bot - e.g. Telegram via direct message from the bot
- e.g. E-Mail via e-mail to the user's address - e.g. E-Mail via e-mail to the user's address
- Web Interface (hood admins and users)
- A card which allows hood admins to add, configure, start, stop, and
delete a platform to their hood
- A pop-up which explains to hood admins how to configure the platform
- A card which allows users to subscribe on a platform or links to the
platform's account
- A pop-up which explains to users how to use the platform

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

@ -1,5 +0,0 @@
# Kibicara backend
Kibicara relays messages between different platforms (= social networks).
This is just the backend. For info about the whole project see [our git
repo](https://git.0x90.space/ticketfrei/ticketfrei3).

View file

@ -1,6 +0,0 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"

View file

@ -1,73 +0,0 @@
[metadata]
name = kibicara
version = 0.1.0
author = 0x90.space
author_email = people@schleuder.0x90.space
description = distribute messages across different social media
long_description = file: README.md
long_description_content_type = text/markdown
url = https://git.0x90.space/ticketfrei/ticketfrei3
project_urls =
Bug Tracker = https://git.0x90.space/ticketfrei/ticketfrei3/issues
classifiers =
Programming Language :: Python :: 3
License :: Public Domain
[options]
package_dir =
= src
packages = find:
python_requires = >=3.10
install_requires =
aiofiles
aiogram
aiosqlite
argon2_cffi
fastapi
httpx
hypercorn
Mastodon.py
passlib
peony-twitter[all]
pydantic[email]
pynacl
python-multipart
pytoml
requests
tortoise-orm
[options.packages.find]
where = src
[options.entry_points]
console_scripts =
kibicara = kibicara.kibicara:Main
kibicara_mda = kibicara.platforms.email.mda:Main
migrate_from_ticketfrei2 = kibicara.migratefromticketfrei:Main
[tox:tox]
envlist = lint, py310
isolated_build = True
[testenv:lint]
skip_install = True
deps =
black
flake8
commands =
black --check --diff src tests
flake8 src tests
[testenv]
deps =
mypy
pytest
pytest-asyncio
types-requests
commands =
# not yet
#mypy --ignore-missing-imports src tests
pytest tests
[flake8]
max_line_length = 88

View file

@ -1,23 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
"""Default configuration.
The default configuration gets overwritten by a configuration file if one exists.
"""
config = {
"database_connection": "sqlite://:memory:",
"frontend_url": "http://localhost:4200", # url of frontend, change in prod
# production params
"frontend_path": None, # required, path to frontend html/css/js files
"production": True,
"behind_proxy": False,
"keyfile": None, # optional for ssl
"certfile": None, # optional for ssl
# dev params
"root_url": "http://localhost:8000", # url of backend
"cors_allow_origin": "http://localhost:4200",
}

View file

@ -1,122 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
"""Entrypoint of Kibicara."""
from argparse import ArgumentParser
from asyncio import run as asyncio_run
from logging import DEBUG, INFO, WARNING, basicConfig, getLogger
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from hypercorn.asyncio import serve
from hypercorn.config import Config
from pytoml import load
from tortoise import Tortoise
from kibicara.config import config
from kibicara.platformapi import Spawner
from kibicara.webapi import router
logger = getLogger(__name__)
class Main:
"""Entrypoint for Kibicara.
Initializes the platform bots and starts the hypercorn webserver serving the
Kibicara application and the specified frontend on port 8000.
"""
def __init__(self):
parser = ArgumentParser()
parser.add_argument(
"-f",
"--config",
dest="configfile",
default="/etc/kibicara.conf",
help="path to config file",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
help="Raise verbosity level",
)
args = parser.parse_args()
try:
with open(args.configfile) as configfile:
config.update(load(configfile))
except FileNotFoundError:
# run with default config
pass
LOGLEVELS = {
None: WARNING,
1: INFO,
2: DEBUG,
}
basicConfig(
level=LOGLEVELS.get(args.verbose, DEBUG),
format="%(asctime)s %(name)s %(message)s",
)
getLogger("aiosqlite").setLevel(WARNING)
asyncio_run(self.__run())
async def __run(self):
await Tortoise.init(
db_url=config["database_connection"],
modules={
"models": [
"kibicara.model",
"kibicara.platforms.email.model",
"kibicara.platforms.mastodon.model",
"kibicara.platforms.telegram.model",
"kibicara.platforms.test.model",
"kibicara.platforms.twitter.model",
]
},
)
await Tortoise.generate_schemas()
await Spawner.init_all()
await self.__start_webserver()
await Tortoise.close_connections()
async def __start_webserver(self):
class SinglePageApplication(StaticFiles):
async def get_response(self, path, scope):
response = await super().get_response(path, scope)
if response.status_code == 404:
response = await super().get_response(".", scope)
return response
app = FastAPI()
server_config = Config()
server_config.accesslog = "-"
server_config.behind_proxy = config["behind_proxy"]
server_config.keyfile = config["keyfile"]
server_config.certfile = config["certfile"]
if config["production"]:
server_config.bind = ["0.0.0.0:8000", "[::]:8000"]
api = FastAPI()
api.include_router(router)
app.mount("/api", api)
if not config["production"] and config["cors_allow_origin"]:
app.add_middleware(
CORSMiddleware,
allow_origins=config["cors_allow_origin"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
if config["frontend_path"] is not None:
app.mount(
"/",
app=SinglePageApplication(directory=config["frontend_path"], html=True),
)
await serve(app, server_config)

View file

@ -1,225 +0,0 @@
import sqlite3
import argparse
from time import time, sleep
from os import urandom
from tortoise import Tortoise
from tortoise.exceptions import DoesNotExist
import asyncio
from mastodon import Mastodon
from kibicara.model import Admin, Hood, IncludePattern, ExcludePattern
from kibicara.platforms.mastodon.model import MastodonInstance, MastodonAccount
from kibicara.platforms.telegram.model import Telegram, TelegramSubscriber
from kibicara.platforms.email.model import Email, EmailSubscriber
from kibicara.platforms.twitter.model import Twitter
class OldDataBase:
def __init__(self, old_database_path):
self.conn = sqlite3.connect(old_database_path)
self.cur = self.conn.cursor()
def execute(self, *args, **kwargs):
return self.cur.execute(*args, **kwargs)
def commit(self):
start_time = time()
while 1:
try:
self.conn.commit()
break
except sqlite3.OperationalError:
# another thread may be writing, give it a chance to finish
sleep(0.1)
if time() - start_time > 5:
# if it takes this long, something is wrong
raise
def close(self):
self.conn.close()
class Main:
def __init__(self):
parser = argparse.ArgumentParser()
parser.add_argument(
"old_database_path", help="path to the ticketfrei2 sqlite3 database"
)
parser.add_argument(
"new_database_path",
help="path to the ticketfrei3 sqlite3 database",
default="ticketfrei3.sqlite",
)
args = parser.parse_args()
# open old database
self.old_db = OldDataBase(args.old_database_path)
# open new database
asyncio.run(self.new_database(args.new_database_path))
async def new_database(self, new_database_path):
await Tortoise.init(
db_url=f"sqlite://{new_database_path}",
modules={
"models": [
"kibicara.model",
"kibicara.platforms.email.model",
"kibicara.platforms.mastodon.model",
"kibicara.platforms.telegram.model",
"kibicara.platforms.test.model",
"kibicara.platforms.twitter.model",
]
},
)
await Tortoise.generate_schemas()
# read table per table and write it to new database.
# mastodon instances
old_mastodon_instances = self.old_db.execute(
"SELECT * FROM mastodon_instances"
).fetchall()
for instance in old_mastodon_instances:
url = instance[1]
client_id = instance[2]
client_secret = instance[3]
await MastodonInstance.create(
name=url, client_id=client_id, client_secret=client_secret
)
print(f"Created Mastodon Instance: {url}")
old_users = self.old_db.execute("SELECT * FROM user;").fetchall()
for user in old_users:
user_id = user[0]
user_passhash = user[1]
user_enabled = user[2]
if user_enabled == 0:
print(f"skipping user {user_id}, inactive")
email = self.old_db.execute(
"SELECT email FROM email WHERE user_id=?", (user_id,)
).fetchone()[0]
try:
await Admin.get(email=email)
continue # skip if the user already exists.
except DoesNotExist:
admin = await Admin.create(email=email, passhash=user_passhash)
city = self.old_db.execute(
"SELECT * FROM cities WHERE user_id=?", (user_id,)
).fetchone()
city_name = city[2]
city_markdown = city[3]
hood = await Hood.create(name=city_name, landingpage=city_markdown)
await hood.admins.add(admin)
print()
print(
"Migrated user %s with email %s for the city %s "
"with the following accounts:" % (user_id, email, city_name)
)
patterns = self.old_db.execute(
"SELECT patterns FROM triggerpatterns WHERE user_id=?", (user_id,)
).fetchone()[0]
for pattern in patterns.splitlines():
await IncludePattern.create(hood=hood, pattern=pattern)
badwords = self.old_db.execute(
"SELECT words FROM badwords WHERE user_id=?", (user_id,)
).fetchone()[0]
for badword in badwords.splitlines():
await ExcludePattern.create(hood=hood, pattern=badword)
mastodon_account = self.old_db.execute(
"SELECT * FROM mastodon_accounts WHERE user_id=?", (user_id,)
).fetchone()
if mastodon_account:
instance_url = self.old_db.execute(
"SELECT instance FROM mastodon_instances WHERE id=?",
(mastodon_account[3],),
).fetchone()[0]
new_instance = await MastodonInstance.get(name=instance_url)
access_token = mastodon_account[2]
mastodon_enabled = mastodon_account[4]
await MastodonAccount.create(
hood=hood,
instance=new_instance,
access_token=access_token,
enabled=mastodon_enabled,
)
await dismiss_notifications(hood)
print(f"Mastodon: {instance_url}, {access_token}")
telegram_account = self.old_db.execute(
"SELECT apikey, active FROM telegram_accounts WHERE user_id=?",
(user_id,),
).fetchone()
if telegram_account[0] != "":
telegram_apikey = telegram_account[0]
telegram_enabled = telegram_account[1]
telegram = await Telegram.create(
hood=hood,
api_token=telegram_apikey,
enabled=telegram_enabled,
welcome_message="",
)
telegram_subscribers = self.old_db.execute(
"SELECT subscriber_id FROM telegram_subscribers WHERE user_id=?",
(user_id,),
).fetchall()
for subscriber in telegram_subscribers:
subscriber_id = subscriber[0]
await TelegramSubscriber.create(bot=telegram, user_id=subscriber_id)
print(f"Telegram: {len(telegram_subscribers)} subscribers")
mail_subscribers = self.old_db.execute(
"SELECT email FROM mailinglist WHERE user_id=?", (user_id,)
).fetchall()
if mail_subscribers is not []:
email_name = f"kibicara-{city_name}"
email = await Email.create(
hood=hood, name=email_name, secret=urandom(32).hex()
)
for subscriber in mail_subscribers:
subscriber_email = subscriber[0]
await EmailSubscriber.create(email=subscriber_email, hood=hood)
print(f"E-Mail: {len(mail_subscribers)} subscribers")
twitter = self.old_db.execute(
"SELECT * FROM twitter_accounts WHERE user_id=?", (user_id,)
).fetchone()
if twitter:
client_id = twitter[2]
client_secret = twitter[3]
seen_tweet = self.old_db.execute(
"SELECT tweet_id FROM seen_tweets WHERE user_id=?", (user_id,)
).fetchone()[0]
await Twitter.create(
hood=hood,
dms_since_id=1, # didn't work in ticketfrei2 anyway
mentions_since_id=seen_tweet,
access_token=client_id,
access_token_secret=client_secret,
username=" ",
verified=False,
enabled=True,
)
print(f"Twitter: last ID: {seen_tweet}")
await Tortoise.close_connections()
async def dismiss_notifications(hood):
model = await MastodonAccount.get(hood=hood)
await model.fetch_related("instance")
account = Mastodon(
client_id=model.instance.client_id,
client_secret=model.instance.client_secret,
api_base_url=model.instance.name,
access_token=model.access_token,
)
notifications = account.notifications()
for status in notifications:
account.notifications_dismiss(status["id"])
print(f"Dismissed {len(notifications)} notifications on Mastodon.")

View file

@ -1,57 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
"""ORM Models for core."""
from tortoise import fields
from tortoise.models import Model
class Admin(Model):
id = fields.IntField(pk=True)
email = fields.CharField(64, unique=True)
passhash = fields.TextField()
hoods: fields.ManyToManyRelation["Hood"] = fields.ManyToManyField(
"models.Hood", related_name="admins", through="admin_hood_relations"
)
class Meta:
table = "admins"
class Hood(Model):
id = fields.IntField(pk=True)
name = fields.CharField(64, unique=True)
landingpage = fields.TextField()
email_enabled = fields.BooleanField(default=True)
admins: fields.ManyToManyRelation[Admin]
include_patterns: fields.ReverseRelation["IncludePattern"]
exclude_patterns: fields.ReverseRelation["ExcludePattern"]
class Meta:
table = "hoods"
class IncludePattern(Model):
id = fields.IntField(pk=True)
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
"models.Hood", related_name="include_patterns"
)
pattern = fields.TextField()
class Meta:
table = "include_patterns"
class ExcludePattern(Model):
id = fields.IntField(pk=True)
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
"models.Hood", related_name="exclude_patterns"
)
pattern = fields.TextField()
class Meta:
table = "exclude_patterns"

View file

@ -1,108 +0,0 @@
# Copyright (C) 2020 by Maike <maike@systemli.org>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from argparse import ArgumentParser
from asyncio import run as asyncio_run
from email.parser import BytesParser
from email.policy import default
from email.utils import parseaddr
from logging import getLogger
from re import sub
from sys import stdin
from fastapi import status
from ormantic import NoMatch
from pytoml import load
from requests import post
from kibicara.config import config
from kibicara.platforms.email.model import Email, EmailSubscriber
logger = getLogger(__name__)
class Main:
def __init__(self):
parser = ArgumentParser()
parser.add_argument(
"-f",
"--config",
dest="configfile",
default="/etc/kibicara.conf",
help="path to config file",
)
# the MDA passes the recipient address as command line argument
parser.add_argument("recipient")
args = parser.parse_args()
try:
with open(args.configfile) as configfile:
config.update(load(configfile))
except FileNotFoundError:
# run with default config
pass
# extract email from the recipient
email_name = args.recipient.lower()
asyncio_run(self.__run(email_name))
async def __run(self, email_name):
try:
email = await Email.get(name=email_name)
except NoMatch:
logger.error("No recipient with this name")
exit(1)
# read mail from STDIN and parse to EmailMessage object
message = BytesParser(policy=default).parsebytes(stdin.buffer.read())
sender = ""
if message.get("sender"):
sender = message.get("sender")
elif message.get("from"):
sender = message.get("from")
else:
logger.error("No Sender of From header")
exit(1)
sender = parseaddr(sender)[1]
if not sender:
logger.error("Could not parse sender")
exit(1)
maybe_subscriber = await EmailSubscriber.filter(email=sender).all()
if len(maybe_subscriber) != 1 or maybe_subscriber[0].hood.id != email.hood.id:
logger.error("Not a subscriber")
exit(1)
# extract relevant data from mail
text = sub(
r"<[^>]*>",
"",
message.get_body(preferencelist=("plain", "html")).get_content(),
)
response = post(
"{0}/api/hoods/{1}/email/messages/".format(
config["root_url"], email.hood.pk
),
json={"text": text, "secret": email.secret},
)
if response.status_code == status.HTTP_201_CREATED:
exit(0)
elif response.status_code == status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS:
logger.error("Message was't accepted: {0}".format(text))
elif response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY:
logger.error("Malformed request: {0}".format(response.json()))
elif response.status_code == status.HTTP_401_UNAUTHORIZED:
logger.error("Wrong API secret. kibicara_mda seems to be misconfigured")
else:
logger.error(
"REST-API failed with response status {0}".format(response.status_code)
)
exit(1)

View file

@ -1,38 +0,0 @@
# Copyright (C) 2020 by Maike <maike@systemli.org>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from tortoise import fields
from tortoise.models import Model
from kibicara.model import Hood
class Email(Model):
"""This table is used to track the names. It also stores the token secret."""
id = fields.IntField(pk=True)
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
"models.Hood", related_name="platforms_email", unique=True
)
name = fields.CharField(32, unique=True)
secret = fields.TextField()
class Meta:
table = "platforms_email"
class EmailSubscriber(Model):
"""This table stores all subscribers, who want to receive messages via email."""
id = fields.IntField(pk=True)
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
"models.Hood", related_name="platforms_email_subscribers"
)
email = fields.CharField(64, unique=True)
class Meta:
table = "platforms_email_subscribers"

View file

@ -1,101 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from asyncio import gather, get_event_loop, sleep
from logging import getLogger
import re
from mastodon import Mastodon, MastodonError
from kibicara.platformapi import Censor, Spawner, Message
from kibicara.platforms.mastodon.model import MastodonAccount
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.filter(hood=hood).all():
await mastodon.delete()
async def run(self):
try:
await self.model.fetch_related("instance")
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,36 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from tortoise import fields
from tortoise.models import Model
from kibicara.model import Hood
class MastodonInstance(Model):
id = fields.IntField(pk=True)
name = fields.TextField()
client_id = fields.TextField()
client_secret = fields.TextField()
accounts: fields.ReverseRelation["MastodonAccount"]
class Meta:
table = "platforms_mastodon_instances"
class MastodonAccount(Model):
id = fields.IntField(pk=True)
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
"models.Hood", related_name="platforms_mastodon"
)
instance: fields.ForeignKeyRelation[MastodonInstance] = fields.ForeignKeyField(
"models.MastodonInstance", related_name="accounts"
)
access_token = fields.TextField()
username = fields.TextField(null=True)
enabled = fields.BooleanField()
class Meta:
table = "platforms_mastodon_accounts"

View file

@ -1,185 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from asyncio import get_event_loop
from logging import getLogger
from fastapi import APIRouter, Depends, HTTPException, Response, status
from mastodon import Mastodon, MastodonNetworkError
from mastodon.errors import MastodonIllegalArgumentError
from pydantic import BaseModel, validate_email, validator
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara.config import config
from kibicara.model import Hood
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
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: int, hood: Hood = Depends(get_hood)
) -> MastodonAccount:
try:
return await MastodonAccount.get(id=mastodon_id, hood=hood)
except DoesNotExist:
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.get(name=instance_url)
except DoesNotExist:
app_name = config.get("frontend_url")
client_id, client_secret = Mastodon.create_app(
app_name, api_base_url=instance_url
)
await MastodonInstance.create(
name=instance_url, client_id=client_id, client_secret=client_secret
)
return await MastodonInstance.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)):
mbots = []
async for mbot in MastodonAccount.filter(hood=hood).prefetch_related("instance"):
if mbot.enabled == 1 and mbot.username:
mbots.append(
BodyMastodonPublic(username=mbot.username, instance=mbot.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.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.fetch_related("instance")
object_with_instance = await MastodonAccount.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.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:
logger.warning("Login to Mastodon failed.", exc_info=True)
raise HTTPException(status_code=status.HTTP_409_CONFLICT)

View file

@ -1,36 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from tortoise import fields
from tortoise.models import Model
from kibicara.model import Hood
class Telegram(Model):
id = fields.IntField(pk=True)
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
"models.Hood", related_name="platforms_telegram"
)
api_token = fields.CharField(64, unique=True)
welcome_message = fields.TextField()
username = fields.TextField(null=True)
enabled = fields.BooleanField(default=True)
subscribers: fields.ReverseRelation["TelegramSubscriber"]
class Meta:
table = "platforms_telegram"
class TelegramSubscriber(Model):
id = fields.IntField(pk=True)
bot: fields.ForeignKeyRelation[Telegram] = fields.ForeignKeyField(
"models.Telegram", related_name="subscribers"
)
user_id = fields.IntField()
class Meta:
table = "platforms_telegram_subscribers"

View file

@ -1,19 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from tortoise import fields
from tortoise.models import Model
from kibicara.model import Hood
class Test(Model):
id = fields.IntField(pk=True)
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
"models.Hood", related_name="platforms_test"
)
class Meta:
table = "platforms_test"

View file

@ -1,67 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from fastapi import APIRouter, Depends, HTTPException, Response, status
from pydantic import BaseModel
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara.model import Hood
from kibicara.platformapi import Message
from kibicara.platforms.test.bot import spawner
from kibicara.platforms.test.model import Test
from kibicara.webapi.hoods import get_hood
class BodyMessage(BaseModel):
text: str
async def get_test(test_id: int, hood: Hood = Depends(get_hood)) -> Test:
try:
return await Test.get(id=test_id, hood=hood)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
router = APIRouter()
@router.get("/")
async def test_read_all(hood: Hood = Depends(get_hood)):
return await Test.filter(hood=hood)
@router.post("/", status_code=status.HTTP_201_CREATED)
async def test_create(response: Response, hood: Hood = Depends(get_hood)):
try:
test = await Test.create(hood=hood)
spawner.start(test)
response.headers["Location"] = str(test.id)
return test
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.get("/{test_id}")
async def test_read(test: Test = Depends(get_test)):
return test
@router.delete("/{test_id}", status_code=status.HTTP_204_NO_CONTENT)
async def test_delete(test: Test = Depends(get_test)):
spawner.stop(test)
await test.delete()
@router.get("/{test_id}/messages/")
async def test_message_read_all(test: Test = Depends(get_test)):
return spawner.get(test).messages
@router.post("/{test_id}/messages/")
async def test_message_create(message: BodyMessage, test: Test = Depends(get_test)):
await spawner.get(test).publish(Message(message.text))
return {}

View file

@ -1,27 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from tortoise import fields
from tortoise.models import Model
from kibicara.model import Hood
class Twitter(Model):
id = fields.IntField(pk=True)
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField(
"models.Hood", related_name="platforms_twitter"
)
dms_since_id = fields.IntField()
mentions_since_id = fields.IntField()
access_token = fields.TextField()
access_token_secret = fields.TextField()
username = fields.TextField()
verified = fields.BooleanField(default=False)
enabled = fields.BooleanField(default=False)
class Meta:
table = "platforms_twitter"

View file

@ -1,120 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
"""REST API Endpoints for managing hoods."""
from fastapi import APIRouter, Depends, HTTPException, Response, status
from pydantic import BaseModel
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara.model import Admin, Hood, IncludePattern
from kibicara.platforms.email.bot import spawner
from kibicara.webapi.admin import get_admin
from kibicara.webapi.utils import delete_hood
class BodyHood(BaseModel):
name: str
landingpage: str = "Default Landing Page"
async def get_hood_unauthorized(hood_id: int) -> Hood:
try:
return await Hood.get(id=hood_id)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
async def get_hood(
hood: Hood = Depends(get_hood_unauthorized), admin: Admin = Depends(get_admin)
) -> Hood:
await hood.fetch_related("admins")
if admin in hood.admins:
return hood
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers={"WWW-Authenticate": "Bearer"},
)
router = APIRouter()
@router.get(
"/",
# TODO response_model,
operation_id="get_hoods",
tags=["hoods"],
)
async def hood_read_all():
"""Get all existing hoods."""
return await Hood.all()
@router.post(
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id="create_hood",
tags=["hoods"],
)
async def hood_create(
values: BodyHood, response: Response, admin: Admin = Depends(get_admin)
):
"""Creates a hood.
- **name**: Name of the hood
- **landingpage**: Markdown formatted description of the hood
"""
try:
hood = await Hood.create(**values.__dict__)
await admin.hoods.add(hood)
spawner.start(hood)
# Initialize Triggers to match all
await IncludePattern.create(hood=hood, pattern=".")
response.headers["Location"] = str(hood.id)
return hood
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@router.get(
"/{hood_id}",
# TODO response_model,
operation_id="get_hood",
tags=["hoods"],
)
async def hood_read(hood: Hood = Depends(get_hood_unauthorized)):
"""Get hood with id **hood_id**."""
return hood
@router.put(
"/{hood_id}",
operation_id="update_hood",
tags=["hoods"],
)
async def hood_update(values: BodyHood, hood: Hood = Depends(get_hood)):
"""Updates hood with id **hood_id**.
- **name**: New name of the hood
- **landingpage**: New Markdown formatted description of the hood
"""
await Hood.filter(id=hood).update(**values.__dict__)
return hood
@router.delete(
"/{hood_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="delete_hood",
tags=["hoods"],
)
async def hood_delete(hood=Depends(get_hood)):
"""Deletes hood with id **hood_id**."""
await delete_hood(hood)

View file

@ -1,116 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
"""REST API endpoints for managing exclude_patterns.
Provides API endpoints for adding, removing and reading regular expressions that block a
received message to be dropped by a censor. This provides a message filter customizable
by the hood admins.
"""
from re import compile as regex_compile, error as RegexError
from fastapi import APIRouter, Depends, HTTPException, Response, status
from pydantic import BaseModel
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara.model import ExcludePattern, Hood
from kibicara.webapi.hoods import get_hood
class BodyExcludePattern(BaseModel):
pattern: str
async def get_exclude_pattern(
exclude_pattern_id: int, hood: Hood = Depends(get_hood)
) -> ExcludePattern:
try:
return await ExcludePattern.get(id=exclude_pattern_id, hood=hood)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
router = APIRouter()
@router.get(
"/",
# TODO response_model,
operation_id="get_exclude_patterns",
)
async def exclude_pattern_read_all(hood: Hood = Depends(get_hood)):
"""Get all exclude_patterns of hood with id **hood_id**."""
return await ExcludePattern.filter(hood=hood)
@router.post(
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id="create_exclude_pattern",
)
async def exclude_pattern_create(
values: BodyExcludePattern, response: Response, hood: Hood = Depends(get_hood)
):
"""Creates a new exclude_pattern for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a exclude_pattern.
"""
try:
regex_compile(values.pattern)
exclude_pattern = await ExcludePattern.create(hood=hood, **values.__dict__)
response.headers["Location"] = str(exclude_pattern.id)
return exclude_pattern
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
except RegexError:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
@router.get(
"/{exclude_pattern_id}",
# TODO response_model,
operation_id="get_exclude_pattern",
)
async def exclude_pattern_read(
exclude_pattern: ExcludePattern = Depends(get_exclude_pattern),
):
"""
Reads exclude_pattern with id **exclude_pattern_id** for hood with id **hood_id**.
"""
return exclude_pattern
@router.put(
"/{exclude_pattern_id}",
operation_id="update_exclude_pattern",
)
async def exclude_pattern_update(
values: BodyExcludePattern,
exclude_pattern: ExcludePattern = Depends(get_exclude_pattern),
):
"""
Updates exclude_pattern with id **exclude_pattern_id** for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a exclude_pattern
"""
await ExcludePattern.filter(id=exclude_pattern).update(**values.__dict__)
return exclude_pattern
@router.delete(
"/{exclude_pattern_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="delete_exclude_pattern",
)
async def exclude_pattern_delete(
exclude_pattern: ExcludePattern = Depends(get_exclude_pattern),
):
"""
Deletes exclude_pattern with id **exclude_pattern_id** for hood with id **hood_id**.
"""
await exclude_pattern.delete()

View file

@ -1,115 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
"""REST API endpoints for managing include_patterns.
Provides API endpoints for adding, removing and reading regular expressions that allow a
message to be passed through by a censor. A published message must match one of these
regular expressions otherwise it gets dropped by the censor. This provides a message
filter customizable by the hood admins.
"""
from re import compile as regex_compile, error as RegexError
from fastapi import APIRouter, Depends, HTTPException, Response, status
from pydantic import BaseModel
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara.model import IncludePattern, Hood
from kibicara.webapi.hoods import get_hood
class BodyIncludePattern(BaseModel):
pattern: str
async def get_include_pattern(include_pattern_id: int, hood: Hood = Depends(get_hood)):
try:
return await IncludePattern.get(id=include_pattern_id, hood=hood)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
router = APIRouter()
@router.get(
"/",
# TODO response_model,
operation_id="get_include_patterns",
)
async def include_pattern_read_all(hood: Hood = Depends(get_hood)):
"""Get all include_patterns of hood with id **hood_id**."""
return await IncludePattern.filter(hood=hood)
@router.post(
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id="create_include_pattern",
)
async def include_pattern_create(
values: BodyIncludePattern, response: Response, hood: Hood = Depends(get_hood)
):
"""Creates a new include_pattern for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a include_pattern.
"""
try:
regex_compile(values.pattern)
include_pattern = await IncludePattern.create(hood=hood, **values.__dict__)
response.headers["Location"] = str(include_pattern.id)
return include_pattern
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
except RegexError:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
@router.get(
"/{include_pattern_id}",
# TODO response_model,
operation_id="get_include_pattern",
)
async def include_pattern_read(
include_pattern: IncludePattern = Depends(get_include_pattern),
):
"""
Reads include_pattern with id **include_pattern_id** for hood with id **hood_id**.
"""
return include_pattern
@router.put(
"/{include_pattern_id}",
operation_id="update_include_pattern",
)
async def include_pattern_update(
values: BodyIncludePattern,
include_pattern: IncludePattern = Depends(get_include_pattern),
):
"""
Updates include_pattern with id **include_pattern_id** for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a include_pattern
"""
await IncludePattern.filter(id=include_pattern).update(**values.__dict__)
return include_pattern
@router.delete(
"/{include_pattern_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="delete_include_pattern",
)
async def include_pattern_delete(
include_pattern: IncludePattern = Depends(get_include_pattern),
):
"""
Deletes include_pattern with id **include_pattern_id** for hood with id **hood_id**.
"""
await include_pattern.delete()

View file

@ -1,14 +0,0 @@
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
#
# SPDX-License-Identifier: 0BSD
from kibicara.model import ExcludePattern, Hood, IncludePattern
from kibicara.platformapi import Spawner
async def delete_hood(hood: Hood) -> None:
await Spawner.destroy_hood(hood)
await IncludePattern.filter(hood=hood).delete()
await ExcludePattern.filter(hood=hood).delete()
await hood.delete()

View file

@ -1,156 +0,0 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from urllib.parse import urlparse
from fastapi import FastAPI, status
from httpx import AsyncClient
import pytest
from tortoise import Tortoise
from kibicara import email
from kibicara.webapi import router
@pytest.fixture(scope="session")
def anyio_backend():
return "asyncio"
@pytest.fixture(scope="session")
@pytest.mark.anyio
async def client():
await Tortoise.init(
db_url="sqlite://:memory:",
modules={
"models": [
"kibicara.model",
"kibicara.platforms.email.model",
"kibicara.platforms.mastodon.model",
"kibicara.platforms.telegram.model",
"kibicara.platforms.test.model",
"kibicara.platforms.twitter.model",
]
},
)
await Tortoise.generate_schemas()
app = FastAPI()
app.include_router(router, prefix="/api")
yield AsyncClient(app=app, base_url="http://test")
await Tortoise.close_connections()
@pytest.fixture(scope="session")
def monkeymodule():
from _pytest.monkeypatch import MonkeyPatch
mpatch = MonkeyPatch()
yield mpatch
mpatch.undo()
@pytest.fixture(scope="session")
def receive_email(monkeymodule):
mailbox = []
def mock_send_email(to, subject, sender="kibicara", body=""):
mailbox.append(dict(to=to, subject=subject, sender=sender, body=body))
def mock_receive_email():
return mailbox.pop()
monkeymodule.setattr(email, "send_email", mock_send_email)
return mock_receive_email
@pytest.fixture(scope="session")
@pytest.mark.anyio
async def register_token(client, receive_email):
response = await client.post(
"/api/admin/register/", json={"email": "user", "password": "password"}
)
assert response.status_code == status.HTTP_202_ACCEPTED
return urlparse(receive_email()["body"]).query.split("=", 1)[1]
@pytest.fixture(scope="session")
@pytest.mark.anyio
async def register_confirmed(client, register_token):
response = await client.post("/api/admin/confirm/{0}".format(register_token))
assert response.status_code == status.HTTP_200_OK
@pytest.fixture(scope="session")
@pytest.mark.anyio
async def access_token(client, register_confirmed):
response = await client.post(
"/api/admin/login/", data={"username": "user", "password": "password"}
)
assert response.status_code == status.HTTP_200_OK
return response.json()["access_token"]
@pytest.fixture(scope="session")
def auth_header(access_token):
return {"Authorization": "Bearer {0}".format(access_token)}
@pytest.fixture(scope="function")
@pytest.mark.anyio
async def hood_id(client, auth_header):
response = await client.post(
"/api/hoods/", json={"name": "hood"}, headers=auth_header
)
assert response.status_code == status.HTTP_201_CREATED
hood_id = int(response.headers["Location"])
yield hood_id
await client.delete("/api/hoods/{0}".format(hood_id), headers=auth_header)
@pytest.fixture(scope="function")
@pytest.mark.anyio
async def trigger_id(client, hood_id, auth_header):
response = await client.post(
"/api/hoods/{0}/triggers/".format(hood_id),
json={"pattern": "test"},
headers=auth_header,
)
assert response.status_code == status.HTTP_201_CREATED
trigger_id = int(response.headers["Location"])
yield trigger_id
await client.delete(
"/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id), headers=auth_header
)
@pytest.fixture(scope="function")
@pytest.mark.anyio
async def badword_id(client, hood_id, auth_header):
response = await client.post(
"/api/hoods/{0}/badwords/".format(hood_id),
json={"pattern": ""},
headers=auth_header,
)
assert response.status_code == status.HTTP_201_CREATED
badword_id = int(response.headers["Location"])
yield badword_id
await client.delete(
"/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id), headers=auth_header
)
@pytest.fixture(scope="function")
@pytest.mark.anyio
async def test_id(client, hood_id, auth_header):
response = await client.post(
"/api/hoods/{0}/test/".format(hood_id), json={}, headers=auth_header
)
assert response.status_code == status.HTTP_201_CREATED
test_id = int(response.headers["Location"])
yield test_id
await client.delete(
"/api/hoods/{0}/test/{1}".format(hood_id, test_id), headers=auth_header
)

View file

@ -1,18 +0,0 @@
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
import pytest
@pytest.mark.anyio
async def test_hoods_unauthorized(client):
response = await client.get("/api/admin/hoods/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_hoods_success(client, auth_header):
response = await client.get("/api/admin/hoods/", headers=auth_header)
assert response.status_code == status.HTTP_200_OK

View file

@ -1,109 +0,0 @@
# Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me>
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
import pytest
from fastapi import status
@pytest.mark.anyio
async def test_hood_read_all(client):
response = await client.get("/api/hoods/")
assert response.status_code == status.HTTP_200_OK
@pytest.mark.anyio
async def test_hood_create_unauthorized(client, hood_id):
response = await client.post("/api/hoods/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_hood_read(client, hood_id):
response = await client.get("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_200_OK
@pytest.mark.anyio
async def test_hood_update_unauthorized(client, hood_id):
response = await client.put("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_hood_delete_unauthorized(client, hood_id):
response = await client.delete("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_trigger_read_all_unauthorized(client, hood_id):
response = await client.get("/api/hoods/{0}/triggers/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_trigger_create_unauthorized(client, hood_id):
response = await client.post("/api/hoods/{0}/triggers/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_trigger_read_unauthorized(client, hood_id, trigger_id):
response = await client.get(
"/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_trigger_update_unauthorized(client, hood_id, trigger_id):
response = await client.put(
"/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_trigger_delete_unauthorized(client, hood_id, trigger_id):
response = await client.delete(
"/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_badword_read_all_unauthorized(client, hood_id):
response = await client.get("/api/hoods/{0}/badwords/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_badword_create_unauthorized(client, hood_id):
response = await client.post("/api/hoods/{0}/badwords/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_badword_read_unauthorized(client, hood_id, badword_id):
response = await client.get(
"/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_badword_update_unauthorized(client, hood_id, badword_id):
response = await client.put(
"/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_badword_delete_unauthorized(client, hood_id, badword_id):
response = await client.delete(
"/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,25 +0,0 @@
# Copyright (C) 2020 by Maike <maike@systemli.org>
# 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
import pytest
@pytest.fixture(scope="function")
@pytest.mark.anyio
async def email_row(client, hood_id, auth_header):
response = await client.post(
"/api/hoods/{0}/email/".format(hood_id),
json={"name": "kibicara-test"},
headers=auth_header,
)
assert response.status_code == status.HTTP_201_CREATED
email_id = int(response.headers["Location"])
yield response.json()
await client.delete(
"/api/hoods/{0}/email/{1}".format(hood_id, email_id), headers=auth_header
)

View file

@ -1,31 +0,0 @@
# Copyright (C) 2020 by Maike <maike@systemli.org>
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
import pytest
@pytest.mark.anyio
async def test_email_create_unauthorized(client, hood_id):
response = await client.post("/api/hoods/{0}/email/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_email_delete_unauthorized(client, hood_id, email_row):
response = await client.delete(
"/api/hoods/{0}/email/{1}".format(hood_id, email_row["id"])
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_email_message_unauthorized(client, hood_id, email_row):
body = {"text": "test", "author": "author", "secret": "wrong"}
response = await client.post(
"/api/hoods/{0}/email/messages/".format(hood_id), json=body
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,67 +0,0 @@
# Copyright (C) 2020 by Maike <maike@systemli.org>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
from nacl.exceptions import CryptoError
import pytest
@pytest.mark.anyio
async def test_email_subscribe_empty(client, hood_id):
response = await client.post("/api/hoods/{0}/email/subscribe/".format(hood_id))
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio
async def test_email_subscribe_confirm_wrong_token(client, hood_id):
try:
response = await client.post(
"/api/hoods/{0}/email/subscribe/confirm/".format(hood_id)
+ "asdfasdfasdfasdfasdfasdfasdfasdf"
)
assert response.status_code is not status.HTTP_201_CREATED
except CryptoError:
pass
@pytest.mark.anyio
async def test_email_subscribe_confirm_wrong_hood(client):
response = await client.delete(
"/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf"
)
assert response.json()["detail"] == "Not Found"
@pytest.mark.anyio
async def test_email_message_wrong(client, hood_id, email_row):
body = {
"text": "",
"author": "test@localhost",
"secret": email_row["secret"],
}
response = await client.post(
"/api/hoods/{0}/email/messages/".format(hood_id), json=body
)
assert response.status_code == status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS
@pytest.mark.anyio
async def test_email_unsubscribe_wrong_token(client, hood_id):
try:
await client.delete(
"/api/hoods/{0}/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf".format(
hood_id
)
)
except CryptoError:
pass
@pytest.mark.anyio
async def test_email_unsubscribe_wrong_hood(client):
response = await client.delete(
"/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf"
)
assert response.json()["detail"] == "Not Found"

View file

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

View file

@ -1,96 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
import pytest
from mastodon.Mastodon import Mastodon
from kibicara.platforms import mastodon
from kibicara.platforms.mastodon.model import MastodonAccount
@pytest.fixture(scope="function")
def disable_spawner(monkeypatch):
class DoNothing:
def start(self, bot):
assert bot is not None
monkeypatch.setattr(mastodon.webapi, "spawner", DoNothing())
@pytest.mark.parametrize(
"body",
[
{
"instance_url": "botsin.space",
"email": "test@example.org",
"password": "string",
}
],
)
@pytest.mark.anyio
async def test_mastodon_create_bot(
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 = await 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 = await MastodonAccount.get(id=bot_id)
assert response.json()["access_token"] == mastodon_obj.access_token
assert mastodon_obj.enabled
@pytest.mark.parametrize(
"body",
[
{"instance_url": "botsin.space", "email": "notanemail", "password": "asdf1234"},
{"instance_url": "wrong", "email": "asdf@example.org", "password": "asdf1234"},
],
)
@pytest.mark.anyio
async def test_mastodon_invalid_input(
client,
disable_spawner,
hood_id,
auth_header,
monkeypatch,
body,
):
response = await client.post(
"/api/hoods/{0}/mastodon/".format(hood_id),
json=body,
headers=auth_header,
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio
async def test_mastodon_create_mastodon_invalid_id(client, auth_header):
response = await client.post("/api/hoods/1337/mastodon/", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.post("/api/hoods/wrong/mastodon/", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio
async def test_mastodon_create_unauthorized(client, hood_id):
response = await client.post("/api/hoods/{hood_id}/mastodon/")
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>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
import pytest
from tortoise.exceptions import DoesNotExist
from kibicara.platforms.mastodon.model import MastodonAccount
@pytest.mark.anyio
async def test_mastodon_delete_bot(client, mastodon_account, auth_header):
response = await 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 pytest.raises(DoesNotExist):
await MastodonAccount.get(id=mastodon_account.id)
@pytest.mark.anyio
async def test_mastodon_delete_bot_invalid_id(client, auth_header, hood_id):
response = await client.delete("/api/hoods/1337/mastodon/123", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.delete("/api/hoods/wrong/mastodon/123", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
response = await client.delete(
"/api/hoods/{0}/mastodon/7331".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.delete(
"/api/hoods/{0}/mastodon/wrong".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio
async def test_mastodon_delete_bot_unauthorized(client, mastodon_account):
response = await 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,55 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
import pytest
from kibicara.platforms.mastodon.model import MastodonAccount
@pytest.mark.anyio
async def test_mastodon_get_bots(
client, auth_header, hood_id, mastodon_account, mastodon_instance
):
mastodon2 = await MastodonAccount.create(
hood=mastodon_account.hood,
instance=mastodon_instance,
access_token="4cc3ss",
enabled=True,
username="us4r",
)
response = await client.get(
"/api/hoods/{0}/mastodon/".format(mastodon_account.hood.id), headers=auth_header
)
print(response.headers)
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
@pytest.mark.anyio
async def test_mastodon_get_bots_invalid_id(client, auth_header, hood_id):
response = await client.get("/api/hoods/1337/mastodon/", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.get("/api/hoods/wrong/mastodon/", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio
async def test_mastodon_get_bots_unauthorized(client, hood_id):
response = await client.get("/api/hoods/{0}/mastodon/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio
async def test_mastodon_public(client, mastodon_account, mastodon_instance):
response = await 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

@ -1,21 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
import pytest
from kibicara.model import Hood
from kibicara.platforms.telegram.model import Telegram
@pytest.fixture(scope="function")
@pytest.mark.anyio
async def telegram(hood_id, bot):
hood = await Hood.get(id=hood_id)
return await Telegram.create(
hood=hood,
api_token=bot["api_token"],
welcome_message=bot["welcome_message"],
)

View file

@ -1,84 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
import pytest
from kibicara.platforms import telegram
from kibicara.platforms.telegram.model import Telegram
@pytest.fixture(scope="function")
def disable_spawner(monkeypatch):
class DoNothing:
def start(self, bot):
assert bot is not None
monkeypatch.setattr(telegram.webapi, "spawner", DoNothing())
@pytest.mark.parametrize("body", [{"api_token": "string", "welcome_message": "string"}])
@pytest.mark.anyio
async def test_telegram_create_bot(
client,
disable_spawner,
hood_id,
auth_header,
monkeypatch,
body,
):
def check_token_mock(token):
return True
monkeypatch.setattr(telegram.webapi, "check_token", check_token_mock)
response = await client.post(
"/api/hoods/{0}/telegram/".format(hood_id),
json=body,
headers=auth_header,
)
assert response.status_code == status.HTTP_201_CREATED
bot_id = response.json()["id"]
telegram_obj = await Telegram.get(id=bot_id)
assert response.json()["api_token"] == body["api_token"] == telegram_obj.api_token
assert (
response.json()["welcome_message"]
== body["welcome_message"]
== telegram_obj.welcome_message
)
assert telegram_obj.enabled
@pytest.mark.parametrize("body", [{"api_token": "string", "welcome_message": "string"}])
@pytest.mark.anyio
async def test_telegram_invalid_api_token(
client,
disable_spawner,
hood_id,
auth_header,
monkeypatch,
body,
):
response = await client.post(
"/api/hoods/{0}/telegram/".format(hood_id),
json=body,
headers=auth_header,
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio
async def test_telegram_create_telegram_invalid_id(client, auth_header):
response = await client.post("/api/hoods/1337/telegram/", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.post("/api/hoods/wrong/telegram/", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio
async def test_telegram_create_unauthorized(client, hood_id):
response = await client.post("/api/hoods/{hood_id}/telegram/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,56 +0,0 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
import pytest
from tortoise.exceptions import DoesNotExist
from kibicara.platforms.telegram.model import Telegram, TelegramSubscriber
@pytest.mark.parametrize(
"bot", [{"api_token": "apitoken123", "welcome_message": "msg"}]
)
@pytest.mark.anyio
async def test_telegram_delete_bot(client, bot, telegram, auth_header):
await TelegramSubscriber.create(user_id=1234, bot=telegram)
await TelegramSubscriber.create(user_id=5678, bot=telegram)
response = await client.delete(
"/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id),
headers=auth_header,
)
assert response.status_code == status.HTTP_204_NO_CONTENT
with pytest.raises(DoesNotExist):
await Telegram.get(id=telegram.id)
with pytest.raises(DoesNotExist):
await TelegramSubscriber.get(id=telegram.id)
@pytest.mark.anyio
async def test_telegram_delete_bot_invalid_id(client, auth_header, hood_id):
response = await client.delete("/api/hoods/1337/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.delete("/api/hoods/wrong/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
response = await client.delete(
"/api/hoods/{0}/telegram/7331".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.delete(
"/api/hoods/{0}/telegram/wrong".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.parametrize(
"bot", [{"api_token": "apitoken123", "welcome_message": "msg"}]
)
@pytest.mark.anyio
async def test_telegram_delete_bot_unauthorized(client, bot, telegram):
response = await client.delete(
"/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id)
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,49 +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
import pytest
@pytest.mark.parametrize(
"bot", [{"api_token": "apitoken123", "welcome_message": "msg"}]
)
@pytest.mark.anyio
async def test_telegram_get_bot(client, auth_header, bot, telegram):
response = await client.get(
"/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id),
headers=auth_header,
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["id"] == telegram.id
assert response.json()["api_token"] == telegram.api_token
assert response.json()["welcome_message"] == telegram.welcome_message
@pytest.mark.anyio
async def test_telegram_get_bot_invalid_id(client, auth_header, hood_id):
response = await client.get("/api/hoods/1337/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.get("/api/hoods/wrong/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
response = await client.get(
"/api/hoods/{0}/telegram/7331".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.get(
"/api/hoods/{0}/telegram/wrong".format(hood_id), headers=auth_header
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.parametrize(
"bot", [{"api_token": "apitoken456", "welcome_message": "msg"}]
)
@pytest.mark.anyio
async def test_telegram_get_bot_unauthorized(client, bot, telegram):
response = await client.get(
"/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id)
)
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>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
#
# SPDX-License-Identifier: 0BSD
from fastapi import status
import pytest
from kibicara.model import Hood
from kibicara.platforms.telegram.model import Telegram
@pytest.mark.anyio
async def test_telegram_get_bots(client, auth_header, hood_id):
hood = await Hood.get(id=hood_id)
telegram0 = await Telegram.create(
hood=hood,
api_token="api_token123",
welcome_message="welcome_message123",
)
telegram1 = await Telegram.create(
hood=hood,
api_token="api_token456",
welcome_message="welcome_message123",
)
response = await client.get(
"/api/hoods/{0}/telegram/".format(telegram0.hood.id), headers=auth_header
)
assert response.status_code == status.HTTP_200_OK
assert response.json()[0]["id"] == telegram0.id
assert response.json()[0]["api_token"] == telegram0.api_token
assert response.json()[1]["id"] == telegram1.id
assert response.json()[1]["api_token"] == telegram1.api_token
@pytest.mark.anyio
async def test_telegram_get_bots_invalid_id(client, auth_header, hood_id):
response = await client.get("/api/hoods/1337/telegram/", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.get("/api/hoods/wrong/telegram/", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio
async def test_telegram_get_bots_unauthorized(client, hood_id):
response = await client.get("/api/hoods/{0}/telegram/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,53 +0,0 @@
# Kibicara Frontend
## Maintenance
### Compatibility List
** The current compatible nodejs version is nodejs18 **
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.
### 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`.
### 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
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.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View file

@ -1,80 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="favicon.svg"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
id="svg964"
version="1.1"
viewBox="0 0 12.7 12.7"
height="48"
width="48">
<defs
id="defs958" />
<sodipodi:namedview
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="0"
inkscape:window-height="726"
inkscape:window-width="1364"
units="px"
showgrid="true"
inkscape:document-rotation="0"
inkscape:current-layer="layer1"
inkscape:document-units="mm"
inkscape:cy="21.735033"
inkscape:cx="23.387597"
inkscape:zoom="10.229167"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base">
<inkscape:grid
empspacing="8"
spacingy="0.26458333"
spacingx="0.26458333"
id="grid1527"
type="xygrid" />
</sodipodi:namedview>
<metadata
id="metadata961">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<circle
r="6.0854168"
cy="6.3499999"
cx="6.3499999"
id="path1529"
style="fill:#673ab7;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;fill-opacity:1" />
<text
id="text1533"
y="10.337475"
x="6.1533628"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:10.5833px;line-height:1.25;font-family:Roboto;-inkscape-font-specification:Roboto;text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:Roboto;fill:#ffffff;fill-opacity:1;stroke-width:0.264583"
y="10.337475"
x="6.1533628"
id="tspan1531"
sodipodi:role="line">k</tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

26775
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,54 +0,0 @@
{
"name": "kibicara-frontend",
"version": "0.0.0",
"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",
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.3",
"@angular/cdk": "^15.2.3",
"@angular/common": "^15.2.3",
"@angular/compiler": "^15.2.3",
"@angular/core": "^15.2.3",
"@angular/forms": "^15.2.3",
"@angular/localize": "^15.2.3",
"@angular/material": "^15.2.3",
"@angular/platform-browser": "^15.2.3",
"@angular/platform-browser-dynamic": "^15.2.3",
"@angular/router": "^15.2.3",
"ng2-search-filter": "^0.5.1",
"rxjs": "~6.5.4",
"tslib": "^2.0.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.4",
"@angular/cli": "^15.2.4",
"@angular/compiler-cli": "^15.2.3",
"@angular/language-service": "^15.2.3",
"@openapitools/openapi-generator-cli": "^1.0.18-5.0.0-beta2",
"@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.12.64",
"codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"ngx-markdown": "^15.1.2",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.9.5"
}
}

View file

@ -1 +0,0 @@
5.0.0-beta2

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

@ -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

@ -1,3 +0,0 @@
<mat-card appearance="outlined" class="card">
<p>{{ status }}</p>
</mat-card>

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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -6,4 +6,4 @@
# #
# client-side git-hook - run tests and stylechecker # client-side git-hook - run tests and stylechecker
cd backend && exec .venv/bin/tox exec tox

View file

@ -32,7 +32,6 @@ speed-measure-plugin*.json
.history/* .history/*
# misc # misc
/.angular/cache
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage /coverage

View file

@ -0,0 +1,27 @@
# KibicaraFrontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 9.1.4.
## 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.
## 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`.
## 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.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

View file

@ -22,6 +22,7 @@
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"aot": true,
"assets": ["src/favicon.ico", "src/assets"], "assets": ["src/favicon.ico", "src/assets"],
"styles": [ "styles": [
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
@ -30,13 +31,7 @@
"stylePreprocessorOptions": { "stylePreprocessorOptions": {
"includePaths": ["src/sass"] "includePaths": ["src/sass"]
}, },
"scripts": [], "scripts": []
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
}, },
"configurations": { "configurations": {
"production": { "production": {
@ -49,6 +44,7 @@
"optimization": true, "optimization": true,
"outputHashing": "all", "outputHashing": "all",
"sourceMap": false, "sourceMap": false,
"extractCss": true,
"namedChunks": false, "namedChunks": false,
"extractLicenses": true, "extractLicenses": true,
"vendorChunk": false, "vendorChunk": false,
@ -66,8 +62,7 @@
} }
] ]
} }
}, }
"defaultConfiguration": ""
}, },
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
@ -104,6 +99,17 @@
"scripts": [] "scripts": []
} }
}, },
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": ["**/node_modules/**", "src/app/core/api/**"]
}
},
"e2e": { "e2e": {
"builder": "@angular-devkit/build-angular:protractor", "builder": "@angular-devkit/build-angular:protractor",
"options": { "options": {
@ -118,5 +124,6 @@
} }
} }
} }
} },
"defaultProject": "kibicara-frontend"
} }

View file

@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "../out-tsc/e2e", "outDir": "../out-tsc/e2e",
"module": "commonjs", "module": "commonjs",
"target": "es2018", "target": "es5",
"types": [ "types": [
"jasmine", "jasmine",
"jasminewd2", "jasminewd2",

View file

@ -0,0 +1,7 @@
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "5.4.0"
}
}

30069
kibicara-frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
{
"name": "kibicara-frontend",
"version": "0.0.0",
"scripts": {
"openapi-generator": "openapi-generator generate -g typescript-angular --additional-properties=providedInRoot=true -o src/app/core/api -i http://localhost:8000/api/openapi.json",
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~9.1.4",
"@angular/cdk": "^9.2.4",
"@angular/common": "~9.1.4",
"@angular/compiler": "~9.1.4",
"@angular/core": "~9.1.4",
"@angular/forms": "~9.1.4",
"@angular/material": "^9.2.4",
"@angular/platform-browser": "~9.1.4",
"@angular/platform-browser-dynamic": "~9.1.4",
"@angular/router": "~9.1.4",
"ng2-search-filter": "^0.5.1",
"ngx-markdown": "^10.1.1",
"rxjs": "~6.5.4",
"tslib": "^1.14.0",
"zone.js": "~0.10.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.901.4",
"@angular/cli": "~9.1.4",
"@angular/compiler-cli": "~9.1.4",
"@angular/language-service": "~9.1.4",
"@openapitools/openapi-generator-cli": "^1.0.18-5.0.0-beta2",
"@types/jasmine": "^3.5.14",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.12.64",
"codelyzer": "^5.1.2",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.0",
"karma-jasmine": "~3.0.1",
"karma-jasmine-html-reporter": "^1.4.2",
"protractor": "^7.0.0",
"ts-node": "~8.3.0",
"tslint": "^6.1.3",
"typescript": "~3.8.3"
}
}

View file

@ -28,7 +28,7 @@ const routes: Routes = [
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, {}), SharedModule], imports: [RouterModule.forRoot(routes), SharedModule],
exports: [RouterModule], exports: [RouterModule],
}) })
export class AppRoutingModule {} export class AppRoutingModule {}

View file

@ -1,9 +1,9 @@
import { TestBed, waitForAsync } from '@angular/core/testing'; import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
describe('AppComponent', () => { describe('AppComponent', () => {
beforeEach(waitForAsync(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
RouterTestingModule RouterTestingModule

View file

@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ConfirmComponent } from './confirm.component'; import { ConfirmComponent } from './confirm.component';
@ -6,7 +6,7 @@ describe('ConfirmComponent', () => {
let component: ConfirmComponent; let component: ConfirmComponent;
let fixture: ComponentFixture<ConfirmComponent>; let fixture: ComponentFixture<ConfirmComponent>;
beforeEach(waitForAsync(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ ConfirmComponent ] declarations: [ ConfirmComponent ]
}) })

View file

@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<mat-card appearance="outlined" class="login-form"> <mat-card class="login-form">
<mat-card-header> <mat-card-header>
<h2>Log in as hood admin!</h2> <h2>Log in as hood admin!</h2>
</mat-card-header> </mat-card-header>

View file

@ -7,7 +7,7 @@
margin-top: 3%; margin-top: 3%;
} }
.mat-mdc-card:not([class*="mat-elevation-z"]) { .mat-card:not([class*="mat-elevation-z"]) {
box-shadow: none; box-shadow: none;
} }

View file

@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LoginComponent } from './login.component'; import { LoginComponent } from './login.component';
@ -6,7 +6,7 @@ describe('LoginComponent', () => {
let component: LoginComponent; let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>; let fixture: ComponentFixture<LoginComponent>;
beforeEach(waitForAsync(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ LoginComponent ] declarations: [ LoginComponent ]
}) })

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { LoginService } from '../../core/auth/login.service'; import { LoginService } from '../../core/auth/login.service';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { Validators, UntypedFormGroup, UntypedFormBuilder } from '@angular/forms'; import { Validators, FormGroup, FormBuilder } from '@angular/forms';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
@ -11,7 +11,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
styleUrls: ['./login.component.scss'], styleUrls: ['./login.component.scss'],
}) })
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
loginForm: UntypedFormGroup; loginForm: FormGroup;
returnUrl: string; returnUrl: string;
loading = false; loading = false;
submitted = false; submitted = false;
@ -21,7 +21,7 @@ export class LoginComponent implements OnInit {
private loginService: LoginService, private loginService: LoginService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private formBuilder: UntypedFormBuilder, private formBuilder: FormBuilder,
private snackBar: MatSnackBar private snackBar: MatSnackBar
) { ) {
if (this.loginService.currentHoodAdminValue) { if (this.loginService.currentHoodAdminValue) {

View file

@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<mat-card appearance="outlined" class="login-form"> <mat-card class="login-form">
<mat-card-header> <mat-card-header>
<h2>Reset password</h2> <h2>Reset password</h2>
</mat-card-header> </mat-card-header>

View file

@ -6,7 +6,7 @@
margin-top: 3%; margin-top: 3%;
} }
.mat-mdc-card:not([class*="mat-elevation-z"]) { .mat-card:not([class*="mat-elevation-z"]) {
box-shadow: none; box-shadow: none;
} }

View file

@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PasswordResetComponent } from './password-reset.component'; import { PasswordResetComponent } from './password-reset.component';
@ -6,7 +6,7 @@ describe('PasswordResetComponent', () => {
let component: PasswordResetComponent; let component: PasswordResetComponent;
let fixture: ComponentFixture<PasswordResetComponent>; let fixture: ComponentFixture<PasswordResetComponent>;
beforeEach(waitForAsync(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PasswordResetComponent], declarations: [PasswordResetComponent],
}).compileComponents(); }).compileComponents();

View file

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
@ -12,7 +12,7 @@ import { LoginService } from 'src/app/core/auth/login.service';
styleUrls: ['./password-reset.component.scss'], styleUrls: ['./password-reset.component.scss'],
}) })
export class PasswordResetComponent implements OnInit { export class PasswordResetComponent implements OnInit {
resetForm: UntypedFormGroup; resetForm: FormGroup;
returnUrl: string; returnUrl: string;
loading = false; loading = false;
submitted = false; submitted = false;
@ -23,7 +23,7 @@ export class PasswordResetComponent implements OnInit {
private loginService: LoginService, private loginService: LoginService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private formBuilder: UntypedFormBuilder, private formBuilder: FormBuilder,
private snackBar: MatSnackBar private snackBar: MatSnackBar
) { ) {
if (this.loginService.currentHoodAdminValue) { if (this.loginService.currentHoodAdminValue) {

View file

@ -1,5 +1,5 @@
<div class="container"> <div class="container">
<mat-card appearance="outlined" class="login-form"> <mat-card class="login-form">
<mat-card-header> <mat-card-header>
<h2>Enter your new password</h2> <h2>Enter your new password</h2>
</mat-card-header> </mat-card-header>

View file

@ -6,7 +6,7 @@
margin-top: 3%; margin-top: 3%;
} }
.mat-mdc-card:not([class*="mat-elevation-z"]) { .mat-card:not([class*="mat-elevation-z"]) {
box-shadow: none; box-shadow: none;
} }

Some files were not shown because too many files have changed in this diff Show more