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`
|
3. Install the dependencies with `npm i`
|
||||||
4. Install Angular with `npm i -g @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. 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
|
6. Start the backend in a different terminal
|
||||||
7. To serve and open the application, run `ng s -o`. The application will open
|
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).
|
under [http://127.0.0.1:4200](http://127.0.0.1:4200).
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ from pytoml import load
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
'database_connection': 'sqlite:////tmp/kibicara.sqlite',
|
'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
|
'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
|
||||||
|
|
@ -36,7 +36,7 @@ config = {
|
||||||
'certfile': None, # optional for ssl
|
'certfile': None, # optional for ssl
|
||||||
# dev params
|
# dev params
|
||||||
'root_url': 'http://localhost:8000', # url of backend
|
'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.
|
"""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.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.twitter.webapi import router as twitter_router
|
from kibicara.platforms.twitter.webapi import router as twitter_router
|
||||||
from kibicara.platforms.twitter.webapi import twitter_callback_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
|
||||||
|
|
@ -37,6 +38,9 @@ hoods_router.include_router(
|
||||||
hoods_router.include_router(
|
hoods_router.include_router(
|
||||||
twitter_router, prefix='/{hood_id}/twitter', tags=['twitter']
|
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'])
|
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')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue