feat(ozone): moderation, report and queue tooling surface #162
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!162
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-131-ozone"
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 #131.
The SDK called 24 of the 68
tools.ozonemethods. It now calls all 68 (checked against the vendored snapshot:commof the upstream query/procedure NSIDs and the SDK's call sites is empty). Every new model uses typed identifiers,AtDatetime,IReadOnlyList<T>,LexObjectextension data and open unions, and passes the drift test. Its allow-lists gain no exemptions: only threeDefOfentries for defs whose names would be poor C# class names (event,option) and nine knownValues locations for the new constants classes, and theReportReasonsentry #123 left for this issue is removed.tools.ozone.moderationModerationClient:GetAccountPreferencesAsync(did): returns the typedapp.bsky.actorPreferenceunion.GetReposAsync(dids),GetRecordsAsync(uris),GetSubjectsAsync(subjects),GetAccountTimelineAsync(did)andGetReporterStatsAsync(dids).ScheduleActionAsync(subjects, action, scheduling, createdBy, modTool?),ListScheduledActionsAsync/EnumerateScheduledActionsAsync(a procedure: the cursor goes in the body) andCancelScheduledActionsAsync.ModerationSubjectViewforgetRepos,getRecordsand the verification views'subjectRepo/issuerRepo:RepoViewDetail,RepoViewNotFound,RecordViewDetail,RecordViewNotFound, plusUnknownModerationSubjectView.RepoViewDetailandRecordViewDetailnow derive from it instead of directly fromLexObject. This adds a class to the hierarchy and is not binary-breaking.getRepo/getRecordstill read them without a$type(tested). Serializing one on its own now writes its$type.modEventViewDetail.subjectis the same family (repoView,repoViewNotFound,recordView,recordViewNotFound,convoView). A single class can only belong to one union, so separate bases per endpoint would block typing that field later.ModEventType:ModEventResolveAppeal,ModEventPriorityScore,AccountEvent,IdentityEvent,RecordEvent,AgeAssuranceEvent,AgeAssuranceOverrideEvent,AgeAssurancePurgeEvent,RevokeAccountCredentialsEvent,ScheduleTakedownEventandCancelScheduledTakedownEvent.UnknownModEventstays the fallback.ScheduledAction(ScheduledTakedown), and new modelsSchedulingConfig,ScheduledActionView,ScheduledActionResults/CancellationResults,SubjectView,ReporterStatsandTimelineItem(Summary).ScheduledActionStatus.tools.ozone.reportandtools.ozone.queueclient.Ozone.Reportcovers all 14 methods:GetReportAsync,GetLatestReportAsync, andQueryReportsAsync(status, ReportFilter?)/EnumerateReportsAsync. The requiredstatusis a parameter. The 12 optional filters go in aReportFilter, following theSubjectStatusFilterpattern.CloseReportsAsyncandReassignQueueAsync.AssignModeratorAsync,UnassignModeratorAsync,GetAssignmentsAsync/EnumerateAssignmentsAsync.CreateActivityAsync(reportId, activity, …)andCreateActivityForEventAsync(eventId, activity, …). The Lexicon requires exactly one ofreportIdoreventId, so two methods replace a runtime check.ListActivitiesAsync/EnumerateReportActivitiesAsyncfor one report, andQueryActivitiesAsync/EnumerateActivitiesAsyncacross all reports.GetLiveStatsAsync,GetHistoricalStatsAsync/EnumerateHistoricalStatsAsync, andRefreshStatsAsync(DateOnly, DateOnly, …), which takesDateOnlybecause the wire format isYYYY-MM-DD.ReportActivityunion: queue, assignment, escalation, close, reopen and note.client.Ozone.Queuecovers all 8 methods: create, update, delete, list/enumerate,RouteReportsAsync, and assign / unassign / get assignments.queue.assignModerator#didis a plainstringin the Lexicon. It is typedDidhere because it is one.ReportStatusandReportSubjectType.Safelink, setting, verification, hosting
client.Ozone.Safelink: add, update and remove rules;QueryRulesAsync/EnumerateRulesAsyncandQueryEventsAsync/EnumerateEventsAsync. Both queries are procedures, so the cursor goes in the body. Constants:SafelinkPatternType,SafelinkActionType,SafelinkReasonTypeandSafelinkEventType.client.Ozone.Setting:ListOptionsAsync/EnumerateOptionsAsync,UpsertOptionAsync(the value is aJsonElement) andRemoveOptionsAsync. Constants:SettingScope.client.Ozone.Verification:GrantVerificationsAsync,RevokeVerificationsAsyncandListVerificationsAsync/EnumerateVerificationsAsync.client.Ozone.Hosting:GetAccountHistoryAsync/EnumerateAccountHistoryAsync, with the openAccountHistoryDetailsunion (AccountCreated,EmailUpdated,EmailConfirmed,PasswordUpdated,HandleUpdated). Constants:AccountHistoryEventType.ReasonTypeReportReasonsgains the 40tools.ozone.report.defs#reason*values, grouped by category.#reasonAppealand#reasonOtherclash with the coarseAppealandOther, so they areOzoneAppealandOzoneOther.MissingKnownValues["ReportReasons:tools.ozone.report.defs#*"]is removed.Fixes to existing event models
ModEventMuteReporter.DurationInHoursisint?, no longerrequired int. Upstream makes it optional, and Ozone omits it for a permanent mute. Before this, one such event madeQueryEventsAsync/GetEventAsyncthrow for the whole page. A regression test reads a mute-reporter event withoutdurationInHours. The CHANGELOG has a Fixed bullet for it.ModEventReport.ReportTypeisrequired string, no longerstring?. This matches the Lexicon, where the field is required. A report event without it is non-conforming and now fails to deserialize, and a test pins that.Breaking changes
ModEventMuteReporter.DurationInHourschanges frominttoint?. This is a binary and source break for readers. Migration: read the value as nullable, wherenullmeans a permanent mute.ModEventReport.ReportTypebecomesrequired. This is a source break for object initializers. Migration: setReportTypewhen constructing aModEventReport.Both changes share one CHANGELOG Breaking changes bullet, as the repo's CHANGELOG rule requires for API signature changes.
The two
*ViewDetailclasses gain an intermediate base class and now serialize their$type. That change is additive and not binary-breaking, so the CHANGELOG notes it under Added.Verification
ca43404(#158).OzoneModerationToolingTests,OzoneReportClientTests,OzoneQueueClientTestsandOzoneToolsClientTests.OpenUnionTests: 4 new union bases, each checked for an unknown variant and a byte-for-byte round trip.KnownValueConstants_MatchUpstreamrows.OpenUnionTestsusedaccountEventandidentityEventas its examples of event types the SDK did not model. It now uses a hypothetical#futureEvent.--no-incrementalwith 0 warnings and 0 errors.TypedIdentifierGuardTests.Exceptionsgains six entries. All are DID-or-AT-URI subjects, an email subject line, orhosting#event.createdBy, which has no format.LexiconDriftTests.Allowlistsgains threeDefOfentries and nine knownValues locations.docs/ozone.mdgets new sections: Reports, Queues, batch lookups, Scheduled Actions, Settings, URL Safety Rules, Verifications and Account History. Its sub-client and event tables are updated.docs/api-reference.mdOzone table is updated.Deferred or flagged
ModEventViewDetail.Subjectis still aJsonElement. Typing it asModerationSubjectViewalso needs therepoView,recordViewandconvoViewvariants, and it changes a public property's type. The union is built for that follow-up.JsonElement.SubjectView.ProfileandVerificationView.SubjectProfile/IssuerProfilestay asJsonElement, because upstream declares these unions with no variants.GetAccountPreferencesResponseusesApp.Bsky.Actor.Preference, which is #127's area. It is referenced there, not edited.🤖 Generated with Claude Code
🤖 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-26T01:54:42.152Z) — nothing to re-trigger.
@claude /review
🤖 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, well-organized surface addition (report/queue/safelink/setting/verification/hosting clients plus moderation lookups, scheduled actions, and new event/reason types). From what's visible in the diff:
ModEventMuteReporter.DurationInHours→int?,ModEventReport.ReportType→required string) are correctly documented in CHANGELOG under Breaking changes with migration notes, matching CLAUDE.md's rule.RepoViewDetail/RecordViewDetailnow derive from the newModerationSubjectViewunion base; this is additive (adds a class to the hierarchy, doesn't remove members) and is correctly filed under Added rather than Breaking changes.AtDatetime?values in the newReportClientmethods (e.g.createdAfter?.ToString()) matches the exact pattern already used in the pre-existingModerationClient.EnumerateEventsAsync/QueryEventsAsync, so it's consistent with established convention rather than a new inconsistency.AssignmentViewclasses (Tools.Ozone.Report.AssignmentViewvsTools.Ozone.Queue.AssignmentView) live in different namespaces with genuinely different shapes (one keyed byReportId, the other byQueue), and C# namespace-member resolution means theusing ATProtoNet.Lexicon.Tools.Ozone.Queue;import inReportModels.csdoesn't create ambiguity with the locally-declaredAssignmentView— so no actual compile hazard there.CreateActivityAsync/CreateActivityForEventAsynccleanly encode the Lexicon's "exactly one of reportId/eventId" constraint as two separate methods rather than a runtime check, which is a nice simplification.The diff was truncated before I could see
ReportFilter,RefreshStatsRequest(including howDateOnlyis (de)serialized toYYYY-MM-DD), and the tail ofReportModels.cs/SafelinkModels.cs/SettingModels.cs/VerificationModels.cs, so I can't fully verify those pieces, and I wasn't able to fetch the branch to check them directly (network access wasn't available in this session). Nothing in the portion I could review looks incorrect, and the PR description's stated verification (93 new tests covering every endpoint, drift tests, allow-list updates, full-solution warning-free build) is consistent with the care shown in the visible code. No blocking issues found.⏱ 1m 52s · 8 turns · tokens: 85.5k in / 9.4k out (+767.0k cache-read)
📜 Full session transcript