Skip to content
goingforbrookePublic

About

Post the same message to Twitter ๐Ÿฆœ, Hachyderm ๐Ÿ˜, and Bluesky๐ŸŒค๏ธ.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

๐Ÿ‘… Yappily ๐Ÿ˜

Post the same message to Twitter ๐Ÿฆœ, Hachyderm ๐Ÿ˜, and Bluesky๐ŸŒค๏ธ.

Installation

Compatibility:

  • MacOS: โœ…
  • *nix: ๐Ÿคท๐Ÿผโ€โ™€๏ธ (probably works)
  • Windows: โŒ
  1. Clone this repo over HTTPS or SSH.

Clone over HTTPS:

git clone https://github.com/goingforbrooke/yappily.git

Clone with SSH:

git clone git@github.com:goingforbrooke/yappily.git
  1. 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:read for channel discovery and posts:write for publishing. posts:read can additionally be enabled for inspecting posts; account-write, ideas, insights, and engagement permissions are not needed.
    • Run yappily --setup-buffer (or uv run main.py --setup-buffer). Paste the key at the hidden terminal prompt and select your X channel.
    • Setup saves buffer_creds/api_key.txt and buffer_creds/channel_id.txt, with directory mode 0700 and file mode 0600. Both are Git-ignored.
    • Alternatively, set BUFFER_API_KEY and BUFFER_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
      • access token
        • also known as "Your access token" on the "Development โžก๏ธ Application" page
        • create yappily/hachyderm_creds/access_token.txt
  • 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
      • password
        • create yappily/bluesky_creds/bluesky_password.txt

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.

  1. Install Dependencies

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.txt

Usage

You can run Yappily with any requirements.txt-friendly project manager, but we recommenduv

Select platforms and retry safely

yappily --only x "Post only to X via Buffer"
yappily --only hachyderm,bluesky "Post to the other two"
yappily --check-buffer

By 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".

Local diagnostics

Logging is automatic for posting, Buffer setup, and connection checks:

yappily --logs      # latest five runs, including successes
yappily --failures  # latest five failed/incomplete runs

Both 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.

Run as uv Script

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, bluesky

Run with python

This 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.py
python 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

Future

  • 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

Contributing

Updating Dependencies

Dependencies are tracked in three locations:

  1. uv'spyproject.toml
  • source of truth
  • created by uv init
  1. inline script metadata dependencies (at the top of main.py)
  • fuels our preferred way to execute Yappily (with uv run main.py`)
  1. requirements.txt
  • for compatibility

Important

Update requirements.txt with the latest from uv:

uv export --format requirements-txt > requirements.txt

License

MIT

About

Post the same message to Twitter ๐Ÿฆœ, Hachyderm ๐Ÿ˜, and Bluesky๐ŸŒค๏ธ.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages