[mastodon] Fix locking issue with synchronous Mastodon.py and replace last_seen with notification_dismiss

This commit is contained in:
ogdbd3h5qze42igcv8wcrqk3 2023-03-19 01:44:30 +01:00
parent f05c43ff59
commit c7062b3bcd
3 changed files with 58 additions and 33 deletions

View file

@ -4,15 +4,15 @@
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from asyncio import get_event_loop, sleep
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 mastodon import Mastodon, MastodonError
from asyncio import gather from asyncio import gather
import re import re
from logging import getLogger
logger = getLogger(__name__) logger = getLogger(__name__)
@ -21,35 +21,49 @@ class MastodonBot(Censor):
super().__init__(mastodon_account_model.hood) super().__init__(mastodon_account_model.hood)
self.model = mastodon_account_model self.model = mastodon_account_model
self.enabled = self.model.enabled 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): async def run(self):
await self.model.instance.load() try:
self.account = Mastodon( await self.model.instance.load()
client_id=self.model.instance.client_id, self.account = Mastodon(
client_secret=self.model.instance.client_secret, client_id=self.model.instance.client_id,
api_base_url=self.model.instance.name, client_secret=self.model.instance.client_secret,
access_token=self.model.access_token, api_base_url=self.model.instance.name,
) access_token=self.model.access_token,
await gather(self.poll(), self.push()) )
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): async def poll(self):
"""Get new mentions and DMs from Mastodon""" """Get new mentions and DMs from Mastodon"""
while True: while True:
try: try:
notifications = self.account.notifications() notifications = await get_event_loop().run_in_executor(
None, self.account.notifications
)
except MastodonError as e: except MastodonError as e:
logger.warning("%s in hood %s" % (e, self.model.hood.name)) logger.warning("%s in hood %s" % (e, self.model.hood.name))
continue continue
last_seen = int(self.model.last_seen)
for status in notifications: for status in notifications:
try: try:
status_id = int(status["status"]["id"]) status_id = int(status["status"]["id"])
except KeyError: except KeyError:
continue # ignore notifications which don't have a status 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(r"<[^>]*>", "", status["status"]["content"])
text = re.sub( text = re.sub(
"(?<=^|(?<=[^a-zA-Z0-9-_.]))@([A-Za-z]+[A-Za-z0-9-_]+)", "", text "(?<=^|(?<=[^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)) await self.publish(Message(text, toot_id=status_id))
else: else:
await self.publish(Message(text)) 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): async def push(self):
"""Push new Ticketfrei reports to Mastodon; if source is mastodon, boost it.""" """Push new Ticketfrei reports to Mastodon; if source is mastodon, boost it."""
@ -69,10 +87,14 @@ class MastodonBot(Censor):
message = await self.receive() message = await self.receive()
if hasattr(message, "tood_id"): if hasattr(message, "tood_id"):
logger.debug("Boosting post %s: %s" % (message.tood_id, message.text)) 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: else:
logger.debug("Posting message: %s" % (message.text,)) 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) spawner = Spawner(MastodonAccount, MastodonBot)

View file

@ -23,8 +23,8 @@ class MastodonAccount(Model):
hood: ForeignKey(Hood) hood: ForeignKey(Hood)
instance: ForeignKey(MastodonInstance) instance: ForeignKey(MastodonInstance)
access_token: Text() access_token: Text()
username: Text(allow_null=True) = None
enabled: Boolean() = False enabled: Boolean() = False
last_seen: Text()
class Mapping(Mapping): class Mapping(Mapping):
table_name = "mastodonaccounts" table_name = "mastodonaccounts"

View file

@ -3,9 +3,11 @@
# #
# SPDX-License-Identifier: 0BSD # SPDX-License-Identifier: 0BSD
from asyncio import get_event_loop
from fastapi import APIRouter, Depends, HTTPException, Response, status from fastapi import APIRouter, Depends, HTTPException, Response, status
from ormantic.exceptions import NoMatch from ormantic.exceptions import NoMatch
from pydantic import BaseModel, validate_email, validator from pydantic import BaseModel, validate_email, validator
from sqlite3 import IntegrityError
from kibicara.config import config from kibicara.config import config
from kibicara.platforms.mastodon.bot import spawner 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 kibicara.webapi.hoods import get_hood, get_hood_unauthorized
from mastodon import Mastodon, MastodonError, MastodonNetworkError from mastodon import Mastodon, MastodonError, MastodonNetworkError
from mastodon.errors import MastodonIllegalArgumentError
from logging import getLogger from logging import getLogger
@ -149,10 +152,7 @@ async def mastodon_stop(mastodon=Depends(get_mastodon)):
@router.post( @router.post(
"/", "/",
status_code=status.HTTP_201_CREATED, status_code=status.HTTP_201_CREATED,
responses={ # TODO response_model
201: {"model": MastodonAccount},
422: {"model": HTTPError, "description": "Invalid Input"},
},
operation_id="create_mastodon", operation_id="create_mastodon",
) )
async def mastodon_create(values: BodyMastodonAccount, hood=Depends(get_hood)): 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 instance.client_id, instance.client_secret, api_base_url=values.instance_url
) )
try: try:
access_token = account.log_in(values.email, values.password) access_token = await get_event_loop().run_in_executor(
except MastodonError: 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) logger.warning("Login to Mastodon failed.", exc_info=True)
return HTTPException(422, "Login to Mastodon failed") raise HTTPException(status_code=status.HTTP_422_INVALID_INPUT)
return await MastodonAccount.objects.create( except IntegrityError:
hood=hood, raise HTTPException(status_code=status.HTTP_409_CONFLICT)
instance=instance,
access_token=access_token,
enabled=True,
last_seen="0",
)