Skip to main content

Declarative thread sync for Slack, Discord, and Bluesky

Project description

thrds (Python)

Declarative thread sync for Slack, Discord, and Bluesky.

Given a desired thread state (list of message contents), diffs against existing messages and applies minimal edits/posts/deletes to converge.

A TypeScript port (Slack subset) lives on the ts branch, published on npm as @rdub/thrds. Both impls share tests/fixtures/sync.json as the cross-language contract for the diff/edit/post/delete algorithm.

Install

pip install thrds            # Core only (zero deps)
pip install thrds[bsky]      # + Bluesky (atproto)

Slack and Discord clients use only stdlib (urllib) and curl subprocess respectively — no extra deps needed.

Usage

from thrds import SlackClient, Thread

slack = SlackClient(token="xoxb-...", channel="C0AQC2VKEJF")
thread = Thread(messages=["OP text", "Reply 1", "Reply 2"])

# Create new thread
result = slack.sync(thread)

# Update existing thread (edits changed messages, appends new, deletes extras)
result = slack.sync(thread, thread_ts="1775516040.743629")

Discord

from thrds import DiscordClient, Thread

discord = DiscordClient(token="your-bot-token", channel_id="1489279547689140505")
thread = Thread(messages=["OP text", "Reply 1", "Reply 2"])
result = discord.sync(thread, thread_id="1490821926288097503")

Bluesky

from thrds import BskyClient, Thread

bsky = BskyClient(handle="you.bsky.social", password="app-password")
thread = Thread(messages=["Root post", "Reply 1"])
result = bsky.sync(thread)

Bluesky doesn't support editing posts — the sync algorithm automatically falls back to delete+repost when content changes.

Linked summary threads

Post summary bullets that link to detail messages in the same thread:

from thrds import LinkedThread, Section

linked = LinkedThread(
    summary_prefix="# Daily Digest",
    sections=[
        Section(title="Topic A", summary="Brief summary", body="Full detail text..."),
        Section(title="Topic B", summary="Another summary", body="More details..."),
    ],
)

# Discord: summary bullets use [**Title**](url) markdown links
result = discord.sync_linked(linked, thread_id="...", guild_id="...")

# Slack: summary bullets use <url|*Title*> mrkdwn links
result = slack.sync_linked(linked, thread_ts="...")

Two-phase sync: posts all messages with placeholder links, then edits summaries with real links once message IDs are known.

Dry run / diff preview

result = slack.sync(thread, thread_ts="...", dry_run=True)
print(result.format_preview(color=True, prefix="thread: "))
thread: SKIP [0] (unchanged)
thread: EDIT [1]
thread:   -old message text
thread:   +new message text
thread: POST [2]
thread:   +entirely new message

Each Action carries prior_content (for EDIT/DELETE) alongside content, enabling colored unified-diff output via action.format().

Sync algorithm

Given desired messages M and existing thread messages N:

  1. Delete extras from the end (backwards — replies before OP)
  2. Edit overlapping messages where content changed (skip unchanged)
  3. Post new messages at the end

Foreign (non-editable) messages — e.g. human replies in a bot thread — are automatically skipped. The sync only operates on the bot's own messages, leaving everyone else's untouched.

Features

  • Foreign message preservation: Non-bot messages in threads are skipped during sync (no more cant_update_message errors)
  • Rate limit handling: Slack 429 retry with Retry-After, configurable pace and jitter between API calls
  • Edit rate limit fallback: Discord's 30046 error (edit limit on old messages) triggers automatic delete+repost
  • Linked summary threads: sync_linked() for summary-with-links threads on Discord and Slack
  • Diff preview: Action.format() and SyncResult.format_preview() for colored diff output
  • Orphan guard: Slack delete() checks for thread replies before deleting (raises OrphanedRepliesError)
  • Unfurl/embed suppression: Slack link previews and Discord embeds suppressed via options
  • Discord system message filtering: Thread starter messages filtered from list_messages
  • Bot token prefix: Discord Bot prefix auto-prepended
  • Metadata support: Slack message metadata passthrough

Used by

API

SyncResult

@dataclass
class SyncResult:
    thread_id: str          # thread_ts (Slack), thread channel ID (Discord), AT URI (Bluesky)
    message_ids: list[str]  # Per-message IDs
    actions: list[Action]   # What was done: Edit, Post, Delete, Skip

Action

@dataclass
class Action:
    type: ActionType        # SKIP, EDIT, POST, DELETE
    index: int
    message_id: str | None
    content: str | None         # Desired text (POST, EDIT, SKIP)
    prior_content: str | None   # Previous text (EDIT, DELETE)

SyncOptions

Option Default Description
dry_run False Print actions without executing
pace 0.0 Seconds between mutating API calls
jitter 0.0 Random additional delay (0 to jitter) added to pace
suppress_embeds False Discord: suppress link previews
suppress_unfurls True Slack: suppress link previews

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

thrds-0.4.0.tar.gz (76.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

thrds-0.4.0-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

File details

Details for the file thrds-0.4.0.tar.gz.

File metadata

  • Download URL: thrds-0.4.0.tar.gz
  • Upload date:
  • Size: 76.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for thrds-0.4.0.tar.gz
Algorithm Hash digest
SHA256 5f7558a3fd2b9381c755b7fe5394bb544381501ee1a9a74787e60a6292eea951
MD5 eb153cfb0fdefb60ca1e79be91acc2ea
BLAKE2b-256 e5cc9f2d195f4a6c6882a52b75c465c9567e3c3103c1258b7517617dc46928c5

See more details on using hashes here.

Provenance

The following attestation bundles were made for thrds-0.4.0.tar.gz:

Publisher: release.yml on runsascoded/thrds

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file thrds-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: thrds-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 16.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for thrds-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a4b77058ebce3b8b90ab77c67785c12b9f21f48b7b0737b26869f4092c409584
MD5 c38d00c0acfad66562c4ee2fb0c3fe0c
BLAKE2b-256 483f649e6d44ed0f5a6da2f6aadf843be2603d09d9df029f7baacd723079e629

See more details on using hashes here.

Provenance

The following attestation bundles were made for thrds-0.4.0-py3-none-any.whl:

Publisher: release.yml on runsascoded/thrds

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page