@atproto/ozone 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +2 -0
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/report/queryActivities.d.ts +4 -0
  6. package/dist/api/report/queryActivities.d.ts.map +1 -0
  7. package/dist/api/report/queryActivities.js +36 -0
  8. package/dist/api/report/queryActivities.js.map +1 -0
  9. package/dist/background.d.ts +5 -3
  10. package/dist/background.d.ts.map +1 -1
  11. package/dist/background.js +13 -4
  12. package/dist/background.js.map +1 -1
  13. package/dist/context.js +1 -1
  14. package/dist/context.js.map +1 -1
  15. package/dist/daemon/context.js +1 -1
  16. package/dist/daemon/context.js.map +1 -1
  17. package/dist/daemon/verification-listener.d.ts +1 -1
  18. package/dist/daemon/verification-listener.d.ts.map +1 -1
  19. package/dist/daemon/verification-listener.js +10 -4
  20. package/dist/daemon/verification-listener.js.map +1 -1
  21. package/dist/db/migrations/20260602T120000000Z-add-report-activity-created-index.d.ts +4 -0
  22. package/dist/db/migrations/20260602T120000000Z-add-report-activity-created-index.d.ts.map +1 -0
  23. package/dist/db/migrations/20260602T120000000Z-add-report-activity-created-index.js +15 -0
  24. package/dist/db/migrations/20260602T120000000Z-add-report-activity-created-index.js.map +1 -0
  25. package/dist/db/migrations/index.d.ts +1 -0
  26. package/dist/db/migrations/index.d.ts.map +1 -1
  27. package/dist/db/migrations/index.js +1 -0
  28. package/dist/db/migrations/index.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +23 -13
  31. package/dist/index.js.map +1 -1
  32. package/dist/jetstream/service.d.ts +1 -1
  33. package/dist/jetstream/service.d.ts.map +1 -1
  34. package/dist/jetstream/service.js +3 -1
  35. package/dist/jetstream/service.js.map +1 -1
  36. package/dist/lexicon/index.d.ts +11 -0
  37. package/dist/lexicon/index.d.ts.map +1 -1
  38. package/dist/lexicon/index.js +18 -0
  39. package/dist/lexicon/index.js.map +1 -1
  40. package/dist/lexicon/lexicons.d.ts +338 -0
  41. package/dist/lexicon/lexicons.d.ts.map +1 -1
  42. package/dist/lexicon/lexicons.js +173 -0
  43. package/dist/lexicon/lexicons.js.map +1 -1
  44. package/dist/lexicon/types/app/bsky/notification/defs.d.ts +1 -0
  45. package/dist/lexicon/types/app/bsky/notification/defs.d.ts.map +1 -1
  46. package/dist/lexicon/types/app/bsky/notification/defs.js.map +1 -1
  47. package/dist/lexicon/types/chat/bsky/notification/defs.d.ts +19 -0
  48. package/dist/lexicon/types/chat/bsky/notification/defs.d.ts.map +1 -0
  49. package/dist/lexicon/types/chat/bsky/notification/defs.js +19 -0
  50. package/dist/lexicon/types/chat/bsky/notification/defs.js.map +1 -0
  51. package/dist/lexicon/types/chat/bsky/notification/getPreferences.d.ts +20 -0
  52. package/dist/lexicon/types/chat/bsky/notification/getPreferences.d.ts.map +1 -0
  53. package/dist/lexicon/types/chat/bsky/notification/getPreferences.js +5 -0
  54. package/dist/lexicon/types/chat/bsky/notification/getPreferences.js.map +1 -0
  55. package/dist/lexicon/types/chat/bsky/notification/putPreferences.d.ts +26 -0
  56. package/dist/lexicon/types/chat/bsky/notification/putPreferences.d.ts.map +1 -0
  57. package/dist/lexicon/types/chat/bsky/notification/putPreferences.js +5 -0
  58. package/dist/lexicon/types/chat/bsky/notification/putPreferences.js.map +1 -0
  59. package/dist/lexicon/types/tools/ozone/report/defs.d.ts +1 -0
  60. package/dist/lexicon/types/tools/ozone/report/defs.d.ts.map +1 -1
  61. package/dist/lexicon/types/tools/ozone/report/defs.js.map +1 -1
  62. package/dist/lexicon/types/tools/ozone/report/queryActivities.d.ts +32 -0
  63. package/dist/lexicon/types/tools/ozone/report/queryActivities.d.ts.map +1 -0
  64. package/dist/lexicon/types/tools/ozone/report/queryActivities.js +5 -0
  65. package/dist/lexicon/types/tools/ozone/report/queryActivities.js.map +1 -0
  66. package/dist/mod-service/report.d.ts +1 -0
  67. package/dist/mod-service/report.d.ts.map +1 -1
  68. package/dist/mod-service/report.js +16 -0
  69. package/dist/mod-service/report.js.map +1 -1
  70. package/dist/report/activity.d.ts +19 -1
  71. package/dist/report/activity.d.ts.map +1 -1
  72. package/dist/report/activity.js +27 -1
  73. package/dist/report/activity.js.map +1 -1
  74. package/package.json +9 -8
  75. package/src/api/index.ts +2 -0
  76. package/src/api/report/queryActivities.ts +64 -0
  77. package/src/background.ts +19 -4
  78. package/src/context.ts +1 -1
  79. package/src/daemon/context.ts +1 -1
  80. package/src/daemon/verification-listener.ts +9 -4
  81. package/src/db/migrations/20260602T120000000Z-add-report-activity-created-index.ts +17 -0
  82. package/src/db/migrations/index.ts +1 -0
  83. package/src/index.ts +25 -15
  84. package/src/jetstream/service.ts +3 -1
  85. package/src/mod-service/report.ts +19 -0
  86. package/src/report/activity.ts +47 -0
  87. package/tests/3p-labeler.test.ts +2 -2
  88. package/tests/_util.ts +8 -25
  89. package/tests/account-strikes.test.ts +1 -1
  90. package/tests/ack-all-subjects-of-account.test.ts +1 -1
  91. package/tests/age-assurance.test.ts +1 -1
  92. package/tests/blob-divert.test.ts +1 -1
  93. package/tests/communication-templates.test.ts +1 -1
  94. package/tests/content-tagger.test.ts +1 -1
  95. package/tests/db.test.ts +1 -1
  96. package/tests/expiring-label.test.ts +1 -1
  97. package/tests/expiring-tags.test.ts +1 -1
  98. package/tests/get-account-timeline.test.ts +1 -1
  99. package/tests/get-config.test.ts +1 -1
  100. package/tests/get-lists.test.ts +2 -1
  101. package/tests/get-profiles.test.ts +1 -1
  102. package/tests/get-record.test.ts +1 -1
  103. package/tests/get-records.test.ts +1 -1
  104. package/tests/get-repo.test.ts +1 -1
  105. package/tests/get-report.test.ts +1 -1
  106. package/tests/get-reporter-stats.test.ts +1 -1
  107. package/tests/get-repos.test.ts +1 -1
  108. package/tests/get-starter-pack.test.ts +1 -1
  109. package/tests/get-subjects.test.ts +1 -1
  110. package/tests/mod-tool.test.ts +1 -1
  111. package/tests/moderation-appeals.test.ts +1 -1
  112. package/tests/moderation-events.test.ts +1 -1
  113. package/tests/moderation-status-tags.test.ts +1 -1
  114. package/tests/moderation-statuses.test.ts +1 -1
  115. package/tests/moderation.test.ts +1 -1
  116. package/tests/protected-tags.test.ts +1 -1
  117. package/tests/query-labels.test.ts +1 -1
  118. package/tests/query-reports.test.ts +1 -1
  119. package/tests/queue-assignment.test.ts +1 -1
  120. package/tests/queue-router.test.ts +1 -1
  121. package/tests/queues.test.ts +1 -1
  122. package/tests/record-and-account-events.test.ts +1 -1
  123. package/tests/repo-search.test.ts +2 -2
  124. package/tests/report-action.test.ts +1 -1
  125. package/tests/report-activity.test.ts +145 -1
  126. package/tests/report-assignment.test.ts +1 -1
  127. package/tests/report-muting.test.ts +1 -1
  128. package/tests/report-reason.test.ts +1 -1
  129. package/tests/report-reassign-queue.test.ts +1 -1
  130. package/tests/report-routing.test.ts +1 -1
  131. package/tests/report-stats.test.ts +1 -1
  132. package/tests/revoke-account-credentials.test.ts +1 -1
  133. package/tests/safelink.test.ts +1 -1
  134. package/tests/scheduled-action-processor.test.ts +1 -1
  135. package/tests/scheduled-action.test.ts +1 -1
  136. package/tests/sequencer.test.ts +1 -1
  137. package/tests/server.test.ts +9 -12
  138. package/tests/sets.test.ts +1 -1
  139. package/tests/settings.test.ts +1 -1
  140. package/tests/strike-expiry-processor.test.ts +1 -1
  141. package/tests/subject-priority-score.test.ts +1 -1
  142. package/tests/takedown.test.ts +1 -1
  143. package/tests/team.test.ts +1 -1
  144. package/tests/verification-listener.test.ts +40 -13
  145. package/tests/verification.test.ts +1 -1
  146. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,4 +1,6 @@
1
1
  import { Database } from '../db/index.js';
2
+ import { ReportView } from '../lexicon/types/tools/ozone/report/defs.js';
3
+ import { QueryParams as QueryActivitiesParams } from '../lexicon/types/tools/ozone/report/queryActivities.js';
2
4
  import { Member } from '../lexicon/types/tools/ozone/team/defs.js';
3
5
  export type ActivityType = 'queueActivity' | 'assignmentActivity' | 'escalationActivity' | 'closeActivity' | 'reopenActivity' | 'noteActivity';
4
6
  export type CreateActivityParams = {
@@ -59,6 +61,21 @@ export declare function listReportActivities(db: Database, params: ListActivitie
59
61
  }[];
60
62
  cursor: string | undefined;
61
63
  }>;
64
+ export declare function queryReportActivities(db: Database, params: QueryActivitiesParams): Promise<{
65
+ activities: {
66
+ activityType: string;
67
+ createdAt: string;
68
+ createdBy: string;
69
+ id: number;
70
+ internalNote: string | null;
71
+ isAutomated: boolean;
72
+ meta: unknown;
73
+ previousStatus: string | null;
74
+ publicNote: string | null;
75
+ reportId: number;
76
+ }[];
77
+ cursor: string | undefined;
78
+ }>;
62
79
  export declare function formatActivityView(activity: {
63
80
  id: number;
64
81
  reportId: number;
@@ -70,7 +87,7 @@ export declare function formatActivityView(activity: {
70
87
  isAutomated: boolean;
71
88
  createdBy: string;
72
89
  createdAt: string;
73
- }, memberViews?: Map<string, Member>): {
90
+ }, memberViews?: Map<string, Member>, reportViews?: Map<number, ReportView>): {
74
91
  id: number;
75
92
  reportId: number;
76
93
  activity: {
@@ -83,6 +100,7 @@ export declare function formatActivityView(activity: {
83
100
  isAutomated: boolean;
84
101
  createdBy: string;
85
102
  moderator: Member | undefined;
103
+ report: ReportView | undefined;
86
104
  createdAt: string;
87
105
  };
88
106
  //# sourceMappingURL=activity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/report/activity.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAOlE,MAAM,MAAM,YAAY,GACpB,eAAe,GACf,oBAAoB,GACpB,oBAAoB,GACpB,eAAe,GACf,gBAAgB,GAChB,cAAc,CAAA;AAElB,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,YAAY,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,oBAAoB;;;;;;;;;;;GAkF7B;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,QAAQ,EACZ,UAAU,EAAE,kBAAkB,EAAE,iBAmBjC;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,oBAAoB;;;;;;;;;;;;;;GA6B7B;AAaD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE;IACR,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,EACD,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAG/B,EAAE;IACF,QAAQ;IACR,QAAQ;;eA1BA,MAAM;;IA8Bd,YAAY;IACZ,UAAU;IACV,IAAI;IACJ,WAAW;IACX,SAAS;IACT,SAAS;IACT,SAAS;EAEZ"}
1
+ {"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/report/activity.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,6CAA6C,CAAA;AACxE,OAAO,EAAE,WAAW,IAAI,qBAAqB,EAAE,MAAM,wDAAwD,CAAA;AAC7G,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAOlE,MAAM,MAAM,YAAY,GACpB,eAAe,GACf,oBAAoB,GACpB,oBAAoB,GACpB,eAAe,GACf,gBAAgB,GAChB,cAAc,CAAA;AAElB,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,YAAY,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,oBAAoB;;;;;;;;;;;GAkF7B;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,QAAQ,EACZ,UAAU,EAAE,kBAAkB,EAAE,iBAmBjC;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,oBAAoB;;;;;;;;;;;;;;GA6B7B;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,qBAAqB;;;;;;;;;;;;;;GAsC9B;AAaD,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE;IACR,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,EAAE,OAAO,CAAA;IACb,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,EACD,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC;IAGnC,EAAE;IACF,QAAQ;IACR,QAAQ;;eA3BA,MAAM;;IA+Bd,YAAY;IACZ,UAAU;IACV,IAAI;IACJ,WAAW;IACX,SAAS;IACT,SAAS;IACT,MAAM;IACN,SAAS;EAEZ"}
@@ -1,4 +1,5 @@
1
1
  import { InvalidRequestError } from '@atproto/xrpc-server';
2
+ import { TimeIdKeyset, paginate } from '../db/pagination.js';
2
3
  import { AlreadyInTargetState, InvalidStateTransition, handleReportUpdate, } from './handle-report-update.js';
3
4
  export async function createReportActivity(db, params) {
4
5
  const { reportId, activityType, internalNote, publicNote, meta, isAutomated = false, createdBy, } = params;
@@ -111,6 +112,30 @@ export async function listReportActivities(db, params) {
111
112
  : undefined;
112
113
  return { activities, cursor: nextCursor };
113
114
  }
115
+ export async function queryReportActivities(db, params) {
116
+ const { activityTypes, createdAfter, createdBefore, sortDirection, limit, cursor, } = params;
117
+ const { ref } = db.db.dynamic;
118
+ let builder = db.db.selectFrom('report_activity').selectAll();
119
+ if (activityTypes && activityTypes.length > 0) {
120
+ builder = builder.where('activityType', 'in', activityTypes);
121
+ }
122
+ if (createdAfter) {
123
+ builder = builder.where('createdAt', '>=', createdAfter);
124
+ }
125
+ if (createdBefore) {
126
+ builder = builder.where('createdAt', '<=', createdBefore);
127
+ }
128
+ const keyset = new TimeIdKeyset(ref('report_activity.createdAt'), ref('report_activity.id'));
129
+ const paginatedBuilder = paginate(builder, {
130
+ limit,
131
+ cursor,
132
+ keyset,
133
+ direction: sortDirection,
134
+ tryIndex: true,
135
+ });
136
+ const activities = await paginatedBuilder.execute();
137
+ return { activities, cursor: keyset.packFromResult(activities) };
138
+ }
114
139
  function buildActivityObject(activityType, previousStatus) {
115
140
  const $type = `tools.ozone.report.defs#${activityType}`;
116
141
  if (previousStatus !== null) {
@@ -118,7 +143,7 @@ function buildActivityObject(activityType, previousStatus) {
118
143
  }
119
144
  return { $type };
120
145
  }
121
- export function formatActivityView(activity, memberViews) {
146
+ export function formatActivityView(activity, memberViews, reportViews) {
122
147
  return {
123
148
  id: activity.id,
124
149
  reportId: activity.reportId,
@@ -129,6 +154,7 @@ export function formatActivityView(activity, memberViews) {
129
154
  isAutomated: activity.isAutomated,
130
155
  createdBy: activity.createdBy,
131
156
  moderator: memberViews?.get(activity.createdBy),
157
+ report: reportViews?.get(activity.reportId),
132
158
  createdAt: activity.createdAt,
133
159
  };
134
160
  }
@@ -1 +1 @@
1
- {"version":3,"file":"activity.js","sourceRoot":"","sources":["../../src/report/activity.ts"],"names":[],"mappings":"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","sourcesContent":["import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { Database } from '../db/index.js'\nimport { Member } from '../lexicon/types/tools/ozone/team/defs.js'\nimport {\n AlreadyInTargetState,\n InvalidStateTransition,\n handleReportUpdate,\n} from './handle-report-update.js'\n\nexport type ActivityType =\n | 'queueActivity'\n | 'assignmentActivity'\n | 'escalationActivity'\n | 'closeActivity'\n | 'reopenActivity'\n | 'noteActivity'\n\nexport type CreateActivityParams = {\n reportId: number\n activityType: ActivityType\n internalNote?: string\n publicNote?: string\n meta?: Record<string, unknown>\n /** Set true for activities created by automated processes (e.g. queue router). */\n isAutomated?: boolean\n createdBy: string\n}\n\nexport async function createReportActivity(\n db: Database,\n params: CreateActivityParams,\n) {\n const {\n reportId,\n activityType,\n internalNote,\n publicNote,\n meta,\n isAutomated = false,\n createdBy,\n } = params\n\n return db.transaction(async (dbTxn) => {\n // Lock the report row for the duration of the transaction to prevent\n // concurrent writes from racing on status validation + update.\n const report = await dbTxn.db\n .selectFrom('report')\n .select(['id', 'status'])\n .where('id', '=', reportId)\n .forUpdate()\n .executeTakeFirst()\n\n if (!report) {\n throw new InvalidRequestError(\n `Report ${reportId} not found`,\n 'ReportNotFound',\n )\n }\n\n let result\n try {\n result = handleReportUpdate(report.status, {\n type: 'activity',\n activityType,\n })\n } catch (err) {\n if (err instanceof AlreadyInTargetState) {\n throw new InvalidRequestError(err.message, 'AlreadyInTargetState')\n }\n if (err instanceof InvalidStateTransition) {\n throw new InvalidRequestError(err.message, 'InvalidStateTransition')\n }\n throw err\n }\n\n const now = new Date().toISOString()\n\n if (result.nextStatus !== null) {\n const updateSet: Record<string, string | null> = {\n status: result.nextStatus,\n updatedAt: now,\n }\n if (result.nextStatus === 'closed') {\n updateSet.closedAt = now\n } else if (result.nextStatus === 'open') {\n updateSet.closedAt = null\n }\n await dbTxn.db\n .updateTable('report')\n .set(updateSet)\n .where('id', '=', reportId)\n .execute()\n }\n\n const [activity] = await dbTxn.db\n .insertInto('report_activity')\n .values({\n reportId,\n activityType,\n previousStatus: result.activity?.previousStatus ?? null,\n internalNote: internalNote ?? null,\n publicNote: publicNote ?? null,\n meta: meta ?? null,\n isAutomated,\n createdBy,\n createdAt: now,\n })\n .returningAll()\n .execute()\n\n return activity\n })\n}\n\nexport type BulkActivityInsert = {\n reportId: number\n activityType: string\n previousStatus: string | null\n internalNote?: string\n publicNote?: string\n meta?: unknown\n isAutomated: boolean\n createdBy: string\n createdAt: string\n}\n\n/**\n * Insert multiple activity rows in a single query. No validation — caller is\n * responsible for correctness and for being inside an appropriate transaction.\n */\nexport async function bulkInsertReportActivities(\n db: Database,\n activities: BulkActivityInsert[],\n) {\n if (!activities.length) return\n await db.db\n .insertInto('report_activity')\n .values(\n activities.map((a) => ({\n reportId: a.reportId,\n activityType: a.activityType,\n previousStatus: a.previousStatus,\n internalNote: a.internalNote ?? null,\n publicNote: a.publicNote ?? null,\n meta: a.meta ?? null,\n isAutomated: a.isAutomated,\n createdBy: a.createdBy,\n createdAt: a.createdAt,\n })),\n )\n .execute()\n}\n\nexport type ListActivitiesParams = {\n reportId: number\n limit?: number\n cursor?: string\n}\n\nexport async function listReportActivities(\n db: Database,\n params: ListActivitiesParams,\n) {\n const { reportId, limit = 50, cursor } = params\n\n let builder = db.db\n .selectFrom('report_activity')\n .selectAll()\n .where('reportId', '=', reportId)\n .orderBy('createdAt', 'desc')\n .orderBy('id', 'desc')\n .limit(limit + 1)\n\n if (cursor) {\n const cursorId = parseInt(cursor, 10)\n if (!isNaN(cursorId)) {\n builder = builder.where('id', '<', cursorId)\n }\n }\n\n const rows = await builder.execute()\n const hasMore = rows.length > limit\n const activities = hasMore ? rows.slice(0, limit) : rows\n\n const nextCursor =\n hasMore && activities.length > 0\n ? String(activities[activities.length - 1].id)\n : undefined\n\n return { activities, cursor: nextCursor }\n}\n\nfunction buildActivityObject(\n activityType: string,\n previousStatus: string | null,\n): { $type: string; [k: string]: unknown } {\n const $type = `tools.ozone.report.defs#${activityType}`\n if (previousStatus !== null) {\n return { $type, previousStatus }\n }\n return { $type }\n}\n\nexport function formatActivityView(\n activity: {\n id: number\n reportId: number\n activityType: string\n previousStatus: string | null\n internalNote: string | null\n publicNote: string | null\n meta: unknown\n isAutomated: boolean\n createdBy: string\n createdAt: string\n },\n memberViews?: Map<string, Member>,\n) {\n return {\n id: activity.id,\n reportId: activity.reportId,\n activity: buildActivityObject(\n activity.activityType,\n activity.previousStatus,\n ),\n internalNote: activity.internalNote ?? undefined,\n publicNote: activity.publicNote ?? undefined,\n meta: (activity.meta as Record<string, unknown>) ?? undefined,\n isAutomated: activity.isAutomated,\n createdBy: activity.createdBy,\n moderator: memberViews?.get(activity.createdBy),\n createdAt: activity.createdAt,\n }\n}\n"]}
1
+ {"version":3,"file":"activity.js","sourceRoot":"","sources":["../../src/report/activity.ts"],"names":[],"mappings":"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","sourcesContent":["import { InvalidRequestError } from '@atproto/xrpc-server'\nimport { Database } from '../db/index.js'\nimport { TimeIdKeyset, paginate } from '../db/pagination.js'\nimport { ReportView } from '../lexicon/types/tools/ozone/report/defs.js'\nimport { QueryParams as QueryActivitiesParams } from '../lexicon/types/tools/ozone/report/queryActivities.js'\nimport { Member } from '../lexicon/types/tools/ozone/team/defs.js'\nimport {\n AlreadyInTargetState,\n InvalidStateTransition,\n handleReportUpdate,\n} from './handle-report-update.js'\n\nexport type ActivityType =\n | 'queueActivity'\n | 'assignmentActivity'\n | 'escalationActivity'\n | 'closeActivity'\n | 'reopenActivity'\n | 'noteActivity'\n\nexport type CreateActivityParams = {\n reportId: number\n activityType: ActivityType\n internalNote?: string\n publicNote?: string\n meta?: Record<string, unknown>\n /** Set true for activities created by automated processes (e.g. queue router). */\n isAutomated?: boolean\n createdBy: string\n}\n\nexport async function createReportActivity(\n db: Database,\n params: CreateActivityParams,\n) {\n const {\n reportId,\n activityType,\n internalNote,\n publicNote,\n meta,\n isAutomated = false,\n createdBy,\n } = params\n\n return db.transaction(async (dbTxn) => {\n // Lock the report row for the duration of the transaction to prevent\n // concurrent writes from racing on status validation + update.\n const report = await dbTxn.db\n .selectFrom('report')\n .select(['id', 'status'])\n .where('id', '=', reportId)\n .forUpdate()\n .executeTakeFirst()\n\n if (!report) {\n throw new InvalidRequestError(\n `Report ${reportId} not found`,\n 'ReportNotFound',\n )\n }\n\n let result\n try {\n result = handleReportUpdate(report.status, {\n type: 'activity',\n activityType,\n })\n } catch (err) {\n if (err instanceof AlreadyInTargetState) {\n throw new InvalidRequestError(err.message, 'AlreadyInTargetState')\n }\n if (err instanceof InvalidStateTransition) {\n throw new InvalidRequestError(err.message, 'InvalidStateTransition')\n }\n throw err\n }\n\n const now = new Date().toISOString()\n\n if (result.nextStatus !== null) {\n const updateSet: Record<string, string | null> = {\n status: result.nextStatus,\n updatedAt: now,\n }\n if (result.nextStatus === 'closed') {\n updateSet.closedAt = now\n } else if (result.nextStatus === 'open') {\n updateSet.closedAt = null\n }\n await dbTxn.db\n .updateTable('report')\n .set(updateSet)\n .where('id', '=', reportId)\n .execute()\n }\n\n const [activity] = await dbTxn.db\n .insertInto('report_activity')\n .values({\n reportId,\n activityType,\n previousStatus: result.activity?.previousStatus ?? null,\n internalNote: internalNote ?? null,\n publicNote: publicNote ?? null,\n meta: meta ?? null,\n isAutomated,\n createdBy,\n createdAt: now,\n })\n .returningAll()\n .execute()\n\n return activity\n })\n}\n\nexport type BulkActivityInsert = {\n reportId: number\n activityType: string\n previousStatus: string | null\n internalNote?: string\n publicNote?: string\n meta?: unknown\n isAutomated: boolean\n createdBy: string\n createdAt: string\n}\n\n/**\n * Insert multiple activity rows in a single query. No validation — caller is\n * responsible for correctness and for being inside an appropriate transaction.\n */\nexport async function bulkInsertReportActivities(\n db: Database,\n activities: BulkActivityInsert[],\n) {\n if (!activities.length) return\n await db.db\n .insertInto('report_activity')\n .values(\n activities.map((a) => ({\n reportId: a.reportId,\n activityType: a.activityType,\n previousStatus: a.previousStatus,\n internalNote: a.internalNote ?? null,\n publicNote: a.publicNote ?? null,\n meta: a.meta ?? null,\n isAutomated: a.isAutomated,\n createdBy: a.createdBy,\n createdAt: a.createdAt,\n })),\n )\n .execute()\n}\n\nexport type ListActivitiesParams = {\n reportId: number\n limit?: number\n cursor?: string\n}\n\nexport async function listReportActivities(\n db: Database,\n params: ListActivitiesParams,\n) {\n const { reportId, limit = 50, cursor } = params\n\n let builder = db.db\n .selectFrom('report_activity')\n .selectAll()\n .where('reportId', '=', reportId)\n .orderBy('createdAt', 'desc')\n .orderBy('id', 'desc')\n .limit(limit + 1)\n\n if (cursor) {\n const cursorId = parseInt(cursor, 10)\n if (!isNaN(cursorId)) {\n builder = builder.where('id', '<', cursorId)\n }\n }\n\n const rows = await builder.execute()\n const hasMore = rows.length > limit\n const activities = hasMore ? rows.slice(0, limit) : rows\n\n const nextCursor =\n hasMore && activities.length > 0\n ? String(activities[activities.length - 1].id)\n : undefined\n\n return { activities, cursor: nextCursor }\n}\n\nexport async function queryReportActivities(\n db: Database,\n params: QueryActivitiesParams,\n) {\n const {\n activityTypes,\n createdAfter,\n createdBefore,\n sortDirection,\n limit,\n cursor,\n } = params\n const { ref } = db.db.dynamic\n\n let builder = db.db.selectFrom('report_activity').selectAll()\n\n if (activityTypes && activityTypes.length > 0) {\n builder = builder.where('activityType', 'in', activityTypes)\n }\n if (createdAfter) {\n builder = builder.where('createdAt', '>=', createdAfter)\n }\n if (createdBefore) {\n builder = builder.where('createdAt', '<=', createdBefore)\n }\n\n const keyset = new TimeIdKeyset(\n ref('report_activity.createdAt'),\n ref('report_activity.id'),\n )\n const paginatedBuilder = paginate(builder, {\n limit,\n cursor,\n keyset,\n direction: sortDirection,\n tryIndex: true,\n })\n\n const activities = await paginatedBuilder.execute()\n return { activities, cursor: keyset.packFromResult(activities) }\n}\n\nfunction buildActivityObject(\n activityType: string,\n previousStatus: string | null,\n): { $type: string; [k: string]: unknown } {\n const $type = `tools.ozone.report.defs#${activityType}`\n if (previousStatus !== null) {\n return { $type, previousStatus }\n }\n return { $type }\n}\n\nexport function formatActivityView(\n activity: {\n id: number\n reportId: number\n activityType: string\n previousStatus: string | null\n internalNote: string | null\n publicNote: string | null\n meta: unknown\n isAutomated: boolean\n createdBy: string\n createdAt: string\n },\n memberViews?: Map<string, Member>,\n reportViews?: Map<number, ReportView>,\n) {\n return {\n id: activity.id,\n reportId: activity.reportId,\n activity: buildActivityObject(\n activity.activityType,\n activity.previousStatus,\n ),\n internalNote: activity.internalNote ?? undefined,\n publicNote: activity.publicNote ?? undefined,\n meta: (activity.meta as Record<string, unknown>) ?? undefined,\n isAutomated: activity.isAutomated,\n createdBy: activity.createdBy,\n moderator: memberViews?.get(activity.createdBy),\n report: reportViews?.get(activity.reportId),\n createdAt: activity.createdAt,\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/ozone",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "license": "MIT",
5
5
  "description": "Backend service for moderating the Bluesky network.",
6
6
  "keywords": [
@@ -31,16 +31,16 @@
31
31
  "structured-headers": "^1.0.1",
32
32
  "typed-emitter": "^2.1.0",
33
33
  "uint8arrays": "^5.0.0",
34
- "undici": "^6.14.1",
34
+ "undici": "^8.5.0",
35
35
  "ws": "^8.12.0",
36
- "@atproto/api": "^0.20.16",
37
- "@atproto/common": "^0.6.3",
38
- "@atproto/identity": "^0.5.1",
36
+ "@atproto/api": "^0.20.17",
39
37
  "@atproto/crypto": "^0.5.1",
40
- "@atproto/lexicon": "^0.7.2",
38
+ "@atproto/identity": "^0.5.1",
41
39
  "@atproto/syntax": "^0.6.2",
42
- "@atproto/ws-client": "^0.1.1",
40
+ "@atproto/ws-client": "^0.1.2",
43
41
  "@atproto/xrpc": "^0.8.1",
42
+ "@atproto/common": "^0.6.3",
43
+ "@atproto/lexicon": "^0.7.2",
44
44
  "@atproto/xrpc-server": "^0.11.2"
45
45
  },
46
46
  "devDependencies": {
@@ -51,10 +51,11 @@
51
51
  "@types/express-serve-static-core": "^4.17.36",
52
52
  "@types/pg": "^8.15.5",
53
53
  "@types/qs": "^6.9.7",
54
+ "@types/ws": "^8.18.1",
54
55
  "jest": "^30.0.0",
55
56
  "ts-node": "^10.8.2",
56
57
  "@atproto/lex-cli": "^0.10.1",
57
- "@atproto/pds": "^0.5.6"
58
+ "@atproto/pds": "^0.5.7"
58
59
  },
59
60
  "type": "module",
60
61
  "exports": {
package/src/api/index.ts CHANGED
@@ -41,6 +41,7 @@ import getLatestReport from './report/getLatestReport.js'
41
41
  import getLiveStats from './report/getLiveStats.js'
42
42
  import getReport from './report/getReport.js'
43
43
  import listActivities from './report/listActivities.js'
44
+ import queryActivities from './report/queryActivities.js'
44
45
  import queryReports from './report/queryReports.js'
45
46
  import reassignQueue from './report/reassignQueue.js'
46
47
  import refreshStats from './report/refreshStats.js'
@@ -139,6 +140,7 @@ export default function (server: Server, ctx: AppContext) {
139
140
  getReportAssignments(server, ctx)
140
141
  createActivity(server, ctx)
141
142
  listActivities(server, ctx)
143
+ queryActivities(server, ctx)
142
144
  reassignQueue(server, ctx)
143
145
  return server
144
146
  }
@@ -0,0 +1,64 @@
1
+ import { AppContext } from '../../context.js'
2
+ import { Server } from '../../lexicon/index.js'
3
+ import { ReportView } from '../../lexicon/types/tools/ozone/report/defs.js'
4
+ import { getReportsByIds } from '../../mod-service/report.js'
5
+ import {
6
+ formatActivityView,
7
+ queryReportActivities,
8
+ } from '../../report/activity.js'
9
+ import { buildReportView, hydrateReportInfo } from '../../report/views.js'
10
+ import { getPdsAccountInfos } from '../util.js'
11
+
12
+ export default function (server: Server, ctx: AppContext) {
13
+ server.tools.ozone.report.queryActivities({
14
+ auth: ctx.authVerifier.modOrAdminToken,
15
+ handler: async ({ params, auth, req }) => {
16
+ const db = ctx.db
17
+ const modService = ctx.modService(db)
18
+ const labelers = ctx.reqLabelers(req)
19
+
20
+ const { activities, cursor: nextCursor } = await queryReportActivities(
21
+ db,
22
+ params,
23
+ )
24
+
25
+ // Dedupe report IDs across the page. Many activities can share the
26
+ // same report so we want one bulk fetch + hydrate rather than N.
27
+ const reportIds = Array.from(new Set(activities.map((a) => a.reportId)))
28
+
29
+ const queueService = ctx.queueService(db)
30
+ const teamService = ctx.teamService(db)
31
+ const reports = await getReportsByIds(db, reportIds)
32
+ const hydrated = await hydrateReportInfo(
33
+ reports,
34
+ modService.views,
35
+ (dids) => getPdsAccountInfos(ctx, dids),
36
+ (queueIds) => queueService.getViewsByIds(queueIds),
37
+ (dids) => teamService.viewByDids(dids),
38
+ labelers,
39
+ )
40
+ const reportViews = new Map<number, ReportView>()
41
+ for (const report of reports) {
42
+ reportViews.set(
43
+ report.id,
44
+ buildReportView(report, hydrated, auth.credentials.isModerator),
45
+ )
46
+ }
47
+
48
+ const createdByDids = Array.from(
49
+ new Set(activities.map((a) => a.createdBy)),
50
+ )
51
+ const memberViews = await teamService.viewByDids(createdByDids)
52
+
53
+ return {
54
+ encoding: 'application/json',
55
+ body: {
56
+ activities: activities.map((activity) =>
57
+ formatActivityView(activity, memberViews, reportViews),
58
+ ),
59
+ cursor: nextCursor,
60
+ },
61
+ }
62
+ },
63
+ })
64
+ }
package/src/background.ts CHANGED
@@ -9,9 +9,19 @@ import {
9
9
 
10
10
  type Task = (db: Database, signal: AbortSignal) => Promise<void>
11
11
 
12
+ export type BackgroundQueueOptions = NonNullable<
13
+ ConstructorParameters<typeof PQueue>[0]
14
+ > & {
15
+ concurrency: number
16
+ }
17
+
12
18
  /**
13
19
  * A simple queue for in-process, out-of-band/backgrounded work
14
20
  */
21
+ // @NOTE Keep this in sync with the BackgroundQueue in
22
+ // - packages/bsky/src/data-plane/server/background.ts
23
+ // - packages/ozone/src/background.ts
24
+ // - packages/pds/src/background.ts
15
25
  export class BackgroundQueue {
16
26
  private abortController = new AbortController()
17
27
  private queue: PQueue
@@ -26,9 +36,9 @@ export class BackgroundQueue {
26
36
 
27
37
  constructor(
28
38
  protected db: Database,
29
- queueOpts?: { concurrency?: number },
39
+ options: BackgroundQueueOptions,
30
40
  ) {
31
- this.queue = new PQueue(queueOpts ?? { concurrency: 20 })
41
+ this.queue = new PQueue(options)
32
42
  }
33
43
 
34
44
  getStats() {
@@ -76,7 +86,8 @@ export class BackgroundQueue {
76
86
  }
77
87
 
78
88
  async processAll() {
79
- await this.queue.onIdle()
89
+ const { queue } = this
90
+ while (queue.size || queue.pending) await queue.onIdle()
80
91
  }
81
92
 
82
93
  /**
@@ -86,8 +97,12 @@ export class BackgroundQueue {
86
97
  * only once http connections have drained (tasks no longer being added).
87
98
  */
88
99
  async destroy() {
100
+ if (this.destroyed) {
101
+ dbLogger.warn('BackgroundQueue.destroy() called multiple times')
102
+ }
103
+
89
104
  this.abortController.abort()
90
- await this.queue.onIdle()
105
+ return this.processAll()
91
106
  }
92
107
  }
93
108
 
package/src/context.ts CHANGED
@@ -135,7 +135,7 @@ export class AppContext {
135
135
  keypair: signingKey,
136
136
  })
137
137
 
138
- const backgroundQueue = new BackgroundQueue(db)
138
+ const backgroundQueue = new BackgroundQueue(db, { concurrency: 20 })
139
139
  const blobDiverter = cfg.blobDivert
140
140
  ? new BlobDiverter(db, {
141
141
  idResolver,
@@ -73,7 +73,7 @@ export class DaemonContext {
73
73
  pds: cfg.pds ?? undefined,
74
74
  })
75
75
 
76
- const backgroundQueue = new BackgroundQueue(db)
76
+ const backgroundQueue = new BackgroundQueue(db, { concurrency: 20 })
77
77
 
78
78
  const settingService = SettingService.creator()
79
79
  const strikeService = StrikeService.creator()
@@ -156,9 +156,14 @@ export class VerificationListener {
156
156
  })
157
157
  }
158
158
 
159
- stop() {
160
- this.jetstream?.close()
161
- this.backgroundQueue.destroy()
162
- this.destroyed = true
159
+ async stop() {
160
+ if (!this.destroyed) {
161
+ this.destroyed = true
162
+ try {
163
+ await this.jetstream?.close()
164
+ } finally {
165
+ await this.backgroundQueue.destroy()
166
+ }
167
+ }
163
168
  }
164
169
  }
@@ -0,0 +1,17 @@
1
+ import { Kysely } from 'kysely'
2
+
3
+ export async function up(db: Kysely<unknown>): Promise<void> {
4
+ // Supports time-ordered scans across all reports for downstream pollers
5
+ // (e.g. Nimbus' report-activity watcher). The existing indexes are all
6
+ // leading-`reportId`, which would force a sequential scan for global
7
+ // ordered queries.
8
+ await db.schema
9
+ .createIndex('idx_report_activity_created')
10
+ .on('report_activity')
11
+ .columns(['createdAt', 'id'])
12
+ .execute()
13
+ }
14
+
15
+ export async function down(db: Kysely<unknown>): Promise<void> {
16
+ await db.schema.dropIndex('idx_report_activity_created').execute()
17
+ }
@@ -41,3 +41,4 @@ export * as _20260313T000000000Z from './20260313T000000000Z-add-report-activity
41
41
  export * as _20260318T152058935Z from './20260318T152058935Z-add-report-stat.js'
42
42
  export * as _20260428T000000000Z from './20260428T000000000Z-add-expiring-tag-table.js'
43
43
  export * as _20260513T202941104Z from './20260513T202941104Z-add-subject-convo-id.js'
44
+ export * as _20260602T120000000Z from './20260602T120000000Z-add-report-activity-created-index.js'
package/src/index.ts CHANGED
@@ -4,11 +4,8 @@ import { AddressInfo } from 'node:net'
4
4
  import compression from 'compression'
5
5
  import cors from 'cors'
6
6
  import express from 'express'
7
- // eslint-disable-next-line import/default, import/no-named-as-default-member
7
+ // eslint-disable-next-line import/default
8
8
  import httpTerminator from 'http-terminator'
9
- // eslint-disable-next-line import/no-named-as-default-member
10
- const { createHttpTerminator } = httpTerminator
11
- type HttpTerminator = ReturnType<typeof createHttpTerminator>
12
9
  import { DAY, SECOND } from '@atproto/common'
13
10
  import API, { health, wellKnown } from './api/index.js'
14
11
  import { OzoneConfig, OzoneSecrets } from './config/index.js'
@@ -29,7 +26,7 @@ export class OzoneService {
29
26
  public ctx: AppContext
30
27
  public app: express.Application
31
28
  public server?: http.Server
32
- private terminator?: HttpTerminator
29
+ private terminator?: httpTerminator.HttpTerminator
33
30
  private dbStatsInterval?: NodeJS.Timeout
34
31
 
35
32
  constructor(opts: { ctx: AppContext; app: express.Application }) {
@@ -108,23 +105,25 @@ export class OzoneService {
108
105
  // so we need to sync them from env var to the database
109
106
  await this.seedInitialMembers()
110
107
 
111
- const { db, backgroundQueue } = this.ctx
112
108
  this.dbStatsInterval = setInterval(() => {
113
109
  dbLogger.info(
114
110
  {
115
- idleCount: db.pool.idleCount,
116
- totalCount: db.pool.totalCount,
117
- waitingCount: db.pool.waitingCount,
111
+ idleCount: this.ctx.db.pool.idleCount,
112
+ totalCount: this.ctx.db.pool.totalCount,
113
+ waitingCount: this.ctx.db.pool.waitingCount,
118
114
  },
119
115
  'db pool stats',
120
116
  )
121
- dbLogger.info(backgroundQueue.getStats(), 'background queue stats')
117
+ dbLogger.info(
118
+ this.ctx.backgroundQueue.getStats(),
119
+ 'background queue stats',
120
+ )
122
121
  }, 10000)
123
122
  await this.ctx.sequencer.start()
124
123
  const server = this.app.listen(this.ctx.cfg.service.port)
125
124
  this.server = server
126
125
  server.keepAliveTimeout = 90000
127
- this.terminator = createHttpTerminator({ server })
126
+ this.terminator = httpTerminator.createHttpTerminator({ server })
128
127
  await events.once(server, 'listening')
129
128
  const { port } = server.address() as AddressInfo
130
129
  this.ctx.assignPort(port)
@@ -132,12 +131,23 @@ export class OzoneService {
132
131
  }
133
132
 
134
133
  async destroy(): Promise<void> {
135
- await this.terminator?.terminate()
136
- await this.ctx.backgroundQueue.destroy()
137
- await this.ctx.sequencer.destroy()
138
- await this.ctx.db.close()
139
134
  clearInterval(this.dbStatsInterval)
140
135
  this.dbStatsInterval = undefined
136
+
137
+ // @TODO Use a disposable stack when Node24 becomes the min supported version
138
+ try {
139
+ await this.terminator?.terminate()
140
+ } finally {
141
+ try {
142
+ await this.ctx.backgroundQueue.destroy()
143
+ } finally {
144
+ try {
145
+ await this.ctx.sequencer.destroy()
146
+ } finally {
147
+ await this.ctx.db.close()
148
+ }
149
+ }
150
+ }
141
151
  }
142
152
  }
143
153
 
@@ -99,7 +99,9 @@ export class Jetstream {
99
99
  /**
100
100
  * Closes the WebSocket connection.
101
101
  */
102
- close() {
102
+ async close() {
103
+ // @TODO This should return a promise that fulfills when the connection is
104
+ // fully closed.
103
105
  this.ws?.ws?.close()
104
106
  }
105
107
  }
@@ -175,6 +175,25 @@ export async function getReportById(
175
175
  .executeTakeFirst()
176
176
  }
177
177
 
178
+ export async function getReportsByIds(
179
+ db: Database,
180
+ ids: number[],
181
+ ): Promise<ReportWithEvent[]> {
182
+ if (!ids.length) return []
183
+ return reportQuery(db)
184
+ .where('r.id', 'in', ids)
185
+ .selectAll('r')
186
+ .select([
187
+ 'me.subjectDid',
188
+ 'me.subjectUri',
189
+ 'me.subjectCid',
190
+ 'me.createdBy as reportedBy',
191
+ 'me.comment',
192
+ 'me.meta',
193
+ ])
194
+ .execute()
195
+ }
196
+
178
197
  export async function getLatestReport(
179
198
  db: Database,
180
199
  ): Promise<ReportWithEvent | undefined> {
@@ -1,5 +1,8 @@
1
1
  import { InvalidRequestError } from '@atproto/xrpc-server'
2
2
  import { Database } from '../db/index.js'
3
+ import { TimeIdKeyset, paginate } from '../db/pagination.js'
4
+ import { ReportView } from '../lexicon/types/tools/ozone/report/defs.js'
5
+ import { QueryParams as QueryActivitiesParams } from '../lexicon/types/tools/ozone/report/queryActivities.js'
3
6
  import { Member } from '../lexicon/types/tools/ozone/team/defs.js'
4
7
  import {
5
8
  AlreadyInTargetState,
@@ -190,6 +193,48 @@ export async function listReportActivities(
190
193
  return { activities, cursor: nextCursor }
191
194
  }
192
195
 
196
+ export async function queryReportActivities(
197
+ db: Database,
198
+ params: QueryActivitiesParams,
199
+ ) {
200
+ const {
201
+ activityTypes,
202
+ createdAfter,
203
+ createdBefore,
204
+ sortDirection,
205
+ limit,
206
+ cursor,
207
+ } = params
208
+ const { ref } = db.db.dynamic
209
+
210
+ let builder = db.db.selectFrom('report_activity').selectAll()
211
+
212
+ if (activityTypes && activityTypes.length > 0) {
213
+ builder = builder.where('activityType', 'in', activityTypes)
214
+ }
215
+ if (createdAfter) {
216
+ builder = builder.where('createdAt', '>=', createdAfter)
217
+ }
218
+ if (createdBefore) {
219
+ builder = builder.where('createdAt', '<=', createdBefore)
220
+ }
221
+
222
+ const keyset = new TimeIdKeyset(
223
+ ref('report_activity.createdAt'),
224
+ ref('report_activity.id'),
225
+ )
226
+ const paginatedBuilder = paginate(builder, {
227
+ limit,
228
+ cursor,
229
+ keyset,
230
+ direction: sortDirection,
231
+ tryIndex: true,
232
+ })
233
+
234
+ const activities = await paginatedBuilder.execute()
235
+ return { activities, cursor: keyset.packFromResult(activities) }
236
+ }
237
+
193
238
  function buildActivityObject(
194
239
  activityType: string,
195
240
  previousStatus: string | null,
@@ -215,6 +260,7 @@ export function formatActivityView(
215
260
  createdAt: string
216
261
  },
217
262
  memberViews?: Map<string, Member>,
263
+ reportViews?: Map<number, ReportView>,
218
264
  ) {
219
265
  return {
220
266
  id: activity.id,
@@ -229,6 +275,7 @@ export function formatActivityView(
229
275
  isAutomated: activity.isAutomated,
230
276
  createdBy: activity.createdBy,
231
277
  moderator: memberViews?.get(activity.createdBy),
278
+ report: reportViews?.get(activity.reportId),
232
279
  createdAt: activity.createdAt,
233
280
  }
234
281
  }
@@ -53,8 +53,8 @@ describe('labels from 3p labelers', () => {
53
53
  })
54
54
 
55
55
  afterAll(async () => {
56
- await network.close()
57
- await thirdPartyLabeler.close()
56
+ await network?.close()
57
+ await thirdPartyLabeler?.close()
58
58
  })
59
59
 
60
60
  const getPostSubject = () => ({