Compare commits

...

2 commits

Author SHA1 Message Date
D. Scott Boggs d41aad6f53 Add CLI 2025-05-26 06:40:35 -04:00
D. Scott Boggs e754f6fe59 Add ability to uppload photos 1 at a time 2025-05-26 06:40:29 -04:00
5 changed files with 121 additions and 9 deletions

View file

@ -1,7 +1,3 @@
from blastodon.client import Client from blastodon.interface import interface
client = Client.init_cli() interface()
post_text = input(' enter a status to post: ')
client.send_text_post(post_text)

View file

@ -59,7 +59,7 @@ def auth_bsky() -> BskyClient:
def _mastodon_find_single_user_login() -> Path | None: def _mastodon_find_single_user_login() -> Path | None:
found_file = None found_file = None
for file in Path.cwd().iterdir(): for file in Path.cwd().iterdir():
if file.name.startswith('login.') and file.name.endswith('.secret'): if file.name.startswith('mastodon.') and file.name.endswith('.secret'):
print('found secret file', file.name) print('found secret file', file.name)
if found_file is None: if found_file is None:
found_file = file found_file = file
@ -72,7 +72,7 @@ def _mastodon_find_single_user_login() -> Path | None:
def _bsky_find_single_user_login() -> Path | None: def _bsky_find_single_user_login() -> Path | None:
found_file = None found_file = None
for file in Path.cwd().iterdir(): for file in Path.cwd().iterdir():
if file.name.startswith('bsky-') and file.name.endswith('-jwt.secret'): if file.name.startswith('bsky_') and file.name.endswith('_session.secret'):
if found_file is None: if found_file is None:
found_file = file found_file = file
else: else:

View file

@ -1,7 +1,9 @@
from typing import Self from typing import Self
from pathlib import Path
from atproto import Client as BskyClient from atproto import Client as BskyClient
from mastodon import Mastodon, MastodonError from mastodon import Mastodon, MastodonError
import magic
from blastodon import auth from blastodon import auth
from blastodon.client import RetreivedPost, CreatedPost from blastodon.client import RetreivedPost, CreatedPost
@ -36,4 +38,18 @@ class Client:
return CreatedPost(bsky=bsky_post, mastodon=mastodon_post) return CreatedPost(bsky=bsky_post, mastodon=mastodon_post)
def send_image_post(self, post_text: str, image_path: Path | str, alt_text: str):
with open(image_path, mode='rb') as file:
image = file.read()
bsky_post = self.bsky.send_image(text=post_text, image=image, image_alt=alt_text)
try:
mime = magic.from_buffer(image, mime=True)
media_upload = self.mastodon.media_post(media_file=image, mime_type=mime, description=alt_text)
mastodon_post = self.mastodon.status_post(status=post_text, media_ids=[media_upload.id])
except MastodonError as err:
print('error posting to mastodon after posting to bsky. Deleting bsky post.')
self.bsky.delete_post(bsky_post.uri)
raise err
mastodon_post.url
return CreatedPost(bsky=bsky_post, mastodon=mastodon_post)

98
blastodon/interface.py Normal file
View file

@ -0,0 +1,98 @@
from functools import cached_property
from os import getenv, SEEK_SET
from pathlib import Path
from shutil import which
from subprocess import run
from tempfile import NamedTemporaryFile
from traceback import print_stack
from click import command, option, group, argument, pass_context, confirm
from blastodon.client import Client
class Context:
"""A context object which may be used accross commands"""
@cached_property
def client(self) -> Client:
return Client.init_cli()
@group
def interface():
...
@interface.group
def post():
...
@post.command('status')
@argument('content', required=False)
@option('--content-file', '-f', help="post the contents of a file")
@pass_context
def text_status(ctx, content: str | None = None, content_file: str | None = None):
ctx.ensure_object(Context)
if content_file and content:
raise SyntaxError('cannot specify content and content source file')
if content_file:
with open(content_file) as file:
content = file.read()
print('status:')
print(content)
if confirm('\n\n Post this status?', show_default=True, default=True):
result = ctx.obj.client.send_text_post(content)
print('status posted ok')
print('mastodon:', result.mastodon.url)
print('bsky: ', result.bsky.uri)
@post.command(help='open the default editor to compose a status')
@pass_context
def compose(ctx):
ctx.ensure_object(Context)
status_text = _compose_message()
if not status_text:
print('not posting empty status')
return
print('status:')
print(status_text)
if confirm('\n\tPost this status?', show_default=True, default=True):
result = ctx.obj.client.send_text_post(status_text)
print('status posted ok')
print('mastodon:', result.mastodon.url)
print('bsky: ', result.bsky.uri)
else:
print('Ok, not posting.')
def _compose_message() -> str:
editor = getenv('VISUAL') or getenv('EDITOR') or which('micro') or which('nano') or 'vi'
with NamedTemporaryFile(prefix='blastodon_', suffix='.post') as tempfile:
tempfile.write(b'# compose a status, then save and close the editor to post it.\n')
tempfile.write(b'# empty lines and lines starting with "#" will be ignored\n')
tempfile.flush()
result = run(executable=editor, args=[editor, tempfile.name])
result.check_returncode()
tempfile.seek(0, SEEK_SET)
return '\n'.join(
line
for l
in (bs.decode() for bs in tempfile.readlines())
if (line := l.strip()) and not line.startswith('#')
)
@post.command('image')
@option('--compose-message', help='Open an editor to compose a text status which this image will be attached to', is_flag=True, default=False)
@option('--message', help='A text status this image will be attached to')
@option('--alt-text', help='Describe the image', prompt=True)
@option('--mime-type', help='The mime type of the attached file. If not specified, determined from the file contents')
@argument('filepath', callback=lambda _ctx, _param, fp: Path(fp))
@pass_context
def post_image(ctx, compose_message: bool, message: str, alt_text: str, mime_type: str, filepath: Path):
ctx.ensure_object(Context)
if compose_message and message:
raise SyntaxError("Can't specify both --message and --compose-message")
if compose_message:
message = _compose_message()
result = ctx.obj.client.send_image_post(post_text=message, image_path=filepath, alt_text=alt_text)
print('status posted ok')
print('mastodon:', result.mastodon.url)
print('bsky: ', result.bsky.uri)

View file

@ -14,6 +14,8 @@ license = "AGPL-3.0-only"
dependencies = [ dependencies = [
"atproto", "atproto",
"mastodon.py", "mastodon.py",
"python-magic",
"click",
] ]
dynamic = ["version"] dynamic = ["version"]