* added basic recap feature (no error handling)

* introduced abstractpublication

* extracted base reports

* added error report to recap

* added test

* added docs

* implemented publisher and formatter

* fixed API for recap

* removed redundant config validation

* added config sample
This commit is contained in:
Simone Robutti 2021-10-17 14:05:16 +02:00 committed by GitHub
parent 5e171216d2
commit c14cdfb67f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 187 additions and 41 deletions

View File

@ -83,6 +83,7 @@ Currently the following publishers are supported:
* Telegram
* Zulip
* Twitter
### Notifier

View File

@ -12,7 +12,11 @@ subject="xxx"
bot_token="xxx"
bot_email="xxx"
[default.publisher.twitter]
active=false
active=true
api_key="xxx"
api_key_secret="xxx"
access_token="xxx"
access_secret="xxx"
[default.publisher.mastodon]
active=false
@ -28,6 +32,10 @@ subject="xxx"
bot_token="xxx"
bot_email="xxx"
[default.notifier.twitter]
active=false
active=true
api_key="xxx"
api_key_secret="xxx"
access_token="xxx"
access_secret="xxx"
[default.notifier.mastodon]
active=false

View File

@ -14,7 +14,12 @@ zulip_validators = [
Validator("publisher.zulip.bot_email", must_exist=True),
]
mastodon_validators = []
twitter_validators = []
twitter_validators = [
Validator("publisher.twitter.api_key", must_exist=True),
Validator("publisher.twitter.api_key_secret", must_exist=True),
Validator("publisher.twitter.access_token", must_exist=True),
Validator("publisher.twitter.access_secret", must_exist=True),
]
notifier_name_to_validators = {
"telegram": telegram_validators,

View File

@ -17,7 +17,14 @@ zulip_validators = [
Validator("publisher.zulip.bot_email", must_exist=True),
]
mastodon_validators = []
twitter_validators = []
twitter_validators = [
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.api_key", must_exist=True),
Validator("publisher.twitter.api_key_secret", must_exist=True),
Validator("publisher.twitter.access_token", must_exist=True),
Validator("publisher.twitter.access_secret", must_exist=True),
]
facebook_validators = []
publisher_name_to_validators = {

View File

@ -3,6 +3,11 @@ from mobilizon_reshare.publishers.platforms.telegram import (
TelegramFormatter,
TelegramNotifier,
)
from mobilizon_reshare.publishers.platforms.twitter import (
TwitterPublisher,
TwitterFormatter,
TwitterNotifier,
)
from mobilizon_reshare.publishers.platforms.zulip import (
ZulipPublisher,
ZulipFormatter,
@ -12,14 +17,17 @@ from mobilizon_reshare.publishers.platforms.zulip import (
name_to_publisher_class = {
"telegram": TelegramPublisher,
"zulip": ZulipPublisher,
"twitter": TwitterPublisher,
}
name_to_formatter_class = {
"telegram": TelegramFormatter,
"zulip": ZulipFormatter,
"twitter": TwitterFormatter,
}
name_to_notifier_class = {
"telegram": TelegramNotifier,
"zulip": ZulipNotifier,
"twitter": TwitterNotifier,
}

View File

@ -10,7 +10,6 @@ from mobilizon_reshare.publishers.abstract import (
)
from mobilizon_reshare.publishers.exceptions import (
InvalidBot,
InvalidCredentials,
InvalidEvent,
InvalidResponse,
PublisherError,
@ -63,26 +62,10 @@ class TelegramPlatform(AbstractPlatform):
return TelegramFormatter.escape_message(message)
def validate_credentials(self):
conf = self.conf
chat_id = conf.chat_id
token = conf.token
username = conf.username
err = []
if not chat_id:
err.append("chat ID")
if not token:
err.append("token")
if not username:
err.append("username")
if err:
self._log_error(
", ".join(err) + " is/are missing", raise_error=InvalidCredentials,
)
res = requests.get(f"https://api.telegram.org/bot{token}/getMe")
res = requests.get(f"https://api.telegram.org/bot{self.conf.token}/getMe")
data = self._validate_response(res)
if not username == data.get("result", {}).get("username"):
if not self.conf.username == data.get("result", {}).get("username"):
self._log_error(
"Found a different bot than the expected one", raise_error=InvalidBot,
)

View File

@ -0,0 +1,76 @@
import pkg_resources
from tweepy import OAuthHandler, API
from tweepy.models import Status
from mobilizon_reshare.event.event import MobilizonEvent
from mobilizon_reshare.publishers.abstract import (
AbstractPlatform,
AbstractEventFormatter,
)
from mobilizon_reshare.publishers.exceptions import (
InvalidCredentials,
InvalidEvent,
PublisherError,
)
class TwitterFormatter(AbstractEventFormatter):
_conf = ("publisher", "twitter")
default_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "twitter.tmpl.j2"
)
default_recap_template_path = pkg_resources.resource_filename(
"mobilizon_reshare.publishers.templates", "twitter_recap.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")) > 140:
raise PublisherError("Message is too long")
class TwitterPlatform(AbstractPlatform):
"""
Twitter publisher class.
"""
_conf = ("publisher", "twitter")
def _get_api(self):
api_key = self.conf.api_key
api_key_secret = self.conf.api_key_secret
access_token = self.conf.access_token
access_secret = self.conf.access_secret
auth = OAuthHandler(api_key, api_key_secret)
auth.set_access_token(access_token, access_secret)
return API(auth)
def _send(self, message: str) -> Status:
return self._get_api().update_status(message)
def validate_credentials(self):
if not self._get_api().verify_credentials():
self._log_error(
"Invalid Twitter credentials. Authentication Failed",
raise_error=InvalidCredentials,
)
def _validate_response(self, res: Status) -> dict:
pass
class TwitterPublisher(TwitterPlatform):
_conf = ("publisher", "twitter")
class TwitterNotifier(TwitterPlatform):
_conf = ("notifier", "twitter")

View File

@ -11,7 +11,6 @@ from mobilizon_reshare.publishers.abstract import (
)
from mobilizon_reshare.publishers.exceptions import (
InvalidBot,
InvalidCredentials,
InvalidEvent,
InvalidResponse,
ZulipError,
@ -81,20 +80,6 @@ class ZulipPlatform(AbstractPlatform):
def validate_credentials(self):
conf = self.conf
chat_id = conf.chat_id
bot_token = conf.bot_token
bot_email = conf.bot_email
err = []
if not chat_id:
err.append("chat ID")
if not bot_token:
err.append("bot token")
if not bot_email:
err.append("bot email")
if err:
self._log_error(
", ".join(err) + " is/are missing", raise_error=InvalidCredentials,
)
res = requests.get(
auth=HTTPBasicAuth(self.conf.bot_email, self.conf.bot_token),
@ -107,11 +92,11 @@ class ZulipPlatform(AbstractPlatform):
"These user is not a bot", raise_error=InvalidBot,
)
if not bot_email == data["email"]:
if not conf.bot_email == data["email"]:
self._log_error(
"Found a different bot than the expected one"
f"\n\tfound: {data['email']}"
f"\n\texpected: {bot_email}",
f"\n\texpected: {conf.bot_email}",
raise_error=InvalidBot,
)

View File

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

View File

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

61
poetry.lock generated
View File

@ -186,6 +186,19 @@ category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "oauthlib"
version = "3.1.1"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
rsa = ["cryptography (>=3.0.0,<4)"]
signals = ["blinker (>=1.4.0)"]
signedtoken = ["cryptography (>=3.0.0,<4)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "packaging"
version = "21.0"
@ -305,6 +318,21 @@ urllib3 = ">=1.21.1,<1.27"
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "requests-oauthlib"
version = "1.3.0"
description = "OAuthlib authentication support for Requests."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
oauthlib = ">=3.0.0"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "responses"
version = "0.13.4"
@ -365,6 +393,24 @@ asyncmy = ["asyncmy"]
asyncpg = ["asyncpg"]
accel = ["ciso8601 (>=2.1.2,<3.0.0)", "python-rapidjson", "uvloop (>=0.14.0,<0.15.0)"]
[[package]]
name = "tweepy"
version = "4.1.0"
description = "Twitter library for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
requests = ">=2.11.1,<3"
requests-oauthlib = ">=1.0.0,<2"
[package.extras]
async = ["aiohttp (>=3.7.3,<4)", "oauthlib (>=3.1.0,<4)"]
dev = ["coveralls (>=2.1.0)", "tox (>=3.14.0)"]
socks = ["requests[socks] (>=2.11.1,<3)"]
test = ["vcrpy (>=1.10.3)"]
[[package]]
name = "typing-extensions"
version = "3.10.0.2"
@ -389,7 +435,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "1a9b33d624371bf57e4988339fe52e8e89269d6c4e699aec1f16504ce4520d16"
content-hash = "4fe276575784de9ed9d3ee66eef2c75355dd66e7717bcc277c38755dd489b4ac"
[metadata.files]
aiosqlite = [
@ -497,6 +543,10 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
oauthlib = [
{file = "oauthlib-3.1.1-py2.py3-none-any.whl", hash = "sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc"},
{file = "oauthlib-3.1.1.tar.gz", hash = "sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"},
]
packaging = [
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
@ -537,6 +587,11 @@ requests = [
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
]
requests-oauthlib = [
{file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
{file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
{file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
]
responses = [
{file = "responses-0.13.4-py2.py3-none-any.whl", hash = "sha256:d8d0f655710c46fd3513b9202a7f0dcedd02ca0f8cf4976f27fa8ab5b81e656d"},
{file = "responses-0.13.4.tar.gz", hash = "sha256:9476775d856d3c24ae660bbebe29fb6d789d4ad16acd723efbfb6ee20990b899"},
@ -557,6 +612,10 @@ tortoise-orm = [
{file = "tortoise-orm-0.17.7.tar.gz", hash = "sha256:74d5c6341fc0e2d4ef7a321f460107715da2679a2b3e2a48e32e276ed7bd3fc0"},
{file = "tortoise_orm-0.17.7-py3-none-any.whl", hash = "sha256:36bb0d1f9bd800d3b91d5490cfc13a598c0e0f570a638b185bc4976abfabd135"},
]
tweepy = [
{file = "tweepy-4.1.0-py2.py3-none-any.whl", hash = "sha256:42c63f5ee2210a8afc7178c74a6d800ef5911b007ad19e774d75dec4b777993e"},
{file = "tweepy-4.1.0.tar.gz", hash = "sha256:88e2938de5ac7043c9ba8b8358996fbc5806059d63c96269d22527a40ca7d511"},
]
typing-extensions = [
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
{file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"},

View File

@ -20,6 +20,7 @@ click = "^8.0"
beautifulsoup4 = "^4.9"
markdownify = "^0.9"
appdirs = "^1.4"
tweepy = "^4.1.0"
[tool.poetry.dev-dependencies]
responses = "^0.13"