Add Mastodon publisher. (#83)
* Add Mastodon publisher. This commit enables publishing on Mastodon and tries to define the minimal requirements for adding a new platform to Mobilizon Reshare. * publishers: exceptions: Add HTTPError. * platforms: mastodon: Make toot length customizable.
This commit is contained in:
parent
8de70ef857
commit
6430de4a84
|
@ -0,0 +1,9 @@
|
||||||
|
# Add a new publisher
|
||||||
|
To add a new publishing platform to Mobilizon Reshare you need to follow these steps.
|
||||||
|
|
||||||
|
## Add an example configuration in `mobilizon_reshare/.secrets.toml`
|
||||||
|
## Add suitable validators to `mobilizon_reshare/config/notifiers.py` and `mobilizon_reshare/config/publishers.py`
|
||||||
|
## Create a new file inside `mobilizon_reshare/publishers/platforms`
|
||||||
|
## Add suitable mappings inside `mobilizon_reshare/publishers/platform_mapping.py`
|
||||||
|
## Create suitable message templates inside `mobilizon_reshare/publishers/templates`
|
||||||
|
## Add some unit test inside `tests/publishers`
|
|
@ -18,7 +18,11 @@ api_key_secret="xxx"
|
||||||
access_token="xxx"
|
access_token="xxx"
|
||||||
access_secret="xxx"
|
access_secret="xxx"
|
||||||
[default.publisher.mastodon]
|
[default.publisher.mastodon]
|
||||||
active=false
|
active=true
|
||||||
|
instance="xxx"
|
||||||
|
token="xxx"
|
||||||
|
name="xxx"
|
||||||
|
toot_length=500
|
||||||
|
|
||||||
[default.notifier.telegram]
|
[default.notifier.telegram]
|
||||||
active=true
|
active=true
|
||||||
|
|
|
@ -8,12 +8,16 @@ telegram_validators = [
|
||||||
Validator("notifier.telegram.username", must_exist=True),
|
Validator("notifier.telegram.username", must_exist=True),
|
||||||
]
|
]
|
||||||
zulip_validators = [
|
zulip_validators = [
|
||||||
Validator("publisher.zulip.chat_id", must_exist=True),
|
Validator("notifier.zulip.chat_id", must_exist=True),
|
||||||
Validator("publisher.zulip.subject", must_exist=True),
|
Validator("notifier.zulip.subject", must_exist=True),
|
||||||
Validator("publisher.zulip.bot_token", must_exist=True),
|
Validator("notifier.zulip.bot_token", must_exist=True),
|
||||||
Validator("publisher.zulip.bot_email", must_exist=True),
|
Validator("notifier.zulip.bot_email", must_exist=True),
|
||||||
|
]
|
||||||
|
mastodon_validators = [
|
||||||
|
Validator("notifier.mastodon.instance", must_exist=True),
|
||||||
|
Validator("notifier.mastodon.token", must_exist=True),
|
||||||
|
Validator("notifier.mastodon.name", must_exist=True),
|
||||||
]
|
]
|
||||||
mastodon_validators = []
|
|
||||||
twitter_validators = [
|
twitter_validators = [
|
||||||
Validator("publisher.twitter.api_key", must_exist=True),
|
Validator("publisher.twitter.api_key", must_exist=True),
|
||||||
Validator("publisher.twitter.api_key_secret", must_exist=True),
|
Validator("publisher.twitter.api_key_secret", must_exist=True),
|
||||||
|
|
|
@ -22,7 +22,17 @@ zulip_validators = [
|
||||||
Validator("publisher.zulip.bot_token", must_exist=True),
|
Validator("publisher.zulip.bot_token", must_exist=True),
|
||||||
Validator("publisher.zulip.bot_email", must_exist=True),
|
Validator("publisher.zulip.bot_email", must_exist=True),
|
||||||
]
|
]
|
||||||
mastodon_validators = []
|
mastodon_validators = [
|
||||||
|
Validator("publisher.mastodon.instance", must_exist=True),
|
||||||
|
Validator("publisher.mastodon.token", must_exist=True),
|
||||||
|
Validator("publisher.mastodon.toot_length", default=500),
|
||||||
|
Validator("publisher.mastodon.msg_template_path", must_exist=True, default=None),
|
||||||
|
Validator("publisher.mastodon.recap_template_path", must_exist=True, default=None),
|
||||||
|
Validator(
|
||||||
|
"publisher.mastodon.recap_header_template_path", must_exist=True, default=None
|
||||||
|
),
|
||||||
|
Validator("publisher.mastodon.name", must_exist=True),
|
||||||
|
]
|
||||||
twitter_validators = [
|
twitter_validators = [
|
||||||
Validator("publisher.twitter.msg_template_path", must_exist=True, default=None),
|
Validator("publisher.twitter.msg_template_path", must_exist=True, default=None),
|
||||||
Validator("publisher.twitter.recap_template_path", must_exist=True, default=None),
|
Validator("publisher.twitter.recap_template_path", must_exist=True, default=None),
|
||||||
|
|
|
@ -32,5 +32,9 @@ class InvalidSettings(PublisherError):
|
||||||
""" Publisher settings are either missing or badly configured """
|
""" Publisher settings are either missing or badly configured """
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPResponseError(PublisherError):
|
||||||
|
""" Response received with an HTTP error status code"""
|
||||||
|
|
||||||
|
|
||||||
class ZulipError(PublisherError):
|
class ZulipError(PublisherError):
|
||||||
""" Publisher receives an error response from Zulip"""
|
""" Publisher receives an error response from Zulip"""
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
import pkg_resources
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests import Response
|
||||||
|
|
||||||
|
from mobilizon_reshare.event.event import MobilizonEvent
|
||||||
|
from mobilizon_reshare.publishers.abstract import (
|
||||||
|
AbstractPlatform,
|
||||||
|
AbstractEventFormatter,
|
||||||
|
)
|
||||||
|
from mobilizon_reshare.publishers.exceptions import (
|
||||||
|
InvalidBot,
|
||||||
|
InvalidEvent,
|
||||||
|
InvalidResponse,
|
||||||
|
PublisherError,
|
||||||
|
HTTPResponseError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MastodonFormatter(AbstractEventFormatter):
|
||||||
|
|
||||||
|
_conf = ("publisher", "mastodon")
|
||||||
|
default_template_path = pkg_resources.resource_filename(
|
||||||
|
"mobilizon_reshare.publishers.templates", "mastodon.tmpl.j2"
|
||||||
|
)
|
||||||
|
|
||||||
|
default_recap_template_path = pkg_resources.resource_filename(
|
||||||
|
"mobilizon_reshare.publishers.templates", "mastodon_recap.tmpl.j2"
|
||||||
|
)
|
||||||
|
|
||||||
|
default_recap_header_template_path = pkg_resources.resource_filename(
|
||||||
|
"mobilizon_reshare.publishers.templates", "mastodon_recap_header.tmpl.j2"
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_event(self, event: MobilizonEvent) -> None:
|
||||||
|
text = event.description
|
||||||
|
if not (text and text.strip()):
|
||||||
|
self._log_error("No text was found", raise_error=InvalidEvent)
|
||||||
|
|
||||||
|
def validate_message(self, message) -> None:
|
||||||
|
if len(message.encode("utf-8")) >= self.conf.toot_length:
|
||||||
|
raise PublisherError("Message is too long")
|
||||||
|
|
||||||
|
|
||||||
|
class MastodonPlatform(AbstractPlatform):
|
||||||
|
"""
|
||||||
|
Mastodon publisher class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_conf = ("publisher", "mastodon")
|
||||||
|
api_uri = "api/v1/"
|
||||||
|
|
||||||
|
def _send(self, message: str) -> Response:
|
||||||
|
"""
|
||||||
|
Send messages
|
||||||
|
"""
|
||||||
|
return requests.post(
|
||||||
|
url=urljoin(self.conf.instance, self.api_uri) + "statuses",
|
||||||
|
headers={"Authorization": f"Bearer {self.conf.token}"},
|
||||||
|
data={
|
||||||
|
"status": message,
|
||||||
|
"visibility": "public",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_credentials(self):
|
||||||
|
res = requests.get(
|
||||||
|
headers={"Authorization": f"Bearer {self.conf.token}"},
|
||||||
|
url=urljoin(self.conf.instance, self.api_uri) + "apps/verify_credentials",
|
||||||
|
)
|
||||||
|
data = self._validate_response(res)
|
||||||
|
|
||||||
|
if not self.conf.name == data["name"]:
|
||||||
|
self._log_error(
|
||||||
|
"Found a different bot than the expected one"
|
||||||
|
f"\n\tfound: {data['name']}"
|
||||||
|
f"\n\texpected: {self.conf.name}",
|
||||||
|
raise_error=InvalidBot,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_response(self, res: Response) -> dict:
|
||||||
|
try:
|
||||||
|
res.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
self._log_debug(str(res))
|
||||||
|
self._log_error(
|
||||||
|
str(e),
|
||||||
|
raise_error=HTTPResponseError,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = res.json()
|
||||||
|
except Exception as e:
|
||||||
|
self._log_error(
|
||||||
|
f"Server returned invalid json data: {str(e)}",
|
||||||
|
raise_error=InvalidResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class MastodonPublisher(MastodonPlatform):
|
||||||
|
|
||||||
|
_conf = ("publisher", "mastodon")
|
||||||
|
|
||||||
|
|
||||||
|
class MastodonNotifier(MastodonPlatform):
|
||||||
|
|
||||||
|
_conf = ("notifier", "mastodon")
|
|
@ -1,3 +1,8 @@
|
||||||
|
from mobilizon_reshare.publishers.platforms.mastodon import (
|
||||||
|
MastodonPublisher,
|
||||||
|
MastodonFormatter,
|
||||||
|
MastodonNotifier,
|
||||||
|
)
|
||||||
from mobilizon_reshare.publishers.platforms.telegram import (
|
from mobilizon_reshare.publishers.platforms.telegram import (
|
||||||
TelegramPublisher,
|
TelegramPublisher,
|
||||||
TelegramFormatter,
|
TelegramFormatter,
|
||||||
|
@ -15,16 +20,19 @@ from mobilizon_reshare.publishers.platforms.zulip import (
|
||||||
)
|
)
|
||||||
|
|
||||||
name_to_publisher_class = {
|
name_to_publisher_class = {
|
||||||
|
"mastodon": MastodonPublisher,
|
||||||
"telegram": TelegramPublisher,
|
"telegram": TelegramPublisher,
|
||||||
"zulip": ZulipPublisher,
|
"zulip": ZulipPublisher,
|
||||||
"twitter": TwitterPublisher,
|
"twitter": TwitterPublisher,
|
||||||
}
|
}
|
||||||
name_to_formatter_class = {
|
name_to_formatter_class = {
|
||||||
|
"mastodon": MastodonFormatter,
|
||||||
"telegram": TelegramFormatter,
|
"telegram": TelegramFormatter,
|
||||||
"zulip": ZulipFormatter,
|
"zulip": ZulipFormatter,
|
||||||
"twitter": TwitterFormatter,
|
"twitter": TwitterFormatter,
|
||||||
}
|
}
|
||||||
name_to_notifier_class = {
|
name_to_notifier_class = {
|
||||||
|
"mastodon": MastodonNotifier,
|
||||||
"telegram": TelegramNotifier,
|
"telegram": TelegramNotifier,
|
||||||
"zulip": ZulipNotifier,
|
"zulip": ZulipNotifier,
|
||||||
"twitter": TwitterNotifier,
|
"twitter": TwitterNotifier,
|
||||||
|
|
|
@ -15,6 +15,7 @@ from mobilizon_reshare.publishers.exceptions import (
|
||||||
InvalidEvent,
|
InvalidEvent,
|
||||||
InvalidResponse,
|
InvalidResponse,
|
||||||
PublisherError,
|
PublisherError,
|
||||||
|
HTTPResponseError,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,11 +109,12 @@ class TelegramPlatform(AbstractPlatform):
|
||||||
|
|
||||||
def _validate_response(self, res):
|
def _validate_response(self, res):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
except requests.exceptions.HTTPError as e:
|
except requests.exceptions.HTTPError as e:
|
||||||
|
self._log_debug(str(res))
|
||||||
self._log_error(
|
self._log_error(
|
||||||
f"Server returned invalid data: {str(e)}", raise_error=InvalidResponse,
|
str(e),
|
||||||
|
raise_error=HTTPResponseError,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{ name }}
|
||||||
|
|
||||||
|
🕒 {{ begin_datetime.format('DD MMMM, HH:mm') }} - {{ end_datetime.format('DD MMMM, HH:mm') }}
|
||||||
|
|
||||||
|
{% if location %}
|
||||||
|
📍 {{ location }}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{{mobilizon_link}}
|
|
@ -0,0 +1,5 @@
|
||||||
|
*{{ name }}*
|
||||||
|
|
||||||
|
🕒 {{ begin_datetime.format('DD MMMM, HH:mm') }} - {{ end_datetime.format('DD MMMM, HH:mm') }}
|
||||||
|
{% if location %}📍 {{ location }}{% endif %}
|
||||||
|
🔗 {{mobilizon_link}}
|
|
@ -0,0 +1 @@
|
||||||
|
📅 Upcoming events
|
|
@ -0,0 +1,67 @@
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from mobilizon_reshare.publishers.exceptions import (
|
||||||
|
InvalidEvent,
|
||||||
|
InvalidResponse,
|
||||||
|
HTTPResponseError,
|
||||||
|
)
|
||||||
|
from mobilizon_reshare.publishers.platforms.mastodon import (
|
||||||
|
MastodonFormatter,
|
||||||
|
MastodonPublisher,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_message_length_success(event):
|
||||||
|
message = "a" * 200
|
||||||
|
event.name = message
|
||||||
|
assert MastodonFormatter().is_message_valid(event)
|
||||||
|
|
||||||
|
|
||||||
|
def test_message_length_failure(event):
|
||||||
|
message = "a" * 500
|
||||||
|
event.name = message
|
||||||
|
assert not MastodonFormatter().is_message_valid(event)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_validation(event):
|
||||||
|
event.description = None
|
||||||
|
with pytest.raises(InvalidEvent):
|
||||||
|
MastodonFormatter().validate_event(event)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_response():
|
||||||
|
response = requests.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b"""{"ok":true}"""
|
||||||
|
MastodonPublisher()._validate_response(response)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_response_invalid_json():
|
||||||
|
response = requests.Response()
|
||||||
|
response.status_code = 200
|
||||||
|
response._content = b"""{"osxsa"""
|
||||||
|
with pytest.raises(InvalidResponse) as e:
|
||||||
|
MastodonPublisher()._validate_response(response)
|
||||||
|
|
||||||
|
e.match("json")
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_response_client_error():
|
||||||
|
response = requests.Response()
|
||||||
|
response.status_code = 403
|
||||||
|
response._content = b"""{"error":true}"""
|
||||||
|
with pytest.raises(HTTPResponseError) as e:
|
||||||
|
MastodonPublisher()._validate_response(response)
|
||||||
|
|
||||||
|
e.match("403 Client Error")
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_response_server_error():
|
||||||
|
response = requests.Response()
|
||||||
|
response.status_code = 500
|
||||||
|
response._content = b"""{"error":true}"""
|
||||||
|
with pytest.raises(HTTPResponseError) as e:
|
||||||
|
MastodonPublisher()._validate_response(response)
|
||||||
|
|
||||||
|
e.match("500 Server Error")
|
|
@ -1,7 +1,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from mobilizon_reshare.publishers.exceptions import InvalidEvent, InvalidResponse
|
from mobilizon_reshare.publishers.exceptions import InvalidEvent, InvalidResponse, HTTPResponseError
|
||||||
from mobilizon_reshare.publishers.platforms.telegram import (
|
from mobilizon_reshare.publishers.platforms.telegram import (
|
||||||
TelegramFormatter,
|
TelegramFormatter,
|
||||||
TelegramPublisher,
|
TelegramPublisher,
|
||||||
|
@ -68,11 +68,11 @@ def test_validate_response_invalid_request():
|
||||||
response = requests.Response()
|
response = requests.Response()
|
||||||
response.status_code = 400
|
response.status_code = 400
|
||||||
response._content = b"""{"error":true}"""
|
response._content = b"""{"error":true}"""
|
||||||
with pytest.raises(InvalidResponse) as e:
|
with pytest.raises(HTTPResponseError) as e:
|
||||||
|
|
||||||
TelegramPublisher()._validate_response(response)
|
TelegramPublisher()._validate_response(response)
|
||||||
|
|
||||||
e.match("Server returned invalid data")
|
e.match("400 Client Error")
|
||||||
|
|
||||||
|
|
||||||
def test_validate_response_invalid_response():
|
def test_validate_response_invalid_response():
|
||||||
|
|
Loading…
Reference in New Issue