@atproto/ozone 0.1.140 → 0.1.141

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 (82) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/api/moderation/emitEvent.d.ts.map +1 -1
  3. package/dist/api/moderation/emitEvent.js +3 -0
  4. package/dist/api/moderation/emitEvent.js.map +1 -1
  5. package/dist/api/report/createReport.d.ts.map +1 -1
  6. package/dist/api/report/createReport.js +7 -5
  7. package/dist/api/report/createReport.js.map +1 -1
  8. package/dist/api/util.d.ts +2 -3
  9. package/dist/api/util.d.ts.map +1 -1
  10. package/dist/api/util.js +7 -17
  11. package/dist/api/util.js.map +1 -1
  12. package/dist/config/config.d.ts +1 -0
  13. package/dist/config/config.d.ts.map +1 -1
  14. package/dist/config/config.js +1 -0
  15. package/dist/config/config.js.map +1 -1
  16. package/dist/config/env.d.ts +1 -0
  17. package/dist/config/env.d.ts.map +1 -1
  18. package/dist/config/env.js +1 -0
  19. package/dist/config/env.js.map +1 -1
  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/20250718T150931000Z-update-appeal-reason-stats.d.ts +5 -0
  25. package/dist/db/migrations/20250718T150931000Z-update-appeal-reason-stats.d.ts.map +1 -0
  26. package/dist/db/migrations/20250718T150931000Z-update-appeal-reason-stats.js +228 -0
  27. package/dist/db/migrations/20250718T150931000Z-update-appeal-reason-stats.js.map +1 -0
  28. package/dist/db/migrations/index.d.ts +1 -0
  29. package/dist/db/migrations/index.d.ts.map +1 -1
  30. package/dist/db/migrations/index.js +2 -1
  31. package/dist/db/migrations/index.js.map +1 -1
  32. package/dist/lexicon/index.d.ts +49 -0
  33. package/dist/lexicon/index.d.ts.map +1 -1
  34. package/dist/lexicon/index.js +52 -1
  35. package/dist/lexicon/index.js.map +1 -1
  36. package/dist/lexicon/lexicons.d.ts +470 -16
  37. package/dist/lexicon/lexicons.d.ts.map +1 -1
  38. package/dist/lexicon/lexicons.js +329 -7
  39. package/dist/lexicon/lexicons.js.map +1 -1
  40. package/dist/lexicon/types/com/atproto/moderation/defs.d.ts +8 -8
  41. package/dist/lexicon/types/com/atproto/moderation/defs.d.ts.map +1 -1
  42. package/dist/lexicon/types/com/atproto/moderation/defs.js +7 -7
  43. package/dist/lexicon/types/com/atproto/moderation/defs.js.map +1 -1
  44. package/dist/lexicon/types/com/atproto/temp/dereferenceScope.d.ts +24 -0
  45. package/dist/lexicon/types/com/atproto/temp/dereferenceScope.d.ts.map +1 -0
  46. package/dist/lexicon/types/com/atproto/temp/dereferenceScope.js +7 -0
  47. package/dist/lexicon/types/com/atproto/temp/dereferenceScope.js.map +1 -0
  48. package/dist/lexicon/types/tools/ozone/report/defs.d.ts +92 -0
  49. package/dist/lexicon/types/tools/ozone/report/defs.d.ts.map +1 -0
  50. package/dist/lexicon/types/tools/ozone/report/defs.js +98 -0
  51. package/dist/lexicon/types/tools/ozone/report/defs.js.map +1 -0
  52. package/dist/mod-service/profile.d.ts +15 -0
  53. package/dist/mod-service/profile.d.ts.map +1 -0
  54. package/dist/mod-service/profile.js +135 -0
  55. package/dist/mod-service/profile.js.map +1 -0
  56. package/dist/mod-service/status.d.ts.map +1 -1
  57. package/dist/mod-service/status.js +18 -17
  58. package/dist/mod-service/status.js.map +1 -1
  59. package/dist/tag-service/util.d.ts.map +1 -1
  60. package/dist/tag-service/util.js +7 -1
  61. package/dist/tag-service/util.js.map +1 -1
  62. package/package.json +9 -9
  63. package/src/api/moderation/emitEvent.ts +4 -0
  64. package/src/api/report/createReport.ts +9 -9
  65. package/src/api/util.ts +7 -28
  66. package/src/config/config.ts +3 -1
  67. package/src/config/env.ts +2 -0
  68. package/src/context.ts +14 -0
  69. package/src/db/migrations/20250718T150931000Z-update-appeal-reason-stats.ts +311 -0
  70. package/src/db/migrations/index.ts +1 -0
  71. package/src/lexicon/index.ts +82 -0
  72. package/src/lexicon/lexicons.ts +341 -7
  73. package/src/lexicon/types/com/atproto/moderation/defs.ts +52 -7
  74. package/src/lexicon/types/com/atproto/temp/dereferenceScope.ts +42 -0
  75. package/src/lexicon/types/tools/ozone/report/defs.ts +154 -0
  76. package/src/mod-service/profile.ts +143 -0
  77. package/src/mod-service/status.ts +3 -2
  78. package/src/tag-service/util.ts +9 -1
  79. package/tests/__snapshots__/report-reason.test.ts.snap +14 -0
  80. package/tests/report-reason.test.ts +154 -0
  81. package/tsconfig.build.tsbuildinfo +1 -1
  82. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -0,0 +1,154 @@
1
+ /**
2
+ * GENERATED CODE - DO NOT MODIFY
3
+ */
4
+ import { type ValidationResult, BlobRef } from '@atproto/lexicon'
5
+ import { CID } from 'multiformats/cid'
6
+ import { validate as _validate } from '../../../../lexicons'
7
+ import {
8
+ type $Typed,
9
+ is$typed as _is$typed,
10
+ type OmitKey,
11
+ } from '../../../../util'
12
+
13
+ const is$typed = _is$typed,
14
+ validate = _validate
15
+ const id = 'tools.ozone.report.defs'
16
+
17
+ export type ReasonType =
18
+ | 'tools.ozone.report.defs#reasonAppeal'
19
+ | 'tools.ozone.report.defs#reasonViolenceAnimalWelfare'
20
+ | 'tools.ozone.report.defs#reasonViolenceThreats'
21
+ | 'tools.ozone.report.defs#reasonViolenceGraphicContent'
22
+ | 'tools.ozone.report.defs#reasonViolenceSelfHarm'
23
+ | 'tools.ozone.report.defs#reasonViolenceGlorification'
24
+ | 'tools.ozone.report.defs#reasonViolenceExtremistContent'
25
+ | 'tools.ozone.report.defs#reasonViolenceTrafficking'
26
+ | 'tools.ozone.report.defs#reasonViolenceOther'
27
+ | 'tools.ozone.report.defs#reasonSexualAbuseContent'
28
+ | 'tools.ozone.report.defs#reasonSexualNCII'
29
+ | 'tools.ozone.report.defs#reasonSexualSextortion'
30
+ | 'tools.ozone.report.defs#reasonSexualDeepfake'
31
+ | 'tools.ozone.report.defs#reasonSexualAnimal'
32
+ | 'tools.ozone.report.defs#reasonSexualUnlabeled'
33
+ | 'tools.ozone.report.defs#reasonSexualOther'
34
+ | 'tools.ozone.report.defs#reasonChildSafetyCSAM'
35
+ | 'tools.ozone.report.defs#reasonChildSafetyGroom'
36
+ | 'tools.ozone.report.defs#reasonChildSafetyMinorPrivacy'
37
+ | 'tools.ozone.report.defs#reasonChildSafetyEndangerment'
38
+ | 'tools.ozone.report.defs#reasonChildSafetyHarassment'
39
+ | 'tools.ozone.report.defs#reasonChildSafetyPromotion'
40
+ | 'tools.ozone.report.defs#reasonChildSafetyOther'
41
+ | 'tools.ozone.report.defs#reasonHarassmentTroll'
42
+ | 'tools.ozone.report.defs#reasonHarassmentTargeted'
43
+ | 'tools.ozone.report.defs#reasonHarassmentHateSpeech'
44
+ | 'tools.ozone.report.defs#reasonHarassmentDoxxing'
45
+ | 'tools.ozone.report.defs#reasonHarassmentOther'
46
+ | 'tools.ozone.report.defs#reasonMisleadingBot'
47
+ | 'tools.ozone.report.defs#reasonMisleadingImpersonation'
48
+ | 'tools.ozone.report.defs#reasonMisleadingSpam'
49
+ | 'tools.ozone.report.defs#reasonMisleadingScam'
50
+ | 'tools.ozone.report.defs#reasonMisleadingSyntheticContent'
51
+ | 'tools.ozone.report.defs#reasonMisleadingMisinformation'
52
+ | 'tools.ozone.report.defs#reasonMisleadingOther'
53
+ | 'tools.ozone.report.defs#reasonRuleSiteSecurity'
54
+ | 'tools.ozone.report.defs#reasonRuleStolenContent'
55
+ | 'tools.ozone.report.defs#reasonRuleProhibitedSales'
56
+ | 'tools.ozone.report.defs#reasonRuleBanEvasion'
57
+ | 'tools.ozone.report.defs#reasonRuleOther'
58
+ | 'tools.ozone.report.defs#reasonCivicElectoralProcess'
59
+ | 'tools.ozone.report.defs#reasonCivicDisclosure'
60
+ | 'tools.ozone.report.defs#reasonCivicInterference'
61
+ | 'tools.ozone.report.defs#reasonCivicMisinformation'
62
+ | 'tools.ozone.report.defs#reasonCivicImpersonation'
63
+ | (string & {})
64
+
65
+ /** Appeal a previously taken moderation action */
66
+ export const REASONAPPEAL = `${id}#reasonAppeal`
67
+ /** Animal welfare violations */
68
+ export const REASONVIOLENCEANIMALWELFARE = `${id}#reasonViolenceAnimalWelfare`
69
+ /** Threats or incitement */
70
+ export const REASONVIOLENCETHREATS = `${id}#reasonViolenceThreats`
71
+ /** Graphic violent content */
72
+ export const REASONVIOLENCEGRAPHICCONTENT = `${id}#reasonViolenceGraphicContent`
73
+ /** Self harm */
74
+ export const REASONVIOLENCESELFHARM = `${id}#reasonViolenceSelfHarm`
75
+ /** Glorification of violence */
76
+ export const REASONVIOLENCEGLORIFICATION = `${id}#reasonViolenceGlorification`
77
+ /** Extremist content. These reports will be sent only be sent to the application's Moderation Authority. */
78
+ export const REASONVIOLENCEEXTREMISTCONTENT = `${id}#reasonViolenceExtremistContent`
79
+ /** Human trafficking */
80
+ export const REASONVIOLENCETRAFFICKING = `${id}#reasonViolenceTrafficking`
81
+ /** Other violent content */
82
+ export const REASONVIOLENCEOTHER = `${id}#reasonViolenceOther`
83
+ /** Adult sexual abuse content */
84
+ export const REASONSEXUALABUSECONTENT = `${id}#reasonSexualAbuseContent`
85
+ /** Non-consensual intimate imagery */
86
+ export const REASONSEXUALNCII = `${id}#reasonSexualNCII`
87
+ /** Sextortion */
88
+ export const REASONSEXUALSEXTORTION = `${id}#reasonSexualSextortion`
89
+ /** Deepfake adult content */
90
+ export const REASONSEXUALDEEPFAKE = `${id}#reasonSexualDeepfake`
91
+ /** Animal sexual abuse */
92
+ export const REASONSEXUALANIMAL = `${id}#reasonSexualAnimal`
93
+ /** Unlabelled adult content */
94
+ export const REASONSEXUALUNLABELED = `${id}#reasonSexualUnlabeled`
95
+ /** Other sexual violence content */
96
+ export const REASONSEXUALOTHER = `${id}#reasonSexualOther`
97
+ /** Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority. */
98
+ export const REASONCHILDSAFETYCSAM = `${id}#reasonChildSafetyCSAM`
99
+ /** Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority. */
100
+ export const REASONCHILDSAFETYGROOM = `${id}#reasonChildSafetyGroom`
101
+ /** Privacy violation involving a minor */
102
+ export const REASONCHILDSAFETYMINORPRIVACY = `${id}#reasonChildSafetyMinorPrivacy`
103
+ /** Child endangerment. These reports will be sent only be sent to the application's Moderation Authority. */
104
+ export const REASONCHILDSAFETYENDANGERMENT = `${id}#reasonChildSafetyEndangerment`
105
+ /** Harassment or bullying of minors */
106
+ export const REASONCHILDSAFETYHARASSMENT = `${id}#reasonChildSafetyHarassment`
107
+ /** Promotion of child exploitation. These reports will be sent only be sent to the application's Moderation Authority. */
108
+ export const REASONCHILDSAFETYPROMOTION = `${id}#reasonChildSafetyPromotion`
109
+ /** Other child safety. These reports will be sent only be sent to the application's Moderation Authority. */
110
+ export const REASONCHILDSAFETYOTHER = `${id}#reasonChildSafetyOther`
111
+ /** Trolling */
112
+ export const REASONHARASSMENTTROLL = `${id}#reasonHarassmentTroll`
113
+ /** Targeted harassment */
114
+ export const REASONHARASSMENTTARGETED = `${id}#reasonHarassmentTargeted`
115
+ /** Hate speech */
116
+ export const REASONHARASSMENTHATESPEECH = `${id}#reasonHarassmentHateSpeech`
117
+ /** Doxxing */
118
+ export const REASONHARASSMENTDOXXING = `${id}#reasonHarassmentDoxxing`
119
+ /** Other harassing or hateful content */
120
+ export const REASONHARASSMENTOTHER = `${id}#reasonHarassmentOther`
121
+ /** Fake account or bot */
122
+ export const REASONMISLEADINGBOT = `${id}#reasonMisleadingBot`
123
+ /** Impersonation */
124
+ export const REASONMISLEADINGIMPERSONATION = `${id}#reasonMisleadingImpersonation`
125
+ /** Spam */
126
+ export const REASONMISLEADINGSPAM = `${id}#reasonMisleadingSpam`
127
+ /** Scam */
128
+ export const REASONMISLEADINGSCAM = `${id}#reasonMisleadingScam`
129
+ /** Unlabelled gen-AI or synthetic content */
130
+ export const REASONMISLEADINGSYNTHETICCONTENT = `${id}#reasonMisleadingSyntheticContent`
131
+ /** Harmful false claims */
132
+ export const REASONMISLEADINGMISINFORMATION = `${id}#reasonMisleadingMisinformation`
133
+ /** Other misleading content */
134
+ export const REASONMISLEADINGOTHER = `${id}#reasonMisleadingOther`
135
+ /** Hacking or system attacks */
136
+ export const REASONRULESITESECURITY = `${id}#reasonRuleSiteSecurity`
137
+ /** Stolen content */
138
+ export const REASONRULESTOLENCONTENT = `${id}#reasonRuleStolenContent`
139
+ /** Promoting or selling prohibited items or services */
140
+ export const REASONRULEPROHIBITEDSALES = `${id}#reasonRuleProhibitedSales`
141
+ /** Banned user returning */
142
+ export const REASONRULEBANEVASION = `${id}#reasonRuleBanEvasion`
143
+ /** Other */
144
+ export const REASONRULEOTHER = `${id}#reasonRuleOther`
145
+ /** Electoral process violations */
146
+ export const REASONCIVICELECTORALPROCESS = `${id}#reasonCivicElectoralProcess`
147
+ /** Disclosure & transparency violations */
148
+ export const REASONCIVICDISCLOSURE = `${id}#reasonCivicDisclosure`
149
+ /** Voter intimidation or interference */
150
+ export const REASONCIVICINTERFERENCE = `${id}#reasonCivicInterference`
151
+ /** Election misinformation */
152
+ export const REASONCIVICMISINFORMATION = `${id}#reasonCivicMisinformation`
153
+ /** Impersonation of electoral officials/entities */
154
+ export const REASONCIVICIMPERSONATION = `${id}#reasonCivicImpersonation`
@@ -0,0 +1,143 @@
1
+ import AtpAgent, { AppBskyLabelerDefs } from '@atproto/api'
2
+ import { InvalidRequestError } from '@atproto/xrpc-server'
3
+ import { OzoneConfig } from '../config'
4
+ import {
5
+ REASONAPPEAL,
6
+ REASONMISLEADING,
7
+ REASONRUDE,
8
+ REASONSEXUAL,
9
+ REASONSPAM,
10
+ REASONVIOLATION,
11
+ } from '../lexicon/types/com/atproto/moderation/defs'
12
+ import { httpLogger } from '../logger'
13
+
14
+ // Reverse mapping from new ozone namespaced reason types to old com.atproto namespaced reason types
15
+ export const NEW_TO_OLD_REASON_MAPPING: Record<string, string> = {
16
+ 'tools.ozone.report.defs#reasonMisleadingSpam': REASONSPAM,
17
+ 'tools.ozone.report.defs#reasonRuleOther': REASONVIOLATION,
18
+ 'tools.ozone.report.defs#reasonMisleadingOther': REASONMISLEADING,
19
+ 'tools.ozone.report.defs#reasonSexualUnlabeled': REASONSEXUAL,
20
+ 'tools.ozone.report.defs#reasonHarassmentOther': REASONRUDE,
21
+ 'tools.ozone.report.defs#reasonAppeal': REASONAPPEAL,
22
+ // Map all violence-related reasons to REASONVIOLATION
23
+ 'tools.ozone.report.defs#reasonViolenceAnimalWelfare': REASONVIOLATION,
24
+ 'tools.ozone.report.defs#reasonViolenceThreats': REASONVIOLATION,
25
+ 'tools.ozone.report.defs#reasonViolenceGraphicContent': REASONVIOLATION,
26
+ 'tools.ozone.report.defs#reasonViolenceSelfHarm': REASONVIOLATION,
27
+ 'tools.ozone.report.defs#reasonViolenceGlorification': REASONVIOLATION,
28
+ 'tools.ozone.report.defs#reasonViolenceExtremistContent': REASONVIOLATION,
29
+ 'tools.ozone.report.defs#reasonViolenceTrafficking': REASONVIOLATION,
30
+ 'tools.ozone.report.defs#reasonViolenceOther': REASONVIOLATION,
31
+ // Map all sexual-related reasons to REASONSEXUAL
32
+ 'tools.ozone.report.defs#reasonSexualAbuseContent': REASONSEXUAL,
33
+ 'tools.ozone.report.defs#reasonSexualNCII': REASONSEXUAL,
34
+ 'tools.ozone.report.defs#reasonSexualSextortion': REASONSEXUAL,
35
+ 'tools.ozone.report.defs#reasonSexualDeepfake': REASONSEXUAL,
36
+ 'tools.ozone.report.defs#reasonSexualAnimal': REASONSEXUAL,
37
+ 'tools.ozone.report.defs#reasonSexualOther': REASONSEXUAL,
38
+ // Map all child safety reasons to REASONVIOLATION
39
+ 'tools.ozone.report.defs#reasonChildSafetyCSAM': REASONVIOLATION,
40
+ 'tools.ozone.report.defs#reasonChildSafetyGroom': REASONVIOLATION,
41
+ 'tools.ozone.report.defs#reasonChildSafetyMinorPrivacy': REASONVIOLATION,
42
+ 'tools.ozone.report.defs#reasonChildSafetyEndangerment': REASONVIOLATION,
43
+ 'tools.ozone.report.defs#reasonChildSafetyHarassment': REASONVIOLATION,
44
+ 'tools.ozone.report.defs#reasonChildSafetyPromotion': REASONVIOLATION,
45
+ 'tools.ozone.report.defs#reasonChildSafetyOther': REASONVIOLATION,
46
+ // Map all harassment reasons to REASONRUDE
47
+ 'tools.ozone.report.defs#reasonHarassmentTroll': REASONRUDE,
48
+ 'tools.ozone.report.defs#reasonHarassmentTargeted': REASONRUDE,
49
+ 'tools.ozone.report.defs#reasonHarassmentHateSpeech': REASONRUDE,
50
+ 'tools.ozone.report.defs#reasonHarassmentDoxxing': REASONRUDE,
51
+ // Map all misleading reasons to REASONMISLEADING
52
+ 'tools.ozone.report.defs#reasonMisleadingBot': REASONMISLEADING,
53
+ 'tools.ozone.report.defs#reasonMisleadingImpersonation': REASONMISLEADING,
54
+ 'tools.ozone.report.defs#reasonMisleadingScam': REASONMISLEADING,
55
+ 'tools.ozone.report.defs#reasonMisleadingSyntheticContent': REASONMISLEADING,
56
+ 'tools.ozone.report.defs#reasonMisleadingMisinformation': REASONMISLEADING,
57
+ // Map all rule-related reasons to REASONVIOLATION
58
+ 'tools.ozone.report.defs#reasonRuleSiteSecurity': REASONVIOLATION,
59
+ 'tools.ozone.report.defs#reasonRuleStolenContent': REASONVIOLATION,
60
+ 'tools.ozone.report.defs#reasonRuleProhibitedSales': REASONVIOLATION,
61
+ 'tools.ozone.report.defs#reasonRuleBanEvasion': REASONVIOLATION,
62
+ // Map all civic reasons to REASONMISLEADING
63
+ 'tools.ozone.report.defs#reasonCivicElectoralProcess': REASONMISLEADING,
64
+ 'tools.ozone.report.defs#reasonCivicDisclosure': REASONMISLEADING,
65
+ 'tools.ozone.report.defs#reasonCivicInterference': REASONMISLEADING,
66
+ 'tools.ozone.report.defs#reasonCivicMisinformation': REASONMISLEADING,
67
+ 'tools.ozone.report.defs#reasonCivicImpersonation': REASONMISLEADING,
68
+ }
69
+
70
+ interface CacheEntry {
71
+ profile: AppBskyLabelerDefs.LabelerViewDetailed | null
72
+ timestamp: number
73
+ }
74
+
75
+ export type ModerationServiceProfileCreator = () => ModerationServiceProfile
76
+
77
+ export class ModerationServiceProfile {
78
+ private cache: CacheEntry | null = null
79
+ private CACHE_TTL: number
80
+
81
+ constructor(
82
+ private cfg: OzoneConfig,
83
+ private appviewAgent: AtpAgent,
84
+ cacheTTL?: number,
85
+ ) {
86
+ this.CACHE_TTL = cacheTTL || cfg.service.serviceRecordCacheTTL
87
+ }
88
+
89
+ static creator(
90
+ cfg: OzoneConfig,
91
+ appviewAgent: AtpAgent,
92
+ ): ModerationServiceProfileCreator {
93
+ return () => new ModerationServiceProfile(cfg, appviewAgent)
94
+ }
95
+
96
+ async getProfile() {
97
+ const now = Date.now()
98
+
99
+ if (!this.cache || now - this.cache.timestamp > this.CACHE_TTL) {
100
+ try {
101
+ const { data } = await this.appviewAgent.app.bsky.labeler.getServices({
102
+ dids: [this.cfg.service.did],
103
+ detailed: true,
104
+ })
105
+
106
+ if (AppBskyLabelerDefs.isLabelerViewDetailed(data.views?.[0])) {
107
+ this.cache = {
108
+ profile: data.views[0],
109
+ timestamp: now,
110
+ }
111
+ }
112
+ } catch (e) {
113
+ // On error, fail open
114
+ httpLogger.error(`Failed to fetch labeler profile: ${e?.['message']}`)
115
+ }
116
+ }
117
+
118
+ return this.cache?.profile || null
119
+ }
120
+
121
+ async validateReasonType(reasonType: string): Promise<string> {
122
+ const profile = await this.getProfile()
123
+
124
+ if (!Array.isArray(profile?.reasonTypes)) {
125
+ return reasonType
126
+ }
127
+
128
+ const supportedReasonTypes = profile.reasonTypes
129
+
130
+ // Check if the reason type is directly supported
131
+ if (supportedReasonTypes.includes(reasonType)) {
132
+ return reasonType
133
+ }
134
+
135
+ // Allow new reason types only if they map to a supported old reason type
136
+ const mappedOldReason = NEW_TO_OLD_REASON_MAPPING[reasonType]
137
+ if (mappedOldReason && supportedReasonTypes.includes(mappedOldReason)) {
138
+ return reasonType
139
+ }
140
+
141
+ throw new InvalidRequestError(`Invalid reason type: ${reasonType}`)
142
+ }
143
+ }
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { HOUR } from '@atproto/common'
4
4
  import { AtUri } from '@atproto/syntax'
5
+ import { isAppealReport } from '../api/util'
5
6
  import { Database } from '../db'
6
7
  import { DatabaseSchema } from '../db/schema'
7
8
  import { jsonb } from '../db/types'
8
- import { REASONAPPEAL } from '../lexicon/types/com/atproto/moderation/defs'
9
9
  import {
10
10
  REVIEWCLOSED,
11
11
  REVIEWESCALATED,
@@ -341,7 +341,8 @@ export const adjustModerationSubjectStatus = async (
341
341
 
342
342
  const isAppealEvent =
343
343
  action === 'tools.ozone.moderation.defs#modEventReport' &&
344
- meta?.reportType === REASONAPPEAL
344
+ meta?.reportType &&
345
+ isAppealReport(`${meta.reportType}`)
345
346
 
346
347
  const subjectStatus = getSubjectStatusForModerationEvent({
347
348
  currentStatus,
@@ -1,5 +1,13 @@
1
1
  import { ReasonType } from '../lexicon/types/com/atproto/moderation/defs'
2
2
 
3
3
  export const getTagForReport = (reasonType: ReasonType) => {
4
- return `report:${reasonType.replace('com.atproto.moderation.defs#reason', '').toLowerCase()}`
4
+ const reasonWithoutPrefix = reasonType
5
+ .replace('com.atproto.moderation.defs#reason', '')
6
+ .replace('tools.ozone.report.defs#reason', '')
7
+
8
+ const kebabCase = reasonWithoutPrefix
9
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
10
+ .toLowerCase()
11
+
12
+ return `report:${kebabCase}`
5
13
  }
@@ -0,0 +1,14 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`report reason createReport only passes for allowed reason types 1`] = `
4
+ Object {
5
+ "createdAt": "1970-01-01T00:00:00.000Z",
6
+ "id": 1,
7
+ "reasonType": "tools.ozone.report.defs#reasonHarassmentTroll",
8
+ "reportedBy": "user(0)",
9
+ "subject": Object {
10
+ "$type": "com.atproto.admin.defs#repoRef",
11
+ "did": "user(1)",
12
+ },
13
+ }
14
+ `;
@@ -0,0 +1,154 @@
1
+ import AtpAgent from '@atproto/api'
2
+ import { SeedClient, TestNetwork, basicSeed } from '@atproto/dev-env'
3
+ import {
4
+ REASONRUDE,
5
+ REASONSPAM,
6
+ } from '../src/lexicon/types/com/atproto/moderation/defs'
7
+ import { ModerationServiceProfile } from '../src/mod-service/profile'
8
+ import { forSnapshot } from './_util'
9
+
10
+ describe('report reason', () => {
11
+ let network: TestNetwork
12
+ let sc: SeedClient
13
+ let pdsAgent: AtpAgent
14
+
15
+ const repoSubject = (did: string) => ({
16
+ $type: 'com.atproto.admin.defs#repoRef',
17
+ did,
18
+ })
19
+
20
+ beforeAll(async () => {
21
+ network = await TestNetwork.create({
22
+ dbPostgresSchema: 'ozone_report',
23
+ })
24
+ sc = network.getSeedClient()
25
+ await basicSeed(sc)
26
+
27
+ // Login with ozone's service account owner and update the service profile definition
28
+ pdsAgent = network.pds.getClient()
29
+ await pdsAgent.login({
30
+ identifier: 'mod-authority.test',
31
+ password: 'hunter2',
32
+ })
33
+ await pdsAgent.com.atproto.repo.putRecord({
34
+ repo: network.ozone.ctx.cfg.service.did,
35
+ collection: 'app.bsky.labeler.service',
36
+ rkey: 'self',
37
+ record: {
38
+ policies: { labelValues: [] },
39
+ reasonTypes: ['tools.ozone.report.defs#reasonHarassmentTroll'],
40
+ createdAt: new Date().toISOString(),
41
+ },
42
+ })
43
+
44
+ await network.processAll()
45
+ })
46
+
47
+ afterAll(async () => {
48
+ await network.close()
49
+ })
50
+
51
+ describe('createReport', () => {
52
+ it('only passes for allowed reason types', async () => {
53
+ await expect(
54
+ sc.createReport({
55
+ reasonType: 'tools.ozone.report.defs#reasonHarassmentFake',
56
+ subject: repoSubject(sc.dids.bob),
57
+ reportedBy: sc.dids.alice,
58
+ }),
59
+ ).rejects.toThrow('Invalid reason type')
60
+
61
+ const validReport = await sc.createReport({
62
+ reasonType: 'tools.ozone.report.defs#reasonHarassmentTroll',
63
+ subject: repoSubject(sc.dids.bob),
64
+ reportedBy: sc.dids.alice,
65
+ })
66
+
67
+ expect(forSnapshot(validReport)).toMatchSnapshot()
68
+ })
69
+ })
70
+ describe('ModerationServiceProfile', () => {
71
+ it('should validate against updated labeler profile when cache expires', async () => {
72
+ const moderationServiceProfile = new ModerationServiceProfile(
73
+ network.ozone.ctx.cfg,
74
+ network.ozone.ctx.appviewAgent,
75
+ 500,
76
+ )
77
+
78
+ await expect(
79
+ moderationServiceProfile.validateReasonType(
80
+ 'tools.ozone.report.defs#reasonHarassmentFake',
81
+ ),
82
+ ).rejects.toThrow('Invalid reason type')
83
+
84
+ // Update labeler profile to add the new reason type
85
+ await pdsAgent.com.atproto.repo.putRecord({
86
+ repo: network.ozone.ctx.cfg.service.did,
87
+ collection: 'app.bsky.labeler.service',
88
+ rkey: 'self',
89
+ record: {
90
+ policies: { labelValues: [] },
91
+ reasonTypes: ['tools.ozone.report.defs#reasonHarassmentFake'],
92
+ createdAt: new Date().toISOString(),
93
+ },
94
+ })
95
+ await network.processAll()
96
+
97
+ // immediately after the update, the reason type still fails due to cache
98
+ await expect(
99
+ moderationServiceProfile.validateReasonType(
100
+ 'tools.ozone.report.defs#reasonHarassmentFake',
101
+ ),
102
+ ).rejects.toThrow('Invalid reason type')
103
+
104
+ // add some manual delay to ensure cache is expired and try again
105
+ await new Promise((resolve) => setTimeout(resolve, 500))
106
+ await expect(
107
+ moderationServiceProfile.validateReasonType(
108
+ 'tools.ozone.report.defs#reasonHarassmentFake',
109
+ ),
110
+ ).resolves.toEqual('tools.ozone.report.defs#reasonHarassmentFake')
111
+ })
112
+
113
+ it('should validate mapped reason types', async () => {
114
+ const moderationServiceProfile = new ModerationServiceProfile(
115
+ network.ozone.ctx.cfg,
116
+ network.ozone.ctx.appviewAgent,
117
+ 500,
118
+ )
119
+
120
+ // Set up labeler profile with old reason types only
121
+ await pdsAgent.com.atproto.repo.putRecord({
122
+ repo: network.ozone.ctx.cfg.service.did,
123
+ collection: 'app.bsky.labeler.service',
124
+ rkey: 'self',
125
+ record: {
126
+ policies: { labelValues: [] },
127
+ reasonTypes: [REASONSPAM, REASONRUDE],
128
+ createdAt: new Date().toISOString(),
129
+ },
130
+ })
131
+ await network.processAll()
132
+
133
+ await new Promise((resolve) => setTimeout(resolve, 500))
134
+
135
+ await expect(
136
+ moderationServiceProfile.validateReasonType(
137
+ 'tools.ozone.report.defs#reasonMisleadingSpam',
138
+ ),
139
+ ).resolves.toEqual('tools.ozone.report.defs#reasonMisleadingSpam')
140
+
141
+ // directly supported old reason types work
142
+ await expect(
143
+ moderationServiceProfile.validateReasonType(REASONSPAM),
144
+ ).resolves.toEqual(REASONSPAM)
145
+
146
+ // new reason types that don't map to supported old reason types are rejected
147
+ await expect(
148
+ moderationServiceProfile.validateReasonType(
149
+ 'tools.ozone.report.defs#reasonViolenceThreats',
150
+ ),
151
+ ).rejects.toThrow('Invalid reason type')
152
+ })
153
+ })
154
+ })