added facebook publisher (#99)

* added facebook publisher

* mobilizon-reshare.git: [propagated-inputs]: Add python-sdk-facebook.

This package definition has been generated with `guix import pypi -r
facebook-sdk`.

* mobilizon-reshare.git: [propagated-inputs]: Use python-facebook-sdk.git.

Co-authored-by: Giacomo Leidi <goodoldpaul@autistici.org>
This commit is contained in:
Simone Robutti 2021-11-20 15:40:10 +01:00 committed by GitHub
parent a91e72c3ef
commit 2476686c33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 217 additions and 20 deletions

View File

@ -335,6 +335,49 @@ development, testing, production]};
(description "Twitter library for Python")
(license license:expat)))
(define-public python-facebook-sdk
(package
(name "python-facebook-sdk")
(version "3.1.0")
(source
(origin
(method url-fetch)
(uri (pypi-uri "facebook-sdk" version))
(sha256
(base32 "138grz0n6plzdqgi4h6hhszf58bsvx9v76cwj51g1nd3kvkd5g6a"))))
(build-system python-build-system)
(propagated-inputs `(("python-requests" ,python-requests)))
(home-page "https://facebook-sdk.readthedocs.io")
(synopsis
"Facebook Graph API client in Python")
(description
"This client library is designed to support the Facebook Graph API and
the official Facebook JavaScript SDK, which is the canonical way to implement
Facebook authentication.")
(license license:asl2.0)))
(define-public python-facebook-sdk.git
(let ((version (package-version python-facebook-sdk))
(revision "0")
(commit "3fa89fec6a20dd070ccf57968c6f89256f237f54"))
(package (inherit python-facebook-sdk)
(name "python-facebook-sdk.git")
(version (git-version version revision commit))
(source
(origin
(method git-fetch)
(uri
(git-reference
(url "https://github.com/mobolic/facebook-sdk")
(commit commit)))
(file-name (git-file-name name version))
(sha256
(base32
"0vayxkg6p8wdj63qvzr24dj3q7rkyhr925b31z2qv2mnbas01dmg"))))
(arguments
;; Tests depend on network access.
`(#:tests? #false)))))
(define-public mobilizon-reshare.git
(let ((source-version (with-input-from-file
(string-append %source-dir
@ -389,6 +432,7 @@ development, testing, production]};
("python-beautifulsoup4" ,python-beautifulsoup4)
("python-click" ,python-click)
("python-dynaconf" ,python-dynaconf)
("python-facebook-sdk" ,python-facebook-sdk.git)
("python-jinja2" ,python-jinja2)
("python-markdownify" ,python-markdownify)
("python-requests" ,python-requests)

View File

@ -3,8 +3,6 @@ active=true
chat_id="xxx"
token="xxx"
username="xxx"
[default.publisher.facebook]
active=false
[default.publisher.zulip]
active=true
instance="xxx"
@ -25,6 +23,11 @@ token="xxx"
name="xxx"
toot_length=500
[default.publisher.facebook]
active=true
page_access_token="xxx"
[default.notifier.telegram]
active=true
chat_id="xxx"
@ -45,3 +48,7 @@ access_token="xxx"
access_secret="xxx"
[default.notifier.mastodon]
active=false
[default.notifier.facebook]
active=false
page_access_token="xxx"

View File

@ -26,7 +26,11 @@ twitter_validators = [
Validator("publisher.twitter.access_secret", must_exist=True),
]
facebook_validators = [
Validator("publisher.facebook.page_access_token", must_exist=True),
]
notifier_name_to_validators = {
"facebook": facebook_validators,
"telegram": telegram_validators,
"twitter": twitter_validators,
"mastodon": mastodon_validators,

View File

@ -45,7 +45,14 @@ twitter_validators = [
Validator("publisher.twitter.access_token", must_exist=True),
Validator("publisher.twitter.access_secret", must_exist=True),
]
facebook_validators = []
facebook_validators = [
Validator("publisher.facebook.msg_template_path", must_exist=True, default=None),
Validator("publisher.facebook.recap_template_path", must_exist=True, default=None),
Validator(
"publisher.facebook.recap_header_template_path", must_exist=True, default=None
),
Validator("publisher.facebook.page_access_token", must_exist=True),
]
publisher_name_to_validators = {
"telegram": telegram_validators,

View File

@ -10,9 +10,7 @@ from jinja2 import Environment, FileSystemLoader, Template
from mobilizon_reshare.config.config import get_settings
from mobilizon_reshare.event.event import MobilizonEvent
from mobilizon_reshare.models.publication import (
Publication as PublicationModel,
)
from mobilizon_reshare.models.publication import Publication as PublicationModel
from .exceptions import PublisherError, InvalidAttribute
JINJA_ENV = Environment(loader=FileSystemLoader("/"))
@ -86,15 +84,15 @@ class AbstractPlatform(ABC, LoggerMixin, ConfLoaderMixin):
pass
@abstractmethod
def _send(self, message: str):
def _send(self, message: str, event: Optional[MobilizonEvent] = None):
raise NotImplementedError # pragma: no cover
def send(self, message: str):
def send(self, message: str, event: Optional[MobilizonEvent] = None):
"""
Sends a message to the target channel
"""
message = self._preprocess_message(message)
response = self._send(message)
response = self._send(message, event)
self._validate_response(response)
def _preprocess_message(self, message: str):

View File

@ -85,7 +85,7 @@ class PublisherCoordinator:
message = publication.formatter.get_message_from_event(
publication.event
)
publication.publisher.send(message)
publication.publisher.send(message, publication.event)
reports.append(
EventPublicationReport(
status=PublicationStatus.COMPLETED,

View File

@ -0,0 +1,82 @@
from typing import Optional
import facebook
import pkg_resources
from mobilizon_reshare.event.event import MobilizonEvent
from mobilizon_reshare.publishers.abstract import (
AbstractPlatform,
AbstractEventFormatter,
)
from mobilizon_reshare.publishers.exceptions import (
InvalidCredentials,
InvalidEvent,
)
class FacebookFormatter(AbstractEventFormatter):
_conf = ("publisher", "facebook")
default_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "facebook.tmpl.j2"
)
default_recap_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "facebook_recap.tmpl.j2"
)
default_recap_header_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "facebook_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:
pass
class FacebookPlatform(AbstractPlatform):
"""
Facebook publisher class.
"""
name = "facebook"
def _get_api(self):
return facebook.GraphAPI(
access_token=self.conf["page_access_token"], version="8.0"
)
def _send(self, message: str, event: Optional[MobilizonEvent] = None):
self._get_api().put_object(
parent_object="me",
connection_name="feed",
message=message,
link=event.mobilizon_link if event else None,
)
def validate_credentials(self):
try:
self._get_api().get_object(id="me", field="name")
except Exception:
self._log_error(
"Invalid Facebook credentials. Authentication Failed",
raise_error=InvalidCredentials,
)
def _validate_response(self, response):
pass
class FacebookPublisher(FacebookPlatform):
_conf = ("publisher", "facebook")
class FacebookNotifier(FacebookPlatform):
_conf = ("notifier", "facebook")

View File

@ -1,3 +1,4 @@
from typing import Optional
from urllib.parse import urljoin
import pkg_resources
@ -52,7 +53,7 @@ class MastodonPlatform(AbstractPlatform):
api_uri = "api/v1/"
name = "mastodon"
def _send(self, message: str) -> Response:
def _send(self, message: str, event: Optional[MobilizonEvent] = None) -> Response:
"""
Send messages
"""

View File

@ -1,3 +1,8 @@
from mobilizon_reshare.publishers.platforms.facebook import (
FacebookPublisher,
FacebookFormatter,
FacebookNotifier,
)
from mobilizon_reshare.publishers.platforms.mastodon import (
MastodonPublisher,
MastodonFormatter,
@ -29,18 +34,21 @@ name_to_publisher_class = {
"telegram": TelegramPublisher,
"zulip": ZulipPublisher,
"twitter": TwitterPublisher,
"facebook": FacebookPublisher,
}
name_to_formatter_class = {
"mastodon": MastodonFormatter,
"telegram": TelegramFormatter,
"zulip": ZulipFormatter,
"twitter": TwitterFormatter,
"facebook": FacebookFormatter,
}
name_to_notifier_class = {
"mastodon": MastodonNotifier,
"telegram": TelegramNotifier,
"zulip": ZulipNotifier,
"twitter": TwitterNotifier,
"facebook": FacebookNotifier,
}

View File

@ -1,4 +1,5 @@
import re
from typing import Optional
import pkg_resources
import requests
@ -98,7 +99,7 @@ class TelegramPlatform(AbstractPlatform):
"Found a different bot than the expected one", raise_error=InvalidBot,
)
def _send(self, message) -> Response:
def _send(self, message: str, event: Optional[MobilizonEvent] = None) -> Response:
return requests.post(
url=f"https://api.telegram.org/bot{self.conf.token}/sendMessage",
json={

View File

@ -1,3 +1,5 @@
from typing import Optional
import pkg_resources
from tweepy import OAuthHandler, API, TweepyException
from tweepy.models import Status
@ -59,7 +61,7 @@ class TwitterPlatform(AbstractPlatform):
auth.set_access_token(access_token, access_secret)
return API(auth)
def _send(self, message: str) -> Status:
def _send(self, message: str, event: Optional[MobilizonEvent] = None) -> Status:
try:
return self._get_api().update_status(message)
except TweepyException as e:

View File

@ -1,3 +1,4 @@
from typing import Optional
from urllib.parse import urljoin
import pkg_resources
@ -59,7 +60,9 @@ class ZulipPlatform(AbstractPlatform):
api_uri = "api/v1/"
name = "zulip"
def _send_private(self, message: str) -> Response:
def _send_private(
self, message: str, event: Optional[MobilizonEvent] = None
) -> Response:
"""
Send private messages
"""
@ -69,7 +72,7 @@ class ZulipPlatform(AbstractPlatform):
data={"type": "private", "to": f"[{self.user_id}]", "content": message},
)
def _send(self, message: str) -> Response:
def _send(self, message: str, event: Optional[MobilizonEvent] = None) -> Response:
"""
Send stream messages
"""

View File

@ -0,0 +1,9 @@
# {{ name }}
🕒 {{ begin_datetime.format('DD MMMM, HH:mm') }} - {{ end_datetime.format('DD MMMM, HH:mm') }}
{% if location %}
📍 {{ location }}
{% endif %}
{{ description }}

View File

@ -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}}

View File

@ -0,0 +1 @@
Upcoming events

21
poetry.lock generated
View File

@ -128,6 +128,24 @@ toml = ["toml"]
vault = ["hvac"]
yaml = ["ruamel.yaml"]
[[package]]
name = "facebook-sdk"
version = "4.0.0rc0"
description = "This client library is designed to support the Facebook Graph API and the official Facebook JavaScript SDK, which is the canonical way to implement Facebook authentication."
category = "main"
optional = false
python-versions = "*"
develop = false
[package.dependencies]
requests = "*"
[package.source]
type = "git"
url = "https://github.com/mobolic/facebook-sdk.git"
reference = "master"
resolved_reference = "3fa89fec6a20dd070ccf57968c6f89256f237f54"
[[package]]
name = "idna"
version = "3.2"
@ -446,7 +464,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "763106b0d68a1b95c690e2ad828a4e847ad2532a3b13354c227f35b70f1c8ad7"
content-hash = "eb95c081f2b389a4702ee0a144a29d3bb0a9d7e26174c2c650acaa14b0f980a7"
[metadata.files]
aiosqlite = [
@ -498,6 +516,7 @@ dynaconf = [
{file = "dynaconf-3.1.5-py2.py3-none-any.whl", hash = "sha256:98f0e5d861e945c1b06ed33844b9341e6412bc121c1a9ea88668df7891d8f308"},
{file = "dynaconf-3.1.5.tar.gz", hash = "sha256:40979cc454d8533fada490dc0945a1dbf5ea96f95dc9113f8a3b3053670fd569"},
]
facebook-sdk = []
idna = [
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},

View File

@ -21,6 +21,7 @@ beautifulsoup4 = "^4.9"
markdownify = "^0.9"
appdirs = "^1.4"
tweepy = "^4.1.0"
facebook-sdk = {git = "https://github.com/mobolic/facebook-sdk.git"}
[tool.poetry.dev-dependencies]
responses = "^0.13"

View File

@ -206,7 +206,7 @@ def mock_publisher_class(message_collector):
class MockPublisher(AbstractPlatform):
name = "mock"
def _send(self, message):
def _send(self, message, event):
message_collector.append(message)
def _validate_response(self, response):
@ -281,7 +281,7 @@ def mock_publisher_invalid_class(message_collector):
name = "mock"
def _send(self, message):
def _send(self, message, event):
message_collector.append(message)
def _validate_response(self, response):

View File

@ -1,4 +1,5 @@
from datetime import timedelta
from typing import Optional
from uuid import UUID
import arrow
@ -48,7 +49,7 @@ def mock_publisher_invalid(message_collector):
name = "mock"
def _send(self, message):
def _send(self, message: str, event: Optional[MobilizonEvent] = None):
message_collector.append(message)
def _validate_response(self, response):
@ -66,7 +67,7 @@ def mock_publisher_invalid_response(message_collector):
name = "mock"
def _send(self, message):
def _send(self, message, event):
message_collector.append(message)
def _validate_response(self, response):