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

208 lines
6.3 KiB
Python

import inspect
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional
from uuid import UUID
from dynaconf.utils.boxing import DynaBox
from jinja2 import Environment, FileSystemLoader, Template
from mobilizon_reshare.config.config import get_settings
from mobilizon_reshare.event.event import MobilizonEvent
from .exceptions import InvalidAttribute
from ..models.publication import Publication
JINJA_ENV = Environment(loader=FileSystemLoader("/"))
logger = logging.getLogger(__name__)
class LoggerMixin:
def _log_debug(self, msg, *args, **kwargs):
self.__log(logging.DEBUG, msg, *args, **kwargs)
def _log_info(self, msg, *args, **kwargs):
self.__log(logging.INFO, msg, *args, **kwargs)
def _log_warning(self, msg, *args, **kwargs):
self.__log(logging.WARNING, msg, *args, **kwargs)
def _log_error(self, msg, *args, **kwargs):
self.__log(logging.ERROR, msg, *args, **kwargs)
def _log_critical(self, msg, *args, **kwargs):
self.__log(logging.CRITICAL, msg, *args, **kwargs)
def __log(self, level, msg, raise_error: type = None, *args, **kwargs):
method = inspect.currentframe().f_back.f_back.f_code.co_name
logger.log(level, f"{self}.{method}(): {msg}", *args, **kwargs)
if raise_error is not None:
raise raise_error(msg)
class ConfLoaderMixin:
_conf = tuple()
@property
def conf(self) -> DynaBox:
"""
Retrieves class's settings.
"""
cls = type(self)
try:
t, n = cls._conf or tuple()
return get_settings()[t][n]
except (KeyError, ValueError):
raise InvalidAttribute(
f"Class {cls.__name__} has invalid ``_conf`` attribute"
f" (should be 2-tuple)"
)
class AbstractPlatform(ABC, LoggerMixin, ConfLoaderMixin):
"""
Generic notifier class.
Shall be inherited from specific subclasses that will manage validation
process for messages and credentials, text formatting, posting, etc.
Attributes:
- ``message``: a formatted ``str``
"""
# Non-abstract subclasses should define ``_conf`` as a 2-tuple, where the
# first element is the type of class (either 'notifier' or 'publisher') and
# the second the name of its service (ie: 'facebook', 'telegram')
def __repr__(self):
return self.name
@property
@abstractmethod
def name(self):
pass
@abstractmethod
def _send(self, message: str, event: Optional[MobilizonEvent] = None):
raise NotImplementedError # pragma: no cover
def send(self, message: str, event: Optional[MobilizonEvent] = None):
"""
Sends a message to the target channel
"""
response = self._send(message, event)
self._validate_response(response)
@abstractmethod
def _validate_response(self, response):
raise NotImplementedError # pragma: no cover
@abstractmethod
def validate_credentials(self) -> None:
"""
Validates credentials.
Should raise ``PublisherError`` (or one of its subclasses) if
credentials are not valid.
"""
raise NotImplementedError # pragma: no cover
class AbstractEventFormatter(LoggerMixin, ConfLoaderMixin):
@abstractmethod
def _validate_event(self, event: MobilizonEvent) -> None:
"""
Validates publisher's event.
Should raise ``PublisherError`` (or one of its subclasses) if event
is not valid.
"""
raise NotImplementedError # pragma: no cover
@abstractmethod
def _validate_message(self, message: str) -> None:
"""
Validates notifier's message.
Should raise ``PublisherError`` (or one of its subclasses) if message
is not valid.
"""
raise NotImplementedError # pragma: no cover
def validate_event(self, event: MobilizonEvent) -> None:
self._validate_event(event)
self._validate_message(self.get_message_from_event(event))
@abstractmethod
def _preprocess_event(self, event):
"""
Allows publishers to preprocess events before feeding them to the template
"""
return event
def get_message_from_event(self, event: MobilizonEvent) -> str:
"""
Retrieves a message from the event itself.
"""
event = self._preprocess_event(event)
message = event.format(self.get_message_template())
message = self._preprocess_message(message)
return message
def get_message_template(self) -> Template:
"""
Retrieves publisher's message template.
"""
template_path = self.conf.msg_template_path or self.default_template_path
return JINJA_ENV.get_template(template_path)
def get_recap_header(self):
template_path = (
self.conf.recap_header_template_path
or self.default_recap_header_template_path
)
return JINJA_ENV.get_template(template_path).render()
def get_recap_fragment_template(self) -> Template:
template_path = (
self.conf.recap_template_path or self.default_recap_template_path
)
return JINJA_ENV.get_template(template_path)
def get_recap_fragment(self, event: MobilizonEvent) -> str:
"""
Retrieves the fragment that describes a single event inside the event recap.
"""
event = self._preprocess_event(event)
return event.format(self.get_recap_fragment_template())
def _preprocess_message(self, message: str):
return message
@dataclass
class BasePublication:
publisher: AbstractPlatform
formatter: AbstractEventFormatter
@dataclass
class EventPublication(BasePublication):
event: MobilizonEvent
id: UUID
@classmethod
def from_orm(cls, model: Publication, event: MobilizonEvent):
# imported here to avoid circular dependencies
from mobilizon_reshare.publishers.platforms.platform_mapping import (
get_publisher_class,
get_formatter_class,
)
publisher = get_publisher_class(model.publisher.name)()
formatter = get_formatter_class(model.publisher.name)()
return cls(publisher, formatter, event, model.id,)
@dataclass
class RecapPublication(BasePublication):
events: List[MobilizonEvent]