Skip to content

feat(sharing): post a teaser for each new article to Substack and X - #9

Merged
tee8z merged 1 commit into
masterfrom
feat/substack-teaser
Oct 3, 2026
Merged

tee8z merged 1 commit into
masterfrom
feat/substack-teaser

Conversation

@tee8z

@tee8z tee8z commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Publishing an article now also announces it on the Owner's other channels. First publication records one short teaser (title, description, link) and posts it to Substack and X when those channels are enabled. The teaser always fits a single 280-character X post, so the same text can be pasted anywhere else from Admin → Sharing.

  • Admin-only setup. Channels, their on/off mode, and their credentials are saved by an Owner with a fresh session and stored in the application database. The worker reads the current settings on every pass, so a change needs no host configuration and no restart. Saved credentials are never rendered; a blank field keeps the saved value; each save is audited without the values.
  • Substack. Substack has no publishing API. The channel uses the private interface of Substack's editor with the Owner's substack.sid session cookie: it creates a draft, then publishes it with send: false, so the post appears on the web and Substack emails nobody. This interface is unofficial and can change without notice; unexpected answers fail closed.
  • X. One post through POST /2/tweets, signed with OAuth 1.0a from the app's API key and secret and the account's access token and secret. X bills each post to the app's credit.
  • Delivery. A teaser is recorded in the publication transaction, next to the newsletter's first-publication marker. Edits, republishing, restarts, and upgrades never share an article again, and articles published before this migration are not shared. A delivery is claimed before any provider request and leaves that state only with a recorded outcome: transient failures retry with doubling waits for about an hour, refused credentials hold the queue until new ones are saved, and an uncertain outcome fails instead of posting twice. An Owner can retry a failed delivery or send an existing teaser to a channel that was set up later.
  • Restore. Offline restore pauses both channels and fails unfinished deliveries, because a provider may have accepted them after the backup was taken.

Validation: cargo clippy -p maincopy-server --all-targets --all-features -- --deny warnings and the full server suite (767 tests) pass. The 28 new tests cover teaser sizing (wide characters, bare domains, clipping), credential parsing, both clients against a local HTTP peer, the OAuth signature against X's published example, the admin page, first-publication queueing, the delivery ledger, and restore. Neither channel has been exercised against a live account yet; the Substack request shapes follow community observations of its web interface and need one real acceptance run before release.

Upgrade: migration 0014 adds five sharing_* tables. Take a verified pre-upgrade backup; an older binary cannot open the newer migration ledger. Sharing credentials are stored in the database and therefore enter its backups. hmac and sha1 become direct dependencies at versions already in the lockfile.

Not included: a link from the article page to its Substack post. Pages are content-addressed snapshots, so adding one after delivery needs a separate design.

First publication records one short teaser (title, description, link)
that always fits a single X post, and queues it for every channel
enabled at that moment. A supervised worker posts it; edits,
republishing, restarts, and upgrades never share an article again.

Channels and their credentials are managed by an Owner in the admin
portal and stored in the application database, so changes need no host
configuration and no restart. Saved credentials are never rendered.

Substack has no publishing API: the channel uses its editor's private
interface with the Owner's session cookie and publishes to the web
without email. X posts are signed with OAuth 1.0a.

A delivery is claimed before any provider request and leaves that state
only with a recorded outcome. Uncertain outcomes fail closed instead of
posting twice, refused credentials hold the queue until new ones are
saved, and an offline restore pauses both channels.
@tee8z
tee8z marked this pull request as ready for review October 3, 2026 16:51
@tee8z
tee8z merged commit 85b4f54 into master Oct 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant