diff --git a/README.md b/README.md index 29249fb..1d16ce4 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Currently the following publishers are supported: * Telegram * Zulip +* Twitter ### Notifier diff --git a/mobilizon_reshare/.secrets.toml b/mobilizon_reshare/.secrets.toml index d8581ca..2f69fdf 100644 --- a/mobilizon_reshare/.secrets.toml +++ b/mobilizon_reshare/.secrets.toml @@ -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 diff --git a/mobilizon_reshare/config/notifiers.py b/mobilizon_reshare/config/notifiers.py index db905cb..ea1032d 100644 --- a/mobilizon_reshare/config/notifiers.py +++ b/mobilizon_reshare/config/notifiers.py @@ -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, diff --git a/mobilizon_reshare/config/publishers.py b/mobilizon_reshare/config/publishers.py index 212179a..01e539c 100644 --- a/mobilizon_reshare/config/publishers.py +++ b/mobilizon_reshare/config/publishers.py @@ -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 = { diff --git a/mobilizon_reshare/publishers/platforms/platform_mapping.py b/mobilizon_reshare/publishers/platforms/platform_mapping.py index a90af9f..1e4a193 100644 --- a/mobilizon_reshare/publishers/platforms/platform_mapping.py +++ b/mobilizon_reshare/publishers/platforms/platform_mapping.py @@ -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, } diff --git a/mobilizon_reshare/publishers/platforms/telegram.py b/mobilizon_reshare/publishers/platforms/telegram.py index a5a6905..20e76ca 100644 --- a/mobilizon_reshare/publishers/platforms/telegram.py +++ b/mobilizon_reshare/publishers/platforms/telegram.py @@ -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, ) diff --git a/mobilizon_reshare/publishers/platforms/twitter.py b/mobilizon_reshare/publishers/platforms/twitter.py new file mode 100644 index 0000000..cba72ae --- /dev/null +++ b/mobilizon_reshare/publishers/platforms/twitter.py @@ -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") diff --git a/mobilizon_reshare/publishers/platforms/zulip.py b/mobilizon_reshare/publishers/platforms/zulip.py index 94e4b55..6ebf1a7 100644 --- a/mobilizon_reshare/publishers/platforms/zulip.py +++ b/mobilizon_reshare/publishers/platforms/zulip.py @@ -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, ) diff --git a/mobilizon_reshare/publishers/templates/twitter.tmpl.j2 b/mobilizon_reshare/publishers/templates/twitter.tmpl.j2 new file mode 100644 index 0000000..a0ccc26 --- /dev/null +++ b/mobilizon_reshare/publishers/templates/twitter.tmpl.j2 @@ -0,0 +1,8 @@ +# {{ name }} + +🕒 {{ begin_datetime.format('DD MMMM, HH:mm') }} - {{ end_datetime.format('DD MMMM, HH:mm') }} + +{% if location %} +📍 {{ location }} + +{% endif %} \ No newline at end of file diff --git a/mobilizon_reshare/publishers/templates/twitter_recap.tmpl.j2 b/mobilizon_reshare/publishers/templates/twitter_recap.tmpl.j2 new file mode 100644 index 0000000..6f40338 --- /dev/null +++ b/mobilizon_reshare/publishers/templates/twitter_recap.tmpl.j2 @@ -0,0 +1,5 @@ +# {{ name }} +🕒 {{ begin_datetime.format('DD MMMM, HH:mm') }} - {{ end_datetime.format('DD MMMM, HH:mm') }} +{% if location %} +📍 {{ location }} +{% endif %} \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 41601f2..31f607b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index 1a2f27a..41fa0f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"