Compare commits
9 commits
f66375cd77
...
48b177d51e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48b177d51e | ||
|
|
d32ccc52ec | ||
|
|
2f32d949b4 | ||
|
|
f7e73ea407 | ||
|
|
4980d72d52 | ||
|
|
9973ea0139 | ||
|
|
a5faf716bb | ||
|
|
bd17d5321b | ||
|
|
32a8712805 |
|
|
@ -33,6 +33,7 @@
|
|||
3. Install the dependencies with `npm i`
|
||||
4. Install Angular with `npm i -g @angular/cli`
|
||||
5. Turn off production mode if you have not already (see above in backend).
|
||||
6. Also make sure to disable strict CORS checking for testing: `sudo su -c 'echo "cors_allow_origin = \"*\"" >> /etc/kibicara.conf'`
|
||||
6. Start the backend in a different terminal
|
||||
7. To serve and open the application, run `ng s -o`. The application will open
|
||||
under [http://127.0.0.1:4200](http://127.0.0.1:4200).
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from pytoml import load
|
|||
|
||||
config = {
|
||||
'database_connection': 'sqlite:////tmp/kibicara.sqlite',
|
||||
'frontend_url': 'http://127.0.0.1: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
|
||||
'frontend_path': None, # required, path to frontend html/css/js files
|
||||
|
|
@ -36,7 +36,7 @@ config = {
|
|||
'certfile': None, # optional for ssl
|
||||
# dev params
|
||||
'root_url': 'http://localhost:8000', # url of backend
|
||||
'cors_allow_origin': 'http://127.0.0.1:4200',
|
||||
'cors_allow_origin': 'http://localhost:4200',
|
||||
}
|
||||
"""Default configuration.
|
||||
|
||||
|
|
|
|||
0
kibicara/platforms/mastodon/__init__.py
Normal file
0
kibicara/platforms/mastodon/__init__.py
Normal file
78
kibicara/platforms/mastodon/bot.py
Normal file
78
kibicara/platforms/mastodon/bot.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
||||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
from kibicara.platformapi import Censor, Spawner, Message
|
||||
from kibicara.platforms.mastodon.model import MastodonAccount
|
||||
|
||||
from mastodon import Mastodon, MastodonError
|
||||
from asyncio import gather
|
||||
import re
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class MastodonBot(Censor):
|
||||
def __init__(self, mastodon_account_model):
|
||||
super().__init__(mastodon_account_model.hood)
|
||||
self.model = mastodon_account_model
|
||||
self.enabled = self.model.enabled
|
||||
|
||||
async def run(self):
|
||||
await self.model.instance.load()
|
||||
self.account = Mastodon(
|
||||
client_id=self.model.instance.client_id,
|
||||
client_secret=self.model.instance.client_secret,
|
||||
api_base_url=self.model.instance.name,
|
||||
access_token=self.model.access_token,
|
||||
)
|
||||
await gather(self.poll(), self.push())
|
||||
|
||||
async def poll(self):
|
||||
"""Get new mentions and DMs from Mastodon"""
|
||||
while True:
|
||||
try:
|
||||
notifications = self.account.notifications()
|
||||
except MastodonError as e:
|
||||
logger.warning("%s in hood %s" % (e, self.model.hood.name))
|
||||
continue
|
||||
last_seen = int(self.model.last_seen)
|
||||
for status in notifications:
|
||||
try:
|
||||
status_id = int(status['status']['id'])
|
||||
except KeyError:
|
||||
continue # ignore notifications which don't have a status
|
||||
if status_id <= last_seen:
|
||||
continue # toot was already processed in the past
|
||||
if status_id > int(self.model.last_seen):
|
||||
await self.model.update(last_seen=str(status_id))
|
||||
text = re.sub(r'<[^>]*>', '', status['status']['content'])
|
||||
text = re.sub(
|
||||
"(?<=^|(?<=[^a-zA-Z0-9-_.]))@([A-Za-z]+[A-Za-z0-9-_]+)", "", text
|
||||
)
|
||||
logger.debug(
|
||||
"Mastodon in %s received toot #%s: %s"
|
||||
% (self.model.hood.name, status_id, text)
|
||||
)
|
||||
if status['status']['visibility'] == 'public':
|
||||
await self.publish(Message(text, toot_id=status_id))
|
||||
else:
|
||||
await self.publish(Message(text))
|
||||
|
||||
async def push(self):
|
||||
"""Push new Ticketfrei reports to Mastodon; if source is mastodon, boost it."""
|
||||
while True:
|
||||
message = await self.receive()
|
||||
if hasattr(message, "tood_id"):
|
||||
logger.debug("Boosting post %s: %s" % (message.tood_id, message.text))
|
||||
self.account.status_reblog(message.tood_id)
|
||||
else:
|
||||
logger.debug("Posting message: %s" % (message.text,))
|
||||
self.account.status_post(message.text)
|
||||
|
||||
|
||||
spawner = Spawner(MastodonAccount, MastodonBot)
|
||||
30
kibicara/platforms/mastodon/model.py
Normal file
30
kibicara/platforms/mastodon/model.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright (C) 2020 by Thomas Lindner <tom@dl6tom.de>
|
||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
from ormantic import ForeignKey, Integer, Text, Boolean, Model
|
||||
|
||||
from kibicara.model import Hood, Mapping
|
||||
|
||||
|
||||
class MastodonInstance(Model):
|
||||
id: Integer(primary_key=True) = None
|
||||
name: Text()
|
||||
client_id: Text()
|
||||
client_secret: Text()
|
||||
|
||||
class Mapping(Mapping):
|
||||
table_name = 'mastodoninstances'
|
||||
|
||||
|
||||
class MastodonAccount(Model):
|
||||
id: Integer(primary_key=True) = None
|
||||
hood: ForeignKey(Hood)
|
||||
instance: ForeignKey(MastodonInstance)
|
||||
access_token: Text()
|
||||
enabled: Boolean() = False
|
||||
last_seen: Text()
|
||||
|
||||
class Mapping(Mapping):
|
||||
table_name = 'mastodonaccounts'
|
||||
168
kibicara/platforms/mastodon/webapi.py
Normal file
168
kibicara/platforms/mastodon/webapi.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Copyright (C) 2020 by Cathy Hu <cathy.hu@fau.de>
|
||||
# Copyright (C) 2020 by Martin Rey <martin.rey@mailbox.org>
|
||||
#
|
||||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from ormantic.exceptions import NoMatch
|
||||
from pydantic import BaseModel
|
||||
|
||||
from kibicara.config import config
|
||||
from kibicara.platforms.mastodon.bot import spawner
|
||||
from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance
|
||||
from kibicara.webapi.hoods import get_hood, get_hood_unauthorized
|
||||
|
||||
from mastodon import Mastodon, MastodonError
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class BodyMastodonPublic(BaseModel):
|
||||
username: str
|
||||
instance: str
|
||||
|
||||
|
||||
async def get_mastodon(mastodon_id, hood=Depends(get_hood)):
|
||||
try:
|
||||
return await MastodonAccount.objects.get(id=mastodon_id, hood=hood)
|
||||
except NoMatch:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
async def get_mastodon_instance(instance_url: str) -> MastodonInstance:
|
||||
"""Return a MastodonInstance ORM object with valid client_id and client_secret.
|
||||
|
||||
:param: instance_url: the API base URL of the mastodon server
|
||||
:return the MastodonInstance ORM object
|
||||
"""
|
||||
try:
|
||||
return await MastodonInstance.objects.get(name=instance_url)
|
||||
except NoMatch:
|
||||
app_name = config.get("frontend_url")
|
||||
client_id, client_secret = Mastodon.create_app(
|
||||
app_name, api_base_url=instance_url
|
||||
)
|
||||
await MastodonInstance.objects.create(
|
||||
name=instance_url, client_id=client_id, client_secret=client_secret
|
||||
)
|
||||
return await MastodonInstance.objects.get(name=instance_url)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
twitter_callback_router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/public',
|
||||
# TODO response_model,
|
||||
operation_id='get_mastodons_public',
|
||||
)
|
||||
async def mastodon_read_all_public(hood=Depends(get_hood_unauthorized)):
|
||||
mastodonbots = await MastodonAccount.objects.filter(hood=hood).all()
|
||||
return [
|
||||
BodyMastodonPublic(username=mbot.username, instance=mbot.model.instance.name)
|
||||
for mbot in mastodonbots
|
||||
if mbot.enabled == 1 and mbot.username
|
||||
]
|
||||
|
||||
|
||||
@router.get(
|
||||
'/',
|
||||
# TODO response_model,
|
||||
operation_id='get_mastodons',
|
||||
)
|
||||
async def mastodon_read_all(hood=Depends(get_hood)):
|
||||
return await MastodonAccount.objects.filter(hood=hood).all()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{mastodon_id}',
|
||||
# TODO response_model
|
||||
operation_id='get_mastodon',
|
||||
)
|
||||
async def mastodon_read(mastodon=Depends(get_mastodon)):
|
||||
return mastodon
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/{mastodon_id}',
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
# TODO response_model
|
||||
operation_id='delete_mastodon',
|
||||
)
|
||||
async def mastodon_delete(mastodon=Depends(get_mastodon)):
|
||||
spawner.stop(mastodon)
|
||||
await mastodon.delete()
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/{mastodon_id}/status',
|
||||
status_code=status.HTTP_200_OK,
|
||||
# TODO response_model
|
||||
operation_id='status_mastodon',
|
||||
)
|
||||
async def mastodon_status(mastodon=Depends(get_mastodon)):
|
||||
return {'status': spawner.get(mastodon).status.name}
|
||||
|
||||
|
||||
@router.post(
|
||||
'/{mastodon_id}/start',
|
||||
status_code=status.HTTP_200_OK,
|
||||
# TODO response_model
|
||||
operation_id='start_mastodon',
|
||||
)
|
||||
async def mastodon_start(mastodon=Depends(get_mastodon)):
|
||||
await mastodon.update(enabled=True)
|
||||
spawner.get(mastodon).start()
|
||||
return {}
|
||||
|
||||
|
||||
@router.post(
|
||||
'/{mastodon_id}/stop',
|
||||
status_code=status.HTTP_200_OK,
|
||||
# TODO response_model
|
||||
operation_id='stop_mastodon',
|
||||
)
|
||||
async def mastodon_stop(mastodon=Depends(get_mastodon)):
|
||||
await mastodon.update(enabled=False)
|
||||
spawner.get(mastodon).stop()
|
||||
return {}
|
||||
|
||||
|
||||
@router.post(
|
||||
'/',
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
# TODO response_model
|
||||
operation_id='create_mastodon',
|
||||
)
|
||||
async def mastodon_create(instance_url, username, password, hood=Depends(get_hood)):
|
||||
"""Add a Mastodon Account to a Ticketfrei account.
|
||||
|
||||
open questions:
|
||||
do we really get the username + password like this?
|
||||
can the instance_url have different ways of writing?
|
||||
|
||||
:param: instance_url: the API base URL of the mastodon server
|
||||
:param: username: the username of the Mastodon account
|
||||
:param: password: the password of the Mastodon account
|
||||
:param: hood: the hood ORM object
|
||||
"""
|
||||
instance = await get_mastodon_instance(instance_url)
|
||||
account = Mastodon(
|
||||
instance.client_id, instance.client_secret, api_base_url=instance_url
|
||||
)
|
||||
try:
|
||||
access_token = account.log_in(username, password)
|
||||
except MastodonError:
|
||||
logger.warning("Login to Mastodon failed.", exc_info=True)
|
||||
return # show error to user
|
||||
return await MastodonAccount.objects.create(
|
||||
hood=hood,
|
||||
instance=instance,
|
||||
access_token=access_token,
|
||||
enabled=True,
|
||||
last_seen="0",
|
||||
)
|
||||
|
|
@ -15,6 +15,7 @@ from fastapi import APIRouter
|
|||
from kibicara.platforms.email.webapi import router as email_router
|
||||
from kibicara.platforms.telegram.webapi import router as telegram_router
|
||||
from kibicara.platforms.test.webapi import router as test_router
|
||||
from kibicara.platforms.mastodon.webapi import router as mastodon_router
|
||||
from kibicara.platforms.twitter.webapi import router as twitter_router
|
||||
from kibicara.platforms.twitter.webapi import twitter_callback_router
|
||||
from kibicara.webapi.admin import router as admin_router
|
||||
|
|
@ -37,6 +38,9 @@ hoods_router.include_router(
|
|||
hoods_router.include_router(
|
||||
twitter_router, prefix='/{hood_id}/twitter', tags=['twitter']
|
||||
)
|
||||
hoods_router.include_router(
|
||||
mastodon_router, prefix='/{hood_id}/mastodon', tags=['mastodon']
|
||||
)
|
||||
router.include_router(twitter_callback_router, prefix='/twitter', tags=['twitter'])
|
||||
hoods_router.include_router(email_router, prefix='/{hood_id}/email', tags=['email'])
|
||||
router.include_router(hoods_router, prefix='/hoods')
|
||||
|
|
|
|||
Loading…
Reference in a new issue