Skip to content

Commit

Permalink
Add mastodon job publisher (#582)
Browse files Browse the repository at this point in the history
* Draft Mastodon.

* Finished mastodon setup.
  • Loading branch information
facundobatista authored Oct 6, 2023
1 parent 958cc9b commit 618ecce
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 12 deletions.
10 changes: 10 additions & 0 deletions joboffers/management/commands/test_mastodon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from joboffers.management.commands import TestPublishCommand
from joboffers.publishers.mastodon import MastodonPublisher


class Command(TestPublishCommand):
help = 'Test sending a post to Mastodon.'

def handle(self, *args, **options):
"""Post a message to Mastodon."""
self._handle_publish(options, MastodonPublisher)
48 changes: 48 additions & 0 deletions joboffers/publishers/mastodon/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging

from django.conf import settings
from mastodon import Mastodon, errors

from joboffers.utils import hash_secret
from joboffers.publishers import Publisher


def _repr_credentials():
"""Show a string representation of mastodon credentials."""
# Need to convert to string, in case they are strings or they are not set
credentials_repr = (
f' MASTODON_AUTH_TOKEN: {hash_secret(settings.MASTODON_AUTH_TOKEN)} '
f' MASTODON_API_BASE_URL: {hash_secret(settings.MASTODON_API_BASE_URL)} '
)
return credentials_repr


ERROR_LOG_MESSAGE = (
'Falló al querer tootear con las siguientes credenciales (hasheadas): %s - Error: %s'
)


class MastodonPublisher(Publisher):
"""Mastodon Publisher."""

name = 'Mastodon'

def _push_to_api(self, message: str, title: str, link: str):
"""Publish a message to mastodon."""
mastodon = Mastodon(
access_token=settings.MASTODON_AUTH_TOKEN,
api_base_url=settings.MASTODON_API_BASE_URL,
)

try:
mastodon.status_post(message)
except errors.MastodonUnauthorizedError as err:
status = None
logging.error(ERROR_LOG_MESSAGE, _repr_credentials(), err)
except Exception as err:
status = None
logging.error("Unknown error when tooting: %s", repr(err))
else:
status = 200

return status
3 changes: 3 additions & 0 deletions joboffers/publishers/mastodon/template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Oferta:
{{ job_offer.short_description }}
{{ job_offer.get_full_url }}
20 changes: 12 additions & 8 deletions joboffers/tests/test_joboffer_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from ..publishers.facebook import FacebookPublisher
from ..publishers.telegram import TelegramPublisher
from ..publishers.twitter import TwitterPublisher
from ..publishers.mastodon import MastodonPublisher
from ..models import OfferState
from .factories import JobOfferFactory

Expand Down Expand Up @@ -78,21 +79,24 @@ def test_publisher_publish_error():
@pytest.mark.django_db
@patch('joboffers.publishers.publish_offer')
def test_publisher_to_all_social_networks_works_ok(publish_offer_function, settings):
"""
Test that publish_to_all_social_networks() uses all the condifured publishers
"""
"""Test that publish_to_all_social_networks() uses all the configured publishers."""
joboffer = JobOfferFactory.create(state=OfferState.ACTIVE)
settings.SOCIAL_NETWORKS_PUBLISHERS = [
'joboffers.publishers.discourse.DiscoursePublisher',
'joboffers.publishers.facebook.FacebookPublisher',
'joboffers.publishers.telegram.TelegramPublisher',
'joboffers.publishers.twitter.TwitterPublisher'
'joboffers.publishers.discourse.DiscoursePublisher',
'joboffers.publishers.facebook.FacebookPublisher',
'joboffers.publishers.telegram.TelegramPublisher',
'joboffers.publishers.twitter.TwitterPublisher',
'joboffers.publishers.mastodon.MastodonPublisher',
]

publish_to_all_social_networks(joboffer)

expected_publishers = [
DiscoursePublisher, FacebookPublisher, TelegramPublisher, TwitterPublisher
DiscoursePublisher,
FacebookPublisher,
TelegramPublisher,
TwitterPublisher,
MastodonPublisher,
]

assert publish_offer_function.called
Expand Down
60 changes: 60 additions & 0 deletions joboffers/tests/test_mastodon_publisher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from unittest.mock import patch

import mastodon

from ..publishers.mastodon import MastodonPublisher

DUMMY_MESSAGE = 'message'
DUMMY_TITLE = 'title'
DUMMY_LINK = 'https://example.com'


class DummyAPIBad:
def __init__(self, to_raise):
self.to_raise = to_raise

def status_post(self, *args, **kwargs):
raise self.to_raise


class DummyAPIOK:
def status_post(*args, **kwargs):
return


@patch('joboffers.publishers.mastodon.Mastodon')
def test_push_to_api_bad_credentials(mock_api, settings, caplog):
"""Test exception when the credentials are wrong."""
mock_api.return_value = DummyAPIBad(mastodon.errors.MastodonUnauthorizedError("bad auth"))
settings.MASTODON_AUTH_TOKEN = "wrong"
settings.MASTODON_API_BASE_URL = "creds"

status = MastodonPublisher()._push_to_api(DUMMY_MESSAGE, DUMMY_TITLE, DUMMY_LINK)
assert status is None

expected_error_message = "Falló al querer tootear con las siguientes credenciales (hasheadas)"
assert expected_error_message in caplog.text


@patch('joboffers.publishers.mastodon.Mastodon')
def test_push_to_api_generic_error(mock_api, settings, caplog):
"""Something went wrong."""
mock_api.return_value = DummyAPIBad(ValueError("boom"))
settings.MASTODON_AUTH_TOKEN = "good"
settings.MASTODON_API_BASE_URL = "creds"

status = MastodonPublisher()._push_to_api(DUMMY_MESSAGE, DUMMY_TITLE, DUMMY_LINK)
assert status is None

expected_error_message = "Unknown error when tooting: ValueError"
assert expected_error_message in caplog.text


@patch('joboffers.publishers.mastodon.Mastodon')
def test_push_to_api_ok(mock_api, settings):
mock_api.return_value = DummyAPIOK
settings.MASTODON_AUTH_TOKEN = "good"
settings.MASTODON_API_BASE_URL = "creds"

status = MastodonPublisher()._push_to_api(DUMMY_MESSAGE, DUMMY_TITLE, DUMMY_LINK)
assert status == 200
13 changes: 9 additions & 4 deletions pyarweb/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,17 +277,22 @@
TWITTER_CONSUMER_KEY = os.environ.get('TWITTER_CONSUMER_KEY')
TWITTER_CONSUMER_SECRET = os.environ.get('TWITTER_CONSUMER_SECRET')

# Mastodon constants
MASTODON_AUTH_TOKEN = os.environ.get('MASTODON_AUTH_TOKEN')
MASTODON_API_BASE_URL = os.environ.get('MASTODON_API_BASE_URL')

# Discourse constants
DISCOURSE_HOST = os.environ.get('DISCOURSE_HOST')
DISCOURSE_API_KEY = os.environ.get('DISCOURSE_API_KEY')
DISCOURSE_USERNAME = os.environ.get('DISCOURSE_USERNAME')
DISCOURSE_CATEGORY = os.environ.get('DISCOURSE_CATEGORY')

SOCIAL_NETWORKS_PUBLISHERS = [
'joboffers.publishers.discourse.DiscoursePublisher',
'joboffers.publishers.facebook.FacebookPublisher',
'joboffers.publishers.telegram.TelegramPublisher',
'joboffers.publishers.twitter.TwitterPublisher'
'joboffers.publishers.discourse.DiscoursePublisher',
'joboffers.publishers.facebook.FacebookPublisher',
'joboffers.publishers.telegram.TelegramPublisher',
'joboffers.publishers.twitter.TwitterPublisher',
'joboffers.publishers.mastodon.MastodonPublisher',
]

DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ django-tagging==0.5.0
django-taggit==1.5.1
django-taggit-autosuggest==0.3.8
lxml==4.9.1
Mastodon.py==1.8.1
plotly==5.7.0
psycopg2-binary==2.9.1
tweepy==4.5.0
Expand Down

0 comments on commit 618ecce

Please sign in to comment.