Compare commits
2 commits
ff83ad6647
...
d41aad6f53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d41aad6f53 | ||
|
|
e754f6fe59 |
|
|
@ -1,7 +1,3 @@
|
|||
from blastodon.client import Client
|
||||
from blastodon.interface import interface
|
||||
|
||||
client = Client.init_cli()
|
||||
|
||||
post_text = input(' enter a status to post: ')
|
||||
|
||||
client.send_text_post(post_text)
|
||||
interface()
|
||||
|
|
@ -59,7 +59,7 @@ def auth_bsky() -> BskyClient:
|
|||
def _mastodon_find_single_user_login() -> Path | None:
|
||||
found_file = None
|
||||
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)
|
||||
if found_file is None:
|
||||
found_file = file
|
||||
|
|
@ -72,7 +72,7 @@ def _mastodon_find_single_user_login() -> Path | None:
|
|||
def _bsky_find_single_user_login() -> Path | None:
|
||||
found_file = None
|
||||
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:
|
||||
found_file = file
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
from typing import Self
|
||||
from pathlib import Path
|
||||
|
||||
from atproto import Client as BskyClient
|
||||
from mastodon import Mastodon, MastodonError
|
||||
import magic
|
||||
|
||||
from blastodon import auth
|
||||
from blastodon.client import RetreivedPost, CreatedPost
|
||||
|
|
@ -36,4 +38,18 @@ class Client:
|
|||
|
||||
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
98
blastodon/interface.py
Normal 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)
|
||||
|
|
@ -14,6 +14,8 @@ license = "AGPL-3.0-only"
|
|||
dependencies = [
|
||||
"atproto",
|
||||
"mastodon.py",
|
||||
"python-magic",
|
||||
"click",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue