@atproto/ozone 0.1.53 → 0.1.55

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 (137) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +6 -0
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/moderation/queryStatuses.d.ts.map +1 -1
  6. package/dist/api/moderation/queryStatuses.js +6 -1
  7. package/dist/api/moderation/queryStatuses.js.map +1 -1
  8. package/dist/api/setting/listOptions.d.ts +4 -0
  9. package/dist/api/setting/listOptions.d.ts.map +1 -0
  10. package/dist/api/setting/listOptions.js +38 -0
  11. package/dist/api/setting/listOptions.js.map +1 -0
  12. package/dist/api/setting/removeOptions.d.ts +4 -0
  13. package/dist/api/setting/removeOptions.d.ts.map +1 -0
  14. package/dist/api/setting/removeOptions.js +55 -0
  15. package/dist/api/setting/removeOptions.js.map +1 -0
  16. package/dist/api/setting/upsertOption.d.ts +4 -0
  17. package/dist/api/setting/upsertOption.d.ts.map +1 -0
  18. package/dist/api/setting/upsertOption.js +109 -0
  19. package/dist/api/setting/upsertOption.js.map +1 -0
  20. package/dist/context.d.ts +3 -0
  21. package/dist/context.d.ts.map +1 -1
  22. package/dist/context.js +6 -0
  23. package/dist/context.js.map +1 -1
  24. package/dist/db/migrations/20241018T205730722Z-setting.d.ts +4 -0
  25. package/dist/db/migrations/20241018T205730722Z-setting.d.ts.map +1 -0
  26. package/dist/db/migrations/20241018T205730722Z-setting.js +26 -0
  27. package/dist/db/migrations/20241018T205730722Z-setting.js.map +1 -0
  28. package/dist/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.d.ts +4 -0
  29. package/dist/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.d.ts.map +1 -0
  30. package/dist/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.js +57 -0
  31. package/dist/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.js.map +1 -0
  32. package/dist/db/migrations/index.d.ts +2 -0
  33. package/dist/db/migrations/index.d.ts.map +1 -1
  34. package/dist/db/migrations/index.js +3 -1
  35. package/dist/db/migrations/index.js.map +1 -1
  36. package/dist/db/schema/index.d.ts +2 -1
  37. package/dist/db/schema/index.d.ts.map +1 -1
  38. package/dist/db/schema/moderation_event.d.ts +1 -1
  39. package/dist/db/schema/moderation_event.d.ts.map +1 -1
  40. package/dist/db/schema/moderation_subject_status.d.ts +6 -0
  41. package/dist/db/schema/moderation_subject_status.d.ts.map +1 -1
  42. package/dist/db/schema/setting.d.ts +21 -0
  43. package/dist/db/schema/setting.d.ts.map +1 -0
  44. package/dist/db/schema/setting.js +5 -0
  45. package/dist/db/schema/setting.js.map +1 -0
  46. package/dist/lexicon/index.d.ts +11 -0
  47. package/dist/lexicon/index.d.ts.map +1 -1
  48. package/dist/lexicon/index.js +32 -1
  49. package/dist/lexicon/index.js.map +1 -1
  50. package/dist/lexicon/lexicons.d.ts +362 -1
  51. package/dist/lexicon/lexicons.d.ts.map +1 -1
  52. package/dist/lexicon/lexicons.js +406 -5
  53. package/dist/lexicon/lexicons.js.map +1 -1
  54. package/dist/lexicon/types/chat/bsky/convo/defs.d.ts +1 -0
  55. package/dist/lexicon/types/chat/bsky/convo/defs.d.ts.map +1 -1
  56. package/dist/lexicon/types/chat/bsky/convo/defs.js.map +1 -1
  57. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +60 -4
  58. package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
  59. package/dist/lexicon/types/tools/ozone/moderation/defs.js +50 -0
  60. package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
  61. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts +1 -1
  62. package/dist/lexicon/types/tools/ozone/moderation/emitEvent.d.ts.map +1 -1
  63. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +10 -0
  64. package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
  65. package/dist/lexicon/types/tools/ozone/setting/defs.d.ts +20 -0
  66. package/dist/lexicon/types/tools/ozone/setting/defs.d.ts.map +1 -0
  67. package/dist/lexicon/types/tools/ozone/setting/defs.js +15 -0
  68. package/dist/lexicon/types/tools/ozone/setting/defs.js.map +1 -0
  69. package/dist/lexicon/types/tools/ozone/setting/listOptions.d.ts +43 -0
  70. package/dist/lexicon/types/tools/ozone/setting/listOptions.d.ts.map +1 -0
  71. package/dist/lexicon/types/tools/ozone/setting/listOptions.js +3 -0
  72. package/dist/lexicon/types/tools/ozone/setting/listOptions.js.map +1 -0
  73. package/dist/lexicon/types/tools/ozone/setting/removeOptions.d.ts +40 -0
  74. package/dist/lexicon/types/tools/ozone/setting/removeOptions.d.ts.map +1 -0
  75. package/dist/lexicon/types/tools/ozone/setting/removeOptions.js +3 -0
  76. package/dist/lexicon/types/tools/ozone/setting/removeOptions.js.map +1 -0
  77. package/dist/lexicon/types/tools/ozone/setting/upsertOption.d.ts +45 -0
  78. package/dist/lexicon/types/tools/ozone/setting/upsertOption.d.ts.map +1 -0
  79. package/dist/lexicon/types/tools/ozone/setting/upsertOption.js +3 -0
  80. package/dist/lexicon/types/tools/ozone/setting/upsertOption.js.map +1 -0
  81. package/dist/mod-service/index.d.ts +19 -2
  82. package/dist/mod-service/index.d.ts.map +1 -1
  83. package/dist/mod-service/index.js +37 -1
  84. package/dist/mod-service/index.js.map +1 -1
  85. package/dist/mod-service/status.d.ts +2 -22
  86. package/dist/mod-service/status.d.ts.map +1 -1
  87. package/dist/mod-service/status.js +91 -1
  88. package/dist/mod-service/status.js.map +1 -1
  89. package/dist/mod-service/types.d.ts +19 -1
  90. package/dist/mod-service/types.d.ts.map +1 -1
  91. package/dist/mod-service/views.d.ts.map +1 -1
  92. package/dist/mod-service/views.js +36 -1
  93. package/dist/mod-service/views.js.map +1 -1
  94. package/dist/setting/service.d.ts +33 -0
  95. package/dist/setting/service.d.ts.map +1 -0
  96. package/dist/setting/service.js +101 -0
  97. package/dist/setting/service.js.map +1 -0
  98. package/package.json +10 -10
  99. package/src/api/index.ts +6 -0
  100. package/src/api/moderation/queryStatuses.ts +10 -0
  101. package/src/api/setting/listOptions.ts +44 -0
  102. package/src/api/setting/removeOptions.ts +63 -0
  103. package/src/api/setting/upsertOption.ts +142 -0
  104. package/src/context.ts +8 -0
  105. package/src/db/migrations/20241018T205730722Z-setting.ts +27 -0
  106. package/src/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.ts +57 -0
  107. package/src/db/migrations/index.ts +2 -0
  108. package/src/db/schema/index.ts +3 -1
  109. package/src/db/schema/moderation_event.ts +3 -0
  110. package/src/db/schema/moderation_subject_status.ts +6 -0
  111. package/src/db/schema/setting.ts +24 -0
  112. package/src/lexicon/index.ts +46 -0
  113. package/src/lexicon/lexicons.ts +417 -5
  114. package/src/lexicon/types/chat/bsky/convo/defs.ts +1 -0
  115. package/src/lexicon/types/tools/ozone/moderation/defs.ts +132 -2
  116. package/src/lexicon/types/tools/ozone/moderation/emitEvent.ts +3 -0
  117. package/src/lexicon/types/tools/ozone/moderation/queryStatuses.ts +10 -0
  118. package/src/lexicon/types/tools/ozone/setting/defs.ts +37 -0
  119. package/src/lexicon/types/tools/ozone/setting/listOptions.ts +53 -0
  120. package/src/lexicon/types/tools/ozone/setting/removeOptions.ts +49 -0
  121. package/src/lexicon/types/tools/ozone/setting/upsertOption.ts +58 -0
  122. package/src/mod-service/index.ts +52 -0
  123. package/src/mod-service/status.ts +114 -2
  124. package/src/mod-service/types.ts +25 -0
  125. package/src/mod-service/views.ts +45 -2
  126. package/src/setting/service.ts +148 -0
  127. package/tests/__snapshots__/get-record.test.ts.snap +8 -0
  128. package/tests/__snapshots__/get-records.test.ts.snap +4 -0
  129. package/tests/__snapshots__/get-repo.test.ts.snap +4 -0
  130. package/tests/__snapshots__/get-repos.test.ts.snap +4 -0
  131. package/tests/__snapshots__/moderation-events.test.ts.snap +4 -0
  132. package/tests/__snapshots__/moderation-statuses.test.ts.snap +24 -0
  133. package/tests/__snapshots__/settings.test.ts.snap +52 -0
  134. package/tests/record-and-account-events.test.ts +185 -0
  135. package/tests/settings.test.ts +310 -0
  136. package/tsconfig.build.tsbuildinfo +1 -1
  137. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -22,6 +22,16 @@ export interface QueryParams {
22
22
  reportedBefore?: string
23
23
  /** Search subjects reviewed after a given timestamp */
24
24
  reviewedAfter?: string
25
+ /** Search subjects where the associated record/account was deleted after a given timestamp */
26
+ hostingDeletedAfter?: string
27
+ /** Search subjects where the associated record/account was deleted before a given timestamp */
28
+ hostingDeletedBefore?: string
29
+ /** Search subjects where the associated record/account was updated after a given timestamp */
30
+ hostingUpdatedAfter?: string
31
+ /** Search subjects where the associated record/account was updated before a given timestamp */
32
+ hostingUpdatedBefore?: string
33
+ /** Search subjects by the status of the associated record/account */
34
+ hostingStatuses?: string[]
25
35
  /** Search subjects reviewed before a given timestamp */
26
36
  reviewedBefore?: string
27
37
  /** By default, we don't include muted subjects in the results. Set this to true to include them. */
@@ -0,0 +1,37 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import { ValidationResult, BlobRef } from '@atproto/lexicon'
5
+ import { lexicons } from '../../../../lexicons'
6
+ import { isObj, hasProp } from '../../../../util'
7
+ import { CID } from 'multiformats/cid'
8
+
9
+ export interface Option {
10
+ key: string
11
+ did: string
12
+ value: {}
13
+ description?: string
14
+ createdAt?: string
15
+ updatedAt?: string
16
+ managerRole?:
17
+ | 'tools.ozone.team.defs#roleModerator'
18
+ | 'tools.ozone.team.defs#roleTriage'
19
+ | 'tools.ozone.team.defs#roleAdmin'
20
+ | (string & {})
21
+ scope: 'instance' | 'personal' | (string & {})
22
+ createdBy: string
23
+ lastUpdatedBy: string
24
+ [k: string]: unknown
25
+ }
26
+
27
+ export function isOption(v: unknown): v is Option {
28
+ return (
29
+ isObj(v) &&
30
+ hasProp(v, '$type') &&
31
+ v.$type === 'tools.ozone.setting.defs#option'
32
+ )
33
+ }
34
+
35
+ export function validateOption(v: unknown): ValidationResult {
36
+ return lexicons.validate('tools.ozone.setting.defs#option', v)
37
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import express from 'express'
5
+ import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
+ import { lexicons } from '../../../../lexicons'
7
+ import { isObj, hasProp } from '../../../../util'
8
+ import { CID } from 'multiformats/cid'
9
+ import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
+ import * as ToolsOzoneSettingDefs from './defs'
11
+
12
+ export interface QueryParams {
13
+ limit: number
14
+ cursor?: string
15
+ scope: 'instance' | 'personal' | (string & {})
16
+ /** Filter keys by prefix */
17
+ prefix?: string
18
+ /** Filter for only the specified keys. Ignored if prefix is provided */
19
+ keys?: string[]
20
+ }
21
+
22
+ export type InputSchema = undefined
23
+
24
+ export interface OutputSchema {
25
+ cursor?: string
26
+ options: ToolsOzoneSettingDefs.Option[]
27
+ [k: string]: unknown
28
+ }
29
+
30
+ export type HandlerInput = undefined
31
+
32
+ export interface HandlerSuccess {
33
+ encoding: 'application/json'
34
+ body: OutputSchema
35
+ headers?: { [key: string]: string }
36
+ }
37
+
38
+ export interface HandlerError {
39
+ status: number
40
+ message?: string
41
+ }
42
+
43
+ export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
44
+ export type HandlerReqCtx<HA extends HandlerAuth = never> = {
45
+ auth: HA
46
+ params: QueryParams
47
+ input: HandlerInput
48
+ req: express.Request
49
+ res: express.Response
50
+ }
51
+ export type Handler<HA extends HandlerAuth = never> = (
52
+ ctx: HandlerReqCtx<HA>,
53
+ ) => Promise<HandlerOutput> | HandlerOutput
@@ -0,0 +1,49 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import express from 'express'
5
+ import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
+ import { lexicons } from '../../../../lexicons'
7
+ import { isObj, hasProp } from '../../../../util'
8
+ import { CID } from 'multiformats/cid'
9
+ import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
+
11
+ export interface QueryParams {}
12
+
13
+ export interface InputSchema {
14
+ keys: string[]
15
+ scope: 'instance' | 'personal' | (string & {})
16
+ [k: string]: unknown
17
+ }
18
+
19
+ export interface OutputSchema {
20
+ [k: string]: unknown
21
+ }
22
+
23
+ export interface HandlerInput {
24
+ encoding: 'application/json'
25
+ body: InputSchema
26
+ }
27
+
28
+ export interface HandlerSuccess {
29
+ encoding: 'application/json'
30
+ body: OutputSchema
31
+ headers?: { [key: string]: string }
32
+ }
33
+
34
+ export interface HandlerError {
35
+ status: number
36
+ message?: string
37
+ }
38
+
39
+ export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
40
+ export type HandlerReqCtx<HA extends HandlerAuth = never> = {
41
+ auth: HA
42
+ params: QueryParams
43
+ input: HandlerInput
44
+ req: express.Request
45
+ res: express.Response
46
+ }
47
+ export type Handler<HA extends HandlerAuth = never> = (
48
+ ctx: HandlerReqCtx<HA>,
49
+ ) => Promise<HandlerOutput> | HandlerOutput
@@ -0,0 +1,58 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import express from 'express'
5
+ import { ValidationResult, BlobRef } from '@atproto/lexicon'
6
+ import { lexicons } from '../../../../lexicons'
7
+ import { isObj, hasProp } from '../../../../util'
8
+ import { CID } from 'multiformats/cid'
9
+ import { HandlerAuth, HandlerPipeThrough } from '@atproto/xrpc-server'
10
+ import * as ToolsOzoneSettingDefs from './defs'
11
+
12
+ export interface QueryParams {}
13
+
14
+ export interface InputSchema {
15
+ key: string
16
+ scope: 'instance' | 'personal' | (string & {})
17
+ value: {}
18
+ description?: string
19
+ managerRole?:
20
+ | 'tools.ozone.team.defs#roleModerator'
21
+ | 'tools.ozone.team.defs#roleTriage'
22
+ | 'tools.ozone.team.defs#roleAdmin'
23
+ | (string & {})
24
+ [k: string]: unknown
25
+ }
26
+
27
+ export interface OutputSchema {
28
+ option: ToolsOzoneSettingDefs.Option
29
+ [k: string]: unknown
30
+ }
31
+
32
+ export interface HandlerInput {
33
+ encoding: 'application/json'
34
+ body: InputSchema
35
+ }
36
+
37
+ export interface HandlerSuccess {
38
+ encoding: 'application/json'
39
+ body: OutputSchema
40
+ headers?: { [key: string]: string }
41
+ }
42
+
43
+ export interface HandlerError {
44
+ status: number
45
+ message?: string
46
+ }
47
+
48
+ export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough
49
+ export type HandlerReqCtx<HA extends HandlerAuth = never> = {
50
+ auth: HA
51
+ params: QueryParams
52
+ input: HandlerInput
53
+ req: express.Request
54
+ res: express.Response
55
+ }
56
+ export type Handler<HA extends HandlerAuth = never> = (
57
+ ctx: HandlerReqCtx<HA>,
58
+ ) => Promise<HandlerOutput> | HandlerOutput
@@ -18,6 +18,9 @@ import {
18
18
  isModEventTakedown,
19
19
  isModEventEmail,
20
20
  isModEventTag,
21
+ isAccountEvent,
22
+ isIdentityEvent,
23
+ isRecordEvent,
21
24
  REVIEWESCALATED,
22
25
  REVIEWOPEN,
23
26
  } from '../lexicon/types/tools/ozone/moderation/defs'
@@ -386,6 +389,25 @@ export class ModerationService {
386
389
  }
387
390
  }
388
391
 
392
+ if (isAccountEvent(event)) {
393
+ meta.active = event.active
394
+ meta.timestamp = event.timestamp
395
+ if (event.status) meta.status = event.status
396
+ }
397
+
398
+ if (isIdentityEvent(event)) {
399
+ meta.timestamp = event.timestamp
400
+ if (event.handle) meta.handle = event.handle
401
+ if (event.pdsHost) meta.pdsHost = event.pdsHost
402
+ if (event.tombstone) meta.tombstone = event.tombstone
403
+ }
404
+
405
+ if (isRecordEvent(event)) {
406
+ meta.timestamp = event.timestamp
407
+ meta.op = event.op
408
+ if (event.cid) meta.cid = event.cid
409
+ }
410
+
389
411
  if (isModEventTakedown(event) && event.acknowledgeAccountSubjects) {
390
412
  meta.acknowledgeAccountSubjects = true
391
413
  }
@@ -758,6 +780,11 @@ export class ModerationService {
758
780
  reportedAfter,
759
781
  reportedBefore,
760
782
  includeMuted,
783
+ hostingDeletedBefore,
784
+ hostingDeletedAfter,
785
+ hostingUpdatedBefore,
786
+ hostingUpdatedAfter,
787
+ hostingStatuses,
761
788
  onlyMuted,
762
789
  ignoreSubjects,
763
790
  sortDirection,
@@ -780,6 +807,11 @@ export class ModerationService {
780
807
  reportedAfter?: string
781
808
  reportedBefore?: string
782
809
  includeMuted?: boolean
810
+ hostingDeletedBefore?: string
811
+ hostingDeletedAfter?: string
812
+ hostingUpdatedBefore?: string
813
+ hostingUpdatedAfter?: string
814
+ hostingStatuses?: string[]
783
815
  onlyMuted?: boolean
784
816
  subject?: string
785
817
  ignoreSubjects?: string[]
@@ -847,6 +879,26 @@ export class ModerationService {
847
879
  builder = builder.where('lastReviewedAt', '<', reviewedBefore)
848
880
  }
849
881
 
882
+ if (hostingUpdatedAfter) {
883
+ builder = builder.where('hostingUpdatedAt', '>', hostingUpdatedAfter)
884
+ }
885
+
886
+ if (hostingUpdatedBefore) {
887
+ builder = builder.where('hostingUpdatedAt', '<', hostingUpdatedBefore)
888
+ }
889
+
890
+ if (hostingDeletedAfter) {
891
+ builder = builder.where('hostingDeletedAt', '>', hostingDeletedAfter)
892
+ }
893
+
894
+ if (hostingDeletedBefore) {
895
+ builder = builder.where('hostingDeletedAt', '<', hostingDeletedBefore)
896
+ }
897
+
898
+ if (hostingStatuses?.length) {
899
+ builder = builder.where('hostingStatus', 'in', hostingStatuses)
900
+ }
901
+
850
902
  if (reportedAfter) {
851
903
  builder = builder.where('lastReviewedAt', '>', reportedAfter)
852
904
  }
@@ -126,6 +126,83 @@ const getSubjectStatusForModerationEvent = ({
126
126
  }
127
127
  }
128
128
 
129
+ const hostingEvents = [
130
+ 'tools.ozone.moderation.defs#accountEvent',
131
+ 'tools.ozone.moderation.defs#identityEvent',
132
+ 'tools.ozone.moderation.defs#recordEvent',
133
+ ]
134
+
135
+ const getSubjectStatusForRecordEvent = ({
136
+ event,
137
+ currentStatus,
138
+ }: {
139
+ event: ModerationEventRow
140
+ currentStatus?: ModerationSubjectStatusRow
141
+ }): Partial<ModerationSubjectStatusRow> => {
142
+ const timestamp =
143
+ typeof event.meta?.timestamp === 'string'
144
+ ? event.meta?.timestamp
145
+ : event.createdAt
146
+
147
+ if (event.action === 'tools.ozone.moderation.defs#recordEvent') {
148
+ if (event.meta?.op === 'delete') {
149
+ return {
150
+ hostingStatus: 'deleted',
151
+ hostingDeletedAt: timestamp,
152
+ }
153
+ } else if (event.meta?.op === 'update') {
154
+ return {
155
+ hostingStatus: 'active',
156
+ hostingUpdatedAt: timestamp,
157
+ }
158
+ }
159
+ return {}
160
+ }
161
+
162
+ if (event.action === 'tools.ozone.moderation.defs#accountEvent') {
163
+ const status: Partial<ModerationSubjectStatusRow> = {
164
+ hostingUpdatedAt: timestamp,
165
+ }
166
+
167
+ if (event.meta?.status) {
168
+ status.hostingStatus = `${event.meta?.status}`
169
+ }
170
+
171
+ if (event.meta?.status === 'deleted') {
172
+ status.hostingDeletedAt = timestamp
173
+ } else if (event.meta?.status === 'deactivated') {
174
+ status.hostingDeactivatedAt = timestamp
175
+ } else {
176
+ // When deactivated accounts are re-activated, we receive the event with just the active flag set to true
177
+ // so we want to make sure that the hostingStatus is not set to an outdated value
178
+ if (
179
+ currentStatus?.hostingStatus === 'deactivated' &&
180
+ event.meta?.active
181
+ ) {
182
+ status.hostingStatus = 'active'
183
+ status.hostingReactivatedAt = timestamp
184
+ }
185
+ }
186
+
187
+ return status
188
+ }
189
+
190
+ if (event.action === 'tools.ozone.moderation.defs#identityEvent') {
191
+ const status: Partial<ModerationSubjectStatusRow> = {
192
+ hostingUpdatedAt: timestamp,
193
+ }
194
+
195
+ if (event.meta?.tombstone) {
196
+ status.hostingStatus = 'tombstoned'
197
+ status.hostingDeletedAt = timestamp
198
+ }
199
+
200
+ return status
201
+ }
202
+
203
+ return {}
204
+ }
205
+
129
206
  // Based on a given moderation action event, this function will update the moderation status of the subject
130
207
  // If there's no existing status, it will create one
131
208
  // If the action event does not affect the status, it will do nothing
@@ -133,7 +210,7 @@ export const adjustModerationSubjectStatus = async (
133
210
  db: Database,
134
211
  moderationEvent: ModerationEventRow,
135
212
  blobCids?: string[],
136
- ) => {
213
+ ): Promise<ModerationSubjectStatusRow | null> => {
137
214
  const {
138
215
  action,
139
216
  subjectDid,
@@ -152,6 +229,7 @@ export const adjustModerationSubjectStatus = async (
152
229
 
153
230
  db.assertTransaction()
154
231
 
232
+ const now = new Date().toISOString()
155
233
  const currentStatus = await db.db
156
234
  .selectFrom('moderation_subject_status')
157
235
  .where('did', '=', identifier.did)
@@ -161,6 +239,41 @@ export const adjustModerationSubjectStatus = async (
161
239
  .selectAll()
162
240
  .executeTakeFirst()
163
241
 
242
+ if (hostingEvents.includes(action)) {
243
+ const newStatus = getSubjectStatusForRecordEvent({
244
+ event: moderationEvent,
245
+ currentStatus,
246
+ })
247
+ if (!Object.keys(newStatus).length) {
248
+ return currentStatus || null
249
+ }
250
+
251
+ const status = await db.db
252
+ .insertInto('moderation_subject_status')
253
+ .values({
254
+ ...identifier,
255
+ ...newStatus,
256
+ // newStatus doesn't contain a reviewState or takendown so in case this is a new entry
257
+ // we need to set a default values so that the insert doesn't fail
258
+ reviewState: currentStatus ? currentStatus.reviewState : REVIEWNONE,
259
+ // @TODO: should we try to update this based on status property of account event?
260
+ // For now we're the only one emitting takedowns so i don't think it makes too much of a difference
261
+ takendown: currentStatus ? currentStatus.takendown : false,
262
+ createdAt: now,
263
+ updatedAt: now,
264
+ })
265
+ .onConflict((oc) =>
266
+ oc.constraint('moderation_status_unique_idx').doUpdateSet({
267
+ ...newStatus,
268
+ updatedAt: now,
269
+ }),
270
+ )
271
+ .returningAll()
272
+ .executeTakeFirst()
273
+
274
+ return status || null
275
+ }
276
+
164
277
  // If reporting is muted for this reporter, we don't want to update the subject status
165
278
  if (meta?.isReporterMuted) {
166
279
  return currentStatus || null
@@ -178,7 +291,6 @@ export const adjustModerationSubjectStatus = async (
178
291
  durationInHours: moderationEvent.durationInHours,
179
292
  })
180
293
 
181
- const now = new Date().toISOString()
182
294
  if (
183
295
  currentStatus?.reviewState === REVIEWESCALATED &&
184
296
  subjectStatus.reviewState !== REVIEWCLOSED
@@ -31,3 +31,28 @@ export type ModEventType =
31
31
  | ToolsOzoneModerationDefs.ModEventMute
32
32
  | ToolsOzoneModerationDefs.ModEventReverseTakedown
33
33
  | ToolsOzoneModerationDefs.ModEventTag
34
+ | ToolsOzoneModerationDefs.AccountEvent
35
+ | ToolsOzoneModerationDefs.IdentityEvent
36
+ | ToolsOzoneModerationDefs.RecordEvent
37
+
38
+ type AccountHostingView = {
39
+ $type: 'tools.ozone.moderation.defs#accountHosting'
40
+ status: 'active' | 'takendown' | 'suspended' | 'deleted' | 'deactivated'
41
+ createdAt?: Date
42
+ updatedAt?: Date
43
+ deletedAt?: Date
44
+ deactivatedAt?: Date
45
+ reactivatedAt?: Date
46
+ }
47
+
48
+ type RecordHostingView = {
49
+ $type: 'tools.ozone.moderation.defs#recordHosting'
50
+ status: 'active' | 'deleted'
51
+ createdAt?: Date
52
+ updatedAt?: Date
53
+ deletedAt?: Date
54
+ }
55
+
56
+ export type ModerationSubjectHostingView =
57
+ | AccountHostingView
58
+ | RecordHostingView
@@ -1,6 +1,10 @@
1
1
  import { sql } from 'kysely'
2
2
  import { AtUri, INVALID_HANDLE, normalizeDatetimeAlways } from '@atproto/syntax'
3
- import { AtpAgent, AppBskyFeedDefs } from '@atproto/api'
3
+ import {
4
+ AtpAgent,
5
+ AppBskyFeedDefs,
6
+ ToolsOzoneModerationDefs,
7
+ } from '@atproto/api'
4
8
  import { dedupeStrs } from '@atproto/common'
5
9
  import { BlobRef } from '@atproto/lexicon'
6
10
  import { Keypair } from '@atproto/crypto'
@@ -195,6 +199,25 @@ export class ModerationViews {
195
199
  eventView.event.remove = event.removedTags || []
196
200
  }
197
201
 
202
+ if (event.action === 'tools.ozone.moderation.defs#accountEvent') {
203
+ eventView.event.active = !!event.meta?.active
204
+ eventView.event.timestamp = event.meta?.timestamp
205
+ eventView.event.status = event.meta?.status
206
+ }
207
+
208
+ if (event.action === 'tools.ozone.moderation.defs#identityEvent') {
209
+ eventView.event.timestamp = event.meta?.timestamp
210
+ eventView.event.handle = event.meta?.handle
211
+ eventView.event.pdsHost = event.meta?.pdsHost
212
+ eventView.event.tombstone = !!event.meta?.tombstone
213
+ }
214
+
215
+ if (event.action === 'tools.ozone.moderation.defs#recordEvent') {
216
+ eventView.event.op = event.meta?.op
217
+ eventView.event.cid = event.meta?.cid
218
+ eventView.event.timestamp = event.meta?.timestamp
219
+ }
220
+
198
221
  return eventView
199
222
  }
200
223
 
@@ -574,7 +597,7 @@ export class ModerationViews {
574
597
  formatSubjectStatus(
575
598
  status: ModerationSubjectStatusRowWithHandle,
576
599
  ): SubjectStatusView {
577
- return {
600
+ const statusView: SubjectStatusView = {
578
601
  id: status.id,
579
602
  reviewState: status.reviewState,
580
603
  createdAt: status.createdAt,
@@ -594,6 +617,26 @@ export class ModerationViews {
594
617
  tags: status.tags || [],
595
618
  subject: subjectFromStatusRow(status).lex(),
596
619
  }
620
+
621
+ if (status.recordPath !== '') {
622
+ statusView.hosting = {
623
+ $type: 'tools.ozone.moderation.defs#recordHosting',
624
+ updatedAt: status.hostingUpdatedAt ?? undefined,
625
+ deletedAt: status.hostingDeletedAt ?? undefined,
626
+ status: status.hostingStatus ?? 'unknown',
627
+ }
628
+ } else {
629
+ statusView.hosting = {
630
+ $type: 'tools.ozone.moderation.defs#accountHosting',
631
+ updatedAt: status.hostingUpdatedAt ?? undefined,
632
+ deletedAt: status.hostingDeletedAt ?? undefined,
633
+ status: status.hostingStatus ?? 'unknown',
634
+ deactivatedAt: status.hostingDeactivatedAt ?? undefined,
635
+ reactivatedAt: status.hostingReactivatedAt ?? undefined,
636
+ }
637
+ }
638
+
639
+ return statusView
597
640
  }
598
641
 
599
642
  async fetchAuthorFeed(
@@ -0,0 +1,148 @@
1
+ import Database from '../db'
2
+ import { Selectable } from 'kysely'
3
+ import { Option } from '../lexicon/types/tools/ozone/setting/defs'
4
+ import { Setting, SettingScope } from '../db/schema/setting'
5
+ import { Member } from '../db/schema/member'
6
+ import assert from 'node:assert'
7
+ import { InvalidRequestError } from '@atproto/xrpc-server'
8
+
9
+ export type SettingServiceCreator = (db: Database) => SettingService
10
+
11
+ export class SettingService {
12
+ constructor(public db: Database) {}
13
+
14
+ static creator() {
15
+ return (db: Database) => new SettingService(db)
16
+ }
17
+
18
+ async query({
19
+ limit = 100,
20
+ scope,
21
+ did,
22
+ cursor,
23
+ prefix,
24
+ keys,
25
+ }: {
26
+ limit: number
27
+ scope?: 'personal' | 'instance'
28
+ did?: string
29
+ cursor?: string
30
+ prefix?: string
31
+ keys?: string[]
32
+ }): Promise<{
33
+ options: Selectable<Setting>[]
34
+ cursor?: string
35
+ }> {
36
+ let builder = this.db.db.selectFrom('setting').selectAll()
37
+
38
+ if (prefix) {
39
+ builder = builder.where('key', 'like', `${prefix}%`)
40
+ } else if (keys?.length) {
41
+ builder = builder.where('key', 'in', keys)
42
+ }
43
+
44
+ if (scope) {
45
+ builder = builder.where('scope', '=', scope)
46
+ }
47
+
48
+ if (did) {
49
+ builder = builder.where('did', '=', did)
50
+ }
51
+
52
+ if (cursor) {
53
+ const cursorId = parseInt(cursor, 10)
54
+ if (isNaN(cursorId)) {
55
+ throw new InvalidRequestError('invalid cursor')
56
+ }
57
+ builder = builder.where('id', '<', cursorId)
58
+ }
59
+
60
+ const options = await builder.orderBy('id', 'desc').limit(limit).execute()
61
+
62
+ return {
63
+ options,
64
+ cursor: options[options.length - 1]?.id.toString(),
65
+ }
66
+ }
67
+
68
+ async upsert(
69
+ option: Omit<Setting, 'id' | 'createdAt' | 'updatedAt'> & {
70
+ createdAt: Date
71
+ updatedAt: Date
72
+ },
73
+ ): Promise<void> {
74
+ await this.db.db
75
+ .insertInto('setting')
76
+ .values(option)
77
+ .onConflict((oc) => {
78
+ return oc.columns(['key', 'scope', 'did']).doUpdateSet({
79
+ value: option.value,
80
+ updatedAt: option.updatedAt,
81
+ description: option.description,
82
+ managerRole: option.managerRole,
83
+ lastUpdatedBy: option.lastUpdatedBy,
84
+ })
85
+ })
86
+ .execute()
87
+ }
88
+
89
+ async removeOptions(
90
+ keys: string[],
91
+ filters: {
92
+ did?: string
93
+ scope: SettingScope
94
+ managerRole: Member['role'][]
95
+ },
96
+ ): Promise<void> {
97
+ if (!keys.length) return
98
+
99
+ if (filters.scope === 'personal') {
100
+ assert(filters.did, 'did is required for personal scope')
101
+ }
102
+
103
+ let qb = this.db.db
104
+ .deleteFrom('setting')
105
+ .where('key', 'in', keys)
106
+ .where('scope', '=', filters.scope)
107
+
108
+ if (filters.managerRole.length) {
109
+ qb = qb.where('managerRole', 'in', filters.managerRole)
110
+ } else {
111
+ qb = qb.where('managerRole', 'is', null)
112
+ }
113
+
114
+ if (filters.did) {
115
+ qb = qb.where('did', '=', filters.did)
116
+ }
117
+
118
+ await qb.execute()
119
+ }
120
+
121
+ view(setting: Selectable<Setting>): Option {
122
+ const {
123
+ key,
124
+ value,
125
+ did,
126
+ description,
127
+ createdAt,
128
+ createdBy,
129
+ updatedAt,
130
+ lastUpdatedBy,
131
+ managerRole,
132
+ scope,
133
+ } = setting
134
+
135
+ return {
136
+ key,
137
+ value,
138
+ did,
139
+ scope,
140
+ createdBy,
141
+ lastUpdatedBy,
142
+ managerRole: managerRole || undefined,
143
+ description: description || undefined,
144
+ createdAt: createdAt.toISOString(),
145
+ updatedAt: updatedAt.toISOString(),
146
+ }
147
+ }
148
+ }