Declarative thread sync for Slack, Discord, and Bluesky
Project description
thrds
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.
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.
Dry run
result = slack.sync(thread, thread_ts="...", dry_run=True)
for action in result.actions:
print(action.type, action.index, action.content)
Sync algorithm
Given desired messages M and existing thread messages N:
- Delete extras from the end (backwards — replies before OP)
- Edit overlapping messages where content changed (skip unchanged)
- Post new messages at the end
This ensures minimal API calls, preserved ordering, and no orphaned thread parents.
Features
- Rate limit handling: Slack 429 retry with
Retry-After, configurable pacing between API calls - Edit rate limit fallback: Discord's 30046 error (edit limit on old messages) triggers automatic delete+repost
- Unfurl suppression: Slack link previews suppressed by default
- Discord system message filtering: Thread starter messages filtered from
list_messages - Bot token prefix: Discord
Botprefix auto-prepended - Metadata support: Slack message metadata passthrough
Used by
- hudcostreets/nj-crashes — Slack crash-notification threads (
SlackClient.sync()) - Open-Athena/marin-discord — Discord summary threads (
DiscordClient.sync())
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
SyncOptions
| Option | Default | Description |
|---|---|---|
dry_run |
False |
Print actions without executing |
pace |
0.0 |
Seconds between mutating API calls |
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file thrds-0.1.2.tar.gz.
File metadata
- Download URL: thrds-0.1.2.tar.gz
- Upload date:
- Size: 19.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c9d8611e776541dece5f3f80ed84317e3a76883ae1eae43a4f9dfbcf51c06f3d
|
|
| MD5 |
2cdec68f4420cc369bd6b22f58f42a63
|
|
| BLAKE2b-256 |
81e685194028e80775ec9ad961fddee413c2c146ec9cbee3223945ea6ba335af
|
Provenance
The following attestation bundles were made for thrds-0.1.2.tar.gz:
Publisher:
release.yml on runsascoded/thrds
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
thrds-0.1.2.tar.gz -
Subject digest:
c9d8611e776541dece5f3f80ed84317e3a76883ae1eae43a4f9dfbcf51c06f3d - Sigstore transparency entry: 1247698027
- Sigstore integration time:
-
Permalink:
runsascoded/thrds@f60837ebb60c16a1e9e5f9e750df7630b6af769f -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/runsascoded
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f60837ebb60c16a1e9e5f9e750df7630b6af769f -
Trigger Event:
push
-
Statement type:
File details
Details for the file thrds-0.1.2-py3-none-any.whl.
File metadata
- Download URL: thrds-0.1.2-py3-none-any.whl
- Upload date:
- Size: 10.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ad5bea669465535f987733e893a08a1e2198c085652ddbd023d57ea6a086e15
|
|
| MD5 |
6b2a287373e568d6b6054df8cfc74e47
|
|
| BLAKE2b-256 |
39f7be1cdd790c7e531b01e48edb42ba3b387b7a87b369d848819679abdb2ff8
|
Provenance
The following attestation bundles were made for thrds-0.1.2-py3-none-any.whl:
Publisher:
release.yml on runsascoded/thrds
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
thrds-0.1.2-py3-none-any.whl -
Subject digest:
1ad5bea669465535f987733e893a08a1e2198c085652ddbd023d57ea6a086e15 - Sigstore transparency entry: 1247698036
- Sigstore integration time:
-
Permalink:
runsascoded/thrds@f60837ebb60c16a1e9e5f9e750df7630b6af769f -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/runsascoded
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@f60837ebb60c16a1e9e5f9e750df7630b6af769f -
Trigger Event:
push
-
Statement type: