Mobilizon-Reshare-condividi.../mobilizon_reshare/publishers/coordinator.py

165 lines
5.7 KiB
Python

import logging
from dataclasses import dataclass, field
from uuid import UUID
from mobilizon_reshare.event.event import MobilizonEvent
from mobilizon_reshare.models.publication import Publication
from mobilizon_reshare.models.publication import PublicationStatus
from mobilizon_reshare.publishers import get_active_notifiers, get_active_publishers
from mobilizon_reshare.publishers.abstract import AbstractPublisher
from mobilizon_reshare.publishers.exceptions import PublisherError
from mobilizon_reshare.publishers.telegram import TelegramPublisher
from mobilizon_reshare.publishers.zulip import ZulipPublisher
logger = logging.getLogger(__name__)
name_to_publisher_class = {"telegram": TelegramPublisher, "zulip": ZulipPublisher}
class BuildPublisherMixin:
@staticmethod
def build_publishers(
event: MobilizonEvent, publisher_names
) -> dict[str, AbstractPublisher]:
return {
publisher_name: name_to_publisher_class[publisher_name](event)
for publisher_name in publisher_names
}
@dataclass
class PublicationReport:
status: PublicationStatus
reason: str
publication_id: UUID
@dataclass
class PublisherCoordinatorReport:
publishers: dict[UUID, AbstractPublisher]
reports: dict[UUID, PublicationReport] = field(default_factory={})
@property
def successful(self):
return all(
r.status == PublicationStatus.COMPLETED for r in self.reports.values()
)
class PublisherCoordinator(BuildPublisherMixin):
def __init__(self, event: MobilizonEvent, publications: dict[UUID, Publication]):
publishers = self.build_publishers(event, get_active_publishers())
self.publishers_by_publication_id = {
publication_id: publishers[publication.publisher.name]
for publication_id, publication in publications.items()
}
def run(self) -> PublisherCoordinatorReport:
errors = self._validate()
if errors:
return PublisherCoordinatorReport(
reports=errors, publishers=self.publishers_by_publication_id
)
return self._post()
def _make_successful_report(self, failed_ids):
return {
publication_id: PublicationReport(
status=PublicationStatus.COMPLETED,
reason="",
publication_id=publication_id,
)
for publication_id in self.publishers_by_publication_id
if publication_id not in failed_ids
}
def _post(self):
failed_publishers_reports = {}
for publication_id, p in self.publishers_by_publication_id.items():
try:
p.publish()
except PublisherError as e:
failed_publishers_reports[publication_id] = PublicationReport(
status=PublicationStatus.FAILED,
reason=str(e),
publication_id=publication_id,
)
reports = failed_publishers_reports | self._make_successful_report(
failed_publishers_reports.keys()
)
return PublisherCoordinatorReport(
publishers=self.publishers_by_publication_id, reports=reports
)
def _validate(self):
errors: dict[UUID, PublicationReport] = {}
for publication_id, p in self.publishers_by_publication_id.items():
reason = []
if not p.are_credentials_valid():
reason.append("Invalid credentials")
if not p.is_event_valid():
reason.append("Invalid event")
if not p.is_message_valid():
reason.append("Invalid message")
if len(reason) > 0:
errors[publication_id] = PublicationReport(
status=PublicationStatus.FAILED,
reason=", ".join(reason),
publication_id=publication_id,
)
return errors
@staticmethod
def get_formatted_message(event: MobilizonEvent, publisher: str) -> str:
"""
Returns the formatted message for a given event and publisher.
"""
if publisher not in name_to_publisher_class:
raise ValueError(
f"Publisher {publisher} does not exist.\nSupported publishers: "
f"{', '.join(list(name_to_publisher_class.keys()))}"
)
return name_to_publisher_class[publisher](event).get_message_from_event()
class AbstractNotifiersCoordinator(BuildPublisherMixin):
def __init__(self, event: MobilizonEvent):
self.event = event
self.notifiers = self.build_publishers(event, get_active_notifiers())
def send_to_all(self, message):
# TODO: failure to notify should fail safely and write to a dedicated log
for notifier in self.notifiers.values():
notifier.send(message)
class PublicationFailureNotifiersCoordinator(AbstractNotifiersCoordinator):
def __init__(
self,
event: MobilizonEvent,
publisher_coordinator_report: PublisherCoordinatorReport,
):
self.report = publisher_coordinator_report
super(PublicationFailureNotifiersCoordinator, self).__init__(event)
def build_failure_message(self, report: PublicationReport):
return (
f"Publication {report.publication_id} failed with status: {report.status}.\n"
f"Reason: {report.reason}"
)
def notify_failures(self):
for publication_id, report in self.report.reports.items():
logger.info(
f"Sending failure notifications for publication: {publication_id}"
)
if report.status == PublicationStatus.FAILED:
self.send_to_all(self.build_failure_message(report))