FediStatusPoster/fedistatusposter.py

202 lines
8.1 KiB
Python
Raw Permalink Normal View History

2024-03-09 17:42:07 -08:00
#! /usr/bin/env python3
# FediStatusPoster - Simple CLI tools and scripts to post to the fediverse
# Copyright (C) 2024 <@Anon@yandere.cc>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
import sys
import argparse
from mastodon import Mastodon, MastodonIllegalArgumentError, MastodonVersionError, MastodonInternalServerError, MastodonAPIError
class FailedLogin(Exception):
pass
class FediStatusPoster:
def __init__(self, client_id, client_secret, access_token, api_base_url, feature_set="pleroma", debug_mode=False):
self.mastodon_api = self.login(client_id, client_secret, access_token, api_base_url, feature_set)
# Callback functions
self.media_post = self._media_post_debug if debug_mode else self.mastodon_api.media_post
self.status_post = self._status_post_debug if debug_mode else self.mastodon_api.status_post
2024-03-09 17:42:07 -08:00
def login(self, client_id, client_secret, access_token, api_base_url, feature_set):
try:
mastodon_api = Mastodon(
client_id=client_id,
client_secret=client_secret,
access_token=access_token,
api_base_url=api_base_url,
feature_set=feature_set
)
return mastodon_api
except (MastodonIllegalArgumentError, MastodonVersionError) as e:
raise FailedLogin("Failed to login: {}".format(str(e)))
def validate_media_list(self, path_list):
for path in path_list:
if path is None or not os.path.isfile(path):
raise FileNotFoundError("Not a File: {}".format(path))
def _media_post_debug(self, path, description):
2024-03-09 17:42:07 -08:00
return {"url": path, "description": description}
def upload_media_list(self, path_list):
self.validate_media_list(path_list)
media_list = []
for path in path_list:
description = os.path.basename(path)
media = self.media_post(path, description=description)
2024-03-09 17:42:07 -08:00
media_dict = {
"path": path,
"media": media
}
media_list.append(media_dict)
return media_list
def get_status_with_media_links(self, status, media_list):
r = status
doonce = True
empty_status = not bool(status)
for media_dict in media_list:
path = media_dict["path"]
media = media_dict["media"]
if path is None or media is None:
continue
image_link = "[url={}]{}[/url]".format(media["url"], os.path.basename(path))
r += image_link if (doonce and empty_status) else "\n" + image_link
doonce = False
return r
def _status_post_debug(self, status, media_ids, sensitive, visibility, content_type):
print("[DEBUG_BEGIN]")
print("STATUS:")
print(status)
print("MEDIA:")
print(os.linesep.join((str(i) for i in media_ids)))
print("SENSITIVE:", sensitive)
print("VISIBILITY:", visibility)
print("CONTENT_TYPE:", content_type)
print("[DEBUG_END]")
# If the user wants to generate html links to the image file,
# switch the content type to markdown. I think this is sane
# default behavior, since the alternative would be generating
# full url links in text (which would be ugly).
def post_with_image_links(self, message, images, sensitive=False, visibility="public", content_type="text/plain"):
if not images:
self.post(message, images, sensitive, visibility, content_type)
media_list = self.upload_media_list(images)
media_ids = [i["media"] for i in media_list]
self.status_post(
status=self.get_status_with_media_links(message, media_list),
media_ids=media_ids,
sensitive=sensitive,
visibility=visibility,
content_type="text/bbcode"
)
def post(self, message, images, sensitive=False, visibility="public", content_type="text/plain"):
media_list = self.upload_media_list(images)
media_ids = [i["media"] for i in media_list]
self.status_post(
status=message,
media_ids=media_ids,
sensitive=sensitive,
visibility=visibility,
content_type=content_type
)
def main():
# Default Arguments
DEFAULT_FEATURE_SET = "pleroma"
DEFAULT_VISIBILITY = "public"
DEFAULT_CONTENT_TYPE = "text/plain"
# Parser
parser = argparse.ArgumentParser(
description="A simple cli tool for posting to the fediverse",
epilog="You must provide api_base_url, client_id, client_secret, and access_token via command line or as uppercase environment variables. This software is licensed under GPLv3.",
add_help=True)
# Required Arguments
parser.add_argument("message", help="The text message of the post. The '-' symbol represents stdin", type=str)
parser.add_argument("paths", help="The paths of the images to be uploaded in a single post. The '-' symbol represents stdin", nargs='*', type=str)
# Optional Switches
parser.add_argument("--dry-run", help="Will not post to Pleroma, but will do everything else.", action="store_true")
parser.add_argument("--debug", help="Same as --dry-run", action="store_true")
parser.add_argument("--sensative", help="Mark post as sensative. This switch will take priority over --safe", action="store_true")
parser.add_argument("--nsfw", help="Same as --sensative", action="store_true")
parser.add_argument("--safe", help="Mark the post as safe (not sensative). This is the default behavior", action="store_true")
parser.add_argument("--post-image-links", help="When uploading images, post direct image links at the end of the message. This might be the default behavior on some instances. If it is, this switch should not be used.", action="store_true")
parser.add_argument("--visibility", help="Set visibility [public, unlisted, private, direct] (Default: {})".format(DEFAULT_VISIBILITY), type=str, default=DEFAULT_VISIBILITY)
parser.add_argument("--content-type", help="Set the content type of the post [text/plain, text/markdown, text/html, text/bbcode] (Default: {})".format(DEFAULT_CONTENT_TYPE), type=str, default=DEFAULT_CONTENT_TYPE)
parser.add_argument("--feature-set", help="The feature to post as (Default: {})".format(DEFAULT_FEATURE_SET), type=str, nargs=1, default=DEFAULT_FEATURE_SET)
# Session tokens via command line
parser.add_argument("--api-base-url", help="The url of the instance", type=str)
parser.add_argument("--client-id", help="Client ID", type=str)
parser.add_argument("--client-secret", help="Client Secret", type=str)
parser.add_argument("--access-token", help="Access Token", type=str)
arguments = parser.parse_args()
# Session Token Variables
2024-07-16 23:35:10 -07:00
api_base_url = arguments.api_base_url or os.environ.get("API_BASE_URL")
client_id = arguments.client_id or os.environ.get("CLIENT_ID")
client_secret = arguments.client_secret or os.environ.get("CLIENT_SECRET")
access_token = arguments.access_token or os.environ.get("ACCESS_TOKEN")
2024-03-09 17:42:07 -08:00
# Flag if the bot is running in debug mode
debug_mode = arguments.dry_run or arguments.debug
nsfw = arguments.sensative or arguments.nsfw
# Positional arguments
message = arguments.message if arguments.message != "-" else sys.stdin.read()
images = []
for path in arguments.paths:
if path == "-":
for line in sys.stdin:
images.append(line.rstrip())
else:
images.append(path)
fedistatusposter = FediStatusPoster(client_id, client_secret, access_token, api_base_url, arguments.feature_set, debug_mode)
if not arguments.post_image_links:
fedistatusposter.post(message, images, nsfw, arguments.visibility, arguments.content_type)
else:
fedistatusposter.post_with_image_links(message, images, nsfw, arguments.visibility)
return 0
if __name__ == "__main__":
try:
sys.exit(main())
# MastodonIllegalArgumentError - Usually incorrect content-type
# MastodonInternalServerError - Something fucked up on the server side usually
# MastodonAPIError - The credentials are usually not valid
except (FailedLogin, FileNotFoundError, MastodonIllegalArgumentError, MastodonInternalServerError, MastodonAPIError) as e:
print(e)
sys.exit(1)