[mastodon] Fix locking issue with synchronous Mastodon.py and replace last_seen with notification_dismiss
This commit is contained in:
parent
f05c43ff59
commit
c7062b3bcd
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
|
||||||
)
|
|
||||||
|
|
Loading…
Reference in a new issue