Compare commits
4 commits
developmen
...
fix-devenv
Author | SHA1 | Date | |
---|---|---|---|
missytake | d6d43f2b0a | ||
missytake | 2bf16aae11 | ||
missytake | 0f6756bce6 | ||
missytake | a0aea38902 |
163
CONTRIBUTING.md
163
CONTRIBUTING.md
|
@ -1,41 +1,41 @@
|
|||
# Kibicara Contribution HowTo
|
||||
# Ticketfrei Contribution HowTo
|
||||
|
||||
|
||||
## Setup Development Environment
|
||||
|
||||
### General
|
||||
|
||||
1. Install `python>=3.10` and development packages
|
||||
(`apt install python3-dev g++` on Ubuntu)
|
||||
2. Run `./setup.sh`
|
||||
|
||||
### Backend
|
||||
|
||||
0. `cd backend`
|
||||
1. Activate your dev environment with `source .venv/bin/activate`
|
||||
2. Install with `pip install .`
|
||||
3. Create a config file: `echo "production = 0" > kibicara.conf`
|
||||
1. Install `python>=3.8`
|
||||
2. Create a virtual environment with `python3 -m venv .venv`
|
||||
3. Activate your dev environment with `source .venv/bin/activate`
|
||||
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 pytest pytest-aiohttp`
|
||||
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
|
||||
|
||||
- Install Kibicara with `pip install .`
|
||||
- Execute Kibicara with `kibicara -f kibicara.conf`
|
||||
(verbose: `kibicara -vvv -f kibicara.conf`)
|
||||
- Install Ticketfrei with `pip install .`
|
||||
- Execute Ticketfrei with `kibicara` (verbose: `kibicara -vvv`)
|
||||
- Interact with Swagger REST-API Documentation: `http://localhost:8000/api/docs`
|
||||
- Test and stylecheck with `tox`
|
||||
- Fix style issues with `black -S src tests`
|
||||
- Fix style issues with `black -S kibicara tests`
|
||||
|
||||
### Frontend
|
||||
|
||||
1. Install node.js (e.g. via
|
||||
[nvm](https://github.com/nvm-sh/nvm#installation-and-update))
|
||||
2. `cd frontend`
|
||||
2. `cd kibicara-frontend`
|
||||
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).
|
||||
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`.
|
||||
The application will open under [http://127.0.0.1:4200](http://127.0.0.1:4200).
|
||||
7. To serve and open the application, run `ng s -o`. The application will open
|
||||
under [http://127.0.0.1:4200](http://127.0.0.1:4200).
|
||||
|
||||
### Creating an account
|
||||
|
||||
|
@ -60,9 +60,14 @@ email address and register via frontend or manually at `http://localhost:8000/ap
|
|||
## Contribution Guidelines
|
||||
### 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.
|
||||
- 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:**
|
||||
- A feature branch will be used to develop a feature.
|
||||
- It belongs to one developer only and force push is allowed.
|
||||
|
@ -83,8 +88,8 @@ following pattern:
|
|||
|
||||
You can use these tags:
|
||||
|
||||
- [core] Feature for Kibicara core
|
||||
- [frontend] Feature for Kibicara frontend
|
||||
- [core] Feature for Ticketfrei core
|
||||
- [frontend] Feature for Ticketfrei frontend
|
||||
- [$platform] Feature for platforms, e.g.
|
||||
- [twitter]
|
||||
- [telegram]
|
||||
|
@ -128,29 +133,17 @@ development team.
|
|||
|
||||
## How to implement a new Platform/Social Network
|
||||
|
||||
For transferring messages, Kibicara supports a range of platforms/social
|
||||
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.
|
||||
### tl;dr
|
||||
|
||||
### Overview:
|
||||
|
||||
1. Implement the backend modules in `platforms/<your-platform>/`:
|
||||
1. Implement the following modules in `platforms/<your-platform>/`:
|
||||
- `bot.py`
|
||||
- `model.py`
|
||||
- `webapi.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
|
||||
3. Generate the FastAPI stuff
|
||||
4. Generate the angular components for the kibicara-frontend from the FastAPI stuff
|
||||
|
||||
At the bottom you can find a checklist what your pull request needs to be
|
||||
merged into kibicara.
|
||||
|
||||
### Step by step
|
||||
|
||||
#### Implement the backend modules
|
||||
### Explanation
|
||||
|
||||
In `kibicara/platforms/<your-platform>/bot.py`, you write the functions through
|
||||
which the platform asks the social network for new messages, and publishes
|
||||
|
@ -166,96 +159,20 @@ You will probably need to store the following things:
|
|||
* platform-specific settings
|
||||
* anything else your platform needs
|
||||
|
||||
In `kibicara/platforms/<your-platform>/webapi.py`, you can define REST API
|
||||
routes. You will need them to:
|
||||
In `kibicara/platforms/<your-platform>/webapi.py`, you can define HTTP routes.
|
||||
You will need them to:
|
||||
|
||||
* let admins authenticate to the social network in the kibicara web interface
|
||||
* update platform-specific settings
|
||||
|
||||
#### Import your bot into the kibicara REST API
|
||||
|
||||
To run the platform, you need to import the bot in
|
||||
`kibicara/webapi/__init__.py`. You can see how the other platforms did it.
|
||||
|
||||
#### 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.
|
||||
`kibicara/webapi/__init__.py`.
|
||||
|
||||
### Acceptance criteria for bots (Checklist)
|
||||
|
||||
A bot should have at least this functionality:
|
||||
|
||||
- Kibicara REST API (hood admins):
|
||||
- Ticketfrei REST API (hood admins):
|
||||
- Endpoint for creating a bot associated with a hood
|
||||
- Endpoint for deleting a bot associated with a hood
|
||||
- Endpoint for updating a bot associated with a hood by id
|
||||
|
@ -288,10 +205,4 @@ A bot should have at least this functionality:
|
|||
- e.g. Telegram via direct message from the bot
|
||||
- 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
|
||||
- Web Interface (hood admins)
|
||||
|
|
3
COPYING
3
COPYING
|
@ -1,8 +1,9 @@
|
|||
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 Christian Hagenest <c.hagenest@pm.me>
|
||||
Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||
Copyright (C) 2020 by Maike <maike@systemli.org>
|
||||
Copyright (C) 2022 by missytake <missytake@systemli.org>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted.
|
||||
|
|
|
@ -48,7 +48,7 @@ export const environment = {
|
|||
- Build frontend with `cd kibicara/kibicara-frontend && ng build --prod`
|
||||
- Copy the generated frontend to your server to `/home/kibicara/kibicara-frontend`: `scp -r kibicara/kibicara-frontend/dist/kibicara-frontend <your_server>:/home/kibicara`
|
||||
|
||||
### Configure Kibicara Core
|
||||
### Configure Ticketfrei Core
|
||||
- Write config file to `/etc/kibicara.conf` and replace the domain with yours:
|
||||
```
|
||||
database_connection = 'sqlite:////home/kibicara/kibicara.sqlite'
|
||||
|
@ -59,7 +59,7 @@ frontend_url = 'https://kibicara.example.com'
|
|||
#### SSL
|
||||
You can use the SSL stuff provided by hypercorn by generating an SSL Certificate and passing its paths to the config options `certfile` and `keyfile` in `/etc/kibicara.conf`.
|
||||
|
||||
### Configure Kibicara platforms
|
||||
### Configure Ticketfrei platforms
|
||||
|
||||
#### Configure E-Mail (OpenSMTPd + Relay)
|
||||
To send and receive e-mails (necessary for registration confirmation and e-mail bot) you will need an MTA. We use OpenSMTPd:
|
||||
|
@ -105,5 +105,5 @@ consumer_secret = '<your_consumer_secret>'
|
|||
#### Configure Telegram
|
||||
Nothing to do, because telegram has a nice API.
|
||||
|
||||
### Start Kibicara
|
||||
### Start Ticketfrei
|
||||
Run `kibicara` with your kibicara user. To have more verbose output add `-vvv`.
|
||||
|
|
16
README.md
16
README.md
|
@ -1,9 +1,9 @@
|
|||
![Angular Frontend](https://github.com/acipm/kibicara/workflows/Angular%20Frontend/badge.svg)
|
||||
![Python Backend](https://github.com/acipm/kibicara/workflows/Python%20Backend/badge.svg)
|
||||
![Angular Frontend](https://git.0x90.space/ticketfrei/ticketfrei3/workflows/Angular%20Frontend/badge.svg)
|
||||
![Python Backend](https://git.0x90.space/ticketfrei/ticketfrei3/workflows/Python%20Backend/badge.svg)
|
||||
|
||||
# Kibicara
|
||||
# Ticketfrei 3
|
||||
|
||||
Kibicara relays messages between different platforms (= social networks).
|
||||
Ticketfrei relays messages between different platforms (= social networks).
|
||||
|
||||
In its web interface, a hood admin (= registered user) can create a hood to
|
||||
build a connection between different platforms.
|
||||
|
@ -11,18 +11,18 @@ build a connection between different platforms.
|
|||
Users can message a specific hood account on a specific platform (e.g. @xyz on
|
||||
Telegram). This pushes the announcement to all platform accounts of a hood.
|
||||
For example: User A writes a message to @xyz on Telegram (which has been
|
||||
connected to Kibicara by a hood admin). This publishes the message on e.g.
|
||||
connected to Ticketfrei by a hood admin). This publishes the message on e.g.
|
||||
Twitter and other platforms which have been connected to the hood.
|
||||
|
||||
The admin of a hood has to define trigger words and bad words. Messages need to
|
||||
contain a trigger word to be relayed, and must not contain a bad word.
|
||||
|
||||
Kibicara needs to be hosted on a server by an instance maintainer. That way,
|
||||
Ticketfrei needs to be hosted on a server by an instance maintainer. That way,
|
||||
hood admins don't need a server of their own.
|
||||
|
||||
## Deploy Kibicara on a production server
|
||||
## Deploy Ticketfrei on a production server
|
||||
|
||||
Read `DEPLOYMENT.md` to learn how to deploy Kibicara.
|
||||
Read `DEPLOYMENT.md` to learn how to deploy Ticketfrei.
|
||||
|
||||
## Contribute!
|
||||
|
||||
|
|
|
@ -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).
|
|
@ -1,6 +0,0 @@
|
|||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
|
@ -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
|
|
@ -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",
|
||||
}
|
|
@ -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)
|
|
@ -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.")
|
|
@ -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"
|
|
@ -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)
|
|
@ -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"
|
|
@ -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)
|
|
@ -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"
|
|
@ -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)
|
|
@ -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"
|
|
@ -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"
|
|
@ -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 {}
|
|
@ -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"
|
|
@ -1,50 +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
|
||||
|
||||
"""Routing definitions for the REST API.
|
||||
|
||||
A platform bot shall add its API router in this `__init__.py`
|
||||
file to get included into the main application.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from kibicara.platforms.email.webapi import router as email_router
|
||||
from kibicara.platforms.telegram.webapi import router as telegram_router
|
||||
from kibicara.platforms.test.webapi import router as test_router
|
||||
from kibicara.platforms.mastodon.webapi import router as mastodon_router
|
||||
from kibicara.platforms.twitter.webapi import router as twitter_router
|
||||
from kibicara.platforms.twitter.webapi import twitter_callback_router
|
||||
from kibicara.webapi.admin import router as admin_router
|
||||
from kibicara.webapi.hoods import router as hoods_router
|
||||
from kibicara.webapi.hoods.exclude_patterns import router as exclude_patterns_router
|
||||
from kibicara.webapi.hoods.include_patterns import router as include_patterns_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(admin_router, prefix="/admin", tags=["admin"])
|
||||
hoods_router.include_router(
|
||||
include_patterns_router,
|
||||
prefix="/{hood_id}/triggers",
|
||||
tags=["include_patterns"],
|
||||
)
|
||||
hoods_router.include_router(
|
||||
exclude_patterns_router,
|
||||
prefix="/{hood_id}/badwords",
|
||||
tags=["exclude_patterns"],
|
||||
)
|
||||
hoods_router.include_router(test_router, prefix="/{hood_id}/test", tags=["test"])
|
||||
hoods_router.include_router(
|
||||
telegram_router, prefix="/{hood_id}/telegram", tags=["telegram"]
|
||||
)
|
||||
hoods_router.include_router(
|
||||
twitter_router, prefix="/{hood_id}/twitter", tags=["twitter"]
|
||||
)
|
||||
hoods_router.include_router(
|
||||
mastodon_router, prefix="/{hood_id}/mastodon", tags=["mastodon"]
|
||||
)
|
||||
router.include_router(twitter_callback_router, prefix="/twitter", tags=["twitter"])
|
||||
hoods_router.include_router(email_router, prefix="/{hood_id}/email", tags=["email"])
|
||||
router.include_router(hoods_router, prefix="/hoods")
|
|
@ -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)
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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
|
||||
)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
)
|
|
@ -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
|
|
@ -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"
|
|
@ -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",
|
||||
)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"],
|
||||
)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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).
|
|
@ -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
26775
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<mat-card appearance="outlined" class="card">
|
||||
<p>{{ status }}</p>
|
||||
</mat-card>
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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 {}
|
||||
}
|
|
@ -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>
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MastodonSettingsComponent } from './mastodon-settings.component';
|
||||
|
||||
describe('MastodonSettingsComponent', () => {
|
||||
let component: MastodonSettingsComponent;
|
||||
let fixture: ComponentFixture<MastodonSettingsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ MastodonSettingsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MastodonSettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,102 +0,0 @@
|
|||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { MastodonService } from 'src/app/core/api';
|
||||
import { Observable } from 'rxjs';
|
||||
import { MastodonBotInfoDialogComponent } from '../mastodon-bot-card/mastodon-bot-info-dialog/mastodon-bot-info-dialog.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MastodonDialogComponent } from './mastodon-dialog/mastodon-dialog.component';
|
||||
import { YesNoDialogComponent } from 'src/app/shared/yes-no-dialog/yes-no-dialog.component';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mastodon-settings',
|
||||
templateUrl: './mastodon-settings.component.html',
|
||||
styleUrls: ['./mastodon-settings.component.scss'],
|
||||
})
|
||||
export class MastodonSettingsComponent implements OnInit {
|
||||
@Input() hoodId;
|
||||
mastodons$: Observable<Array<any>>;
|
||||
|
||||
constructor(
|
||||
private mastodonService: MastodonService,
|
||||
public dialog: MatDialog,
|
||||
private snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reload();
|
||||
}
|
||||
|
||||
private reload() {
|
||||
this.mastodons$ = this.mastodonService.getMastodons(this.hoodId);
|
||||
}
|
||||
|
||||
onInfoClick() {
|
||||
this.dialog.open(MastodonBotInfoDialogComponent);
|
||||
}
|
||||
|
||||
onDelete(mastodonId) {
|
||||
const dialogRef = this.dialog.open(YesNoDialogComponent, {
|
||||
data: {
|
||||
title: 'Warning',
|
||||
content:
|
||||
'This will also delete the list of subscribers of the mastodon bot.',
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((response) => {
|
||||
if (response) {
|
||||
this.mastodonService
|
||||
.deleteMastodon(mastodonId, this.hoodId)
|
||||
.subscribe(() => {
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCreate() {
|
||||
const dialogRef = this.dialog.open(MastodonDialogComponent, {
|
||||
data: { hoodId: this.hoodId },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
||||
onEdit(mastodonId) {
|
||||
const dialogRef = this.dialog.open(MastodonDialogComponent, {
|
||||
data: { hoodId: this.hoodId, mastodonId },
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.reload();
|
||||
});
|
||||
}
|
||||
|
||||
onChange(mastodon) {
|
||||
if (mastodon.enabled === 0) {
|
||||
this.mastodonService.startMastodon(mastodon.id, this.hoodId).subscribe(
|
||||
() => {},
|
||||
(error) => {
|
||||
this.snackBar.open('Could not start. Check your settings.', 'Close', {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
);
|
||||
} else if (mastodon.enabled === 1) {
|
||||
this.mastodonService.stopMastodon(mastodon.id, this.hoodId).subscribe(
|
||||
() => {},
|
||||
(error) => {
|
||||
this.snackBar.open('Could not stop. Check your settings.', 'Close', {
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
// TODO yeah i know this is bad, implement disabling/enabling
|
||||
setTimeout(() => {
|
||||
this.reload();
|
||||
}, 100);
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
|
@ -6,4 +6,4 @@
|
|||
#
|
||||
# client-side git-hook - run tests and stylechecker
|
||||
|
||||
cd backend && exec .venv/bin/tox
|
||||
exec tox
|
||||
|
|
|
@ -32,7 +32,6 @@ speed-measure-plugin*.json
|
|||
.history/*
|
||||
|
||||
# misc
|
||||
/.angular/cache
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
27
kibicara-frontend/README.md
Normal file
27
kibicara-frontend/README.md
Normal 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).
|
|
@ -22,6 +22,7 @@
|
|||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": true,
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": [
|
||||
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
|
||||
|
@ -30,13 +31,7 @@
|
|||
"stylePreprocessorOptions": {
|
||||
"includePaths": ["src/sass"]
|
||||
},
|
||||
"scripts": [],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
"sourceMap": true,
|
||||
"optimization": false,
|
||||
"namedChunks": true
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
|
@ -49,6 +44,7 @@
|
|||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
|
@ -66,8 +62,7 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": ""
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
|
@ -104,6 +99,17 @@
|
|||
"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": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
|
@ -118,5 +124,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "kibicara-frontend"
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"target": "es5",
|
||||
"types": [
|
||||
"jasmine",
|
||||
"jasminewd2",
|
13550
kibicara-frontend/package-lock.json
generated
Normal file
13550
kibicara-frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
53
kibicara-frontend/package.json
Normal file
53
kibicara-frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ const routes: Routes = [
|
|||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {}), SharedModule],
|
||||
imports: [RouterModule.forRoot(routes), SharedModule],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
|
@ -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 { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule
|
|
@ -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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('ConfirmComponent', () => {
|
|||
let component: ConfirmComponent;
|
||||
let fixture: ComponentFixture<ConfirmComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ ConfirmComponent ]
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
<div class="container">
|
||||
<mat-card appearance="outlined" class="login-form">
|
||||
<mat-card class="login-form">
|
||||
<mat-card-header>
|
||||
<h2>Log in as hood admin!</h2>
|
||||
</mat-card-header>
|
|
@ -7,7 +7,7 @@
|
|||
margin-top: 3%;
|
||||
}
|
||||
|
||||
.mat-mdc-card:not([class*="mat-elevation-z"]) {
|
||||
.mat-card:not([class*="mat-elevation-z"]) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
@ -6,7 +6,7 @@ describe('LoginComponent', () => {
|
|||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ LoginComponent ]
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { LoginService } from '../../core/auth/login.service';
|
||||
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 { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
|
@ -11,7 +11,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
|||
styleUrls: ['./login.component.scss'],
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
loginForm: UntypedFormGroup;
|
||||
loginForm: FormGroup;
|
||||
returnUrl: string;
|
||||
loading = false;
|
||||
submitted = false;
|
||||
|
@ -21,7 +21,7 @@ export class LoginComponent implements OnInit {
|
|||
private loginService: LoginService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private formBuilder: FormBuilder,
|
||||
private snackBar: MatSnackBar
|
||||
) {
|
||||
if (this.loginService.currentHoodAdminValue) {
|
|
@ -1,5 +1,5 @@
|
|||
<div class="container">
|
||||
<mat-card appearance="outlined" class="login-form">
|
||||
<mat-card class="login-form">
|
||||
<mat-card-header>
|
||||
<h2>Reset password</h2>
|
||||
</mat-card-header>
|
|
@ -6,7 +6,7 @@
|
|||
margin-top: 3%;
|
||||
}
|
||||
|
||||
.mat-mdc-card:not([class*="mat-elevation-z"]) {
|
||||
.mat-card:not([class*="mat-elevation-z"]) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue