Post the same message to Twitter ๐ฆ, Hachyderm ๐, and Bluesky๐ค๏ธ.
Compatibility:
- MacOS: โ
- *nix: ๐คท๐ผโโ๏ธ (probably works)
- Windows: โ
- Clone this repo over HTTPS or SSH.
Clone over HTTPS:
git clone https://github.com/goingforbrooke/yappily.gitClone with SSH:
git clone git@github.com:goingforbrooke/yappily.git- Get credentials.
X posting uses Buffer; Hachyderm and Bluesky use their own APIs. Create credential directories as needed. Keep secrets out of Git and chat.
- Twitter/X via Buffer
- Connect your X profile to Buffer.
- Create a personal key in Buffer Settings โ API.
- Use
account:readfor channel discovery andposts:writefor publishing.posts:readcan additionally be enabled for inspecting posts; account-write, ideas, insights, and engagement permissions are not needed. - Run
yappily --setup-buffer(oruv run main.py --setup-buffer). Paste the key at the hidden terminal prompt and select your X channel. - Setup saves
buffer_creds/api_key.txtandbuffer_creds/channel_id.txt, with directory mode0700and file mode0600. Both are Git-ignored. - Alternatively, set
BUFFER_API_KEYandBUFFER_X_CHANNEL_ID; environment settings override credential files. - Verify without publishing:
yappily --check-buffer. - No direct X API keys or X API credit balance are used. Existing
twitter_creds/files are left untouched but are no longer read. - Renew the key in Buffer before its selected expiration and rerun setup.
- Hachyderm
- hachyderm.io/home
- Development Tab
- "New Application"
- permissions
- โ๏ธ
write:statuses: publish posts
- โ๏ธ
- required keys
- client ID
- also known as "Client Key" on the "Development โก๏ธ Application" page
- create
yappily/hachyderm_creds/client_id.txt
- client secret
- create
yappily/hachyderm_creds/client_secret.txt
- create
- access token
- also known as "Your access token" on the "Development โก๏ธ Application" page
- create
yappily/hachyderm_creds/access_token.txt
- client ID
- Bluesky
- "Settings"
- "Privacy and Security"
- "App passwords"
- "Add App Password"
- don't check "Allow access to your direct messages"
- necessary keys
- username
- should be in the format
your.username.bsky.social - create
yappily/bluesky_creds/bluesky_username.txt
- should be in the format
- password
- create
yappily/bluesky_creds/bluesky_password.txt
- create
- username
Note
X posts are submitted through Buffer using shareNow, not added to your regular
queue. Buffer acceptance is not proof of publication: Yappily prints the returned
post ID and status and explicitly notes when publication is unconfirmed.
See Buffer's API guide.
Historical limit note (original label 25-1-26, apparently January 26, 2025):
this README recorded a direct-X free-tier allowance of 100 posts/month. That is
not a current limit and is unrelated to the Buffer integration.
- Install Dependencies
Tip
Avoid manually installing dependencies and skip to uv run.
Tip
Zsh users can run Yappily from anywhere by adding this file as yapply to ~/bin:
#!/bin/zsh
uv run ~/path/to/where/you/clone/repos/yappily/main.py "$@"Install with uv
This isn't necessary for uv run, but is included here for those who would like to set up a virtual environment.
Install with pip
pip install -r requirements.txtYou can run Yappily with any requirements.txt-friendly project manager, but we recommenduv
yappily --only x "Post only to X via Buffer"
yappily --only hachyderm,bluesky "Post to the other two"
yappily --check-bufferBy default all three platforms are attempted independently. A failure returns exit
status 1 after the remaining platforms are attempted; the summary lists accepted
and failed/unconfirmed destinations. There are no automatic retries. If a response
is lost, check the affected service before retrying to avoid duplicates. Use
--only so a retry does not repeat posts on successful platforms.
Use -- before literal post text starting with a dash, e.g.
yappily -- "--not-an-option".
Logging is automatic for posting, Buffer setup, and connection checks:
yappily --logs # latest five runs, including successes
yappily --failures # latest five failed/incomplete runsBoth commands are read-only, offline, and do not create new log entries. On macOS,
logs live in ~/Library/Logs/Yappily/; elsewhere they use
${XDG_STATE_HOME:-~/.local/state}/yappily/logs/. YAPPILY_LOG_DIR overrides the
location (use a dedicated directory, outside the repository).
Each invocation writes a private JSONL file named with its UTC start timestamp and unique run ID. The newest 50 files are retained, capped at 256 KiB each (about 12.5 MiB total). Directory/file permissions are 0700/0600. Events are flushed as they happen; overlapping invocations use separate files.
Diagnostics include:
- Python/SDK versions, Git revision and dirty-worktree flag;
- selected platforms, post length (not content), timings, exit status;
- per-platform progress and returned post IDs/statuses;
- Buffer HTTP status, request ID/retry-after when provided, GraphQL error codes and known categories (billing, authentication, rate limits, etc.);
- exception types, numeric HTTP status/OS errno when available, and stack filename/function/line locations, without frame locals or source lines.
API keys, credentials, request/response bodies, headers other than the two selected Buffer diagnostic headers, raw exception messages, and full post text are not recorded. Logs still contain account-linked post IDs, timestamps, and local code details: treat them as private. Nothing is uploaded automatically.
A logging-storage error warns on stderr but does not block posting. An interrupted
run may have no run_finished record; check the platform before retrying. These
are client-side observations, not background delivery monitoring: Buffer
acceptance without sent remains unconfirmed. CLI parsing errors, dependency
installation/import failures before logging starts, and OS-level process kills
cannot produce a complete run log. Earlier posts are not backfilled.
For future diagnosis, inspect yappily --failures, then the matching JSONL file
for the complete run and returned post ID. Tests: uv run python -m unittest -q.
Use uv run:
uv run main.py "<some_awesome_text>"`Surrounding the post text is optional, but recommended. It prevents your shell from interpreting special characters. For example, the ' in uv run main.py Yappily's awesome will cause issues in zsh. Using uv run main.py "Yappily's awesome" instead.
Example:
uv run main.py "using QR codes to sign into Slack workspaces on mobile brings me such unbridled joy"๐
Yapping "using QR codes to sign into Slack workspaces on mobile brings me such unbridled joy"
๐ฆ Submitted to Buffer for immediate X posting (post <id>, status: scheduled).
Publication is not confirmed yet; check Buffer before retrying.
๐ Posted to Hachyderm: using QR codes to sign into Slack workspaces on mobile brings me such unbridled joy
๐ค๏ธ Posted to Bluesky: using QR codes to sign into Slack workspaces on mobile brings me such unbridled joy
โ
Accepted by: x, hachyderm, blueskyThis works the same as the uv run, but replace uv run with python.
Important
Install dependencies and/or activate a virtual environment first.
python main.pypython main.py "using QR codes to sign into Slack workspaces on mobile brings me such unbridled joy"๐
Yapping "using QR codes to sign into Slack workspaces on mobile brings me such unbridled joy"
๐ฆ Submitted to Buffer for immediate X posting (post <id>, status: scheduled).
Publication is not confirmed yet; check Buffer before retrying.
๐ Posted to Hachyderm: using QR codes to sign into Slack workspaces on mobile brings me such unbridled joy
๐ค๏ธ Posted to Bluesky: using QR codes to sign into Slack workspaces on mobile brings me such unbridled joy
โ
Accepted by: x, hachyderm, bluesky- check for Bluesky's 300 grapheme limit
- post to Insta Threads ๐งต
- RIIW (Rewrite in Rust) ๐ฆ
- make mobile app ๐คณ๐ป
- use oAuth for credentials? ๐
- add image uploads ๐ธ
- aspect ratio cropping would be nice
- parallel posting ๐๏ธ
- post to YouTube communities ๐ฝ๏ธ
- allow threads? ๐งต
- add logging ๐ชต
- slick packaging ๐ฆ
- collect stats ๐
- 30 day averages in terminal
- matplotlib graphs in webpage
Dependencies are tracked in three locations:
uv'spyproject.toml
- source of truth
- created by
uv init
- inline script metadata dependencies (at the top of
main.py)
- fuels our preferred way to execute Yappily (with
uv runmain.py`)
requirements.txt
- for compatibility
Important
Update requirements.txt with the latest from uv:
uv export --format requirements-txt > requirements.txt