feat(chat): group chats and the current chat.bsky surface #163
No reviewers
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!163
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-128-group-chat"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #128.
Group chats (upstream April–August 2026) and the rest of the current
chat.bsky.*surface. Coverage goes from 20 to 50 of the 51 chat methods. Every new model uses typed identifiers,AtDatetime, open unions with anUnknown*fallback andLexObjectextension data, and passes the upstream drift test.What changed
chat.bsky.group.*(16):client.Chat.Group(GroupClient)CreateGroupAsync,EditGroupAsync,AddMembersAsync,RemoveMembersAsync,ListMutualGroupsAsync/EnumerateMutualGroupsAsync.CreateJoinLinkAsync,EditJoinLinkAsync,EnableJoinLinkAsync,DisableJoinLinkAsync,GetJoinLinkPreviewsAsync.RequestJoinAsync,ApproveJoinRequestAsync,RejectJoinRequestAsync,WithdrawJoinRequestAsync,ListJoinRequestsAsync/EnumerateJoinRequestsAsync,UpdateJoinRequestsReadAsync.{convo}/{joinLink}unwrap it (asMuteConvoAsyncalready does);{}outputs returnTask.chat.bsky.embed.joinLinkATProtoNet.Lexicon.Chat.Bsky.Embed: the message-embed unionsMessageEmbed(MessageRecordEmbed=app.bsky.embed.record,JoinLinkEmbed) andMessageEmbedView(MessageRecordEmbedView,JoinLinkEmbedView). They are dedicated chat unions rather than new variants of the postEmbedBase, so a chat message cannot be given an embed the chat service rejects, andApp/Bsky/Embedis untouched.Convo
GetUnreadCountsAsync,ListConvoRequestsAsync/EnumerateConvoRequestsAsync,GetConvoMembersAsync/EnumerateConvoMembersAsync,LockConvoAsync,UnlockConvoAsync.ListConvosAsync/EnumerateConvosAsyncgainkindandlockStatus.Typed views (each union open, with an unknown fallback)
ConvoView.Kind:DirectConvo/GroupConvo(name, member count and limit, lock status and moderation override, join link, join request counts, created at).ConvoView.LastReaction:MessageAndReactionView.ConvoView.LastMessageis typed.ChatMemberView.Kind:DirectConvoMember/GroupConvoMember(role, added by) /PastGroupConvoMember(viewer,createdAt,verificationwere already there).ConvoMessagefor every message union (MessageView,DeletedMessageView,SystemMessageView,MessageBeforeUserJoinedGroupView).MessageViewgainsReactions(ReactionView) andReplyTo, and itsFacets/Embedare typed.MessageInputgainsReplyTo(MessageReplyRef) and typedFacets/Embed.GetMessagesResponse.MessagesisIReadOnlyList<ConvoMessage>(RelatedProfilesalready existed).SystemMessageView.Datais one of 12SystemMessageData*types.ConvoLogEntryis the abstract base of the 30getLogvariants (LogBeginConvo…LogReadJoinRequests). It keepsRevandConvoId, which every variant has, so routing an entry needs no pattern match.UnknownConvoLogEntryreads both from the raw object.ConvoStatus,ConvoKinds,ConvoLockStatus,ConvoReadState,ChatMemberRole,JoinRule,JoinLinkEnabledStatus,RequestJoinStatus,ChatPreferenceInclude, all checked by the drift test's knownValues theory.Actor, notification, moderation
ChatActorClient.GetStatusAsync(chat.bsky.actor.getStatus).ChatDeclarationRecord.AllowGroupInvitesalready existed.client.Chat.Notification(ChatNotificationClient):GetPreferencesAsync/PutPreferencesAsync, the replacement for the deprecatedapp.bsky.notification.defs#preferences.chat(which the SDK never modelled, so there is nothing to deprecate).client.Chat.Moderation(ChatModerationClient):GetActorMetadataAsync,GetMessageContextAsync,GetConvoAsync,GetConvosAsync,GetConvoMembersAsync/EnumerateConvoMembersAsync,UpdateActorAccessAsync. Design decision: these calls send no fixedatproto-proxyheader. The chat service answers them only for moderation services (moderators reach them through Ozone), so a forced chat proxy would only produce auth errors. They follow the client-wideSetProxydefault, as thetools.ozone.*clients do. A test pins this.Registration, drift test, docs
ChatClientsgainsGroup,NotificationandModeration. TheAtProtoClient.csdiff is threeusinglines, the registration call, and theChatClientsclass.#128OmittedPropertiesentries and the looseConvoLogEntryDefOfentry are removed. NewDefOfentries coverMessageReplyRef, the sharedConvoOutput/JoinLinkOutput,ModerationConvoView,ChatActorMetadataandChatNotificationPreferences. TheConvoLogEntrybase andUnknownConvoLogEntryare inNotLexiconObjects. There are nine new knownValues locations.TypedIdentifierGuardTests:SystemMessageView.RevandModerationConvoView.Revare added to the chat-rev exceptions.docs/chat.mdis rewritten around the typed views, with a new Group Chats section (creating a group, join links, join requests, moderation hooks), notification preferences and the typed log.api-reference.md,architecture.md,custom-records.mdandREADME.mdare updated to match.Breaking changes
GetMessagesResponse.Messages,EnumerateMessagesAsyncandConvoView.LastMessagewereJsonElementand are nowConvoMessage.ConvoLogEntryis abstract, and losesTypeandMessage.MessageInput.Embed/MessageView.EmbedwereJsonElementand are nowMessageEmbed/MessageEmbedView.FacetswereJsonElementand are nowFacet.MessageView.Textisrequired string.ConvoView,MessageViewandDeletedMessageViewnow derive from union bases. Migration: pattern-match (msg is MessageView m,entry is LogCreateMessage { Message: MessageView m }) instead of reading$type, and writeEmbed = new MessageRecordEmbed { Record = strongRef }.ListConvosAsync/EnumerateConvosAsynctakekindandlockStatusbeforelimit/pageSize, per the CONTRIBUTING parameter order. Migration: passlimit,cursorandpageSizeby name.Verification
dotnet build --no-incrementalof the full solution: 0 warnings, 0 errors (rebased ontoca43404, #158).ATProtoNet.Tests.Chat.*) are 121, up from 16.convoViewthat round-trips field for field, messages with facets, joinLink and record embeds, reactions and each reply variant, agetMessagespage. There are theories over all 12 system-message kinds and all 30 log entries (each round-trips, with no extension data left over), and a test that the log theory covers exactly the upstreamgetLogunion. Nine unknown-variant tests check that an unknown variant survives and writes back byte for byte.chat.bsky.*Lexicons field by field rather than captured traffic.LexiconDriftTests(34) pass with the allow-list entries removed, andAllowlists_AllStillMatchconfirms that none of them is stale.Deferred
chat.bsky.moderation.subscribeModEvents(the seventh moderation method) is a private, service-authenticated event stream. Wrapping it needs an authenticated WebSocket and frame decoding, which #126 is consolidating into one streaming core with typed error frames. The 11 event models should land together with a transport after #126, not as unused types now.com.atproto.moderation.createReportwith achat.bsky.convo.defs#messageRefsubject.ReportSubjectdoes not model that variant yet (it reads asUnknownReportSubject). Adding it touchesCom/AtProto/Moderation, and Ozone'sModerationSubjectneedsmessageRef/convoReftoo (#131), so it is left out here.chat.bsky.convo.defs#logReadMessageis deprecated upstream in favour oflogReadConvo. It is modelled (the service may still send it) but not marked[Obsolete], which would warn at its own[JsonDerivedType]registration.🤖 Generated with Claude Code
@claude /review
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
⏳ Held on the OpenCode Go window — the 5-hour window is at 0% (the bot holds at 95%).
This continues automatically after the window resets (2026-09-26T02:00:33.556Z) — nothing to re-trigger.
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
Large but well-structured PR extending chat.bsky.* coverage to group chats and the remaining surface (convo requests, unread counts, members, locking, moderation, notification prefs). Spot-checked against upstream:
chat.bsky.group.createGroupsemantics (100-member cap, non-idempotent) match the SDK docs (see atproto.blue), giving confidence the rest of the group/join-link/join-request/log surface is similarly grounded rather than invented.Walked the diff for correctness issues:
ConvoKind,ConvoMessage,ChatMemberKind,SystemMessageData,ConvoLogEntry,MessageEmbed/MessageEmbedView,JoinLinkPreview,ConvoRequestView) all follow the codebase's establishedUnknown*fallback pattern (AtProtoUnion+IUnknownUnionVariant) consistently, matching prior PRs (#155-#159).EnumerateConvosAsync's call toListConvosAsyncwas updated correctly to match the newkind/lockStatusparameter positions — no argument-order mismatch despite the breaking signature change.ChatModerationClient's methods correctly omitoptions: ChatProxyeverywhere, consistent with the PR's stated design decision that moderation calls follow the client-wide proxy instead of the fixed chat-service header.ConvoOutput(internal) andGetConvoMembersResponseare correctly shared/reused acrossConvoClient,GroupClient, andChatModerationClientvia theConvonamespace import — no visibility or circular-reference problems (internal is assembly-scoped, and the Convo/Group/Embed namespace cross-references are all within the same project).UnknownConvoLogEntrycorrectly backfillsRev/ConvoId(defaulting to empty string) from the raw JSON via[SetsRequiredMembers], needed becauseConvoLogEntrynow declares those asrequiredabstract-base members.app.bsky.embed.record$typediscriminator on the newMessageRecordEmbed/MessageRecordEmbedViewchat types (separate hierarchy from the postEmbedBaseunion) is intentional and matches how upstream reuses that def inchat.bsky.convo.defs.Nothing here looks like a functional bug; the CHANGELOG entries accurately describe the breaking changes (typed messages/log entries/embeds,
ListConvosAsyncparameter reordering). Docs updates inchat.mdare thorough and consistent with the new API shapes.⏱ 1m 4s · 4 turns · tokens: 83.8k in / 5.8k out (+338.7k cache-read)
📜 Full session transcript
🤖 Resolving the merge conflicts — the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the fix runs.
⏳ Held on the OpenCode Go window — the 5-hour window is at 0% (the bot holds at 95%).
This continues automatically after the window resets (2026-09-26T02:00:33.556Z) — nothing to re-trigger.
fa6c20c5d18289c5413c🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
⏳ Held on the OpenCode Go window — the 5-hour window is at 0% (the bot holds at 95%).
This continues automatically after the window resets (2026-09-26T02:31:16.041Z) — nothing to re-trigger.
Rebased onto main to resolve a CHANGELOG-only conflict (#162 merged).
verify-rebase: the PR's patch outsideCHANGELOG.mdis byte-identical between the approved headfa6c20c5and the new head8289c541(4699 changed lines). CI is green on the new head (3479 unit tests pass locally), so the approval carries over under the maintainer-approved policy for CHANGELOG-only rebases.