social media crossposting tool. 3rd time's the charm
mastodon misskey crossposting bluesky

i miss mojang's codecs...

zenfyr.dev 23a3b296 f74811d4

verified
+17
bluesky.py
···
LOGGER.info("Listening to %s...", streaming + '/com.atproto.sync.subscribeRepos')
await client.start(on_message)
+
ALLOWED_GATES = ['mentioned', 'following', 'followers', 'everybody']
+
+
class BlueskyOutputOptions:
+
def __init__(self, o: dict) -> None:
+
self.quote_gate = False
+
self.thread_gate = ['everybody']
+
+
quote_gate = o.get('quote_gate')
+
if quote_gate is not None:
+
self.quote_gate = bool(quote_gate)
+
+
thread_gate = o.get('thread_gate')
+
if thread_gate is not None:
+
if any([v not in ALLOWED_GATES for v in ALLOWED_GATES]):
+
raise ValueError(f"'thread_gate' only accepts {', '.join(ALLOWED_GATES)} or [], got: {thread_gate}")
+
self.thread_gate = thread_gate
+
class BlueskyOutput(cross.Output):
def __init__(self, input: cross.Input, settings: dict, db: DataBaseWorker) -> None:
super().__init__(input, settings, db)
+1 -1
cross.py
···
from typing import Callable, Any
from database import DataBaseWorker
from datetime import datetime, timezone
-
from media_util import MediaInfo, get_media_meta
+
from media_util import MediaInfo
import util
import re
+4 -13
main.py
···
import database
import mastodon, misskey, bluesky, cross
import asyncio, threading, queue, traceback
+
import util
DEFAULT_SETTINGS: dict = {
'input': {
'type': 'mastodon-wss',
'instance': 'env:MASTODON_INSTANCE',
'token': 'env:MASTODON_TOKEN',
-
"options": {
-
"allowed_visibility": [
-
"public",
-
"unlisted"
-
]
-
}
+
"options": mastodon.MastodonInputOptions({})
},
'outputs': [
{
'type': 'bluesky',
'handle': 'env:BLUESKY_HANDLE',
'app-password': 'env:BLUESKY_APP_PASSWORD',
-
'options': {
-
'quote_gate': False,
-
'thread_gate': [
-
'everybody'
-
]
-
}
+
'options': bluesky.BlueskyOutputOptions({})
}
]
}
···
LOGGER.info("First launch detected! Creating %s and exiting!", settings_path)
with open(settings_path, 'w') as f:
-
json.dump(DEFAULT_SETTINGS, f, indent=2)
+
f.write(util.as_json(DEFAULT_SETTINGS, indent=2))
return 0
LOGGER.info('Loading settings...')
+26 -9
mastodon.py
···
from bs4.element import NavigableString
from html_to_markdown import markdownify as md
-
FORMATS = {
-
'video': 'video',
-
'image': 'image',
-
'gifv': 'gif',
-
'audio': 'audio',
-
'unknown': 'other'
-
}
POSSIBLE_MIMES = [
'audio/ogg',
'audio/mp3',
···
def get_attachments(self) -> list[media_util.MediaInfo]:
return self.media_attachments
+
ALLOWED_VISIBILITY = ['public', 'unlisted']
+
+
class MastodonInputOptions():
+
def __init__(self, o: dict) -> None:
+
self.allowed_visibility = ALLOWED_VISIBILITY
+
+
allowed_visibility = o.get('allowed_visibility')
+
if allowed_visibility is not None:
+
if any([v not in ALLOWED_VISIBILITY for v in allowed_visibility]):
+
raise ValueError(f"'allowed_visibility' only accepts {', '.join(ALLOWED_VISIBILITY)}, got: {allowed_visibility}")
+
self.allowed_visibility = allowed_visibility
+
class MastodonInput(cross.Input):
def __init__(self, settings: dict, db: DataBaseWorker) -> None:
-
self.options = settings.get('options', {})
+
self.options = MastodonInputOptions(settings.get('options', {}))
self.token = util.as_envvar(settings.get('token')) or (_ for _ in ()).throw(ValueError("'token' is required"))
instance: str = util.as_envvar(settings.get('instance')) or (_ for _ in ()).throw(ValueError("'instance' is required"))
···
LOGGER.info("Skipping '%s'! Reply to other user..", status['id'])
return
-
if status.get('visibility') not in self.options.get('allowed_visibility', []):
+
if status.get('visibility') not in self.options.allowed_visibility:
# Skip f/o and direct posts
LOGGER.info("Skipping '%s'! '%s' visibility..", status['id'], status.get('visibility'))
return
···
LOGGER.error(e, stack_info=True, exc_info=True)
LOGGER.info("Reconnecting to %s...", self.streaming)
continue
+
+
ALLOWED_POSTING_VISIBILITY = ['public', 'unlisted', 'private']
+
+
class MastodonOutputOptions():
+
def __init__(self, o: dict) -> None:
+
self.visibility = 'public'
+
+
visibility = o.get('visibility')
+
if visibility is not None:
+
if visibility not in ALLOWED_POSTING_VISIBILITY:
+
raise ValueError(f"'visibility' only accepts {', '.join(ALLOWED_POSTING_VISIBILITY)}, got: {visibility}")
+
self.visibility = visibility
class MastodonOutput(cross.Output):
def __init__(self, input: cross.Input, settings: dict, db: DataBaseWorker) -> None:
+14 -14
misskey.py
···
MD_AUTOLINK = re.compile(r"<((?:https?://[^\s>]+|mailto:[^\s>]+))>")
HASHTAG = re.compile(r'(?<!\w)\#([\w]+)')
FEDIVERSE_HANDLE = re.compile(r'(?<![\w@])@([\w-]+)(?:@([\w\.-]+\.[\w\.-]+))?')
-
-
def get_image_common(mime: str):
-
if mime.startswith('image/'):
-
if mime == 'image/gif':
-
return 'gif'
-
return 'image'
-
elif mime.startswith('video/'):
-
return 'video'
-
elif mime.startswith('audio/'):
-
return 'audio'
-
else:
-
return 'other'
def tokenize_note(note: dict) -> list[cross.Token]:
text: str = note.get('text', '')
···
def is_sensitive(self) -> bool:
return self.sensitive
+
+
ALLOWED_VISIBILITY = ['public', 'home']
+
+
class MisskeyInputOptions():
+
def __init__(self, o: dict) -> None:
+
self.allowed_visibility = ALLOWED_VISIBILITY
+
+
allowed_visibility = o.get('allowed_visibility')
+
if allowed_visibility is not None:
+
if any([v not in ALLOWED_VISIBILITY for v in allowed_visibility]):
+
raise ValueError(f"'allowed_visibility' only accepts {', '.join(ALLOWED_VISIBILITY)}, got: {allowed_visibility}")
+
self.allowed_visibility = allowed_visibility
class MisskeyInput(cross.Input):
def __init__(self, settings: dict, db: cross.DataBaseWorker) -> None:
-
self.options = settings.get('options', {})
+
self.options = MisskeyInputOptions(settings.get('options', {}))
self.token = util.as_envvar(settings.get('token')) or (_ for _ in ()).throw(ValueError("'token' is required"))
instance: str = util.as_envvar(settings.get('instance')) or (_ for _ in ()).throw(ValueError("'instance' is required"))
···
LOGGER.info("Skipping '%s'! Reply to other user..", note['id'])
return
-
if note.get('visibility') not in self.options.get('allowed_visibility', []):
+
if note.get('visibility') not in self.options.allowed_visibility:
LOGGER.info("Skipping '%s'! '%s' visibility..", note['id'], note.get('visibility'))
return
+10
util.py
···
import logging, sys, os
+
import json
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
LOGGER = logging.getLogger("XPost")
+
+
import json
+
+
def as_json(obj, indent=None,sort_keys=False) -> str:
+
return json.dumps(
+
obj.__dict__ if not isinstance(obj, dict) else obj,
+
default=lambda o: o.__json__() if hasattr(o, '__json__') else o.__dict__,
+
indent=indent,
+
sort_keys=sort_keys)
def canonical_label(label: str | None, href: str):
if not label or label == href: