Compare commits

..

5 commits

80 changed files with 859 additions and 2821 deletions
CONTRIBUTING.mdDEPLOYMENT.md
backend
frontend/src/app
git-hooks

View file

@ -29,13 +29,13 @@
1. Install node.js (e.g. via 1. Install node.js (e.g. via
[nvm](https://github.com/nvm-sh/nvm#installation-and-update)) [nvm](https://github.com/nvm-sh/nvm#installation-and-update))
2. `cd frontend` 2. `cd kibicara-frontend`
3. Install the dependencies with `npm i` 3. Install the dependencies with `npm i`
4. Install Angular with `npm i @angular/cli` 4. Install Angular with `npm i -g @angular/cli`
5. Turn off production mode if you have not already (see above in backend). 5. Turn off production mode if you have not already (see above in backend).
6. Start the backend in a different terminal 6. Start the backend in a different terminal
7. To serve and open the application, run `node_modules/@angular/cli/bin/ng.js s -o`. 7. To serve and open the application, run `ng s -o`. The application will open
The application will open under [http://127.0.0.1:4200](http://127.0.0.1:4200). under [http://127.0.0.1:4200](http://127.0.0.1:4200).
### Creating an account ### Creating an account
@ -128,29 +128,17 @@ development team.
## How to implement a new Platform/Social Network ## How to implement a new Platform/Social Network
For transferring messages, Kibicara supports a range of platforms/social ### tl;dr
networks, e.g. Mastodon, E-Mail, and Telegram - but more can be added easily.
This guide explains what you need to do to add another platform, e.g. Matrix or
XMPP.
### Overview: 1. Implement the following modules in `platforms/<your-platform>/`:
1. Implement the backend modules in `platforms/<your-platform>/`:
- `bot.py` - `bot.py`
- `model.py` - `model.py`
- `webapi.py` - `webapi.py`
2. Import your bot in `kibicara/webapi/__init__.py`. 2. Import your bot in `kibicara/webapi/__init__.py`.
3. Generate the FastAPI boilerplate code 3. Generate the FastAPI stuff
4. Generate the angular boilerplate code 4. Generate the angular components for the kibicara-frontend from the FastAPI stuff
5. Copy-paste frontend components from other bots into the angular boilerplate
and adjust them to your needs
At the bottom you can find a checklist what your pull request needs to be ### Explanation
merged into kibicara.
### Step by step
#### Implement the backend modules
In `kibicara/platforms/<your-platform>/bot.py`, you write the functions through In `kibicara/platforms/<your-platform>/bot.py`, you write the functions through
which the platform asks the social network for new messages, and publishes which the platform asks the social network for new messages, and publishes
@ -166,90 +154,14 @@ You will probably need to store the following things:
* platform-specific settings * platform-specific settings
* anything else your platform needs * anything else your platform needs
In `kibicara/platforms/<your-platform>/webapi.py`, you can define REST API In `kibicara/platforms/<your-platform>/webapi.py`, you can define HTTP routes.
routes. You will need them to: You will need them to:
* let admins authenticate to the social network in the kibicara web interface * let admins authenticate to the social network in the kibicara web interface
* update platform-specific settings * update platform-specific settings
#### Import your bot into the kibicara REST API
To run the platform, you need to import the bot in To run the platform, you need to import the bot in
`kibicara/webapi/__init__.py`. You can see how the other platforms did it. `kibicara/webapi/__init__.py`.
#### Generate the FastAPI boilerplate code
Whenever you changed the REST API in the backend, you need to re-generate the
FastAPI boilerplate code:
1. Start backend with `kibicara > /dev/null 2>&1 &`
2. Go to the frontend directory: `cd frontend`
3. Use this command to download the openapi.json from the backend and
generate the boilerplate: `npm run openapi-generator`
4. (Now you can stop the backend again, e.g. with `pkill kibicara`)
5. Append `/api` to all relevant URLs:
`find src/app/core/api/ -type f -exec sed -i "s#{this.configuration.basePath}#{this.configuration.basePath}/api#g" {} +`
6. Check if everything is okay (e.g. all api calls need the `/api` prefix)
#### Generate the Angular boilerplate code
##### Generate the platform-specific "public facing" page
Generate boilerplate "card" for myplatform:
```
ng generate component platforms/myplatform/myplatform-bot-card
```
Generate boilerplate for the popup that shows the users the guide on how to use
the bot:
```
ng generate component platforms/myplatform/myplatform-bot-card/myplatform-bot-info-dialog
```
##### Generate the platform-specific "settings" page
Generate bot card for settings page:
```
ng generate component platforms/myplatform/myplatform-settings
```
Generate popup that will show when you click on "Add" to add the myplatform
credentials (e.g. login data, api tokens etc):
```
ng generate component platforms/myplatform/myplatform-settings/mastodon-dialog
```
If something does not work, try to check `platforms.module.ts` and check if the
module was imported there. Every module needs to be imported there
#### Adjust the Angular code for your specific platform
Every frontend part for a bot has a similar structure. Basically copy the
content of the other files e.g. the telegram bot into the generated boilerplate
above and search and replace all occurrences with `myplatform`. You can see
the UI with `ng s -o`, it will auto-update on code change.
A component in angular has 3-4 files, only these ones ending with
`*.component.ts` (typescript) and `*.component.html`(html) are important for
us. Basically the typescript controls what is shown in the html. Please correct
every error that stops the angular page build or shows up on the page while you
go, otherwise this can become a mess.
With that in mind, first write the logic to call the /create endpoint:
- `src/app/platforms/myplatform/myplatform-settings/myplatform-dialog/myplatform-dialog.component.ts`:
implement the form to take the user inputs and the onSubmit() function
- `src/app/platforms/myplatform/myplatform-settings/myplatform-dialog/myplatform-dialog.component.html`:
implement the html skeleton that takes the form from the user
Then, fix up the public user facing page:
- `src/app/platforms/myplatform/myplatform-bot-card/myplatform-bot-info-dialog/myplatform-bot-info-dialog.component.html`
Finally, check the other typescript and html pages and adjust e.g. the tutorial
text for the users.
### Acceptance criteria for bots (Checklist) ### Acceptance criteria for bots (Checklist)
@ -288,10 +200,4 @@ A bot should have at least this functionality:
- e.g. Telegram via direct message from the bot - e.g. Telegram via direct message from the bot
- e.g. E-Mail via e-mail to the user's address - e.g. E-Mail via e-mail to the user's address
- Web Interface (hood admins and users) - Web Interface (hood admins)
- A card which allows hood admins to add, configure, start, stop, and
delete a platform to their hood
- A pop-up which explains to hood admins how to configure the platform
- A card which allows users to subscribe on a platform or links to the
platform's account
- A pop-up which explains to users how to use the platform

View file

@ -87,21 +87,6 @@ query_mailaddr SELECT 1 FROM email WHERE ? IN (name || '@kibicara.example.com');
``` ```
- Don't forget to restart OpenSMTPd when you change your database: `rcctl stop && rcctl start` - Don't forget to restart OpenSMTPd when you change your database: `rcctl stop && rcctl start`
#### Configure Twitter
Twitter needs you to create a Twitter App, which hood admins can permit to read and write messages.
- Create Twitter account and app: https://developer.twitter.com
- Get your customer key and customer secret and append this to `/etc/kibicara.conf`:
```
[twitter]
consumer_key = '<your_consumer_key>'
consumer_secret = '<your_consumer_secret>'
```
- You need to configure a Callback Url in your Twitter App:
- Go to: `https://developer.twitter.com/en/apps`
- Add `https://kibicara.example.com/dashboard/twitter-callback` as Callback Url of your Twitter App. This is needed to successfully create a twitter oauth handshake.
#### Configure Telegram #### Configure Telegram
Nothing to do, because telegram has a nice API. Nothing to do, because telegram has a nice API.

View file

@ -26,15 +26,16 @@ install_requires =
fastapi fastapi
httpx httpx
hypercorn hypercorn
Mastodon.py ormantic @ https://github.com/dl6tom/ormantic/tarball/master#egg=ormantic-0.0.32
passlib passlib
peony-twitter[all] peony-twitter[all]
pydantic[email]
pynacl pynacl
python-multipart python-multipart
pytoml pytoml
requests requests
tortoise-orm scrypt
Mastodon.py
pydantic[email]
[options.packages.find] [options.packages.find]
where = src where = src
@ -43,7 +44,6 @@ where = src
console_scripts = console_scripts =
kibicara = kibicara.kibicara:Main kibicara = kibicara.kibicara:Main
kibicara_mda = kibicara.platforms.email.mda:Main kibicara_mda = kibicara.platforms.email.mda:Main
migrate_from_ticketfrei2 = kibicara.migratefromticketfrei:Main
[tox:tox] [tox:tox]
envlist = lint, py310 envlist = lint, py310
@ -54,19 +54,19 @@ skip_install = True
deps = deps =
black black
flake8 flake8
mypy
types-requests
commands = commands =
black --check --diff src tests black --check --diff src tests
flake8 src tests flake8 src tests
# not yet
#mypy --ignore-missing-imports src tests
[testenv] [testenv]
deps = deps =
mypy
pytest pytest
pytest-asyncio pytest-asyncio
types-requests
commands = commands =
# not yet
#mypy --ignore-missing-imports src tests
pytest tests pytest tests
[flake8] [flake8]

View file

@ -4,13 +4,17 @@
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from nacl.secret import SecretBox
from nacl.utils import random
"""Default configuration. """Default configuration.
The default configuration gets overwritten by a configuration file if one exists. The default configuration gets overwritten by a configuration file if one exists.
""" """
config = { config = {
"database_connection": "sqlite://:memory:", "database_connection": "sqlite:////tmp/kibicara.sqlite",
"frontend_url": "http://localhost:4200", # url of frontend, change in prod "frontend_url": "http://localhost:4200", # url of frontend, change in prod
"secret": random(SecretBox.KEY_SIZE).hex(), # generate with: openssl rand -hex 32
# production params # production params
"frontend_path": None, # required, path to frontend html/css/js files "frontend_path": None, # required, path to frontend html/css/js files
"production": True, "production": True,

View file

@ -16,9 +16,9 @@ from fastapi.staticfiles import StaticFiles
from hypercorn.asyncio import serve from hypercorn.asyncio import serve
from hypercorn.config import Config from hypercorn.config import Config
from pytoml import load from pytoml import load
from tortoise import Tortoise
from kibicara.config import config from kibicara.config import config
from kibicara.model import Mapping
from kibicara.platformapi import Spawner from kibicara.platformapi import Spawner
from kibicara.webapi import router from kibicara.webapi import router
@ -66,26 +66,12 @@ class Main:
format="%(asctime)s %(name)s %(message)s", format="%(asctime)s %(name)s %(message)s",
) )
getLogger("aiosqlite").setLevel(WARNING) getLogger("aiosqlite").setLevel(WARNING)
Mapping.create_all()
asyncio_run(self.__run()) asyncio_run(self.__run())
async def __run(self): 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 Spawner.init_all()
await self.__start_webserver() await self.__start_webserver()
await Tortoise.close_connections()
async def __start_webserver(self): async def __start_webserver(self):
class SinglePageApplication(StaticFiles): class SinglePageApplication(StaticFiles):

View file

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

View file

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
@ -6,52 +6,69 @@
"""ORM Models for core.""" """ORM Models for core."""
from tortoise import fields from databases import Database
from tortoise.models import Model from ormantic import Boolean, ForeignKey, Integer, Model, Text
from sqlalchemy import MetaData, create_engine
from kibicara.config import config
class Mapping:
database = Database(config["database_connection"])
metadata = MetaData()
@classmethod
def create_all(cls):
engine = create_engine(str(cls.database.url))
cls.metadata.create_all(engine)
@classmethod
def drop_all(cls):
engine = create_engine(str(cls.database.url))
cls.metadata.drop_all(engine)
class Admin(Model): class Admin(Model):
id = fields.IntField(pk=True) id: Integer(primary_key=True) = None
email = fields.CharField(64, unique=True) email: Text(unique=True)
passhash = fields.TextField() passhash: Text()
hoods: fields.ManyToManyRelation["Hood"] = fields.ManyToManyField(
"models.Hood", related_name="admins", through="admin_hood_relations"
)
class Meta: class Mapping(Mapping):
table = "admins" table_name = "admins"
class Hood(Model): class Hood(Model):
id = fields.IntField(pk=True) id: Integer(primary_key=True) = None
name = fields.CharField(64, unique=True) name: Text(unique=True)
landingpage = fields.TextField() landingpage: Text()
email_enabled = fields.BooleanField(default=True) email_enabled: Boolean() = True
admins: fields.ManyToManyRelation[Admin]
include_patterns: fields.ReverseRelation["IncludePattern"]
exclude_patterns: fields.ReverseRelation["ExcludePattern"]
class Meta: class Mapping(Mapping):
table = "hoods" table_name = "hoods"
class IncludePattern(Model): class AdminHoodRelation(Model):
id = fields.IntField(pk=True) id: Integer(primary_key=True) = None
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField( admin: ForeignKey(Admin)
"models.Hood", related_name="include_patterns" hood: ForeignKey(Hood)
)
pattern = fields.TextField()
class Meta: class Mapping(Mapping):
table = "include_patterns" table_name = "admin_hood_relations"
class ExcludePattern(Model): class Trigger(Model):
id = fields.IntField(pk=True) id: Integer(primary_key=True) = None
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField( hood: ForeignKey(Hood)
"models.Hood", related_name="exclude_patterns" pattern: Text()
)
pattern = fields.TextField()
class Meta: class Mapping(Mapping):
table = "exclude_patterns" table_name = "triggers"
class BadWord(Model):
id: Integer(primary_key=True) = None
hood: ForeignKey(Hood)
pattern: Text()
class Mapping(Mapping):
table_name = "badwords"

View file

@ -6,15 +6,12 @@
"""API classes for implementing bots for platforms.""" """API classes for implementing bots for platforms."""
from asyncio import Queue, Task, create_task from asyncio import Queue, create_task
from enum import Enum, auto from enum import Enum, auto
from logging import getLogger from logging import getLogger
from re import IGNORECASE, search from re import IGNORECASE, search
from typing import Generic, Optional, Type, TypeVar
from tortoise.models import Model from kibicara.model import BadWord, Trigger
from kibicara.model import ExcludePattern, Hood, IncludePattern
logger = getLogger(__name__) logger = getLogger(__name__)
@ -32,7 +29,7 @@ class Message:
**kwargs (object, optional): Other platform-specific data. **kwargs (object, optional): Other platform-specific data.
""" """
def __init__(self, text: str, **kwargs) -> None: def __init__(self, text: str, **kwargs):
self.text = text self.text = text
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@ -76,11 +73,11 @@ class Censor:
__instances: dict[int, list["Censor"]] = {} __instances: dict[int, list["Censor"]] = {}
def __init__(self, hood: Hood) -> None: def __init__(self, hood):
self.hood = hood self.hood = hood
self.enabled = True self.enabled = True
self._inbox: Queue[Message] = Queue() self._inbox = Queue()
self.__task: Optional[Task[None]] = None self.__task = None
self.__hood_censors = self.__instances.setdefault(hood.id, []) self.__hood_censors = self.__instances.setdefault(hood.id, [])
self.__hood_censors.append(self) self.__hood_censors.append(self)
self.status = BotStatus.INSTANTIATED self.status = BotStatus.INSTANTIATED
@ -96,8 +93,7 @@ class Censor:
self.__task.cancel() self.__task.cancel()
async def __run(self) -> None: async def __run(self) -> None:
assert self.__task is not None await self.hood.load()
await self.hood.refresh_from_db()
self.__task.set_name("{0} {1}".format(self.__class__.__name__, self.hood.name)) self.__task.set_name("{0} {1}".format(self.__class__.__name__, self.hood.name))
try: try:
self.status = BotStatus.RUNNING self.status = BotStatus.RUNNING
@ -116,7 +112,7 @@ class Censor:
pass pass
@classmethod @classmethod
async def destroy_hood(cls, hood: Hood) -> None: async def destroy_hood(cls, hood) -> None:
"""Remove all of its database entries. """Remove all of its database entries.
Note: Override this in the derived bot class. Note: Override this in the derived bot class.
@ -144,29 +140,25 @@ class Censor:
return await self._inbox.get() return await self._inbox.get()
async def __is_appropriate(self, message: Message) -> bool: async def __is_appropriate(self, message: Message) -> bool:
for exclude in await ExcludePattern.filter(hood=self.hood): for badword in await BadWord.objects.filter(hood=self.hood).all():
if search(exclude.pattern, message.text, IGNORECASE): if search(badword.pattern, message.text, IGNORECASE):
logger.info( logger.debug(
"Matched bad word - dropped message: {0}".format(message.text) "Matched bad word - dropped message: {0}".format(message.text)
) )
return False return False
for include in await IncludePattern.filter(hood=self.hood): for trigger in await Trigger.objects.filter(hood=self.hood).all():
if search(include.pattern, message.text, IGNORECASE): if search(trigger.pattern, message.text, IGNORECASE):
logger.info( logger.debug(
"Matched trigger - passed message: {0}".format(message.text) "Matched trigger - passed message: {0}".format(message.text)
) )
return True return True
logger.info( logger.debug(
"Did not match any trigger - dropped message: {0}".format(message.text) "Did not match any trigger - dropped message: {0}".format(message.text)
) )
return False return False
ORMClass = TypeVar("ORMClass", bound=Model) class Spawner:
BotClass = TypeVar("BotClass", bound=Censor)
class Spawner(Generic[ORMClass, BotClass]):
"""Spawns a bot with a specific bot model. """Spawns a bot with a specific bot model.
Examples: Examples:
@ -187,10 +179,10 @@ class Spawner(Generic[ORMClass, BotClass]):
__instances: list["Spawner"] = [] __instances: list["Spawner"] = []
def __init__(self, orm_class: Type[ORMClass], bot_class: Type[BotClass]) -> None: def __init__(self, ORMClass, BotClass):
self.ORMClass = orm_class self.ORMClass = ORMClass
self.BotClass = bot_class self.BotClass = BotClass
self.__bots: dict[int, BotClass] = {} self.__bots = {}
self.__instances.append(self) self.__instances.append(self)
@classmethod @classmethod
@ -200,7 +192,7 @@ class Spawner(Generic[ORMClass, BotClass]):
await spawner._init() await spawner._init()
@classmethod @classmethod
async def destroy_hood(cls, hood: Hood) -> None: async def destroy_hood(cls, hood) -> None:
for spawner in cls.__instances: for spawner in cls.__instances:
for pk in list(spawner.__bots): for pk in list(spawner.__bots):
bot = spawner.__bots[pk] bot = spawner.__bots[pk]
@ -210,15 +202,15 @@ class Spawner(Generic[ORMClass, BotClass]):
await spawner.BotClass.destroy_hood(hood) await spawner.BotClass.destroy_hood(hood)
async def _init(self) -> None: async def _init(self) -> None:
async for item in self.ORMClass.all(): for item in await self.ORMClass.objects.all():
self.start(item) self.start(item)
def start(self, item: ORMClass) -> None: def start(self, item) -> None:
"""Instantiate and start a bot with the provided ORM object. """Instantiate and start a bot with the provided ORM object.
Example: Example:
``` ```
xyz = await XYZ.create(hood=hood, **values.__dict__) xyz = await XYZ.objects.create(hood=hood, **values.__dict__)
spawner.start(xyz) spawner.start(xyz)
``` ```
@ -229,7 +221,7 @@ class Spawner(Generic[ORMClass, BotClass]):
if bot.enabled: if bot.enabled:
bot.start() bot.start()
def stop(self, item: ORMClass) -> None: def stop(self, item) -> None:
"""Stop and delete a bot. """Stop and delete a bot.
Args: Args:
@ -239,10 +231,10 @@ class Spawner(Generic[ORMClass, BotClass]):
if bot is not None: if bot is not None:
bot.stop() bot.stop()
def get(self, item: ORMClass) -> BotClass: def get(self, item) -> Censor:
"""Get a running bot. """Get a running bot.
Args: Args:
item (ORM Model object): ORM object corresponding to bot. item (ORM Model object): ORM object corresponding to bot.
""" """
return self.__bots[item.pk] return self.__bots.get(item.pk)

View file

@ -1,6 +1,6 @@
# Copyright (C) 2020 by Maike <maike@systemli.org> # Copyright (C) 2020 by Maike <maike@systemli.org>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
@ -12,7 +12,7 @@ from kibicara import email
from kibicara.config import config from kibicara.config import config
from kibicara.model import Hood from kibicara.model import Hood
from kibicara.platformapi import Censor, Spawner from kibicara.platformapi import Censor, Spawner
from kibicara.platforms.email.model import Email, EmailSubscriber from kibicara.platforms.email.model import Email, EmailSubscribers
from kibicara.webapi.admin import to_token from kibicara.webapi.admin import to_token
logger = getLogger(__name__) logger = getLogger(__name__)
@ -26,9 +26,9 @@ class EmailBot(Censor):
@classmethod @classmethod
async def destroy_hood(cls, hood): async def destroy_hood(cls, hood):
"""Removes all its database entries.""" """Removes all its database entries."""
for inbox in await Email.filter(hood=hood).all(): for inbox in await Email.objects.filter(hood=hood).all():
await inbox.delete() await inbox.delete()
for subscriber in await EmailSubscriber.filter(hood=hood).all(): for subscriber in await EmailSubscribers.objects.filter(hood=hood).all():
await subscriber.delete() await subscriber.delete()
async def run(self): async def run(self):
@ -40,7 +40,9 @@ class EmailBot(Censor):
self.hood.name, message.text self.hood.name, message.text
) )
) )
for subscriber in await EmailSubscriber.filter(hood=self.hood).all(): for subscriber in await EmailSubscribers.objects.filter(
hood=self.hood
).all():
token = to_token(email=subscriber.email, hood=self.hood.id) token = to_token(email=subscriber.email, hood=self.hood.id)
body = ( body = (
"{0}\n\n--\n" "{0}\n\n--\n"

View file

@ -20,7 +20,7 @@ from pytoml import load
from requests import post from requests import post
from kibicara.config import config from kibicara.config import config
from kibicara.platforms.email.model import Email, EmailSubscriber from kibicara.platforms.email.model import Email, EmailSubscribers
logger = getLogger(__name__) logger = getLogger(__name__)
@ -53,7 +53,7 @@ class Main:
async def __run(self, email_name): async def __run(self, email_name):
try: try:
email = await Email.get(name=email_name) email = await Email.objects.get(name=email_name)
except NoMatch: except NoMatch:
logger.error("No recipient with this name") logger.error("No recipient with this name")
exit(1) exit(1)
@ -75,7 +75,7 @@ class Main:
logger.error("Could not parse sender") logger.error("Could not parse sender")
exit(1) exit(1)
maybe_subscriber = await EmailSubscriber.filter(email=sender).all() maybe_subscriber = await EmailSubscribers.objects.filter(email=sender).all()
if len(maybe_subscriber) != 1 or maybe_subscriber[0].hood.id != email.hood.id: if len(maybe_subscriber) != 1 or maybe_subscriber[0].hood.id != email.hood.id:
logger.error("Not a subscriber") logger.error("Not a subscriber")
exit(1) exit(1)

View file

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

View file

@ -1,6 +1,6 @@
# Copyright (C) 2020 by Maike <maike@systemli.org> # Copyright (C) 2020 by Maike <maike@systemli.org>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
@ -8,18 +8,18 @@
from logging import getLogger from logging import getLogger
from os import urandom from os import urandom
from smtplib import SMTPException from smtplib import SMTPException
from sqlite3 import IntegrityError
from fastapi import APIRouter, Depends, HTTPException, Response, status from fastapi import APIRouter, Depends, HTTPException, Response, status
from nacl import exceptions from nacl import exceptions
from ormantic.exceptions import NoMatch
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara import email from kibicara import email
from kibicara.config import config from kibicara.config import config
from kibicara.model import Hood
from kibicara.platformapi import Message from kibicara.platformapi import Message
from kibicara.platforms.email.bot import spawner from kibicara.platforms.email.bot import spawner
from kibicara.platforms.email.model import Email, EmailSubscriber from kibicara.platforms.email.model import Email, EmailSubscribers
from kibicara.webapi.admin import from_token, to_token from kibicara.webapi.admin import from_token, to_token
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
@ -53,7 +53,7 @@ class BodySubscriber(BaseModel):
email: str email: str
async def get_email(email_id: int, hood: Hood = Depends(get_hood)): async def get_email(email_id: int, hood=Depends(get_hood)):
"""Get Email row by hood. """Get Email row by hood.
You can specify an email_id to nail it down, but it works without as well. You can specify an email_id to nail it down, but it works without as well.
@ -62,16 +62,16 @@ async def get_email(email_id: int, hood: Hood = Depends(get_hood)):
:return: Email row of the found email bot. :return: Email row of the found email bot.
""" """
try: try:
return await Email.get(id=email_id, hood=hood) return await Email.objects.get(id=email_id, hood=hood)
except DoesNotExist: except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return HTTPException(status_code=status.HTTP_404_NOT_FOUND)
async def get_subscriber(subscriber_id: int, hood: Hood = Depends(get_hood)): async def get_subscriber(subscriber_id: int, hood=Depends(get_hood)):
try: try:
return await EmailSubscriber.get(id=subscriber_id, hood=hood) return await EmailSubscribers.objects.get(id=subscriber_id, hood=hood)
except DoesNotExist: except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) return HTTPException(status_code=status.HTTP_404_NOT_FOUND)
# registers the routes, gets imported in /kibicara/webapi/__init__.py # registers the routes, gets imported in /kibicara/webapi/__init__.py
@ -83,9 +83,9 @@ router = APIRouter()
# TODO response_model # TODO response_model
operation_id="get_emails_public", operation_id="get_emails_public",
) )
async def email_read_all_public(hood: Hood = Depends(get_hood_unauthorized)): async def email_read_all_public(hood=Depends(get_hood_unauthorized)):
if hood.email_enabled: if hood.email_enabled:
emails = await Email.filter(hood=hood) emails = await Email.objects.filter(hood=hood).all()
return [BodyEmailPublic(name=email.name) for email in emails] return [BodyEmailPublic(name=email.name) for email in emails]
return [] return []
@ -95,8 +95,8 @@ async def email_read_all_public(hood: Hood = Depends(get_hood_unauthorized)):
# TODO response_model # TODO response_model
operation_id="get_emails", operation_id="get_emails",
) )
async def email_read_all(hood: Hood = Depends(get_hood)): async def email_read_all(hood=Depends(get_hood)):
return await Email.filter(hood=hood) return await Email.objects.filter(hood=hood).select_related("hood").all()
@router.post( @router.post(
@ -112,7 +112,7 @@ async def email_create(values: BodyEmail, response: Response, hood=Depends(get_h
:return: Email row of the new email bot. :return: Email row of the new email bot.
""" """
try: try:
email = await Email.create( email = await Email.objects.create(
hood=hood, secret=urandom(32).hex(), **values.__dict__ hood=hood, secret=urandom(32).hex(), **values.__dict__
) )
response.headers["Location"] = str(hood.id) response.headers["Location"] = str(hood.id)
@ -200,7 +200,7 @@ async def email_subscribe(
token, token,
) )
try: try:
subs = await EmailSubscriber.filter(email=subscriber.email).all() subs = await EmailSubscribers.objects.filter(email=subscriber.email).all()
if subs: if subs:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
email.send_email( email.send_email(
@ -239,7 +239,7 @@ async def email_subscribe_confirm(token, hood=Depends(get_hood_unauthorized)):
if hood.id is not payload["hood"]: if hood.id is not payload["hood"]:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
try: try:
await EmailSubscriber.create(hood=hood, email=payload["email"]) await EmailSubscribers.objects.create(hood=hood.id, email=payload["email"])
return {} return {}
except IntegrityError: except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@ -262,12 +262,12 @@ async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)):
# If token.hood and url.hood are different, raise an error: # If token.hood and url.hood are different, raise an error:
if hood.id is not payload["hood"]: if hood.id is not payload["hood"]:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
subscriber = await EmailSubscriber.filter( subscriber = await EmailSubscribers.objects.filter(
hood=payload["hood"], email=payload["email"] hood=payload["hood"], email=payload["email"]
).get() ).get()
await subscriber.delete() await subscriber.delete()
return Response(status_code=status.HTTP_204_NO_CONTENT) return Response(status_code=status.HTTP_204_NO_CONTENT)
except DoesNotExist: except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
except exceptions.CryptoError: except exceptions.CryptoError:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
@ -279,7 +279,7 @@ async def email_unsubscribe(token, hood=Depends(get_hood_unauthorized)):
operation_id="get_subscribers", operation_id="get_subscribers",
) )
async def subscribers_read_all(hood=Depends(get_hood)): async def subscribers_read_all(hood=Depends(get_hood)):
return await EmailSubscriber.filter(hood=hood).all() return await EmailSubscribers.objects.filter(hood=hood).all()
@router.get( @router.get(
@ -306,7 +306,7 @@ async def email_message_create(
:param hood: Hood the Email bot belongs to. :param hood: Hood the Email bot belongs to.
:return: returns status code 201 if the message is accepted by the censor. :return: returns status code 201 if the message is accepted by the censor.
""" """
for receiver in await Email.filter(hood=hood).all(): for receiver in await Email.objects.filter(hood=hood).all():
if message.secret == receiver.secret: if message.secret == receiver.secret:
# pass message.text to bot.py # pass message.text to bot.py
if await spawner.get(hood).publish(Message(message.text)): if await spawner.get(hood).publish(Message(message.text)):

View file

@ -1,17 +1,17 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from asyncio import gather, get_event_loop, sleep from asyncio import get_event_loop, sleep
from logging import getLogger
import re
from mastodon import Mastodon, MastodonError
from kibicara.platformapi import Censor, Spawner, Message from kibicara.platformapi import Censor, Spawner, Message
from kibicara.platforms.mastodon.model import MastodonAccount from kibicara.platforms.mastodon.model import MastodonAccount
from logging import getLogger
from mastodon import Mastodon, MastodonError
from asyncio import gather
import re
logger = getLogger(__name__) logger = getLogger(__name__)
@ -26,12 +26,12 @@ class MastodonBot(Censor):
@classmethod @classmethod
async def destroy_hood(cls, hood): async def destroy_hood(cls, hood):
"""Removes all its database entries.""" """Removes all its database entries."""
for mastodon in await MastodonAccount.filter(hood=hood).all(): for mastodon in await MastodonAccount.objects.filter(hood=hood).all():
await mastodon.delete() await mastodon.delete()
async def run(self): async def run(self):
try: try:
await self.model.fetch_related("instance") await self.model.instance.load()
self.account = Mastodon( self.account = Mastodon(
client_id=self.model.instance.client_id, client_id=self.model.instance.client_id,
client_secret=self.model.instance.client_secret, client_secret=self.model.instance.client_secret,

View file

@ -1,36 +1,30 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from tortoise import fields from ormantic import ForeignKey, Integer, Text, Boolean, Model
from tortoise.models import Model
from kibicara.model import Hood from kibicara.model import Hood, Mapping
class MastodonInstance(Model): class MastodonInstance(Model):
id = fields.IntField(pk=True) id: Integer(primary_key=True) = None
name = fields.TextField() name: Text()
client_id = fields.TextField() client_id: Text()
client_secret = fields.TextField() client_secret: Text()
accounts: fields.ReverseRelation["MastodonAccount"]
class Meta: class Mapping(Mapping):
table = "platforms_mastodon_instances" table_name = "mastodoninstances"
class MastodonAccount(Model): class MastodonAccount(Model):
id = fields.IntField(pk=True) id: Integer(primary_key=True) = None
hood: fields.ForeignKeyRelation[Hood] = fields.ForeignKeyField( hood: ForeignKey(Hood)
"models.Hood", related_name="platforms_mastodon" instance: ForeignKey(MastodonInstance)
) access_token: Text()
instance: fields.ForeignKeyRelation[MastodonInstance] = fields.ForeignKeyField( username: Text(allow_null=True) = None
"models.MastodonInstance", related_name="accounts" enabled: Boolean() = False
)
access_token = fields.TextField()
username = fields.TextField(null=True)
enabled = fields.BooleanField()
class Meta: class Mapping(Mapping):
table = "platforms_mastodon_accounts" table_name = "mastodonaccounts"

View file

@ -1,24 +1,24 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from asyncio import get_event_loop from asyncio import get_event_loop
from logging import getLogger
from fastapi import APIRouter, Depends, HTTPException, Response, status from fastapi import APIRouter, Depends, HTTPException, Response, status
from mastodon import Mastodon, MastodonNetworkError from ormantic.exceptions import NoMatch
from mastodon.errors import MastodonIllegalArgumentError
from pydantic import BaseModel, validate_email, validator from pydantic import BaseModel, validate_email, validator
from tortoise.exceptions import DoesNotExist, IntegrityError from sqlite3 import IntegrityError
from kibicara.config import config from kibicara.config import config
from kibicara.model import Hood
from kibicara.platforms.mastodon.bot import spawner from kibicara.platforms.mastodon.bot import spawner
from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
from mastodon import Mastodon, MastodonNetworkError
from mastodon.errors import MastodonIllegalArgumentError
from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
@ -37,12 +37,10 @@ class BodyMastodonAccount(BaseModel):
return validate_email(value) return validate_email(value)
async def get_mastodon( async def get_mastodon(mastodon_id, hood=Depends(get_hood)):
mastodon_id: int, hood: Hood = Depends(get_hood)
) -> MastodonAccount:
try: try:
return await MastodonAccount.get(id=mastodon_id, hood=hood) return await MastodonAccount.objects.get(id=mastodon_id, hood=hood)
except DoesNotExist: except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@ -53,16 +51,16 @@ async def get_mastodon_instance(instance_url: str) -> MastodonInstance:
:return the MastodonInstance ORM object :return the MastodonInstance ORM object
""" """
try: try:
return await MastodonInstance.get(name=instance_url) return await MastodonInstance.objects.get(name=instance_url)
except DoesNotExist: except NoMatch:
app_name = config.get("frontend_url") app_name = config.get("frontend_url")
client_id, client_secret = Mastodon.create_app( client_id, client_secret = Mastodon.create_app(
app_name, api_base_url=instance_url app_name, api_base_url=instance_url
) )
await MastodonInstance.create( await MastodonInstance.objects.create(
name=instance_url, client_id=client_id, client_secret=client_secret name=instance_url, client_id=client_id, client_secret=client_secret
) )
return await MastodonInstance.get(name=instance_url) return await MastodonInstance.objects.get(name=instance_url)
router = APIRouter() router = APIRouter()
@ -75,11 +73,13 @@ twitter_callback_router = APIRouter()
operation_id="get_mastodons_public", operation_id="get_mastodons_public",
) )
async def mastodon_read_all_public(hood=Depends(get_hood_unauthorized)): async def mastodon_read_all_public(hood=Depends(get_hood_unauthorized)):
mastodonbots = await MastodonAccount.objects.filter(hood=hood).all()
mbots = [] mbots = []
async for mbot in MastodonAccount.filter(hood=hood).prefetch_related("instance"): for mbot in mastodonbots:
if mbot.enabled == 1 and mbot.username: if mbot.enabled == 1 and mbot.username:
instance = await MastodonInstance.objects.get(id=mbot.instance)
mbots.append( mbots.append(
BodyMastodonPublic(username=mbot.username, instance=mbot.instance.name) BodyMastodonPublic(username=mbot.username, instance=instance.name)
) )
return mbots return mbots
@ -90,7 +90,7 @@ async def mastodon_read_all_public(hood=Depends(get_hood_unauthorized)):
operation_id="get_mastodons", operation_id="get_mastodons",
) )
async def mastodon_read_all(hood=Depends(get_hood)): async def mastodon_read_all(hood=Depends(get_hood)):
return await MastodonAccount.filter(hood=hood).all() return await MastodonAccount.objects.filter(hood=hood).all()
@router.delete( @router.delete(
@ -101,8 +101,8 @@ async def mastodon_read_all(hood=Depends(get_hood)):
) )
async def mastodon_delete(mastodon=Depends(get_mastodon)): async def mastodon_delete(mastodon=Depends(get_mastodon)):
spawner.stop(mastodon) spawner.stop(mastodon)
await mastodon.fetch_related("instance") await mastodon.instance.load()
object_with_instance = await MastodonAccount.filter( object_with_instance = await MastodonAccount.objects.filter(
instance=mastodon.instance instance=mastodon.instance
).all() ).all()
if len(object_with_instance) == 1 and object_with_instance[0] == mastodon: if len(object_with_instance) == 1 and object_with_instance[0] == mastodon:
@ -172,7 +172,7 @@ async def mastodon_create(values: BodyMastodonAccount, hood=Depends(get_hood)):
None, account.log_in, values.email, values.password None, account.log_in, values.email, values.password
) )
logger.debug(f"{access_token}") logger.debug(f"{access_token}")
mastodon = await MastodonAccount.create( mastodon = await MastodonAccount.objects.create(
hood=hood, instance=instance, access_token=access_token, enabled=True hood=hood, instance=instance, access_token=access_token, enabled=True
) )
spawner.start(mastodon) spawner.start(mastodon)
@ -181,5 +181,4 @@ async def mastodon_create(values: BodyMastodonAccount, hood=Depends(get_hood)):
logger.warning("Login to Mastodon failed.", exc_info=True) logger.warning("Login to Mastodon failed.", exc_info=True)
raise HTTPException(status_code=status.HTTP_422_INVALID_INPUT) raise HTTPException(status_code=status.HTTP_422_INVALID_INPUT)
except IntegrityError: except IntegrityError:
logger.warning("Login to Mastodon failed.", exc_info=True)
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)

View file

@ -1,17 +1,17 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from asyncio import CancelledError, gather, sleep from asyncio import CancelledError, gather, sleep
from logging import getLogger from logging import getLogger
from sqlite3 import IntegrityError
from aiogram import Bot, Dispatcher, exceptions, types from aiogram import Bot, Dispatcher, exceptions, types
from tortoise.exceptions import DoesNotExist, IntegrityError from ormantic.exceptions import NoMatch
from kibicara.platformapi import Censor, Message, Spawner from kibicara.platformapi import Censor, Message, Spawner
from kibicara.platforms.telegram.model import Telegram, TelegramSubscriber from kibicara.platforms.telegram.model import Telegram, TelegramUser
logger = getLogger(__name__) logger = getLogger(__name__)
@ -25,8 +25,8 @@ class TelegramBot(Censor):
@classmethod @classmethod
async def destroy_hood(cls, hood): async def destroy_hood(cls, hood):
"""Removes all its database entries.""" """Removes all its database entries."""
for telegram in await Telegram.filter(hood=hood).all(): for telegram in await Telegram.objects.filter(hood=hood).all():
for user in await TelegramSubscriber.filter(bot=telegram).all(): for user in await TelegramUser.objects.filter(bot=telegram).all():
await user.delete() await user.delete()
await telegram.delete() await telegram.delete()
@ -69,7 +69,9 @@ class TelegramBot(Censor):
self.telegram_model.hood.name, message.text self.telegram_model.hood.name, message.text
) )
) )
for user in await TelegramSubscriber.filter(bot=self.telegram_model).all(): for user in await TelegramUser.objects.filter(
bot=self.telegram_model
).all():
await self._send_message(user.user_id, message.text) await self._send_message(user.user_id, message.text)
async def _send_message(self, user_id, message): async def _send_message(self, user_id, message):
@ -114,7 +116,7 @@ class TelegramBot(Censor):
if message.from_user.is_bot: if message.from_user.is_bot:
await message.reply("Error: Bots can not join here.") await message.reply("Error: Bots can not join here.")
return return
await TelegramSubscriber.create( await TelegramUser.objects.create(
user_id=message.from_user.id, bot=self.telegram_model user_id=message.from_user.id, bot=self.telegram_model
) )
await message.reply(self.telegram_model.welcome_message) await message.reply(self.telegram_model.welcome_message)
@ -123,12 +125,12 @@ class TelegramBot(Censor):
async def _remove_user(self, message: types.Message): async def _remove_user(self, message: types.Message):
try: try:
telegram_user = await TelegramSubscriber.get( telegram_user = await TelegramUser.objects.get(
user_id=message.from_user.id, bot=self.telegram_model user_id=message.from_user.id, bot=self.telegram_model
) )
await telegram_user.delete() await telegram_user.delete()
await message.reply("You were removed successfully from this bot.") await message.reply("You were removed successfully from this bot.")
except DoesNotExist: except NoMatch:
await message.reply("Error: You are not subscribed to this bot.") await message.reply("Error: You are not subscribed to this bot.")
async def _send_help(self, message: types.Message): async def _send_help(self, message: types.Message):

View file

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

View file

@ -1,19 +1,19 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from logging import getLogger from logging import getLogger
from sqlite3 import IntegrityError
from aiogram import exceptions from aiogram import exceptions
from aiogram.bot.api import check_token from aiogram.bot.api import check_token
from fastapi import APIRouter, Depends, HTTPException, Response, status from fastapi import APIRouter, Depends, HTTPException, Response, status
from ormantic.exceptions import NoMatch
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara.platforms.telegram.bot import spawner from kibicara.platforms.telegram.bot import spawner
from kibicara.platforms.telegram.model import Telegram, TelegramSubscriber from kibicara.platforms.telegram.model import Telegram, TelegramUser
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
logger = getLogger(__name__) logger = getLogger(__name__)
@ -38,8 +38,8 @@ class BodyTelegramPublic(BaseModel):
async def get_telegram(telegram_id: int, hood=Depends(get_hood)): async def get_telegram(telegram_id: int, hood=Depends(get_hood)):
try: try:
return await Telegram.get(id=telegram_id, hood=hood) return await Telegram.objects.get(id=telegram_id, hood=hood)
except DoesNotExist: except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@ -53,7 +53,7 @@ telegram_callback_router = APIRouter()
operation_id="get_telegrams_public", operation_id="get_telegrams_public",
) )
async def telegram_read_all_public(hood=Depends(get_hood_unauthorized)): async def telegram_read_all_public(hood=Depends(get_hood_unauthorized)):
telegrambots = await Telegram.filter(hood=hood).all() telegrambots = await Telegram.objects.filter(hood=hood).all()
return [ return [
BodyTelegramPublic(username=telegrambot.username) BodyTelegramPublic(username=telegrambot.username)
for telegrambot in telegrambots for telegrambot in telegrambots
@ -67,7 +67,7 @@ async def telegram_read_all_public(hood=Depends(get_hood_unauthorized)):
operation_id="get_telegrams", operation_id="get_telegrams",
) )
async def telegram_read_all(hood=Depends(get_hood)): async def telegram_read_all(hood=Depends(get_hood)):
return await Telegram.filter(hood=hood).all() return await Telegram.objects.filter(hood=hood).all()
@router.get( @router.get(
@ -86,7 +86,7 @@ async def telegram_read(telegram=Depends(get_telegram)):
) )
async def telegram_delete(telegram=Depends(get_telegram)): async def telegram_delete(telegram=Depends(get_telegram)):
spawner.stop(telegram) spawner.stop(telegram)
for user in await TelegramSubscriber.filter(bot=telegram).all(): for user in await TelegramUser.objects.filter(bot=telegram).all():
await user.delete() await user.delete()
await telegram.delete() await telegram.delete()
return Response(status_code=status.HTTP_204_NO_CONTENT) return Response(status_code=status.HTTP_204_NO_CONTENT)
@ -102,7 +102,7 @@ async def telegram_create(
response: Response, values: BodyTelegram, hood=Depends(get_hood) response: Response, values: BodyTelegram, hood=Depends(get_hood)
): ):
try: try:
telegram = await Telegram.create(hood=hood, **values.__dict__) telegram = await Telegram.objects.create(hood=hood, **values.__dict__)
spawner.start(telegram) spawner.start(telegram)
response.headers["Location"] = str(telegram.id) response.headers["Location"] = str(telegram.id)
return telegram return telegram

View file

@ -1,17 +1,17 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from kibicara.platformapi import Censor, Message, Spawner from kibicara.platformapi import Censor, Spawner
from kibicara.platforms.test.model import Test from kibicara.platforms.test.model import Test
class TestBot(Censor): class TestBot(Censor):
def __init__(self, test: Test): def __init__(self, test):
super().__init__(test.hood) super().__init__(test.hood)
self.messages: list[Message] = [] self.messages = []
async def run(self): async def run(self):
while True: while True:

View file

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

View file

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

View file

@ -1,165 +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 CancelledError, gather, sleep
from logging import getLogger
from peony import PeonyClient, exceptions
from kibicara.config import config
from kibicara.platformapi import Censor, Message, Spawner
from kibicara.platforms.twitter.model import Twitter
logger = getLogger(__name__)
class TwitterBot(Censor):
def __init__(self, twitter_model):
super().__init__(twitter_model.hood)
self.twitter_model = twitter_model
self.enabled = self.twitter_model.enabled
self.polling_interval_sec = 60
self.mentions_since_id = self.twitter_model.mentions_since_id
self.dms_since_id = self.twitter_model.dms_since_id
@classmethod
async def destroy_hood(cls, hood):
"""Removes all its database entries."""
for twitter in await Twitter.filter(hood=hood).all():
await twitter.delete()
async def run(self):
try:
if not self.twitter_model.verified:
raise ValueError("Oauth Handshake not completed")
self.client = PeonyClient(
consumer_key=config["twitter"]["consumer_key"],
consumer_secret=config["twitter"]["consumer_secret"],
access_token=self.twitter_model.access_token,
access_token_secret=self.twitter_model.access_token_secret,
)
if self.twitter_model.mentions_since_id is None:
logger.debug("since_id is None in model, fetch newest mention id")
await self._poll_mentions()
if self.twitter_model.dms_since_id is None:
logger.debug("since_id is None in model, fetch newest dm id")
await self._poll_direct_messages()
user = await self.client.user
if user.screen_name:
await self.twitter_model.update(username=user.screen_name)
logger.debug(
"Starting Twitter bot: {0}".format(self.twitter_model.__dict__)
)
await gather(self.poll(), self.push())
except CancelledError:
logger.debug(
"Bot {0} received Cancellation.".format(self.twitter_model.hood.name)
)
except exceptions.Unauthorized:
logger.debug(
"Bot {0} has invalid auth token.".format(self.twitter_model.hood.name)
)
await self.twitter_model.update(enabled=False)
self.enabled = self.twitter_model.enabled
except (KeyError, ValueError, exceptions.NotAuthenticated):
logger.warning("Missing consumer_keys for Twitter in your configuration.")
await self.twitter_model.update(enabled=False)
self.enabled = self.twitter_model.enabled
finally:
logger.debug("Bot {0} stopped.".format(self.twitter_model.hood.name))
async def poll(self):
while True:
dms = await self._poll_direct_messages()
logger.debug(
"Polled dms ({0}): {1}".format(self.twitter_model.hood.name, str(dms))
)
mentions = await self._poll_mentions()
logger.debug(
"Polled mentions ({0}): {1}".format(
self.twitter_model.hood.name, str(mentions)
)
)
await self.twitter_model.update(
dms_since_id=self.dms_since_id, mentions_since_id=self.mentions_since_id
)
for message in dms:
await self.publish(Message(message))
for message_id, message in mentions:
await self.publish(Message(message, twitter_mention_id=message_id))
await sleep(self.polling_interval_sec)
async def _poll_direct_messages(self):
dms = await self.client.api.direct_messages.events.list.get()
dms = dms.events
# TODO check for next_cursor (see twitter api)
dms_filtered = []
if dms:
for dm in dms:
if int(dm.id) == self.dms_since_id:
break
dms_filtered.append(dm)
self.dms_since_id = int(dms[0].id)
messages = []
for dm in dms_filtered:
filtered_text = await self._filter_text(
dm.message_create.message_data.entities,
dm.message_create.message_data.text,
)
if not filtered_text:
continue
messages.append(filtered_text)
return messages
async def _poll_mentions(self):
mentions = await self.client.api.statuses.mentions_timeline.get(
since_id=self.mentions_since_id
)
if mentions:
self.mentions_since_id = mentions[0].id
messages = []
for mention in mentions:
filtered_text = await self._filter_text(mention.entities, mention.text)
if not filtered_text:
continue
messages.append((mention.id, filtered_text))
return messages
async def _filter_text(self, entities, text):
remove_indices = set()
for user in entities.user_mentions:
remove_indices.update(range(user.indices[0], user.indices[1] + 1))
for url in entities.urls:
remove_indices.update(range(url.indices[0], url.indices[1] + 1))
for symbol in entities.symbols:
remove_indices.update(range(symbol.indices[0], symbol.indices[1] + 1))
filtered_text = ""
for index, character in enumerate(text):
if index not in remove_indices:
filtered_text += character
return filtered_text.strip()
async def push(self):
while True:
message = await self.receive()
logger.debug(
"Received message from censor ({0}): {1}".format(
self.twitter_model.hood.name, message.text
)
)
if hasattr(message, "twitter_mention_id"):
await self._retweet(message.twitter_mention_id)
else:
await self._post_tweet(message.text)
async def _post_tweet(self, message):
return await self.client.api.statuses.update.post(status=message)
async def _retweet(self, message_id):
return await self.client.api.statuses.retweet.post(id=message_id)
spawner = Spawner(Twitter, TwitterBot)

View file

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

View file

@ -1,183 +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 logging import getLogger
from fastapi import APIRouter, Depends, HTTPException, Response, status
from peony.exceptions import NotAuthenticated
from peony.oauth_dance import get_access_token, get_oauth_token
from pydantic import BaseModel
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara.config import config
from kibicara.platforms.twitter.bot import spawner
from kibicara.platforms.twitter.model import Twitter
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
logger = getLogger(__name__)
class BodyTwitterPublic(BaseModel):
username: str
async def get_twitter(twitter_id: int, hood=Depends(get_hood)):
try:
return await Twitter.get(id=twitter_id, hood=hood)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
router = APIRouter()
twitter_callback_router = APIRouter()
@router.get(
"/public",
# TODO response_model,
operation_id="get_twitters_public",
)
async def twitter_read_all_public(hood=Depends(get_hood_unauthorized)):
twitterbots = await Twitter.filter(hood=hood).all()
return [
BodyTwitterPublic(username=twitterbot.username)
for twitterbot in twitterbots
if twitterbot.verified == 1 and twitterbot.enabled == 1 and twitterbot.username
]
@router.get(
"/",
# TODO response_model,
operation_id="get_twitters",
)
async def twitter_read_all(hood=Depends(get_hood)):
return await Twitter.filter(hood=hood).all()
@router.get(
"/{twitter_id}",
# TODO response_model
operation_id="get_twitter",
)
async def twitter_read(twitter=Depends(get_twitter)):
return twitter
@router.delete(
"/{twitter_id}",
status_code=status.HTTP_204_NO_CONTENT,
# TODO response_model
operation_id="delete_twitter",
)
async def twitter_delete(twitter=Depends(get_twitter)):
spawner.stop(twitter)
await twitter.delete()
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get(
"/{twitter_id}/status",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id="status_twitter",
)
async def twitter_status(twitter=Depends(get_twitter)):
return {"status": spawner.get(twitter).status.name}
@router.post(
"/{twitter_id}/start",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id="start_twitter",
)
async def twitter_start(twitter=Depends(get_twitter)):
await twitter.update(enabled=True)
spawner.get(twitter).start()
return {}
@router.post(
"/{twitter_id}/stop",
status_code=status.HTTP_200_OK,
# TODO response_model
operation_id="stop_twitter",
)
async def twitter_stop(twitter=Depends(get_twitter)):
await twitter.update(enabled=False)
spawner.get(twitter).stop()
return {}
@router.post(
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model
operation_id="create_twitter",
)
async def twitter_create(response: Response, hood=Depends(get_hood)):
"""
`https://api.twitter.com/oauth/authorize?oauth_token=`
"""
try:
# Purge Twitter corpses
for corpse in await Twitter.filter(hood=hood, verified=False).all():
await corpse.delete()
# Create Twitter
request_token = await get_oauth_token(
config["twitter"]["consumer_key"],
config["twitter"]["consumer_secret"],
callback_uri="{0}/dashboard/twitter-callback?hood={1}".format(
config["frontend_url"], hood.id
),
)
if request_token["oauth_callback_confirmed"] != "true":
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE)
twitter = await Twitter.create(
hood=hood,
access_token=request_token["oauth_token"],
access_token_secret=request_token["oauth_token_secret"],
)
response.headers["Location"] = str(twitter.id)
return twitter
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
except (KeyError, ValueError, NotAuthenticated):
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
@twitter_callback_router.get(
"/callback",
# TODO response_model
operation_id="callback_twitter",
)
async def twitter_read_callback(
oauth_token: str, oauth_verifier: str, hood=Depends(get_hood)
):
try:
twitter = await Twitter.filter(access_token=oauth_token).get()
access_token = await get_access_token(
config["twitter"]["consumer_key"],
config["twitter"]["consumer_secret"],
twitter.access_token,
twitter.access_token_secret,
oauth_verifier,
)
await Twitter.filter(id=twitter).update(
access_token=access_token["oauth_token"],
access_token_secret=access_token["oauth_token_secret"],
verified=True,
enabled=True,
)
spawner.start(twitter)
return {}
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
except (KeyError, ValueError, NotAuthenticated):
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)

View file

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
@ -16,35 +16,25 @@ from kibicara.platforms.email.webapi import router as email_router
from kibicara.platforms.telegram.webapi import router as telegram_router from kibicara.platforms.telegram.webapi import router as telegram_router
from kibicara.platforms.test.webapi import router as test_router from kibicara.platforms.test.webapi import router as test_router
from kibicara.platforms.mastodon.webapi import router as mastodon_router from kibicara.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.admin import router as admin_router
from kibicara.webapi.hoods import router as hoods_router from kibicara.webapi.hoods import router as hoods_router
from kibicara.webapi.hoods.exclude_patterns import router as exclude_patterns_router from kibicara.webapi.hoods.badwords import router as badwords_router
from kibicara.webapi.hoods.include_patterns import router as include_patterns_router from kibicara.webapi.hoods.triggers import router as triggers_router
router = APIRouter() router = APIRouter()
router.include_router(admin_router, prefix="/admin", tags=["admin"]) router.include_router(admin_router, prefix="/admin", tags=["admin"])
hoods_router.include_router( hoods_router.include_router(
include_patterns_router, triggers_router, prefix="/{hood_id}/triggers", tags=["triggers"]
prefix="/{hood_id}/triggers",
tags=["include_patterns"],
) )
hoods_router.include_router( hoods_router.include_router(
exclude_patterns_router, badwords_router, prefix="/{hood_id}/badwords", tags=["badwords"]
prefix="/{hood_id}/badwords",
tags=["exclude_patterns"],
) )
hoods_router.include_router(test_router, prefix="/{hood_id}/test", tags=["test"]) hoods_router.include_router(test_router, prefix="/{hood_id}/test", tags=["test"])
hoods_router.include_router( hoods_router.include_router(
telegram_router, prefix="/{hood_id}/telegram", tags=["telegram"] telegram_router, prefix="/{hood_id}/telegram", tags=["telegram"]
) )
hoods_router.include_router(
twitter_router, prefix="/{hood_id}/twitter", tags=["twitter"]
)
hoods_router.include_router( hoods_router.include_router(
mastodon_router, prefix="/{hood_id}/mastodon", tags=["mastodon"] mastodon_router, prefix="/{hood_id}/mastodon", tags=["mastodon"]
) )
router.include_router(twitter_callback_router, prefix="/twitter", tags=["twitter"])
hoods_router.include_router(email_router, prefix="/{hood_id}/email", tags=["email"]) hoods_router.include_router(email_router, prefix="/{hood_id}/email", tags=["email"])
router.include_router(hoods_router, prefix="/hoods") router.include_router(hoods_router, prefix="/hoods")

View file

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me> # Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
@ -11,20 +11,20 @@ from datetime import datetime, timedelta
from logging import getLogger from logging import getLogger
from pickle import dumps, loads from pickle import dumps, loads
from smtplib import SMTPException from smtplib import SMTPException
from sqlite3 import IntegrityError
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from nacl.encoding import URLSafeBase64Encoder from nacl.encoding import URLSafeBase64Encoder
from nacl.exceptions import CryptoError from nacl.exceptions import CryptoError
from nacl.secret import SecretBox from nacl.secret import SecretBox
from nacl.utils import random from ormantic.exceptions import NoMatch
from passlib.hash import argon2 from passlib.hash import argon2
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara import email from kibicara import email
from kibicara.config import config from kibicara.config import config
from kibicara.model import Admin, Hood from kibicara.model import Admin, AdminHoodRelation, Hood
from kibicara.webapi.utils import delete_hood from kibicara.webapi.utils import delete_hood
logger = getLogger(__name__) logger = getLogger(__name__)
@ -54,42 +54,41 @@ class BodyAccessToken(BaseModel):
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/admin/login")
secret_box = SecretBox( secret_box = SecretBox(bytes.fromhex(config["secret"]))
bytes.fromhex(str(config.get("secret", random(SecretBox.KEY_SIZE).hex())))
)
def to_token(**kwargs) -> str: def to_token(**kwargs):
return secret_box.encrypt(dumps(kwargs), encoder=URLSafeBase64Encoder).decode( return secret_box.encrypt(dumps(kwargs), encoder=URLSafeBase64Encoder).decode(
"ascii" "ascii"
) )
def from_token(token: str) -> dict: def from_token(token):
return loads( return loads(
secret_box.decrypt(token.encode("ascii"), encoder=URLSafeBase64Encoder) secret_box.decrypt(token.encode("ascii"), encoder=URLSafeBase64Encoder)
) )
async def get_auth(email: str, password: str) -> Admin: async def get_auth(email, password):
try: try:
admin = await Admin.get(email=email) admin = await Admin.objects.get(email=email)
if argon2.verify(password, admin.passhash): if argon2.verify(password, admin.passhash):
return admin return admin
raise ValueError raise ValueError
except DoesNotExist: except NoMatch:
raise ValueError raise ValueError
async def get_admin(access_token: str = Depends(oauth2_scheme)) -> Admin: async def get_admin(access_token=Depends(oauth2_scheme)):
try: try:
return await get_auth(**from_token(access_token)) admin = await get_auth(**from_token(access_token))
except (CryptoError, ValueError): except (CryptoError, ValueError):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials", detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"}, headers={"WWW-Authenticate": "Bearer"},
) )
return admin
router = APIRouter() router = APIRouter()
@ -110,9 +109,9 @@ async def admin_register(values: BodyAdmin):
register_token = to_token(**values.__dict__) register_token = to_token(**values.__dict__)
logger.debug("register_token={0}".format(register_token)) logger.debug("register_token={0}".format(register_token))
try: try:
if await Admin.exists(email=values.email): admin = await Admin.objects.filter(email=values.email).all()
if admin:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
# link goes to frontend. this is not the confirm API endpoint below!
body = "{0}/confirm?token={1}".format(config["frontend_url"], register_token) body = "{0}/confirm?token={1}".format(config["frontend_url"], register_token)
logger.debug(body) logger.debug(body)
email.send_email( email.send_email(
@ -139,8 +138,7 @@ async def admin_confirm(register_token: str):
try: try:
values = from_token(register_token) values = from_token(register_token)
passhash = argon2.hash(values["password"]) passhash = argon2.hash(values["password"])
await Admin.create(email=values["email"], passhash=passhash) await Admin.objects.create(email=values["email"], passhash=passhash)
# XXX login and registration tokens are exchangeable. does this hurt?
return BodyAccessToken(access_token=register_token) return BodyAccessToken(access_token=register_token)
except IntegrityError: except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
@ -180,14 +178,14 @@ async def admin_reset_password(values: BodyEmail):
- **email**: E-Mail Address of new hood admin - **email**: E-Mail Address of new hood admin
- **password**: Password of new hood admin - **password**: Password of new hood admin
""" """
reset_token = to_token(datetime=datetime.now().isoformat(), **values.__dict__) register_token = to_token(datetime=datetime.now().isoformat(), **values.__dict__)
logger.debug("reset_token={0}".format(reset_token)) logger.debug("register_token={0}".format(register_token))
try: try:
if not await Admin.exists(email=values.email): admin = await Admin.objects.filter(email=values.email).all()
if not admin:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
# link goes to frontend. this is not the reset API endpoint below!
body = "{0}/password-reset?token={1}".format( body = "{0}/password-reset?token={1}".format(
config["frontend_url"], reset_token config["frontend_url"], register_token
) )
logger.debug(body) logger.debug(body)
email.send_email( email.send_email(
@ -215,10 +213,11 @@ async def admin_confirm_reset(reset_token: str, values: BodyPassword):
): ):
raise HTTPException(status_code=status.HTTP_410_GONE) raise HTTPException(status_code=status.HTTP_410_GONE)
passhash = argon2.hash(values.password) passhash = argon2.hash(values.password)
await Admin.filter(email=token_values["email"]).update(passhash=passhash) admins = await Admin.objects.filter(email=token_values["email"]).all()
if len(admins) != 1:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
await admins[0].update(passhash=passhash)
return BodyAccessToken(access_token=reset_token) return BodyAccessToken(access_token=reset_token)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
except IntegrityError: except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
except CryptoError: except CryptoError:
@ -230,9 +229,11 @@ async def admin_confirm_reset(reset_token: str, values: BodyPassword):
# TODO response_model, # TODO response_model,
operation_id="get_hoods_admin", operation_id="get_hoods_admin",
) )
async def admin_hood_read_all(admin: Admin = Depends(get_admin)): async def admin_hood_read_all(admin=Depends(get_admin)):
"""Get a list of all hoods of a given admin.""" """Get a list of all hoods of a given admin."""
return await Hood.filter(admins=admin) return (
await AdminHoodRelation.objects.select_related("hood").filter(admin=admin).all()
)
@router.get( @router.get(
@ -240,8 +241,12 @@ async def admin_hood_read_all(admin: Admin = Depends(get_admin)):
# TODO response_model, # TODO response_model,
operation_id="get_admin", operation_id="get_admin",
) )
async def admin_read(admin: Admin = Depends(get_admin)): async def admin_read(admin=Depends(get_admin)):
return BodyEmail(email=admin.email) """Get a list of all hoods of a given admin."""
admin = await Admin.objects.filter(email=admin.email).all()
if len(admin) != 1:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return BodyEmail(email=admin[0].email)
@router.delete( @router.delete(
@ -249,10 +254,18 @@ async def admin_read(admin: Admin = Depends(get_admin)):
status_code=status.HTTP_204_NO_CONTENT, status_code=status.HTTP_204_NO_CONTENT,
operation_id="delete_admin", operation_id="delete_admin",
) )
async def admin_delete(admin: Admin = Depends(get_admin)): async def admin_delete(admin=Depends(get_admin)):
async for hood in Hood.filter(admins__contains=admin): hood_relations = (
await hood.admins.remove(admin) await AdminHoodRelation.objects.select_related("hood").filter(admin=admin).all()
await hood.fetch_related("admins") )
if len(hood.admins) == 0: for hood in hood_relations:
await delete_hood(hood) admins = (
await AdminHoodRelation.objects.select_related("admin")
.filter(hood=hood.id)
.all()
)
if len(admins) == 1 and admins[0].id == admin.id:
actual_hood = await Hood.objects.filter(id=hood.id).all()
await delete_hood(actual_hood[0])
await admin.delete() await admin.delete()
return Response(status_code=status.HTTP_204_NO_CONTENT)

View file

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
@ -6,11 +6,13 @@
"""REST API Endpoints for managing hoods.""" """REST API Endpoints for managing hoods."""
from fastapi import APIRouter, Depends, HTTPException, Response, status from sqlite3 import IntegrityError
from pydantic import BaseModel
from tortoise.exceptions import DoesNotExist, IntegrityError
from kibicara.model import Admin, Hood, IncludePattern from fastapi import APIRouter, Depends, HTTPException, Response, status
from ormantic.exceptions import NoMatch
from pydantic import BaseModel
from kibicara.model import AdminHoodRelation, Hood, Trigger
from kibicara.platforms.email.bot import spawner from kibicara.platforms.email.bot import spawner
from kibicara.webapi.admin import get_admin from kibicara.webapi.admin import get_admin
from kibicara.webapi.utils import delete_hood from kibicara.webapi.utils import delete_hood
@ -18,26 +20,28 @@ from kibicara.webapi.utils import delete_hood
class BodyHood(BaseModel): class BodyHood(BaseModel):
name: str name: str
landingpage: str = "Default Landing Page" landingpage: str = """
Default Landing Page
"""
async def get_hood_unauthorized(hood_id: int) -> Hood: async def get_hood_unauthorized(hood_id: int):
try: try:
return await Hood.get(id=hood_id) hood = await Hood.objects.get(id=hood_id)
except DoesNotExist: except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return hood
async def get_hood( async def get_hood(hood=Depends(get_hood_unauthorized), admin=Depends(get_admin)):
hood: Hood = Depends(get_hood_unauthorized), admin: Admin = Depends(get_admin) try:
) -> Hood: await AdminHoodRelation.objects.get(admin=admin, hood=hood)
await hood.fetch_related("admins") except NoMatch:
if admin in hood.admins: raise HTTPException(
return hood status_code=status.HTTP_401_UNAUTHORIZED,
raise HTTPException( headers={"WWW-Authenticate": "Bearer"},
status_code=status.HTTP_401_UNAUTHORIZED, )
headers={"WWW-Authenticate": "Bearer"}, return hood
)
router = APIRouter() router = APIRouter()
@ -51,7 +55,7 @@ router = APIRouter()
) )
async def hood_read_all(): async def hood_read_all():
"""Get all existing hoods.""" """Get all existing hoods."""
return await Hood.all() return await Hood.objects.all()
@router.post( @router.post(
@ -61,21 +65,19 @@ async def hood_read_all():
operation_id="create_hood", operation_id="create_hood",
tags=["hoods"], tags=["hoods"],
) )
async def hood_create( async def hood_create(values: BodyHood, response: Response, admin=Depends(get_admin)):
values: BodyHood, response: Response, admin: Admin = Depends(get_admin)
):
"""Creates a hood. """Creates a hood.
- **name**: Name of the hood - **name**: Name of the hood
- **landingpage**: Markdown formatted description of the hood - **landingpage**: Markdown formatted description of the hood
""" """
try: try:
hood = await Hood.create(**values.__dict__) hood = await Hood.objects.create(**values.__dict__)
await admin.hoods.add(hood) await AdminHoodRelation.objects.create(admin=admin.id, hood=hood.id)
spawner.start(hood) spawner.start(hood)
# Initialize Triggers to match all # Initialize Triggers to match all
await IncludePattern.create(hood=hood, pattern=".") await Trigger.objects.create(hood=hood, pattern=".")
response.headers["Location"] = str(hood.id) response.headers["Location"] = str(hood.id)
return hood return hood
@ -89,24 +91,25 @@ async def hood_create(
operation_id="get_hood", operation_id="get_hood",
tags=["hoods"], tags=["hoods"],
) )
async def hood_read(hood: Hood = Depends(get_hood_unauthorized)): async def hood_read(hood=Depends(get_hood_unauthorized)):
"""Get hood with id **hood_id**.""" """Get hood with id **hood_id**."""
return hood return hood
@router.put( @router.put(
"/{hood_id}", "/{hood_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="update_hood", operation_id="update_hood",
tags=["hoods"], tags=["hoods"],
) )
async def hood_update(values: BodyHood, hood: Hood = Depends(get_hood)): async def hood_update(values: BodyHood, hood=Depends(get_hood)):
"""Updates hood with id **hood_id**. """Updates hood with id **hood_id**.
- **name**: New name of the hood - **name**: New name of the hood
- **landingpage**: New Markdown formatted description of the hood - **landingpage**: New Markdown formatted description of the hood
""" """
await Hood.filter(id=hood).update(**values.__dict__) await hood.update(**values.__dict__)
return hood return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.delete( @router.delete(
@ -118,3 +121,4 @@ async def hood_update(values: BodyHood, hood: Hood = Depends(get_hood)):
async def hood_delete(hood=Depends(get_hood)): async def hood_delete(hood=Depends(get_hood)):
"""Deletes hood with id **hood_id**.""" """Deletes hood with id **hood_id**."""
await delete_hood(hood) await delete_hood(hood)
return Response(status_code=status.HTTP_204_NO_CONTENT)

View file

@ -0,0 +1,106 @@
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
"""REST API endpoints for managing badwords.
Provides API endpoints for adding, removing and reading regular expressions that block a
received message to be dropped by a censor. This provides a message filter customizable
by the hood admins.
"""
from re import compile as regex_compile
from re import error as RegexError
from sqlite3 import IntegrityError
from fastapi import APIRouter, Depends, HTTPException, Response, status
from ormantic.exceptions import NoMatch
from pydantic import BaseModel
from kibicara.model import BadWord
from kibicara.webapi.hoods import get_hood
class BodyBadWord(BaseModel):
pattern: str
async def get_badword(badword_id: int, hood=Depends(get_hood)):
try:
return await BadWord.objects.get(id=badword_id, hood=hood)
except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
router = APIRouter()
@router.get(
"/",
# TODO response_model,
operation_id="get_badwords",
)
async def badword_read_all(hood=Depends(get_hood)):
"""Get all badwords of hood with id **hood_id**."""
return await BadWord.objects.filter(hood=hood).all()
@router.post(
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id="create_badword",
)
async def badword_create(
values: BodyBadWord, response: Response, hood=Depends(get_hood)
):
"""Creates a new badword for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a badword.
"""
try:
regex_compile(values.pattern)
badword = await BadWord.objects.create(hood=hood, **values.__dict__)
response.headers["Location"] = str(badword.id)
return badword
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
except RegexError:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
@router.get(
"/{badword_id}",
# TODO response_model,
operation_id="get_badword",
)
async def badword_read(badword=Depends(get_badword)):
"""Reads badword with id **badword_id** for hood with id **hood_id**."""
return badword
@router.put(
"/{badword_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="update_badword",
)
async def badword_update(values: BodyBadWord, badword=Depends(get_badword)):
"""Updates badword with id **badword_id** for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a badword
"""
await badword.update(**values.__dict__)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{badword_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="delete_badword",
)
async def badword_delete(badword=Depends(get_badword)):
"""Deletes badword with id **badword_id** for hood with id **hood_id**."""
await badword.delete()
return Response(status_code=status.HTTP_204_NO_CONTENT)

View file

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

View file

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

View file

@ -0,0 +1,107 @@
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
#
# SPDX-License-Identifier: 0BSD
"""REST API endpoints for managing triggers.
Provides API endpoints for adding, removing and reading regular expressions that allow a
message to be passed through by a censor. A published message must match one of these
regular expressions otherwise it gets dropped by the censor. This provides a message
filter customizable by the hood admins.
"""
from re import compile as regex_compile
from re import error as RegexError
from sqlite3 import IntegrityError
from fastapi import APIRouter, Depends, HTTPException, Response, status
from ormantic.exceptions import NoMatch
from pydantic import BaseModel
from kibicara.model import Trigger
from kibicara.webapi.hoods import get_hood
class BodyTrigger(BaseModel):
pattern: str
async def get_trigger(trigger_id: int, hood=Depends(get_hood)):
try:
return await Trigger.objects.get(id=trigger_id, hood=hood)
except NoMatch:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
router = APIRouter()
@router.get(
"/",
# TODO response_model,
operation_id="get_triggers",
)
async def trigger_read_all(hood=Depends(get_hood)):
"""Get all triggers of hood with id **hood_id**."""
return await Trigger.objects.filter(hood=hood).all()
@router.post(
"/",
status_code=status.HTTP_201_CREATED,
# TODO response_model,
operation_id="create_trigger",
)
async def trigger_create(
values: BodyTrigger, response: Response, hood=Depends(get_hood)
):
"""Creates a new trigger for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a trigger.
"""
try:
regex_compile(values.pattern)
trigger = await Trigger.objects.create(hood=hood, **values.__dict__)
response.headers["Location"] = str(trigger.id)
return trigger
except IntegrityError:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
except RegexError:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
@router.get(
"/{trigger_id}",
# TODO response_model,
operation_id="get_trigger",
)
async def trigger_read(trigger=Depends(get_trigger)):
"""Reads trigger with id **trigger_id** for hood with id **hood_id**."""
return trigger
@router.put(
"/{trigger_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="update_trigger",
)
async def trigger_update(values: BodyTrigger, trigger=Depends(get_trigger)):
"""Updates trigger with id **trigger_id** for hood with id **hood_id**.
- **pattern**: Regular expression which is used to match a trigger
"""
await trigger.update(**values.__dict__)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{trigger_id}",
status_code=status.HTTP_204_NO_CONTENT,
operation_id="delete_trigger",
)
async def trigger_delete(trigger=Depends(get_trigger)):
"""Deletes trigger with id **trigger_id** for hood with id **hood_id**."""
await trigger.delete()
return Response(status_code=status.HTTP_204_NO_CONTENT)

View file

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

View file

@ -1,4 +1,4 @@
# Copyright (C) 2020, 2023 by Thomas Lindner <tom@dl6tom.de> # Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
# Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me> # Copyright (C) 2020 by Christian Hagenest <c.hagenest@pm.me>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
@ -7,43 +7,24 @@
from urllib.parse import urlparse from urllib.parse import urlparse
from fastapi import FastAPI, status from fastapi import FastAPI, status
from httpx import AsyncClient from fastapi.testclient import TestClient
import pytest from pytest import fixture
from tortoise import Tortoise
from kibicara import email from kibicara import email
from kibicara.model import Mapping
from kibicara.webapi import router from kibicara.webapi import router
@pytest.fixture(scope="session") @fixture(scope="module")
def anyio_backend(): def client():
return "asyncio" Mapping.drop_all()
Mapping.create_all()
@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 = FastAPI()
app.include_router(router, prefix="/api") app.include_router(router, prefix="/api")
yield AsyncClient(app=app, base_url="http://test") return TestClient(app)
await Tortoise.close_connections()
@pytest.fixture(scope="session") @fixture(scope="module")
def monkeymodule(): def monkeymodule():
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
@ -52,7 +33,7 @@ def monkeymodule():
mpatch.undo() mpatch.undo()
@pytest.fixture(scope="session") @fixture(scope="module")
def receive_email(monkeymodule): def receive_email(monkeymodule):
mailbox = [] mailbox = []
@ -66,54 +47,47 @@ def receive_email(monkeymodule):
return mock_receive_email return mock_receive_email
@pytest.fixture(scope="session") @fixture(scope="module")
@pytest.mark.anyio def register_token(client, receive_email):
async def register_token(client, receive_email): response = client.post(
response = await client.post(
"/api/admin/register/", json={"email": "user", "password": "password"} "/api/admin/register/", json={"email": "user", "password": "password"}
) )
assert response.status_code == status.HTTP_202_ACCEPTED assert response.status_code == status.HTTP_202_ACCEPTED
return urlparse(receive_email()["body"]).query.split("=", 1)[1] return urlparse(receive_email()["body"]).query.split("=", 1)[1]
@pytest.fixture(scope="session") @fixture(scope="module")
@pytest.mark.anyio def register_confirmed(client, register_token):
async def register_confirmed(client, register_token): response = client.post("/api/admin/confirm/{0}".format(register_token))
response = await client.post("/api/admin/confirm/{0}".format(register_token))
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
@pytest.fixture(scope="session") @fixture(scope="module")
@pytest.mark.anyio def access_token(client, register_confirmed):
async def access_token(client, register_confirmed): response = client.post(
response = await client.post(
"/api/admin/login/", data={"username": "user", "password": "password"} "/api/admin/login/", data={"username": "user", "password": "password"}
) )
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
return response.json()["access_token"] return response.json()["access_token"]
@pytest.fixture(scope="session") @fixture(scope="module")
def auth_header(access_token): def auth_header(access_token):
return {"Authorization": "Bearer {0}".format(access_token)} return {"Authorization": "Bearer {0}".format(access_token)}
@pytest.fixture(scope="function") @fixture(scope="function")
@pytest.mark.anyio def hood_id(client, auth_header):
async def hood_id(client, auth_header): response = client.post("/api/hoods/", json={"name": "hood"}, headers=auth_header)
response = await client.post(
"/api/hoods/", json={"name": "hood"}, headers=auth_header
)
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
hood_id = int(response.headers["Location"]) hood_id = int(response.headers["Location"])
yield hood_id yield hood_id
await client.delete("/api/hoods/{0}".format(hood_id), headers=auth_header) client.delete("/api/hoods/{0}".format(hood_id), headers=auth_header)
@pytest.fixture(scope="function") @fixture(scope="function")
@pytest.mark.anyio def trigger_id(client, hood_id, auth_header):
async def trigger_id(client, hood_id, auth_header): response = client.post(
response = await client.post(
"/api/hoods/{0}/triggers/".format(hood_id), "/api/hoods/{0}/triggers/".format(hood_id),
json={"pattern": "test"}, json={"pattern": "test"},
headers=auth_header, headers=auth_header,
@ -121,15 +95,14 @@ async def trigger_id(client, hood_id, auth_header):
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
trigger_id = int(response.headers["Location"]) trigger_id = int(response.headers["Location"])
yield trigger_id yield trigger_id
await client.delete( client.delete(
"/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id), headers=auth_header "/api/hoods/{0}/triggers/{1}".format(hood_id, trigger_id), headers=auth_header
) )
@pytest.fixture(scope="function") @fixture(scope="function")
@pytest.mark.anyio def badword_id(client, hood_id, auth_header):
async def badword_id(client, hood_id, auth_header): response = client.post(
response = await client.post(
"/api/hoods/{0}/badwords/".format(hood_id), "/api/hoods/{0}/badwords/".format(hood_id),
json={"pattern": ""}, json={"pattern": ""},
headers=auth_header, headers=auth_header,
@ -137,20 +110,19 @@ async def badword_id(client, hood_id, auth_header):
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
badword_id = int(response.headers["Location"]) badword_id = int(response.headers["Location"])
yield badword_id yield badword_id
await client.delete( client.delete(
"/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id), headers=auth_header "/api/hoods/{0}/badwords/{1}".format(hood_id, badword_id), headers=auth_header
) )
@pytest.fixture(scope="function") @fixture(scope="function")
@pytest.mark.anyio def test_id(client, hood_id, auth_header):
async def test_id(client, hood_id, auth_header): response = client.post(
response = await client.post(
"/api/hoods/{0}/test/".format(hood_id), json={}, headers=auth_header "/api/hoods/{0}/test/".format(hood_id), json={}, headers=auth_header
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
test_id = int(response.headers["Location"]) test_id = int(response.headers["Location"])
yield test_id yield test_id
await client.delete( client.delete(
"/api/hoods/{0}/test/{1}".format(hood_id, test_id), headers=auth_header "/api/hoods/{0}/test/{1}".format(hood_id, test_id), headers=auth_header
) )

View file

@ -3,16 +3,13 @@
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from fastapi import status from fastapi import status
import pytest
@pytest.mark.anyio def test_hoods_unauthorized(client):
async def test_hoods_unauthorized(client): response = client.get("/api/admin/hoods/")
response = await client.get("/api/admin/hoods/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_hoods_success(client, auth_header):
async def test_hoods_success(client, auth_header): response = client.get("/api/admin/hoods/", headers=auth_header)
response = await client.get("/api/admin/hoods/", headers=auth_header)
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK

View file

@ -3,107 +3,80 @@
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
import pytest
from fastapi import status from fastapi import status
@pytest.mark.anyio def test_hood_read_all(client):
async def test_hood_read_all(client): response = client.get("/api/hoods/")
response = await client.get("/api/hoods/")
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
@pytest.mark.anyio def test_hood_create_unauthorized(client, hood_id):
async def test_hood_create_unauthorized(client, hood_id): response = client.post("/api/hoods/")
response = await client.post("/api/hoods/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_hood_read(client, hood_id):
async def test_hood_read(client, hood_id): response = client.get("/api/hoods/{0}".format(hood_id))
response = await client.get("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
@pytest.mark.anyio def test_hood_update_unauthorized(client, hood_id):
async def test_hood_update_unauthorized(client, hood_id): response = client.put("/api/hoods/{0}".format(hood_id))
response = await client.put("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_hood_delete_unauthorized(client, hood_id):
async def test_hood_delete_unauthorized(client, hood_id): response = client.delete("/api/hoods/{0}".format(hood_id))
response = await client.delete("/api/hoods/{0}".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_trigger_read_all_unauthorized(client, hood_id):
async def test_trigger_read_all_unauthorized(client, hood_id): response = client.get("/api/hoods/{0}/triggers/".format(hood_id))
response = await client.get("/api/hoods/{0}/triggers/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_trigger_create_unauthorized(client, hood_id):
async def test_trigger_create_unauthorized(client, hood_id): response = client.post("/api/hoods/{0}/triggers/".format(hood_id))
response = await client.post("/api/hoods/{0}/triggers/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_trigger_read_unauthorized(client, hood_id, trigger_id):
async def test_trigger_read_unauthorized(client, hood_id, trigger_id): response = client.get("/api/hoods/{0}/triggers/{1}".format(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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_trigger_update_unauthorized(client, hood_id, trigger_id):
async def test_trigger_update_unauthorized(client, hood_id, trigger_id): response = client.put("/api/hoods/{0}/triggers/{1}".format(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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_trigger_delete_unauthorized(client, hood_id, trigger_id):
async def test_trigger_delete_unauthorized(client, hood_id, trigger_id): response = client.delete("/api/hoods/{0}/triggers/{1}".format(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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_badword_read_all_unauthorized(client, hood_id):
async def test_badword_read_all_unauthorized(client, hood_id): response = client.get("/api/hoods/{0}/badwords/".format(hood_id))
response = await client.get("/api/hoods/{0}/badwords/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_badword_create_unauthorized(client, hood_id):
async def test_badword_create_unauthorized(client, hood_id): response = client.post("/api/hoods/{0}/badwords/".format(hood_id))
response = await client.post("/api/hoods/{0}/badwords/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_badword_read_unauthorized(client, hood_id, badword_id):
async def test_badword_read_unauthorized(client, hood_id, badword_id): response = client.get("/api/hoods/{0}/badwords/{1}".format(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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_badword_update_unauthorized(client, hood_id, badword_id):
async def test_badword_update_unauthorized(client, hood_id, badword_id): response = client.put("/api/hoods/{0}/badwords/{1}".format(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 assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_badword_delete_unauthorized(client, hood_id, badword_id):
async def test_badword_delete_unauthorized(client, hood_id, badword_id): response = client.delete("/api/hoods/{0}/badwords/{1}".format(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 assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

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

View file

@ -8,14 +8,13 @@ from re import findall
from urllib.parse import urlparse from urllib.parse import urlparse
from fastapi import status from fastapi import status
import pytest from pytest import skip
from kibicara.webapi.admin import to_token from kibicara.webapi.admin import to_token
@pytest.mark.anyio def test_email_subscribe_unsubscribe(client, hood_id, receive_email):
async def test_email_subscribe_unsubscribe(client, hood_id, receive_email): response = client.post(
response = await client.post(
"/api/hoods/{0}/email/subscribe/".format(hood_id), "/api/hoods/{0}/email/subscribe/".format(hood_id),
json={"email": "test@localhost"}, json={"email": "test@localhost"},
) )
@ -28,41 +27,37 @@ async def test_email_subscribe_unsubscribe(client, hood_id, receive_email):
body, body,
)[0] )[0]
start = len("token=") start = len("token=")
response = await client.post( response = client.post(
"/api/hoods/{0}/email/subscribe/confirm/{1}".format( "/api/hoods/{0}/email/subscribe/confirm/{1}".format(
hood_id, urlparse(confirm_url).query[start:] hood_id, urlparse(confirm_url).query[start:]
) )
) )
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
response = await client.post( response = client.post(
"/api/hoods/{0}/email/subscribe/confirm/{1}".format( "/api/hoods/{0}/email/subscribe/confirm/{1}".format(
hood_id, urlparse(confirm_url).query[start:] hood_id, urlparse(confirm_url).query[start:]
) )
) )
assert response.status_code == status.HTTP_409_CONFLICT assert response.status_code == status.HTTP_409_CONFLICT
token = to_token(email=mail["to"], hood=hood_id) token = to_token(email=mail["to"], hood=hood_id)
response = await client.delete( response = client.delete(
"/api/hoods/{0}/email/unsubscribe/{1}".format(hood_id, token) "/api/hoods/{0}/email/unsubscribe/{1}".format(hood_id, token)
) )
assert response.status_code == status.HTTP_204_NO_CONTENT assert response.status_code == status.HTTP_204_NO_CONTENT
@pytest.mark.anyio def test_email_message(client, hood_id, trigger_id, email_row):
async def test_email_message(client, hood_id, trigger_id, email_row):
body = { body = {
"text": "test", "text": "test",
"author": "test@localhost", "author": "test@localhost",
"secret": email_row["secret"], "secret": email_row["secret"],
} }
response = await client.post( response = client.post("/api/hoods/{0}/email/messages/".format(hood_id), json=body)
"/api/hoods/{0}/email/messages/".format(hood_id), json=body
)
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
@pytest.mark.anyio def test_email_send_mda(trigger_id, email_row):
async def test_email_send_mda(trigger_id, email_row): skip("Only works if kibicara is listening on port 8000, and only sometimes")
pytest.skip("Only works if kibicara is listening on port 8000, and only sometimes")
mail = """From test@example.com Tue Jun 16 15:33:19 2020 mail = """From test@example.com Tue Jun 16 15:33:19 2020
Return-path: <test@example.com> Return-path: <test@example.com>
Envelope-to: hood@localhost Envelope-to: hood@localhost

View file

@ -5,27 +5,21 @@
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from fastapi import status from fastapi import status
import pytest
@pytest.mark.anyio def test_email_create_unauthorized(client, hood_id):
async def test_email_create_unauthorized(client, hood_id): response = client.post("/api/hoods/{0}/email/".format(hood_id))
response = await client.post("/api/hoods/{0}/email/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_email_delete_unauthorized(client, hood_id, email_row):
async def test_email_delete_unauthorized(client, hood_id, email_row): response = client.delete(
response = await client.delete(
"/api/hoods/{0}/email/{1}".format(hood_id, email_row["id"]) "/api/hoods/{0}/email/{1}".format(hood_id, email_row["id"])
) )
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.anyio def test_email_message_unauthorized(client, hood_id, email_row):
async def test_email_message_unauthorized(client, hood_id, email_row):
body = {"text": "test", "author": "author", "secret": "wrong"} body = {"text": "test", "author": "author", "secret": "wrong"}
response = await client.post( response = client.post("/api/hoods/{0}/email/messages/".format(hood_id), json=body)
"/api/hoods/{0}/email/messages/".format(hood_id), json=body
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -5,19 +5,16 @@
from fastapi import status from fastapi import status
from nacl.exceptions import CryptoError from nacl.exceptions import CryptoError
import pytest
@pytest.mark.anyio def test_email_subscribe_empty(client, hood_id):
async def test_email_subscribe_empty(client, hood_id): response = client.post("/api/hoods/{0}/email/subscribe/".format(hood_id))
response = await client.post("/api/hoods/{0}/email/subscribe/".format(hood_id))
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio def test_email_subscribe_confirm_wrong_token(client, hood_id):
async def test_email_subscribe_confirm_wrong_token(client, hood_id):
try: try:
response = await client.post( response = client.post(
"/api/hoods/{0}/email/subscribe/confirm/".format(hood_id) "/api/hoods/{0}/email/subscribe/confirm/".format(hood_id)
+ "asdfasdfasdfasdfasdfasdfasdfasdf" + "asdfasdfasdfasdfasdfasdfasdfasdf"
) )
@ -26,31 +23,26 @@ async def test_email_subscribe_confirm_wrong_token(client, hood_id):
pass pass
@pytest.mark.anyio def test_email_subscribe_confirm_wrong_hood(client):
async def test_email_subscribe_confirm_wrong_hood(client): response = client.delete(
response = await client.delete(
"/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf" "/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf"
) )
assert response.json()["detail"] == "Not Found" assert response.json()["detail"] == "Not Found"
@pytest.mark.anyio def test_email_message_wrong(client, hood_id, email_row):
async def test_email_message_wrong(client, hood_id, email_row):
body = { body = {
"text": "", "text": "",
"author": "test@localhost", "author": "test@localhost",
"secret": email_row["secret"], "secret": email_row["secret"],
} }
response = await client.post( response = client.post("/api/hoods/{0}/email/messages/".format(hood_id), json=body)
"/api/hoods/{0}/email/messages/".format(hood_id), json=body
)
assert response.status_code == status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS assert response.status_code == status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS
@pytest.mark.anyio def test_email_unsubscribe_wrong_token(client, hood_id):
async def test_email_unsubscribe_wrong_token(client, hood_id):
try: try:
await client.delete( client.delete(
"/api/hoods/{0}/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf".format( "/api/hoods/{0}/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf".format(
hood_id hood_id
) )
@ -59,9 +51,8 @@ async def test_email_unsubscribe_wrong_token(client, hood_id):
pass pass
@pytest.mark.anyio def test_email_unsubscribe_wrong_hood(client):
async def test_email_unsubscribe_wrong_hood(client): response = client.delete(
response = await client.delete(
"/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf" "/api/hoods/99999/email/unsubscribe/asdfasdfasdfasdfasdfasdfasdfasdf"
) )
assert response.json()["detail"] == "Not Found" assert response.json()["detail"] == "Not Found"

View file

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

View file

@ -1,18 +1,17 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from fastapi import status from fastapi import status
import pytest from pytest import fixture, mark
from mastodon.Mastodon import Mastodon from mastodon.Mastodon import Mastodon
from kibicara.platforms import mastodon from kibicara.platforms import mastodon
from kibicara.platforms.mastodon.model import MastodonAccount from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
@pytest.fixture(scope="function") @fixture(scope="function")
def disable_spawner(monkeypatch): def disable_spawner(monkeypatch):
class DoNothing: class DoNothing:
def start(self, bot): def start(self, bot):
@ -21,7 +20,7 @@ def disable_spawner(monkeypatch):
monkeypatch.setattr(mastodon.webapi, "spawner", DoNothing()) monkeypatch.setattr(mastodon.webapi, "spawner", DoNothing())
@pytest.mark.parametrize( @mark.parametrize(
"body", "body",
[ [
{ {
@ -31,8 +30,8 @@ def disable_spawner(monkeypatch):
} }
], ],
) )
@pytest.mark.anyio def test_mastodon_create_bot(
async def test_mastodon_create_bot( event_loop,
client, client,
disable_spawner, disable_spawner,
hood_id, hood_id,
@ -45,7 +44,7 @@ async def test_mastodon_create_bot(
monkeypatch.setattr(Mastodon, "log_in", log_in_mock) monkeypatch.setattr(Mastodon, "log_in", log_in_mock)
response = await client.post( response = client.post(
"/api/hoods/{0}/mastodon/".format(hood_id), "/api/hoods/{0}/mastodon/".format(hood_id),
json=body, json=body,
headers=auth_header, headers=auth_header,
@ -53,20 +52,33 @@ async def test_mastodon_create_bot(
print(response.json()) print(response.json())
assert response.status_code == status.HTTP_201_CREATED assert response.status_code == status.HTTP_201_CREATED
bot_id = response.json()["id"] bot_id = response.json()["id"]
mastodon_obj = await MastodonAccount.get(id=bot_id) mastodon_obj = event_loop.run_until_complete(MastodonAccount.objects.get(id=bot_id))
assert response.json()["access_token"] == mastodon_obj.access_token assert response.json()["access_token"] == mastodon_obj.access_token
mastodon_instance = event_loop.run_until_complete(
MastodonInstance.objects.get(id=mastodon_obj.instance.id)
)
assert (
response.json()["instance"]["name"]
== body["instance_url"]
== mastodon_instance.name
)
assert response.json()["hood"]["id"] == mastodon_obj.hood.id
assert response.json()["instance"]["client_id"] == mastodon_instance.client_id
assert (
response.json()["instance"]["client_secret"] == mastodon_instance.client_secret
)
assert mastodon_obj.enabled assert mastodon_obj.enabled
@pytest.mark.parametrize( @mark.parametrize(
"body", "body",
[ [
{"instance_url": "botsin.space", "email": "notanemail", "password": "asdf1234"}, {"instance_url": "botsin.space", "email": "notanemail", "password": "asdf1234"},
{"instance_url": "wrong", "email": "asdf@example.org", "password": "asdf1234"}, {"instance_url": "wrong", "email": "asdf@example.org", "password": "asdf1234"},
], ],
) )
@pytest.mark.anyio def test_mastodon_invalid_input(
async def test_mastodon_invalid_input( event_loop,
client, client,
disable_spawner, disable_spawner,
hood_id, hood_id,
@ -74,7 +86,7 @@ async def test_mastodon_invalid_input(
monkeypatch, monkeypatch,
body, body,
): ):
response = await client.post( response = client.post(
"/api/hoods/{0}/mastodon/".format(hood_id), "/api/hoods/{0}/mastodon/".format(hood_id),
json=body, json=body,
headers=auth_header, headers=auth_header,
@ -82,15 +94,13 @@ async def test_mastodon_invalid_input(
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio def test_mastodon_create_mastodon_invalid_id(client, auth_header):
async def test_mastodon_create_mastodon_invalid_id(client, auth_header): response = client.post("/api/hoods/1337/mastodon/", headers=auth_header)
response = await client.post("/api/hoods/1337/mastodon/", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.post("/api/hoods/wrong/mastodon/", headers=auth_header) response = client.post("/api/hoods/wrong/mastodon/", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio def test_mastodon_create_unauthorized(client, hood_id):
async def test_mastodon_create_unauthorized(client, hood_id): response = client.post("/api/hoods/{hood_id}/mastodon/")
response = await client.post("/api/hoods/{hood_id}/mastodon/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,15 +4,12 @@
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from fastapi import status from fastapi import status
import pytest from pytest import mark
@pytest.mark.parametrize( @mark.parametrize("bot", [{"api_token": "apitoken123", "welcome_message": "msg"}])
"bot", [{"api_token": "apitoken123", "welcome_message": "msg"}] def test_telegram_get_bot(client, auth_header, event_loop, bot, telegram):
) response = client.get(
@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), "/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id),
headers=auth_header, headers=auth_header,
) )
@ -22,28 +19,24 @@ async def test_telegram_get_bot(client, auth_header, bot, telegram):
assert response.json()["welcome_message"] == telegram.welcome_message assert response.json()["welcome_message"] == telegram.welcome_message
@pytest.mark.anyio def test_telegram_get_bot_invalid_id(client, auth_header, hood_id):
async def test_telegram_get_bot_invalid_id(client, auth_header, hood_id): response = client.get("/api/hoods/1337/telegram/123", headers=auth_header)
response = await client.get("/api/hoods/1337/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.get("/api/hoods/wrong/telegram/123", headers=auth_header) response = client.get("/api/hoods/wrong/telegram/123", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
response = await client.get( response = client.get(
"/api/hoods/{0}/telegram/7331".format(hood_id), headers=auth_header "/api/hoods/{0}/telegram/7331".format(hood_id), headers=auth_header
) )
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.get( response = client.get(
"/api/hoods/{0}/telegram/wrong".format(hood_id), headers=auth_header "/api/hoods/{0}/telegram/wrong".format(hood_id), headers=auth_header
) )
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.parametrize( @mark.parametrize("bot", [{"api_token": "apitoken456", "welcome_message": "msg"}])
"bot", [{"api_token": "apitoken456", "welcome_message": "msg"}] def test_telegram_get_bot_unauthorized(client, bot, telegram):
) response = client.get(
@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) "/api/hoods/{0}/telegram/{1}".format(telegram.hood.id, telegram.id)
) )
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -1,31 +1,32 @@
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de> # Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org> # Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
# Copyright (C) 2023 by Thomas Lindner <tom@dl6tom.de>
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from fastapi import status from fastapi import status
import pytest
from kibicara.model import Hood from kibicara.model import Hood
from kibicara.platforms.telegram.model import Telegram from kibicara.platforms.telegram.model import Telegram
@pytest.mark.anyio def test_telegram_get_bots(client, auth_header, event_loop, hood_id):
async def test_telegram_get_bots(client, auth_header, hood_id): hood = event_loop.run_until_complete(Hood.objects.get(id=hood_id))
hood = await Hood.get(id=hood_id) telegram0 = event_loop.run_until_complete(
telegram0 = await Telegram.create( Telegram.objects.create(
hood=hood, hood=hood,
api_token="api_token123", api_token="api_token123",
welcome_message="welcome_message123", welcome_message="welcome_message123",
)
) )
telegram1 = await Telegram.create( telegram1 = event_loop.run_until_complete(
hood=hood, Telegram.objects.create(
api_token="api_token456", hood=hood,
welcome_message="welcome_message123", api_token="api_token456",
welcome_message="welcome_message123",
)
) )
response = await client.get( response = client.get(
"/api/hoods/{0}/telegram/".format(telegram0.hood.id), headers=auth_header "/api/hoods/{0}/telegram".format(telegram0.hood.id), headers=auth_header
) )
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert response.json()[0]["id"] == telegram0.id assert response.json()[0]["id"] == telegram0.id
@ -34,15 +35,13 @@ async def test_telegram_get_bots(client, auth_header, hood_id):
assert response.json()[1]["api_token"] == telegram1.api_token assert response.json()[1]["api_token"] == telegram1.api_token
@pytest.mark.anyio def test_telegram_get_bots_invalid_id(client, auth_header, hood_id):
async def test_telegram_get_bots_invalid_id(client, auth_header, hood_id): response = client.get("/api/hoods/1337/telegram", headers=auth_header)
response = await client.get("/api/hoods/1337/telegram/", headers=auth_header)
assert response.status_code == status.HTTP_404_NOT_FOUND assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.get("/api/hoods/wrong/telegram/", headers=auth_header) response = client.get("/api/hoods/wrong/telegram", headers=auth_header)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@pytest.mark.anyio def test_telegram_get_bots_unauthorized(client, hood_id):
async def test_telegram_get_bots_unauthorized(client, hood_id): response = client.get("/api/hoods/{0}/telegram".format(hood_id))
response = await client.get("/api/hoods/{0}/telegram/".format(hood_id))
assert response.status_code == status.HTTP_401_UNAUTHORIZED assert response.status_code == status.HTTP_401_UNAUTHORIZED

View file

@ -10,7 +10,6 @@ import { MastodonService } from './api/mastodon.service';
import { TelegramService } from './api/telegram.service'; import { TelegramService } from './api/telegram.service';
import { TestService } from './api/test.service'; import { TestService } from './api/test.service';
import { TriggersService } from './api/triggers.service'; import { TriggersService } from './api/triggers.service';
import { TwitterService } from './api/twitter.service';
@NgModule({ @NgModule({
imports: [], imports: [],

View file

@ -14,6 +14,4 @@ export * from './test.service';
import { TestService } from './test.service'; import { TestService } from './test.service';
export * from './triggers.service'; export * from './triggers.service';
import { TriggersService } from './triggers.service'; import { TriggersService } from './triggers.service';
export * from './twitter.service'; export const APIS = [AdminService, BadwordsService, EmailService, HoodsService, MastodonService, TelegramService, TestService, TriggersService];
import { TwitterService } from './twitter.service';
export const APIS = [AdminService, BadwordsService, EmailService, HoodsService, MastodonService, TelegramService, TestService, TriggersService, TwitterService];

View file

@ -1,595 +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 { HTTPValidationError } from '../model/models';
import { BASE_PATH, COLLECTION_FORMATS } from '../variables';
import { Configuration } from '../configuration';
@Injectable({
providedIn: 'root'
})
export class TwitterService {
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;
}
/**
* Twitter Read Callback
* @param oauthToken
* @param oauthVerifier
* @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 callbackTwitter(oauthToken: string, oauthVerifier: string, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public callbackTwitter(oauthToken: string, oauthVerifier: string, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public callbackTwitter(oauthToken: string, oauthVerifier: string, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public callbackTwitter(oauthToken: string, oauthVerifier: string, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (oauthToken === null || oauthToken === undefined) {
throw new Error('Required parameter oauthToken was null or undefined when calling callbackTwitter.');
}
if (oauthVerifier === null || oauthVerifier === undefined) {
throw new Error('Required parameter oauthVerifier was null or undefined when calling callbackTwitter.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling callbackTwitter.');
}
let queryParameters = new HttpParams({encoder: this.encoder});
if (oauthToken !== undefined && oauthToken !== null) {
queryParameters = this.addToHttpParams(queryParameters,
<any>oauthToken, 'oauth_token');
}
if (oauthVerifier !== undefined && oauthVerifier !== null) {
queryParameters = this.addToHttpParams(queryParameters,
<any>oauthVerifier, 'oauth_verifier');
}
if (hoodId !== undefined && hoodId !== null) {
queryParameters = this.addToHttpParams(queryParameters,
<any>hoodId, 'hood_id');
}
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/twitter/callback`,
{
params: queryParameters,
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Twitter Create
* &#x60;https://api.twitter.com/oauth/authorize?oauth_token&#x3D;&#x60;
* @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 createTwitter(hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public createTwitter(hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public createTwitter(hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public createTwitter(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 createTwitter.');
}
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))}/twitter/`,
null,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Twitter Delete
* @param twitterId
* @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 deleteTwitter(twitterId: number, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public deleteTwitter(twitterId: number, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public deleteTwitter(twitterId: number, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public deleteTwitter(twitterId: number, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (twitterId === null || twitterId === undefined) {
throw new Error('Required parameter twitterId was null or undefined when calling deleteTwitter.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling deleteTwitter.');
}
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))}/twitter/${encodeURIComponent(String(twitterId))}`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Twitter Read
* @param twitterId
* @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 getTwitter(twitterId: number, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public getTwitter(twitterId: number, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public getTwitter(twitterId: number, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public getTwitter(twitterId: number, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (twitterId === null || twitterId === undefined) {
throw new Error('Required parameter twitterId was null or undefined when calling getTwitter.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling getTwitter.');
}
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))}/twitter/${encodeURIComponent(String(twitterId))}`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Twitter 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 getTwitters(hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public getTwitters(hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public getTwitters(hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public getTwitters(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 getTwitters.');
}
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))}/twitter/`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Twitter 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 getTwittersPublic(hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public getTwittersPublic(hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public getTwittersPublic(hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public getTwittersPublic(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 getTwittersPublic.');
}
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))}/twitter/public`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Twitter Start
* @param twitterId
* @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 startTwitter(twitterId: number, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public startTwitter(twitterId: number, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public startTwitter(twitterId: number, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public startTwitter(twitterId: number, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (twitterId === null || twitterId === undefined) {
throw new Error('Required parameter twitterId was null or undefined when calling startTwitter.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling startTwitter.');
}
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))}/twitter/${encodeURIComponent(String(twitterId))}/start`,
null,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Twitter Status
* @param twitterId
* @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 statusTwitter(twitterId: number, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public statusTwitter(twitterId: number, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public statusTwitter(twitterId: number, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public statusTwitter(twitterId: number, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (twitterId === null || twitterId === undefined) {
throw new Error('Required parameter twitterId was null or undefined when calling statusTwitter.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling statusTwitter.');
}
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))}/twitter/${encodeURIComponent(String(twitterId))}/status`,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
/**
* Twitter Stop
* @param twitterId
* @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 stopTwitter(twitterId: number, hoodId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<any>;
public stopTwitter(twitterId: number, hoodId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<any>>;
public stopTwitter(twitterId: number, hoodId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<any>>;
public stopTwitter(twitterId: number, hoodId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (twitterId === null || twitterId === undefined) {
throw new Error('Required parameter twitterId was null or undefined when calling stopTwitter.');
}
if (hoodId === null || hoodId === undefined) {
throw new Error('Required parameter hoodId was null or undefined when calling stopTwitter.');
}
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))}/twitter/${encodeURIComponent(String(twitterId))}/stop`,
null,
{
responseType: <any>responseType,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}
}

View file

@ -49,8 +49,8 @@
<p> <p>
<strong>Share the hood page: </strong> Just send the link to your hood <strong>Share the hood page: </strong> Just send the link to your hood
page to your community. We also recommend to link it in your platform page to your community. We also recommend to link it in your platform
account description (e.g. Twitter description) to give the users account description (e.g. Mastodon account description) to give the
context and more information. users context and more information.
</p> </p>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>

View file

@ -37,7 +37,6 @@
<div class="platforms-container"> <div class="platforms-container">
<app-email-settings [hoodId]="hoodId"></app-email-settings> <app-email-settings [hoodId]="hoodId"></app-email-settings>
<app-twitter-settings [hoodId]="hoodId"></app-twitter-settings>
<app-telegram-settings [hoodId]="hoodId"></app-telegram-settings> <app-telegram-settings [hoodId]="hoodId"></app-telegram-settings>
<app-mastodon-settings [hoodId]="hoodId"></app-mastodon-settings> <app-mastodon-settings [hoodId]="hoodId"></app-mastodon-settings>
</div> </div>

View file

@ -4,7 +4,6 @@ import { DashboardComponent } from './dashboard.component';
import { HoodsComponent } from './hoods/hoods.component'; import { HoodsComponent } from './hoods/hoods.component';
import { AuthGuard } from '../core/auth/auth.guard'; import { AuthGuard } from '../core/auth/auth.guard';
import { BoardComponent } from './board/board.component'; import { BoardComponent } from './board/board.component';
import { TwitterCallbackComponent } from '../platforms/twitter/twitter-callback/twitter-callback.component';
import { AccountSettingsComponent } from './account-settings/account-settings.component'; import { AccountSettingsComponent } from './account-settings/account-settings.component';
const routes: Routes = [ const routes: Routes = [
@ -15,8 +14,6 @@ const routes: Routes = [
{ path: '', component: HoodsComponent }, { path: '', component: HoodsComponent },
{ path: 'hoods/:id', component: BoardComponent }, { path: 'hoods/:id', component: BoardComponent },
{ path: 'settings', component: AccountSettingsComponent }, { path: 'settings', component: AccountSettingsComponent },
// Platform-specific Routes
{ path: 'twitter-callback', component: TwitterCallbackComponent },
], ],
canActivate: [AuthGuard], canActivate: [AuthGuard],
}, },

View file

@ -1,6 +1,5 @@
<h2>Platforms - choose one, get all hood messages</h2> <h2>Platforms - choose one, get all hood messages</h2>
<div class="container"> <div class="container">
<app-twitter-bot-card [hoodId]="hoodId"></app-twitter-bot-card>
<app-telegram-bot-card [hoodId]="hoodId"></app-telegram-bot-card> <app-telegram-bot-card [hoodId]="hoodId"></app-telegram-bot-card>
<app-email-bot-card [hoodId]="hoodId"></app-email-bot-card> <app-email-bot-card [hoodId]="hoodId"></app-email-bot-card>
<app-mastodon-bot-card [hoodId]="hoodId"></app-mastodon-bot-card> <app-mastodon-bot-card [hoodId]="hoodId"></app-mastodon-bot-card>

View file

@ -2,22 +2,16 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TelegramSettingsComponent } from './telegram/telegram-settings/telegram-settings.component'; import { TelegramSettingsComponent } from './telegram/telegram-settings/telegram-settings.component';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { TwitterSettingsComponent } from './twitter/twitter-settings/twitter-settings.component';
import { EmailSettingsComponent } from './email/email-settings/email-settings.component'; import { EmailSettingsComponent } from './email/email-settings/email-settings.component';
import { EmailDialogComponent } from './email/email-settings/email-dialog/email-dialog.component'; import { EmailDialogComponent } from './email/email-settings/email-dialog/email-dialog.component';
import { EmailInfoDialogComponent } from './email/email-settings/email-info-dialog/email-info-dialog.component'; import { EmailInfoDialogComponent } from './email/email-settings/email-info-dialog/email-info-dialog.component';
import { TelegramInfoDialogComponent } from './telegram/telegram-settings/telegram-info-dialog/telegram-info-dialog.component'; import { TelegramInfoDialogComponent } from './telegram/telegram-settings/telegram-info-dialog/telegram-info-dialog.component';
import { TelegramDialogComponent } from './telegram/telegram-settings/telegram-dialog/telegram-dialog.component'; import { TelegramDialogComponent } from './telegram/telegram-settings/telegram-dialog/telegram-dialog.component';
import { TwitterInfoDialogComponent } from './twitter/twitter-settings/twitter-info-dialog/twitter-info-dialog.component';
import { TwitterCallbackComponent } from './twitter/twitter-callback/twitter-callback.component';
import { TwitterCorpsesPipe } from './twitter/twitter-corpses-pipe/twitter-corpses.pipe';
import { PlatformsInfoPageComponent } from './platforms-info-page/platforms-info-page.component'; import { PlatformsInfoPageComponent } from './platforms-info-page/platforms-info-page.component';
import { EmailBotCardComponent } from './email/email-bot-card/email-bot-card.component'; import { EmailBotCardComponent } from './email/email-bot-card/email-bot-card.component';
import { TelegramBotCardComponent } from './telegram/telegram-bot-card/telegram-bot-card.component'; import { TelegramBotCardComponent } from './telegram/telegram-bot-card/telegram-bot-card.component';
import { TwitterBotCardComponent } from './twitter/twitter-bot-card/twitter-bot-card.component';
import { EmailBotInfoDialogComponent } from './email/email-bot-card/email-bot-info-dialog/email-bot-info-dialog.component'; import { EmailBotInfoDialogComponent } from './email/email-bot-card/email-bot-info-dialog/email-bot-info-dialog.component';
import { TelegramBotInfoDialogComponent } from './telegram/telegram-bot-card/telegram-bot-info-dialog/telegram-bot-info-dialog.component'; import { TelegramBotInfoDialogComponent } from './telegram/telegram-bot-card/telegram-bot-info-dialog/telegram-bot-info-dialog.component';
import { TwitterBotInfoDialogComponent } from './twitter/twitter-bot-card/twitter-bot-info-dialog/twitter-bot-info-dialog.component';
import { EmailConfirmationComponent } from './email/email-confirmation/email-confirmation.component'; import { EmailConfirmationComponent } from './email/email-confirmation/email-confirmation.component';
import { EmailUnsubscribeComponent } from './email/email-unsubscribe/email-unsubscribe.component'; import { EmailUnsubscribeComponent } from './email/email-unsubscribe/email-unsubscribe.component';
import { MastodonBotCardComponent } from './mastodon/mastodon-bot-card/mastodon-bot-card.component'; import { MastodonBotCardComponent } from './mastodon/mastodon-bot-card/mastodon-bot-card.component';
@ -28,22 +22,16 @@ import { MastodonBotInfoDialogComponent } from './mastodon/mastodon-bot-card/mas
@NgModule({ @NgModule({
declarations: [ declarations: [
TelegramSettingsComponent, TelegramSettingsComponent,
TwitterSettingsComponent,
EmailSettingsComponent, EmailSettingsComponent,
EmailDialogComponent, EmailDialogComponent,
EmailInfoDialogComponent, EmailInfoDialogComponent,
TelegramInfoDialogComponent, TelegramInfoDialogComponent,
TelegramDialogComponent, TelegramDialogComponent,
TwitterInfoDialogComponent,
TwitterCallbackComponent,
TwitterCorpsesPipe,
PlatformsInfoPageComponent, PlatformsInfoPageComponent,
EmailBotCardComponent, EmailBotCardComponent,
TelegramBotCardComponent, TelegramBotCardComponent,
TwitterBotCardComponent,
EmailBotInfoDialogComponent, EmailBotInfoDialogComponent,
TelegramBotInfoDialogComponent, TelegramBotInfoDialogComponent,
TwitterBotInfoDialogComponent,
EmailConfirmationComponent, EmailConfirmationComponent,
EmailUnsubscribeComponent, EmailUnsubscribeComponent,
MastodonBotCardComponent, MastodonBotCardComponent,
@ -55,7 +43,6 @@ import { MastodonBotInfoDialogComponent } from './mastodon/mastodon-bot-card/mas
exports: [ exports: [
TelegramSettingsComponent, TelegramSettingsComponent,
MastodonSettingsComponent, MastodonSettingsComponent,
TwitterSettingsComponent,
EmailSettingsComponent, EmailSettingsComponent,
PlatformsInfoPageComponent, PlatformsInfoPageComponent,
], ],

View file

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

View file

@ -1,11 +0,0 @@
.twitter {
background-image: url("../../../../assets/twitter.png");
background-size: cover;
}
.platform-title {
display: grid;
grid-template-columns: 1fr 40px;
width: 100%;
align-items: center;
}

View file

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

View file

@ -1,27 +0,0 @@
import { Component, OnInit, Input } from '@angular/core';
import { TwitterService } from 'src/app/core/api';
import { TwitterBotInfoDialogComponent } from './twitter-bot-info-dialog/twitter-bot-info-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'app-twitter-bot-card',
templateUrl: './twitter-bot-card.component.html',
styleUrls: ['./twitter-bot-card.component.scss'],
})
export class TwitterBotCardComponent implements OnInit {
@Input() hoodId;
twitters$;
constructor(
private twitterService: TwitterService,
private dialog: MatDialog
) {}
ngOnInit(): void {
this.twitters$ = this.twitterService.getTwittersPublic(this.hoodId);
}
onInfoClick() {
this.dialog.open(TwitterBotInfoDialogComponent);
}
}

View file

@ -1,51 +0,0 @@
<mat-dialog-content>
<div class="container">
<h2>How to communicate with the hood via Twitter?</h2>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title
>How to subscribe to the hood via Twitter?</mat-panel-title
>
</mat-expansion-panel-header>
<p>
Click on the twitter bot name that is shown in the twitter card. Just
follow the twitter account that the link leads to.
</p>
</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>You have two options:</p>
<ul>
<li>
Mention the bot in your tweet. The message of your tweet will be
broadcasted.
</li>
<li>
Write a direct message to the bot. The message will be broadcasted.
</li>
</ul>
<p>Both options have the equal result.</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>
Follow one of the twitter accounts in the twitter card. It will
broadcast all messages it receives by tweeting the content.
</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>Just unfollow the twitter accounts that you previously followed.</p>
</mat-expansion-panel>
</mat-accordion>
</div>
</mat-dialog-content>

View file

@ -1,4 +0,0 @@
.container {
margin-top: 10px;
margin-bottom: 10px;
}

View file

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

View file

@ -1,3 +0,0 @@
<div>
<mat-spinner class="spinner"></mat-spinner>
</div>

View file

@ -1,4 +0,0 @@
.spinner {
display: block;
margin: auto;
}

View file

@ -1,24 +0,0 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TwitterCallbackComponent } from './twitter-callback.component';
describe('TwitterCallbackComponent', () => {
let component: TwitterCallbackComponent;
let fixture: ComponentFixture<TwitterCallbackComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [TwitterCallbackComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TwitterCallbackComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -1,47 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TwitterService } from 'src/app/core/api';
@Component({
selector: 'app-twitter-callback',
templateUrl: './twitter-callback.component.html',
styleUrls: ['./twitter-callback.component.scss'],
})
export class TwitterCallbackComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private router: Router,
private twitterService: TwitterService
) {}
ngOnInit(): void {
if (
this.route.snapshot.queryParams.hood &&
this.route.snapshot.queryParams.oauth_token &&
this.route.snapshot.queryParams.oauth_verifier
) {
this.twitterService
.callbackTwitter(
this.route.snapshot.queryParams.oauth_token,
this.route.snapshot.queryParams.oauth_verifier,
this.route.snapshot.queryParams.hood
)
.subscribe(() => {
this.router.navigate([
'/dashboard/hoods',
this.route.snapshot.queryParams.hood,
]);
});
} else if (
this.route.snapshot.queryParams.hood &&
this.route.snapshot.queryParams.denied
) {
this.router.navigate([
'/dashboard/hoods',
this.route.snapshot.queryParams.hood,
]);
} else {
this.router.navigate(['/404']);
}
}
}

View file

@ -1,10 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'twitterCorpses',
})
export class TwitterCorpsesPipe implements PipeTransform {
transform(twitters) {
return twitters.filter((x) => x.verified === 1);
}
}

View file

@ -1,41 +0,0 @@
<mat-dialog-content>
<h2>How to add an twitter bot to your hood</h2>
<h3>What is a twitter bot?</h3>
<p>
Twitter bots are twitter accounts operated by software. In our case we will
show you how to add a bot for users to subscribe to, so they can send
messages into your hood system by mentioning the bot or direct messaging it,
which get distributed to the other platforms. Also they can receive messages
from other platforms by reading the tweets of the bot.
</p>
<p>
<strong>Example: </strong> You as a hood admin added the bot
@kibicara-my-hood to your hood. John wants to send a message to all hood
subscribers. He then uses his twitter account to follow to the bot
@kibicara-my-hood and sends his message to the bot by sending a tweet. His
message will be distributed to the other platforms and Sandra who is
subscribed to your email hood bot gets John's message via email.
</p>
<h3>How to add a twitter bot to your hood?</h3>
<ol>
<li>
<a href="https://twitter.com" target="_blank">Create a twitter account</a>
for the twitter bot and log in. We recommend <strong>NOT</strong> to use
your own personal twitter account but to create a new seperate hood
account since you will give full access to the account to our software.
</li>
<li>
Click on the <strong>Add a new platform connection!</strong>-Button. This
will redirect you to the twitter page to authorize access to the twitter
account. Click the
<strong>Authorize app</strong>
Button. You will be redirected back here. Done!
</li>
</ol>
<img class="example-image" src="assets/auth-app-twitter.png" />
<h3>Can I stop relaying twitter messages?</h3>
<p>
Yes, if you want to stop relaying twitter messages from a specific twitter
bot, just use the switch to the bot to stop relaying.
</p>
</mat-dialog-content>

View file

@ -1,9 +0,0 @@
.example-image {
margin-left: 10%;
margin-right: 10%;
@media screen and (max-width: 600px) {
width: 100%;
margin-left: 0%;
margin-right: 0%;
}
}

View file

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

View file

@ -1,64 +0,0 @@
<mat-card appearance="outlined">
<mat-card-header>
<div mat-card-avatar class="twitter"></div>
<mat-card-title class="platform-title">
Twitter
<button mat-icon-button aria-label="How to use">
<mat-icon
matTooltip="How to add an twitter 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="twitters$ | loading | async as twitters">
<ng-template [ngIf]="twitters.value">
<mat-list-item *ngIf="(twitters.value | twitterCorpses).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 twitter of twitters.value | twitterCorpses">
<div class="entry">
@{{ twitter.username }}
<mat-slide-toggle
[checked]="twitter.enabled === 1"
(change)="onChange(twitter)"
></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)="onDelete(twitter.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]="twitters.error"
><mat-icon class="warning">warning</mat-icon></ng-template
>
<ng-template [ngIf]="twitters.loading">
<mat-spinner [diameter]="45" class="spinner"></mat-spinner>
</ng-template>
</mat-list>
</mat-card-content>
</mat-card>

View file

@ -1,22 +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;
}
.twitter {
background-image: url("../../../../assets/twitter.png");
background-size: cover;
}

View file

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

View file

@ -1,77 +0,0 @@
import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { TwitterService } from 'src/app/core/api';
import { TwitterInfoDialogComponent } from './twitter-info-dialog/twitter-info-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-twitter-settings',
templateUrl: './twitter-settings.component.html',
styleUrls: ['./twitter-settings.component.scss'],
})
export class TwitterSettingsComponent implements OnInit {
@Input() hoodId;
twitters$: Observable<Array<any>>;
constructor(
private twitterService: TwitterService,
public dialog: MatDialog,
private snackBar: MatSnackBar
) {}
ngOnInit(): void {
this.reload();
}
private reload() {
this.twitters$ = this.twitterService.getTwitters(this.hoodId);
}
onInfoClick() {
this.dialog.open(TwitterInfoDialogComponent);
}
onDelete(twitterId) {
this.twitterService.deleteTwitter(twitterId, this.hoodId).subscribe(() => {
this.reload();
});
}
onCreate() {
this.twitterService.createTwitter(this.hoodId).subscribe((twitter) => {
if (twitter && twitter.access_token) {
const redirectUrl =
'https://api.twitter.com/oauth/authorize?oauth_token=' +
twitter.access_token;
window.location.href = redirectUrl;
}
});
}
onChange(twitter) {
if (twitter.enabled === 0) {
this.twitterService.startTwitter(twitter.id, this.hoodId).subscribe(
() => {},
(error) => {
this.snackBar.open('Could not start. Check your settings.', 'Close', {
duration: 2000,
});
}
);
} else if (twitter.enabled === 1) {
this.twitterService.stopTwitter(twitter.id, this.hoodId).subscribe(
() => {},
(error) => {
this.snackBar.open('Could not stop. Check your settings.', 'Close', {
duration: 2000,
});
}
);
}
// TODO yeah i know this is bad, implement disabling/enabling
setTimeout(() => {
this.reload();
}, 100);
}
}

View file

@ -112,11 +112,6 @@
use. If you are a normal user: use. If you are a normal user:
</p> </p>
<ul> <ul>
<li>
Twitter: We save the id of the last received message (mentions and
direct messages). This can be a message of yours if you sent the
latest message.
</li>
<li>Telegram: We save your telegram username.</li> <li>Telegram: We save your telegram username.</li>
<li>E-Mail: We save your e-mail address.</li> <li>E-Mail: We save your e-mail address.</li>
</ul> </ul>
@ -130,12 +125,6 @@
<p>For hood admins:</p> <p>For hood admins:</p>
<ul> <ul>
<li>We save your e-mail address and your password.</li> <li>We save your e-mail address and your password.</li>
<li>
Twitter: We save oauth tokens provided by twitter that give us access
to the twitter account and the username of the twitter account. We
disadvice the use of your private twitter account for acting as
Kibicara platform bot. Please create a new twitter account.
</li>
<li> <li>
Telegram: We save the telegram API token to the provided telegram bot Telegram: We save the telegram API token to the provided telegram bot
and a welcome message. Also we retrieve the name of the telegram bot. and a welcome message. Also we retrieve the name of the telegram bot.

View file

@ -31,7 +31,7 @@
<div class="big-paragraph-font"> <div class="big-paragraph-font">
<h2 class="big-h2-font">Inclusive and easy</h2> <h2 class="big-h2-font">Inclusive and easy</h2>
<p> <p>
Messages sent with Kibicara reach people via Telegram, Twitter and even Messages sent with Kibicara reach people via Telegram, Mastodon and even
E-Mail - perfect for announcing the next neighborhood meetings, creating E-Mail - perfect for announcing the next neighborhood meetings, creating
news tickers or even providing disaster warnings. news tickers or even providing disaster warnings.
</p> </p>
@ -47,7 +47,7 @@
community-administrated instances of Kibicara, being able to customize community-administrated instances of Kibicara, being able to customize
different platform accounts and filters. You subscribe to a Kibicara hood different platform accounts and filters. You subscribe to a Kibicara hood
through your service of choice. This can either be E-Mail, Telegram or through your service of choice. This can either be E-Mail, Telegram or
Twitter. Mastodon.
</p> </p>
<a mat-raised-button [routerLink]="['/hoods']">Discover hoods!</a> <a mat-raised-button [routerLink]="['/hoods']">Discover hoods!</a>
</div> </div>

View file

@ -6,7 +6,7 @@
# #
# client-side git-hook - checks commit message style # client-side git-hook - checks commit message style
pattern='\[(core|frontend|twitter|telegram|email|xmpp|mastodon|tests|doc|misc)\] [[:upper:]].*[^.]' pattern='\[(core|frontend|telegram|email|xmpp|mastodon|tests|doc|misc)\] [[:upper:]].*[^.]'
head -n 1 "$1" | egrep -x "$pattern" > /dev/null head -n 1 "$1" | egrep -x "$pattern" > /dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "commit message doesn't match \"$pattern\"" >&2 echo "commit message doesn't match \"$pattern\"" >&2