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()
|
interface()
|
||||||
|
|
||||||
post_text = input(' enter a status to post: ')
|
|
||||||
|
|
||||||
client.send_text_post(post_text)
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
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 = [
|
dependencies = [
|
||||||
"atproto",
|
"atproto",
|
||||||
"mastodon.py",
|
"mastodon.py",
|
||||||
|
"python-magic",
|
||||||
|
"click",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue