2021-04-23 17:04:27 +02:00
|
|
|
import inspect
|
|
|
|
import logging
|
|
|
|
from abc import ABC, abstractmethod
|
2021-10-02 18:09:03 +02:00
|
|
|
from dataclasses import dataclass
|
2021-10-16 01:25:45 +02:00
|
|
|
from typing import List
|
2021-10-02 18:09:03 +02:00
|
|
|
from uuid import UUID
|
2021-07-12 22:17:49 +02:00
|
|
|
|
2021-07-05 21:44:11 +02:00
|
|
|
from dynaconf.utils.boxing import DynaBox
|
|
|
|
from jinja2 import Environment, FileSystemLoader, Template
|
|
|
|
|
2021-08-16 10:49:52 +02:00
|
|
|
from mobilizon_reshare.config.config import get_settings
|
|
|
|
from mobilizon_reshare.event.event import MobilizonEvent
|
2021-10-02 18:09:03 +02:00
|
|
|
from mobilizon_reshare.models.publication import Publication as PublicationModel
|
2021-07-05 21:44:11 +02:00
|
|
|
from .exceptions import PublisherError, InvalidAttribute
|
|
|
|
|
|
|
|
JINJA_ENV = Environment(loader=FileSystemLoader("/"))
|
2021-04-23 17:04:27 +02:00
|
|
|
|
2021-05-02 10:24:46 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
2021-04-23 17:04:27 +02:00
|
|
|
|
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
class LoggerMixin:
|
|
|
|
def _log_debug(self, msg, *args, **kwargs):
|
|
|
|
self.__log(logging.DEBUG, msg, *args, **kwargs)
|
2021-04-23 17:04:27 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def _log_info(self, msg, *args, **kwargs):
|
|
|
|
self.__log(logging.INFO, msg, *args, **kwargs)
|
2021-04-23 17:04:27 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def _log_warning(self, msg, *args, **kwargs):
|
|
|
|
self.__log(logging.WARNING, msg, *args, **kwargs)
|
2021-07-05 21:44:11 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def _log_error(self, msg, *args, **kwargs):
|
|
|
|
self.__log(logging.ERROR, msg, *args, **kwargs)
|
2021-04-23 17:04:27 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def _log_critical(self, msg, *args, **kwargs):
|
|
|
|
self.__log(logging.CRITICAL, msg, *args, **kwargs)
|
|
|
|
|
|
|
|
def __log(self, level, msg, raise_error: PublisherError = 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()
|
2021-04-23 17:04:27 +02:00
|
|
|
|
2021-07-05 21:44:11 +02:00
|
|
|
@property
|
|
|
|
def conf(self) -> DynaBox:
|
|
|
|
"""
|
|
|
|
Retrieves class's settings.
|
|
|
|
"""
|
|
|
|
cls = type(self)
|
2021-10-02 18:09:03 +02:00
|
|
|
|
2021-07-05 21:44:11 +02:00
|
|
|
try:
|
2021-09-09 23:04:19 +02:00
|
|
|
t, n = cls._conf or tuple()
|
2021-07-12 22:17:49 +02:00
|
|
|
return get_settings()[t][n]
|
2021-07-05 21:44:11 +02:00
|
|
|
except (KeyError, ValueError):
|
|
|
|
raise InvalidAttribute(
|
|
|
|
f"Class {cls.__name__} has invalid ``_conf`` attribute"
|
|
|
|
f" (should be 2-tuple)"
|
|
|
|
)
|
|
|
|
|
2021-08-27 23:45:24 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
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.
|
2021-07-05 21:44:11 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
Attributes:
|
|
|
|
- ``message``: a formatted ``str``
|
|
|
|
"""
|
2021-07-05 21:44:11 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
# 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')
|
2021-07-05 21:44:11 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def __repr__(self):
|
|
|
|
return type(self).__name__
|
2021-07-05 21:44:11 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
__str__ = __repr__
|
2021-07-05 21:44:11 +02:00
|
|
|
|
2021-04-23 17:04:27 +02:00
|
|
|
@abstractmethod
|
2021-10-02 18:09:03 +02:00
|
|
|
def _send(self, message: str):
|
2021-10-05 15:32:07 +02:00
|
|
|
raise NotImplementedError # pragma: no cover
|
2021-04-23 17:04:27 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def send(self, message: str):
|
2021-04-23 17:04:27 +02:00
|
|
|
"""
|
2021-10-02 18:09:03 +02:00
|
|
|
Sends a message to the target channel
|
2021-04-23 17:04:27 +02:00
|
|
|
"""
|
2021-10-02 18:09:03 +02:00
|
|
|
message = self._preprocess_message(message)
|
|
|
|
response = self._send(message)
|
|
|
|
self._validate_response(response)
|
|
|
|
|
|
|
|
def _preprocess_message(self, message: str):
|
|
|
|
return message
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def _validate_response(self, response):
|
2021-10-05 15:32:07 +02:00
|
|
|
raise NotImplementedError # pragma: no cover
|
2021-04-23 17:04:27 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def are_credentials_valid(self) -> bool:
|
2021-05-02 10:46:58 +02:00
|
|
|
try:
|
2021-10-02 18:09:03 +02:00
|
|
|
self.validate_credentials()
|
2021-07-05 21:44:11 +02:00
|
|
|
except PublisherError:
|
2021-05-02 10:46:58 +02:00
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2021-07-05 21:44:11 +02:00
|
|
|
@abstractmethod
|
2021-10-02 18:09:03 +02:00
|
|
|
def validate_credentials(self) -> None:
|
2021-07-05 21:44:11 +02:00
|
|
|
"""
|
2021-10-02 18:09:03 +02:00
|
|
|
Validates credentials.
|
|
|
|
Should raise ``PublisherError`` (or one of its subclasses) if
|
|
|
|
credentials are not valid.
|
2021-07-05 21:44:11 +02:00
|
|
|
"""
|
2021-10-05 15:32:07 +02:00
|
|
|
raise NotImplementedError # pragma: no cover
|
2021-07-05 21:44:11 +02:00
|
|
|
|
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
class AbstractEventFormatter(LoggerMixin, ConfLoaderMixin):
|
2021-04-23 17:04:27 +02:00
|
|
|
@abstractmethod
|
2021-10-02 18:09:03 +02:00
|
|
|
def validate_event(self, message) -> None:
|
2021-04-23 17:04:27 +02:00
|
|
|
"""
|
|
|
|
Validates publisher's event.
|
2021-07-05 21:44:11 +02:00
|
|
|
Should raise ``PublisherError`` (or one of its subclasses) if event
|
|
|
|
is not valid.
|
2021-04-23 17:04:27 +02:00
|
|
|
"""
|
2021-10-05 15:32:07 +02:00
|
|
|
raise NotImplementedError # pragma: no cover
|
2021-04-23 17:04:27 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def _preprocess_event(self, event):
|
2021-07-21 09:13:32 +02:00
|
|
|
"""
|
|
|
|
Allows publishers to preprocess events before feeding them to the template
|
|
|
|
"""
|
2021-10-16 01:25:45 +02:00
|
|
|
return event
|
2021-07-21 09:13:32 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def get_message_from_event(self, event) -> str:
|
2021-07-05 21:44:11 +02:00
|
|
|
"""
|
|
|
|
Retrieves a message from the event itself.
|
|
|
|
"""
|
2021-10-02 18:09:03 +02:00
|
|
|
event = self._preprocess_event(event)
|
|
|
|
return event.format(self.get_message_template())
|
2021-05-02 11:27:07 +02:00
|
|
|
|
2021-07-05 21:44:11 +02:00
|
|
|
def get_message_template(self) -> Template:
|
|
|
|
"""
|
|
|
|
Retrieves publisher's message template.
|
|
|
|
"""
|
2021-07-18 18:23:30 +02:00
|
|
|
template_path = self.conf.msg_template_path or self.default_template_path
|
2021-07-12 22:17:49 +02:00
|
|
|
return JINJA_ENV.get_template(template_path)
|
2021-08-27 23:45:24 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
def is_message_valid(self, event: MobilizonEvent) -> bool:
|
|
|
|
try:
|
|
|
|
self.validate_message(self.get_message_from_event(event))
|
|
|
|
except PublisherError:
|
|
|
|
return False
|
|
|
|
return True
|
2021-08-27 23:45:24 +02:00
|
|
|
|
|
|
|
@abstractmethod
|
2021-10-02 18:09:03 +02:00
|
|
|
def validate_message(self, message: str) -> None:
|
|
|
|
"""
|
|
|
|
Validates notifier's message.
|
|
|
|
Should raise ``PublisherError`` (or one of its subclasses) if message
|
|
|
|
is not valid.
|
|
|
|
"""
|
2021-10-05 15:32:07 +02:00
|
|
|
raise NotImplementedError # pragma: no cover
|
2021-10-02 18:09:03 +02:00
|
|
|
|
|
|
|
def is_event_valid(self, event) -> bool:
|
|
|
|
try:
|
|
|
|
self.validate_event(event)
|
|
|
|
except PublisherError:
|
|
|
|
return False
|
|
|
|
return True
|
2021-08-27 23:45:24 +02:00
|
|
|
|
2021-10-16 01:25:45 +02:00
|
|
|
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:
|
|
|
|
event = self._preprocess_event(event)
|
|
|
|
return event.format(self.get_recap_fragment_template())
|
|
|
|
|
2021-08-27 23:45:24 +02:00
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
@dataclass
|
2021-10-16 01:25:45 +02:00
|
|
|
class BasePublication:
|
2021-10-02 18:09:03 +02:00
|
|
|
publisher: AbstractPlatform
|
|
|
|
formatter: AbstractEventFormatter
|
|
|
|
|
2021-10-16 01:25:45 +02:00
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class EventPublication(BasePublication):
|
|
|
|
event: MobilizonEvent
|
|
|
|
id: UUID
|
|
|
|
|
2021-10-02 18:09:03 +02:00
|
|
|
@classmethod
|
|
|
|
def from_orm(cls, model: PublicationModel, 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)()
|
2021-10-16 01:25:45 +02:00
|
|
|
return cls(publisher, formatter, event, model.id,)
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class RecapPublication(BasePublication):
|
|
|
|
events: List[MobilizonEvent]
|