feat: DM per-message ops + attachments + group avatar (3/3)#26
Merged
Conversation
Wraps the group-DM surface at /api/v1/messages/groups/* that the
Python SDK shipped in colony-sdk v1.13.0 (#56 over there). First of
three PRs that complete group-DM coverage in the JS SDK; per-message
ops + attachments will follow.
Lifecycle (6 methods):
- createGroupConversation(title, members, options?)
- listGroupTemplates(options?)
- createGroupFromTemplate(template, members, { titleOverride? })
- getGroupConversation(convId, { limit?, offset? })
- updateGroupConversation(convId, { title?, description? })
- sendGroupMessage(convId, body, { replyToMessageId?, idempotencyKey? })
Member management (7 methods):
- listGroupMembers, addGroupMember, removeGroupMember, setGroupAdmin,
transferGroupCreator, respondToGroupInvite, markGroupAllRead
New exported types: GroupConversation, GroupConversationDetail,
GroupMember, GroupMembersResponse, GroupTemplate,
GroupTemplatesResponse, GroupInviteResponse, MarkGroupReadResponse.
Internal: RequestOptions gains an extraHeaders field so write methods
can set per-request headers like Idempotency-Key cleanly. Booleans on
query-string endpoints use the lowercase "true"/"false" FastAPI
expects, not JavaScript's default capitalised String(true).
19 new unit tests cover request shape, header threading, default-vs-
omitted parameters, the FastAPI lowercase-bool quirk, query-string
escaping (`R&D Lab` → `title=R%26D+Lab`), and the three-state
description contract on update (empty clears, undefined omits).
No version bump per the multi-PR release plan; CHANGELOG entry lands
in Unreleased.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Two branches in the new methods weren't exercised by the original test set: - updateGroupConversation with only `title` (not just description) — the `if (options.title !== undefined)` true-branch was never hit - createGroupFromTemplate without `titleOverride` — the optional- param branch was only exercised in the override-set case Adds two focused tests; client.ts branch coverage moves from 97.41 to 98.27. All remaining uncovered branches are pre-existing in register() and extractItems(), not introduced by this PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors colony-sdk Python PR #57 / v1.13.0 — 8 new methods layered
over the lifecycle methods from the prior PR.
State (per-participant — affects only the caller's notifications):
- muteGroupConversation(convId, { until? })
- unmuteGroupConversation(convId)
- snoozeGroupConversation(convId, duration)
- unsnoozeGroupConversation(convId)
- setGroupReadReceipts(convId, { show? }) — three-state override:
true/false/undefined (undefined clears, falling back to user pref)
Pins (group-wide, admin-only):
- pinGroupMessage(convId, msgId)
- unpinGroupMessage(convId, msgId) — idempotent
Search:
- searchGroupMessages(convId, q, { limit?, offset? }) — PostgreSQL
FTS within one group, with <mark>...</mark> highlights pre-rendered
New exported types: GroupMuteResponse, GroupSnoozeResponse,
GroupReadReceiptsResponse, GroupPinResponse, GroupSearchHit,
GroupSearchResponse.
13 new unit tests cover the three-state set-receipts surface
(true/false/undefined), the FastAPI lowercase-bool quirk
(`show=false` not `show=False`), query-string escaping
(`R&D` → `q=R%26D`), default-vs-custom pagination, and the bare-POST
shape for muteGroupConversation when `until` is omitted.
No version bump per the multi-PR release plan; CHANGELOG entry lands
in Unreleased above the lifecycle entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors colony-sdk Python PR #58 / v1.13.0. Third and final PR of
the group-DM coverage series — with this in, the JS SDK now wraps
the full /api/v1/messages/* surface.
15 new methods + brand-new multipart-upload + binary-download
transport helpers.
Per-message operations (1:1 + group):
- markMessageRead, listMessageReads
- addMessageReaction, removeMessageReaction (emoji is percent-
encoded in the DELETE path)
- editMessage, listMessageEdits
- deleteMessage (sender-only soft delete)
- toggleStarMessage
- listSavedMessages({ limit?, offset? })
- forwardMessage(messageId, recipientUsername, { comment? })
Attachments (multipart):
- uploadMessageAttachment(filename, fileBytes, contentType)
accepts Uint8Array or ArrayBuffer
- deleteMessageAttachment(attachmentId)
- getMessageAttachment(attachmentId, { variant? }) → Uint8Array
Group avatar (multipart):
- uploadGroupAvatar(convId, filename, fileBytes, contentType)
- getGroupAvatar(convId) → Uint8Array
New transport helpers (private):
- rawMultipartUpload uses fetch's native FormData + Blob; the SDK
deliberately omits the Content-Type header so fetch derives it
(including the boundary token) from the body itself
- rawRequestBytes uses response.arrayBuffer() → Uint8Array;
shares auth with rawRequest, skips the configurable retry loop
(uploads + downloads are rarely safe to retry blindly)
- Both helpers route errors through buildApiError so 4xx/5xx
responses surface as the same error classes as JSON callers
New exported types: MessageReadEntry, MessageReadsResponse,
MessageReaction, MessageEditVersion, MessageEditsResponse,
StarMessageResponse, SavedMessageEntry, SavedMessagesResponse,
MessageAttachmentUploadResponse, MessageAttachmentVariant,
GroupAvatarUploadResponse.
23 new unit tests cover the happy paths, the percent-encoded-emoji
DELETE path (👍 → %F0%9F%91%8D), 413 / 403 error envelopes,
network-error wrapping, the Content-Type-not-set contract on
multipart (so fetch can derive it with the boundary), and
ArrayBuffer-as-input support.
No version bump per the multi-PR release plan.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds 7 tests covering the branches in rawMultipartUpload and
rawRequestBytes that the original test set didn't exercise:
- Empty 200 body on upload → returns {} (the !text fall-through)
- Non-JSON 200 body → returns {} (the catch around JSON.parse)
- 429 + numeric Retry-After header → retryAfter populated on
ColonyRateLimitError (covers the regex match + cond-expr + 429-
conditional Retry-After plumbing in one shot)
- Caller-supplied AbortSignal on both upload and download (covers
the `signal ? AbortSignal.any([...]) : timeoutSignal` ternary)
- Non-Error thrown by fetch impl on both upload and download
(covers the `err instanceof Error ? err.message : String(err)`
fallback)
All remaining uncovered branches in client.ts after this are
pre-existing in register() and extractItems(), not introduced by
this PR.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
05d8c64 to
18c1bdf
Compare
fa0be12 to
5adc6e0
Compare
Two more diff branches surfaced by the codecov/patch check: the
false arm of `if (this.token)` inside rawMultipartUpload and
rawRequestBytes. Exercises them by responding to /auth/token with
{access_token: ""} — ensureToken assigns "" (falsy) and the helper
correctly omits the Authorization header rather than sending
"Bearer ".
After this commit the diff has **zero** uncovered branches/lines
according to lcov.info; remaining gaps in client.ts are all in
pre-existing code (register() + extractItems()).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7 tasks
ColonistOne
added a commit
to ColonistOne/colony-sdk-js
that referenced
this pull request
Jun 3, 2026
Three PRs landed back-to-back wrapping the entire group-DM surface: - TheColonyCC#24 group DM lifecycle + members (13 methods) - TheColonyCC#25 group DM state + search (8 methods) - TheColonyCC#26 per-message ops + attachments + group avatar (15 methods) 36 new SDK methods total + new multipart-upload + binary-download transport helpers. Reaches feature parity with colony-sdk Python v1.13.0. Bumps package.json + jsr.json from 0.2.0 to 0.3.0 and moves the three Unreleased CHANGELOG entries under "## 0.3.0 — 2026-05-27". Per .github/workflows/release.yml, the v0.3.0 tag push after this merges to master is what triggers the OIDC npm + JSR publish + the GitHub release creation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third and final PR of the group-DM coverage series. With this in, the JS SDK now wraps the full
/api/v1/messages/*surface and reaches parity withcolony-sdkPython v1.13.0. Stacked on top of #25; switch the base tomasterafter #24 + #25 land. No version bump — per the multi-PR plan we'll combine all three into one release tag.15 new methods plus brand-new multipart-upload + binary-download transport helpers.
Per-message operations (1:1 + group)
markMessageRead,listMessageReadsaddMessageReaction(messageId, emoji),removeMessageReaction(messageId, emoji)— emoji is percent-encoded in the DELETE path (👍→%F0%9F%91%8D)editMessage(messageId, body)— 5-minute edit window enforced server-sidelistMessageEdits— walk the edit timelinedeleteMessage— sender-only soft deletetoggleStarMessagelistSavedMessages({ limit?, offset? })forwardMessage(messageId, recipientUsername, { comment? })Attachments (multipart)
uploadMessageAttachment(filename, fileBytes, contentType)— acceptsUint8ArrayorArrayBufferdeleteMessageAttachment(attachmentId)getMessageAttachment(attachmentId, { variant? })→Uint8ArrayGroup avatar (multipart)
uploadGroupAvatar(convId, filename, fileBytes, contentType)getGroupAvatar(convId)→Uint8ArrayNew transport infrastructure
rawMultipartUpload(private) — wrapsfetch's nativeFormData+Blob. The SDK deliberately omits theContent-Typeheader sofetchderives it (including the boundary token) from the body itself. Pinned byexpect(headers["content-type"]).toBeUndefined()on every multipart test — if a future change starts pre-settingContent-Type, the multipart boundary would be missing and the server would 400.rawRequestBytes(private) —fetch+response.arrayBuffer()→Uint8Array. Distinct fromrawRequest's JSON path; auth shared, retry loop deliberately skipped (uploads + downloads are rarely safe to retry blindly).buildApiErrorplumbing so 4xx/5xx responses surface asColonyAPIError/ColonyAuthErroretc. and transport failures surface asColonyNetworkError— same shape as the JSON path.New exported types
MessageReadEntry,MessageReadsResponse,MessageReaction,MessageEditVersion,MessageEditsResponse,StarMessageResponse,SavedMessageEntry,SavedMessagesResponse,MessageAttachmentUploadResponse,MessageAttachmentVariant,GroupAvatarUploadResponse.Test plan
npm run typecheck— cleannpm run lint— cleannpm run format:check— cleannpm run test— 260 passed (+23 new tests vs PR feat: group DM conversations — state + search (2/3) #25's 237)The 23 new tests pin:
👍→ exact%F0%9F%91%8D)ColonyAPIError/ColonyAuthErrorColonyNetworkErroron both multipart upload and binary GETContent-Type-not-setcontract on multipart so fetch derives it with the boundaryArrayBufferaccepted as input (not justUint8Array)getMessageAttachmentdefaults to"full"variant and respects{ variant: "thumb" }forwardMessagealways sendscomment=on the wire (even when omitted, defaulted to"")Per the TheColonyCC/* convention, holding the merge button for human review.
🤖 Generated with Claude Code