feat!: typed identifiers, AtDatetime and enumerators for chat.bsky, tools.ozone and site.standard (3/4 of #119) #156
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
5 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!156
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-119-typed-chat-ozone-site"
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?
Part of #119 (3/4: chat.bsky, tools.ozone, site.standard).
Converts
Lexicon/Chat/**,Lexicon/Tools/Ozone/**andLexicon/Site/Standard/**to the conventions part 1 (#154) wrote down inCONTRIBUTING.md: typed identifiers andAtDatetime, one parameter order,IReadOnlyList<T>out andIEnumerable<T>in,ICursorPage<T>on every cursored response,Enumerate*on the shared paginator, and internal single-use request DTOs.StandardSiteClientalso gets typed signatures and typed listings. Based onissue-119-typed-foundation(#154).How to review this
Most of the diff is mechanical. Look at these first:
src/ATProtoNet/Lexicon/Site/Standard/StandardSiteClient.csList*AsyncreturnRecordPage<T>instead ofListRecordsResponse. NewEnumerate*Asyncmethods andGet*Async(AtUri)overloads; an overload rejects a URI of another collection, or one without an rkey, before sending anything.src/ATProtoNet/RecordCollection.csListFromAsyncbecomesinternal static ToPage, soStandardSiteClientdeserializes records the same way (a bad record throwsXrpcResponseFormatException). No public change.tests/ATProtoNet.Tests/Conventions/TypedIdentifierGuardTests.csPendingConversions. There are 14 new commentedExceptions; they are the only identifier-named members still typedstring(see below).src/ATProtoNet/Lexicon/Tools/Ozone/Moderation/ModerationClient.cslimitandcursormove behind the filters. AlsoAtDatetime?query filters, and three enumerators.The rest follows one pattern per file: property types,
List<T>→IReadOnlyList<T>,ICursorPage<T>with an explicitItems,public→internalon request DTOs, XML<param>docs, and call sites in tests and docs.What changed
chat.bsky
ChatMemberView.Did/Handle,MessageSender.DidandMessageView/DeletedMessageView.SentAt(AtDatetime).GetConvoForMembersAsyncandGetConvoAvailabilityAsynctakeIEnumerable<Did>, andSendMessageBatchAsynctakesIEnumerable<BatchMessageItem>.IReadOnlyList<T>, includingMessageInput.Facets.ListConvosAsync(readOnly, status, limit, cursor).ICursorPage<T>onListConvosResponse,GetMessagesResponseandGetLogResponse. NewEnumerateConvosAsync,EnumerateMessagesAsyncandEnumerateLogAsync.SendMessageRequesttoRemoveReactionRequest.DeleteChatAccountRequestwas never sent (chat.bsky.actor.deleteAccounthas no input), so it is removed.revstaysstring: the chat Lexicons give it no format. Its doc comment no longer calls it "the repository revision (a TID)".tools.ozone
RepoSubject.Did, andRecordSubject.Uri/Cid.CreatedBy(Did),CreatedAt/UpdatedAt/IndexedAtand the other timestamps (AtDatetime).SubjectBlobCids/BlobCids(IReadOnlyList<Cid>).CreatorHandle/SubjectHandle/SubjectRepoHandle(Handle?),LastReviewedBy(Did?), andRepoView/RepoViewDetail.Did/Handle.EmitEventRequest.SubjectBlobCids/CreatedBy.GetRecordAsync(AtUri, Cid?)andGetRepoAsync(Did).QueryEventsAsync(…, Did? createdBy, AtDatetime? createdAfter/createdBefore, …, IEnumerable<string>? …, limit, cursor).QuerySubjectsAsync(…, Did? lastReviewedBy, …, limit, cursor).Did/Handle/AtDatetimefields.DeleteMemberAsync(Did),FindCorrelationAsync(IEnumerable<Did>), andFindRelatedAccountsAsync(Did, limit, cursor).SearchAccountsAsync(IEnumerable<SigDetail>, limit, cursor)(wascursor, limit).AddValuesAsync/DeleteValuesAsynctakeIEnumerable<string>.ICursorPage<T>onQueryEventsResponse,QuerySubjectsResponse,SearchReposResponse,QuerySetsResponse,GetValuesResponse,ListMembersResponse,SearchAccountsResponseandFindRelatedAccountsResponse.EnumerateEventsAsync,EnumerateSubjectsAsync,EnumerateReposAsync,EnumerateSetsAsync,EnumerateValuesAsyncandEnumerateMembersAsync.listTemplatesis not cursored.DeleteTemplateRequest,DeleteSetRequest,AddValuesRequest,DeleteValuesRequestandDeleteMemberRequest.EmitEventRequest,Create/UpdateTemplateRequest,UpsertSetRequestandAdd/UpdateMemberRequest, because public methods accept them.site.standard
DocumentRecord.PublishedAt/UpdatedAt(AtDatetime),Tags(IReadOnlyList<string>?) andSubscriptionRecord.Publication(AtUri).DocumentRecord.Sitestaysstring, because its Lexicon format isuri(at://orhttps://).StandardSiteClient:AtIdentifier repoandRecordKey/RecordKey? rkey, andCid? swapRecord.ListPublicationsAsync/ListDocumentsAsync/ListSubscriptionsAsyncreturnRecordPage<T>of typedRecordView<T>.EnumeratePublicationsAsync/EnumerateDocumentsAsync/EnumerateSubscriptionsAsync.GetPublicationAsync(AtUri)/GetDocumentAsync(AtUri)/GetSubscriptionAsync(AtUri)let you follow aSubscriptionRecord.Publicationdirectly.RepoClient, with the same method set, so #129 can extend it.Deliberate
strings (guardExceptions, each commented)rev:ConvoView,MessageView,DeletedMessageView,ConvoLogEntry,LeaveConvoResponseandAcceptConvoResponse. The Lexicon gives it no format.subjectfilter:QueryEventsAsync/EnumerateEventsAsync/QuerySubjectsAsync/EnumerateSubjectsAsync(subject). The format isuri: an account is a bare DID and a record an AT URI. This is the same reasoning as part 1'sLabel.Uri.Subject:CommunicationTemplateView/CreateTemplateRequest/UpdateTemplateRequest.Subjecthold an email subject line.TeamMember.LastUpdatedBy: the Lexicon gives it no format, and Ozone writesadmin_tokenwhen an admin token made the change (packages/ozone/src/api/team/addMember.ts).Breaking changes and migration
Did.Parse("…"),Handle.Parse,AtUri.Parse,Cid.Parse,RecordKey.Parse, andAtDatetime.Now()/Parse/FromDateTimeOffset. Values the API returns pass through unchanged and still convert implicitly tostring. A response with an invalid identifier now throwsXrpcResponseFormatException.ListConvosAsync,QueryEventsAsync,QuerySubjectsAsync,SearchAccountsAsyncandFindRelatedAccountsAsyncno longer compile, or bind differently. Reorder them or name the arguments.StandardSiteClientlistings:entry.Valueis already the typed record. Drop theDeserialize<T>(…)call.DeleteChatAccountRequestis gone.List<T>→IReadOnlyList<T>: copy a list you need to change ([.. page.Convos]).CHANGELOG.mdcovers these in threeBreaking changesbullets, and has oneAddedbullet for the enumerators, the typedsite.standardlistings and the AT URI overloads.Verification
--no-incremental): 0 warnings, 0 errors.ConvoClientTests:ListConvosAsyncparameter order on the wire.membersas repeateddidparameters, with typedDid/Handleread back.SendMessageBatchAsyncfrom a lazy sequence.EnumerateConvosAsyncwalking two pages with filters andpageSize.EnumerateMessagesAsyncstopping on a repeated cursor after 2 requests.EnumerateLogAsyncstopping when the log stops advancing.TypedOzoneClientTests(new):AtDatetimekept verbatim and one canonicalized.ModEventViewread (subjectAtUri, blobCids,Did,Handle,AtDatetime).XrpcResponseFormatException.GetRecordAsync, andEmitEventAsyncbody serialization.admin_tokeninLastUpdatedBy.limit/cursororder of both signature methods.pageSize→limit, cursors threaded, and a stop on a repeated cursor.StandardSiteClientTests:ListDocumentsAsync(record key, typed value, verbatimAtDatetime).XrpcResponseFormatException.EnumeratePublicationsAsyncpaging.GetPublicationAsync(AtUri)from aSubscriptionRecord.Publication.Wire-fidelity bugs noticed (left for #123, JSON names and HTTP methods untouched)
canConvo→canChat(+convo),ListConvosAsync(readOnly)→readState,ConvoView.Opened,AcceptConvoResponse.Convo.querySubjects→queryStatuses(subjectStatuses, booleantakendown/appealed).searchAccountsas POST, andAccountResultvsadmin.defs#accountView.updateAllReadbody andupdatedCount, andexportAccountDataJSONL.ModerationSubjectmessageRef/convoRef.MuteConvoAsync,UnmuteConvoAsyncandUpdateReadAsyncdeserialize aConvoView, but the output is{convo: convoView}.AddReactionAsyncandRemoveReactionAsyncdeserialize aMessageView, but the output is{message: messageView}. Against a real chat service, all five fail on the missing requiredid.RecordViewDetail.BlobCidsisrecordView's field;recordViewDetailhasblobs(#blobView[]).ModEventView.subjectBlobCidsis required upstream but optional here.modEventLabel.createLabelVals/negateLabelValsandmodEventTag.add/removeare required upstream but optional here, so an event built without them is rejected by Ozone.server.getConfigverifierDid.getMessagesrelatedProfiles.queryEventsfilters (collections,subjectType,policies,modTool,batchId,ageAssuranceState,withStrike,includeAllUserRecords).querySetsnamePrefix/sortBy/sortDirection, andlistMembersq/disabled/roles.GetEventAsyncformatsidwith the current culture (id.ToString()).Deliberately deferred
RecordSubject.Cidand themodEvent*arrays, stay nullable. Only types changed. Making themrequiredchanges what a response or event must contain, which belongs to #123.SearchAccountsAsyncorFindRelatedAccountsAsync: their responses implementICursorPage<T>, but the brief's list doesn't include them, andsearchAccountsis the broken POST above.JsonElement:EnumerateMessagesAsyncyields rawJsonElementmessages, likeGetMessagesResponse.Messages. Typing the message union belongs to #123/#128.src/ATProtoNet/RecordCollection.cs: the internalToPageextraction. Part 1's file, not owned by parts 2 or 4.tests/ATProtoNet.Tests/Http/EmptyResponseBodyTests.cs(DeleteMemberAsync).StandardSiteClienttable indocs/api-reference.md.PendingConversions(only the PR-3 lines removed) andExceptions(entries appended), andCHANGELOG.md(bullets appended after part 1's #119 bullets). Expect trivial merge conflicts there.🤖 Generated with Claude Code
@claude /review — large and mostly mechanical; the description opens with How to review this.
🤖 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.
This is a large, mechanical conversion PR, and it holds up well on inspection. The pieces flagged by the author as worth the closest look all check out:
StandardSiteClient:RecordKeyOf(AtUri, Nsid)correctly rejects a URI whoseCollectiondoesn't match or whoseRecordKeyis null before any request is sent (AtUriis asealed record, soCollection/RecordKeyare reference-typed nullable properties and the pattern matchis { } rkeybehaves as intended). The newGet*Async(AtUri)overloads delegate to the existing(AtIdentifier, RecordKey)overloads without recursion or ambiguity risk.RecordCollection.ToPage: a clean, behavior-preserving extraction;StandardSiteClient's newListAsync<T>private helper reuses it correctly (same assembly, sointernalaccess works), preserving theXrpcResponseFormatException-on-bad-record behavior.ModerationClient: thelimit/cursorreorder is applied consistently acrossQueryEventsAsync/EnumerateEventsAsync/QuerySubjectsAsync/EnumerateSubjectsAsync, with theEnumerate*wrappers forwarding every filter parameter in the same order as the correspondingQuery*Async/List*Async/Search*Asyncmethod, so pagination composes correctly.Did/Handle/AtUri/Cid/AtDatetimeare applied consistently to fields the Lexicon marks with that format, while the documented exceptions (chatrev, Ozonesubjectfilter, templateSubject,TeamMember.LastUpdatedBy) are left as plain strings with a clear rationale, matching the corresponding guard-test entries.ListConvosAsync,SearchAccountsAsync,FindRelatedAccountsAsync, etc.) are reflected in theCHANGELOG.mdbullets added in this same commit, satisfying the repo's changelog policy.I didn't find correctness bugs, security issues, or notable omissions in the reviewed portion of the diff (the diff is truncated at the end, but the visible material is consistent and well-tested per the PR's own verification section). The many wire-fidelity issues the author found (unwrapped
chat.bskyoutputs, missing filters, etc.) are explicitly called out and deferred to #123 rather than silently left as bugs, which is the right call for a typing-only PR.⏱ 1m 20s · 8 turns · tokens: 86.7k in / 7.7k out (+767.2k cache-read)
📜 Full session transcript
Rebased onto main after #155 merged. The conflicts were CHANGELOG plus the guard test's pending-conversions list, where both PRs removed their own entries. Outside CHANGELOG, the patch now differs from the approved head
d92aef84only in that list's context lines. 3203 unit tests pass locally. Requesting a fresh review, per the policy that only CHANGELOG-only rebases carry an approval.@claude /review
d92aef84e8fb029940f5🤖 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.
Re-reviewed against my prior approve pass; nothing that changes that assessment. Re-checked the areas the PR calls out as worth attention:
ModerationClient,SignatureClient,TeamClient, etc. passDid?/AtUri/Cid?values directly intoXrpcParams.Add(string, string?)— this compiles and works correctly becauseDid,AtUri,Cid,Handle, etc. all defineimplicit operator string?, so no manual.Value/.ToString()calls are needed and none were missed.Pagination.EnumerateAsync(unchanged, from part 1) correctly backs every newEnumerate*method here: it stops on a null/empty cursor or a repeated one, which matches the newEnumerateLogAsync/EnumerateMessagesAsynctests (repeated-cursor and log-stops-advancing cases both terminate correctly after tracing through the loop).ModerationClient.EnumerateEventsAsync/EnumerateSubjectsAsyncforward every filter parameter to the correspondingQuery*Asyncmethod in the exact same order the method declares them, so thelimit/cursorreorder composes correctly with pagination.StandardSiteClient: theAtUrioverloads andRecordKeyOfguard, and theRecordCollection.ToPageextraction reused via the new privateListAsync<T>helper, are unchanged from my last pass and still correct.Did,Handle,AtUri,Cid,AtDatetime) are applied consistently, and the documented exceptions (chatrev, Ozonesubjectfilter, templateSubject,TeamMember.LastUpdatedBy) match their guard-test entries.One purely cosmetic nit, not worth blocking on: the
CHANGELOG.mdhunk leaves three blank lines before### Addedand before### Changed(two added on top of the pre-existing one) instead of the single blank line used elsewhere in the file.No correctness bugs, security issues, or omissions found in the reviewed diff.
⏱ 1m 20s · 11 turns · tokens: 94.6k in / 5.6k out (+1108.4k cache-read)
📜 Full session transcript
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
💬 Verdict: comment — the reviewer did not submit a structured review; the raw output follows.
(empty)
⏱ 0m 28s · 2 steps · $0.0080 · tokens: 45.5k in / 27 out (+0 cache-read) · Go: 5h 2% · wk 99% · mo 82%
📜 Full session transcript
🔧 Let opencode-bot fix this — tick the box and opencode-bot picks up the review above, on this PR branch. Nothing runs until you do.