From dfd17aa27c823a539ad73990c51c81b7987d03b5 Mon Sep 17 00:00:00 2001 From: ogdbd3h5qze42igcv8wcrqk3 Date: Sun, 19 Mar 2023 01:44:30 +0100 Subject: [PATCH] [mastodon] Fix locking issue with synchronous Mastodon.py and replace last_seen with notification_dismiss --- .../src/kibicara/platforms/mastodon/bot.py | 58 +++++++++++++------ .../src/kibicara/platforms/mastodon/model.py | 2 +- .../src/kibicara/platforms/mastodon/webapi.py | 31 +++++----- 3 files changed, 58 insertions(+), 33 deletions(-) diff --git a/backend/src/kibicara/platforms/mastodon/bot.py b/backend/src/kibicara/platforms/mastodon/bot.py index 82033e2..dad7de1 100644 --- a/backend/src/kibicara/platforms/mastodon/bot.py +++ b/backend/src/kibicara/platforms/mastodon/bot.py @@ -4,15 +4,15 @@ # # SPDX-License-Identifier: 0BSD +from asyncio import get_event_loop, sleep from kibicara.platformapi import Censor, Spawner, Message from kibicara.platforms.mastodon.model import MastodonAccount +from logging import getLogger from mastodon import Mastodon, MastodonError from asyncio import gather import re -from logging import getLogger - logger = getLogger(__name__) @@ -21,35 +21,49 @@ class MastodonBot(Censor): super().__init__(mastodon_account_model.hood) self.model = mastodon_account_model self.enabled = self.model.enabled + self.polling_interval_sec = 60 + + @classmethod + async def destroy_hood(cls, hood): + """Removes all its database entries.""" + for mastodon in await Mastodon.objects.filter(hood=hood).all(): + await mastodon.delete() 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()) + try: + 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, + ) + account_details = await get_event_loop().run_in_executor( + None, self.account.account_verify_credentials + ) + if username := account_details.get("username"): + await self.model.update(username=username) + await gather(self.poll(), self.push()) + except Exception as e: + logger.debug("Bot {0} threw an Error: {1}".format(self.model.hood.name, e)) + finally: + logger.debug("Bot {0} stopped.".format(self.model.hood.name)) async def poll(self): """Get new mentions and DMs from Mastodon""" while True: try: - notifications = self.account.notifications() + notifications = await get_event_loop().run_in_executor( + None, self.account.notifications + ) except MastodonError as e: logger.warning("%s in hood %s" % (e, self.model.hood.name)) continue - 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 @@ -62,6 +76,10 @@ class MastodonBot(Censor): await self.publish(Message(text, toot_id=status_id)) else: await self.publish(Message(text)) + await get_event_loop().run_in_executor( + None, self.account.notifications_dismiss, status["id"] + ) + await sleep(self.polling_interval_sec) async def push(self): """Push new Ticketfrei reports to Mastodon; if source is mastodon, boost it.""" @@ -69,10 +87,14 @@ class MastodonBot(Censor): 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) + await get_event_loop().run_in_executor( + None, self.account.status_reblog, message.tood_id + ) else: logger.debug("Posting message: %s" % (message.text,)) - self.account.status_post(message.text) + await get_event_loop().run_in_executor( + None, self.account.status_post, message.text + ) spawner = Spawner(MastodonAccount, MastodonBot) diff --git a/backend/src/kibicara/platforms/mastodon/model.py b/backend/src/kibicara/platforms/mastodon/model.py index 31de304..a6a1d34 100644 --- a/backend/src/kibicara/platforms/mastodon/model.py +++ b/backend/src/kibicara/platforms/mastodon/model.py @@ -23,8 +23,8 @@ class MastodonAccount(Model): hood: ForeignKey(Hood) instance: ForeignKey(MastodonInstance) access_token: Text() + username: Text(allow_null=True) = None enabled: Boolean() = False - last_seen: Text() class Mapping(Mapping): table_name = "mastodonaccounts" diff --git a/backend/src/kibicara/platforms/mastodon/webapi.py b/backend/src/kibicara/platforms/mastodon/webapi.py index 7753081..8119297 100644 --- a/backend/src/kibicara/platforms/mastodon/webapi.py +++ b/backend/src/kibicara/platforms/mastodon/webapi.py @@ -3,9 +3,11 @@ # # SPDX-License-Identifier: 0BSD +from asyncio import get_event_loop from fastapi import APIRouter, Depends, HTTPException, Response, status from ormantic.exceptions import NoMatch from pydantic import BaseModel, validate_email, validator +from sqlite3 import IntegrityError from kibicara.config import config from kibicara.platforms.mastodon.bot import spawner @@ -13,6 +15,7 @@ from kibicara.platforms.mastodon.model import MastodonAccount, MastodonInstance from kibicara.webapi.hoods import get_hood, get_hood_unauthorized from mastodon import Mastodon, MastodonError, MastodonNetworkError +from mastodon.errors import MastodonIllegalArgumentError from logging import getLogger @@ -149,10 +152,7 @@ async def mastodon_stop(mastodon=Depends(get_mastodon)): @router.post( "/", status_code=status.HTTP_201_CREATED, - responses={ - 201: {"model": MastodonAccount}, - 422: {"model": HTTPError, "description": "Invalid Input"}, - }, + # TODO response_model operation_id="create_mastodon", ) async def mastodon_create(values: BodyMastodonAccount, hood=Depends(get_hood)): @@ -172,14 +172,17 @@ async def mastodon_create(values: BodyMastodonAccount, hood=Depends(get_hood)): instance.client_id, instance.client_secret, api_base_url=values.instance_url ) try: - access_token = account.log_in(values.email, values.password) - except MastodonError: + access_token = await get_event_loop().run_in_executor( + None, account.log_in, username, password + ) + logger.debug(f"{access_token}") + mastodon = await MastodonAccount.objects.create( + hood=hood, instance=instance, access_token=access_token, enabled=True + ) + spawner.start(mastodon) + return mastodon + except MastodonIllegalArgumentError: logger.warning("Login to Mastodon failed.", exc_info=True) - return HTTPException(422, "Login to Mastodon failed") - return await MastodonAccount.objects.create( - hood=hood, - instance=instance, - access_token=access_token, - enabled=True, - last_seen="0", - ) + raise HTTPException(status_code=status.HTTP_422_INVALID_INPUT) + except IntegrityError: + raise HTTPException(status_code=status.HTTP_409_CONFLICT)