@atproto/ozone 0.1.176 → 0.2.1
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.
- package/CHANGELOG.md +22 -0
- package/dist/api/chat/getConvo.d.ts +4 -0
- package/dist/api/chat/getConvo.d.ts.map +1 -0
- package/dist/api/chat/getConvo.js +18 -0
- package/dist/api/chat/getConvo.js.map +1 -0
- package/dist/api/chat/getConvoMembers.d.ts +4 -0
- package/dist/api/chat/getConvoMembers.d.ts.map +1 -0
- package/dist/api/chat/getConvoMembers.js +18 -0
- package/dist/api/chat/getConvoMembers.js.map +1 -0
- package/dist/api/chat/index.d.ts.map +1 -1
- package/dist/api/chat/index.js +4 -0
- package/dist/api/chat/index.js.map +1 -1
- package/dist/daemon/event-reverser.d.ts.map +1 -1
- package/dist/daemon/event-reverser.js +1 -0
- package/dist/daemon/event-reverser.js.map +1 -1
- package/dist/db/migrations/20260513T202941104Z-add-subject-convo-id.d.ts +4 -0
- package/dist/db/migrations/20260513T202941104Z-add-subject-convo-id.d.ts.map +1 -0
- package/dist/db/migrations/20260513T202941104Z-add-subject-convo-id.js +107 -0
- package/dist/db/migrations/20260513T202941104Z-add-subject-convo-id.js.map +1 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +1 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/schema/expiring_tag.d.ts +1 -0
- package/dist/db/schema/expiring_tag.d.ts.map +1 -1
- package/dist/db/schema/expiring_tag.js.map +1 -1
- package/dist/db/schema/moderation_event.d.ts +2 -1
- package/dist/db/schema/moderation_event.d.ts.map +1 -1
- package/dist/db/schema/moderation_event.js.map +1 -1
- package/dist/db/schema/moderation_subject_status.d.ts +1 -0
- package/dist/db/schema/moderation_subject_status.d.ts.map +1 -1
- package/dist/db/schema/moderation_subject_status.js.map +1 -1
- package/dist/db/schema/report.d.ts +1 -0
- package/dist/db/schema/report.d.ts.map +1 -1
- package/dist/db/schema/report.js.map +1 -1
- package/dist/lexicon/index.d.ts +21 -2
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +36 -2
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +1617 -305
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +762 -44
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts +4 -0
- package/dist/lexicon/types/app/bsky/actor/defs.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/actor/defs.js.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/external.d.ts +48 -2
- package/dist/lexicon/types/app/bsky/embed/external.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/external.js +21 -0
- package/dist/lexicon/types/app/bsky/embed/external.js.map +1 -1
- package/dist/lexicon/types/app/bsky/embed/getEmbedExternalView.d.ts +31 -0
- package/dist/lexicon/types/app/bsky/embed/getEmbedExternalView.d.ts.map +1 -0
- package/dist/lexicon/types/app/bsky/embed/getEmbedExternalView.js +5 -0
- package/dist/lexicon/types/app/bsky/embed/getEmbedExternalView.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/actor/getStatus.d.ts +24 -0
- package/dist/lexicon/types/chat/bsky/actor/getStatus.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/{group/getJoinLinkPreview.js → actor/getStatus.js} +2 -2
- package/dist/lexicon/types/chat/bsky/actor/getStatus.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/convo/defs.d.ts +39 -7
- package/dist/lexicon/types/chat/bsky/convo/defs.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/convo/defs.js +21 -0
- package/dist/lexicon/types/chat/bsky/convo/defs.js.map +1 -1
- package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.d.ts +1 -1
- package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/convo/getConvoForMembers.js.map +1 -1
- package/dist/lexicon/types/chat/bsky/convo/getLog.d.ts +1 -1
- package/dist/lexicon/types/chat/bsky/convo/getLog.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/convo/getLog.js.map +1 -1
- package/dist/lexicon/types/chat/bsky/convo/listConvoRequests.d.ts +1 -1
- package/dist/lexicon/types/chat/bsky/convo/listConvoRequests.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/convo/listConvoRequests.js.map +1 -1
- package/dist/lexicon/types/chat/bsky/embed/joinLink.d.ts +19 -0
- package/dist/lexicon/types/chat/bsky/embed/joinLink.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/embed/joinLink.js +19 -0
- package/dist/lexicon/types/chat/bsky/embed/joinLink.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/group/addMembers.d.ts +1 -1
- package/dist/lexicon/types/chat/bsky/group/addMembers.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/group/addMembers.js.map +1 -1
- package/dist/lexicon/types/chat/bsky/group/createGroup.d.ts +1 -1
- package/dist/lexicon/types/chat/bsky/group/createGroup.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/group/createGroup.js.map +1 -1
- package/dist/lexicon/types/chat/bsky/group/defs.d.ts +23 -0
- package/dist/lexicon/types/chat/bsky/group/defs.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/group/defs.js +14 -0
- package/dist/lexicon/types/chat/bsky/group/defs.js.map +1 -1
- package/dist/lexicon/types/chat/bsky/group/{getJoinLinkPreview.d.ts → getJoinLinkPreviews.d.ts} +3 -4
- package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreviews.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreviews.js +5 -0
- package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreviews.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/group/updateJoinRequestsRead.d.ts +24 -0
- package/dist/lexicon/types/chat/bsky/group/updateJoinRequestsRead.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/group/updateJoinRequestsRead.js +5 -0
- package/dist/lexicon/types/chat/bsky/group/updateJoinRequestsRead.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/group/withdrawJoinRequest.d.ts +24 -0
- package/dist/lexicon/types/chat/bsky/group/withdrawJoinRequest.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/group/withdrawJoinRequest.js +5 -0
- package/dist/lexicon/types/chat/bsky/group/withdrawJoinRequest.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/defs.d.ts +42 -0
- package/dist/lexicon/types/chat/bsky/moderation/defs.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/defs.js +26 -0
- package/dist/lexicon/types/chat/bsky/moderation/defs.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvo.d.ts +23 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvo.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvo.js +5 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvo.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvoMembers.d.ts +26 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvoMembers.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvoMembers.js +5 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvoMembers.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvos.d.ts +22 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvos.d.ts.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvos.js +5 -0
- package/dist/lexicon/types/chat/bsky/moderation/getConvos.js.map +1 -0
- package/dist/lexicon/types/chat/bsky/moderation/subscribeModEvents.d.ts +13 -1
- package/dist/lexicon/types/chat/bsky/moderation/subscribeModEvents.d.ts.map +1 -1
- package/dist/lexicon/types/chat/bsky/moderation/subscribeModEvents.js +7 -0
- package/dist/lexicon/types/chat/bsky/moderation/subscribeModEvents.js.map +1 -1
- package/dist/lexicon/types/com/atproto/server/getServiceAuth.d.ts +1 -1
- package/dist/lexicon/types/com/atproto/server/getServiceAuth.d.ts.map +1 -1
- package/dist/lexicon/types/com/atproto/server/getServiceAuth.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +10 -3
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js +7 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts +2 -2
- package/dist/lexicon/types/tools/ozone/moderation/queryEvents.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryEvents.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts +2 -2
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/queryStatuses.js.map +1 -1
- package/dist/mod-service/expiring-tags.d.ts +3 -0
- package/dist/mod-service/expiring-tags.d.ts.map +1 -1
- package/dist/mod-service/expiring-tags.js +5 -2
- package/dist/mod-service/expiring-tags.js.map +1 -1
- package/dist/mod-service/index.d.ts +3 -1
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +48 -10
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/status.d.ts +7 -1
- package/dist/mod-service/status.d.ts.map +1 -1
- package/dist/mod-service/status.js +18 -3
- package/dist/mod-service/status.js.map +1 -1
- package/dist/mod-service/subject.d.ts +38 -2
- package/dist/mod-service/subject.d.ts.map +1 -1
- package/dist/mod-service/subject.js +67 -0
- package/dist/mod-service/subject.js.map +1 -1
- package/dist/mod-service/views.d.ts +2 -1
- package/dist/mod-service/views.d.ts.map +1 -1
- package/dist/mod-service/views.js +45 -23
- package/dist/mod-service/views.js.map +1 -1
- package/dist/queue/service.d.ts.map +1 -1
- package/dist/queue/service.js +7 -3
- package/dist/queue/service.js.map +1 -1
- package/dist/safelink/service.d.ts +1 -1
- package/package.json +5 -5
- package/src/api/chat/getConvo.ts +23 -0
- package/src/api/chat/getConvoMembers.ts +23 -0
- package/src/api/chat/index.ts +4 -0
- package/src/daemon/event-reverser.ts +1 -0
- package/src/db/migrations/20260513T202941104Z-add-subject-convo-id.ts +114 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/db/schema/expiring_tag.ts +1 -0
- package/src/db/schema/moderation_event.ts +2 -0
- package/src/db/schema/moderation_subject_status.ts +4 -0
- package/src/db/schema/report.ts +2 -1
- package/src/mod-service/expiring-tags.ts +8 -2
- package/src/mod-service/index.ts +52 -16
- package/src/mod-service/status.ts +24 -2
- package/src/mod-service/subject.ts +79 -1
- package/src/mod-service/views.ts +52 -24
- package/src/queue/service.ts +8 -4
- package/tests/__snapshots__/verification.test.ts.snap +2 -0
- package/tests/expiring-tags.test.ts +1 -0
- package/tests/moderation-events.test.ts +108 -1
- package/tests/moderation-status-tags.test.ts +23 -0
- package/tests/moderation-statuses.test.ts +82 -0
- package/tests/moderation.test.ts +73 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreview.d.ts.map +0 -1
- package/dist/lexicon/types/chat/bsky/group/getJoinLinkPreview.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/ozone",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backend service for moderating the Bluesky network.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
"uint8arrays": "^5.0.0",
|
|
34
34
|
"undici": "^6.14.1",
|
|
35
35
|
"ws": "^8.12.0",
|
|
36
|
-
"@atproto/api": "^0.20.1",
|
|
37
36
|
"@atproto/common": "^0.6.1",
|
|
38
37
|
"@atproto/crypto": "^0.5.0",
|
|
39
38
|
"@atproto/identity": "^0.5.0",
|
|
40
39
|
"@atproto/lexicon": "^0.7.1",
|
|
40
|
+
"@atproto/api": "^0.20.8",
|
|
41
41
|
"@atproto/syntax": "^0.6.1",
|
|
42
42
|
"@atproto/ws-client": "^0.1.0",
|
|
43
43
|
"@atproto/xrpc": "^0.8.0",
|
|
44
|
-
"@atproto/xrpc-server": "^0.11.
|
|
44
|
+
"@atproto/xrpc-server": "^0.11.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@did-plc/server": "^0.0.1",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"jest": "^30.0.0",
|
|
55
55
|
"ts-node": "^10.8.2",
|
|
56
56
|
"typescript": "^6.0.3",
|
|
57
|
-
"@atproto/
|
|
58
|
-
"@atproto/
|
|
57
|
+
"@atproto/lex-cli": "^0.10.0",
|
|
58
|
+
"@atproto/pds": "^0.5.1"
|
|
59
59
|
},
|
|
60
60
|
"type": "module",
|
|
61
61
|
"exports": {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
2
|
+
import { AppContext } from '../../context.js'
|
|
3
|
+
import { Server } from '../../lexicon/index.js'
|
|
4
|
+
import { ids } from '../../lexicon/lexicons.js'
|
|
5
|
+
|
|
6
|
+
export default function (server: Server, ctx: AppContext) {
|
|
7
|
+
server.chat.bsky.moderation.getConvo({
|
|
8
|
+
auth: ctx.authVerifier.modOrAdminToken,
|
|
9
|
+
handler: async ({ params }) => {
|
|
10
|
+
if (!ctx.chatAgent) {
|
|
11
|
+
throw new InvalidRequestError('No chat agent configured')
|
|
12
|
+
}
|
|
13
|
+
const res = await ctx.chatAgent.api.chat.bsky.moderation.getConvo(
|
|
14
|
+
params,
|
|
15
|
+
await ctx.chatAuth(ids.ChatBskyModerationGetConvo),
|
|
16
|
+
)
|
|
17
|
+
return {
|
|
18
|
+
encoding: 'application/json',
|
|
19
|
+
body: res.data,
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
2
|
+
import { AppContext } from '../../context.js'
|
|
3
|
+
import { Server } from '../../lexicon/index.js'
|
|
4
|
+
import { ids } from '../../lexicon/lexicons.js'
|
|
5
|
+
|
|
6
|
+
export default function (server: Server, ctx: AppContext) {
|
|
7
|
+
server.chat.bsky.moderation.getConvoMembers({
|
|
8
|
+
auth: ctx.authVerifier.modOrAdminToken,
|
|
9
|
+
handler: async ({ params }) => {
|
|
10
|
+
if (!ctx.chatAgent) {
|
|
11
|
+
throw new InvalidRequestError('No chat agent configured')
|
|
12
|
+
}
|
|
13
|
+
const res = await ctx.chatAgent.api.chat.bsky.moderation.getConvoMembers(
|
|
14
|
+
params,
|
|
15
|
+
await ctx.chatAuth(ids.ChatBskyModerationGetConvoMembers),
|
|
16
|
+
)
|
|
17
|
+
return {
|
|
18
|
+
encoding: 'application/json',
|
|
19
|
+
body: res.data,
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
package/src/api/chat/index.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { AppContext } from '../../context.js'
|
|
2
2
|
import { Server } from '../../lexicon/index.js'
|
|
3
3
|
import getActorMetadata from './getActorMetadata.js'
|
|
4
|
+
import getConvo from './getConvo.js'
|
|
5
|
+
import getConvoMembers from './getConvoMembers.js'
|
|
4
6
|
import getMessageContext from './getMessageContext.js'
|
|
5
7
|
|
|
6
8
|
export default function (server: Server, ctx: AppContext) {
|
|
7
9
|
getActorMetadata(server, ctx)
|
|
10
|
+
getConvo(server, ctx)
|
|
11
|
+
getConvoMembers(server, ctx)
|
|
8
12
|
getMessageContext(server, ctx)
|
|
9
13
|
return server
|
|
10
14
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Kysely, sql } from 'kysely'
|
|
2
|
+
|
|
3
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
+
// moderation_event
|
|
5
|
+
await db.schema
|
|
6
|
+
.alterTable('moderation_event')
|
|
7
|
+
.addColumn('subjectConvoId', 'varchar')
|
|
8
|
+
.execute()
|
|
9
|
+
/// broad index to support conversation-based queries
|
|
10
|
+
/// Users is a convo is capped at 50 so volume of matching events should be low
|
|
11
|
+
/// subsequent ordering and filtering will be efficient
|
|
12
|
+
await sql`
|
|
13
|
+
CREATE INDEX "moderation_event_convo_idx"
|
|
14
|
+
ON moderation_event("subjectConvoId")
|
|
15
|
+
WHERE "subjectConvoId" IS NOT NULL
|
|
16
|
+
`.execute(db)
|
|
17
|
+
|
|
18
|
+
// moderation_subject_status
|
|
19
|
+
await db.schema
|
|
20
|
+
.alterTable('moderation_subject_status')
|
|
21
|
+
.addColumn('convoId', 'varchar', (col) => col.notNull().defaultTo(''))
|
|
22
|
+
.execute()
|
|
23
|
+
/// Update unique constraint: [did, recordPath] -> [did, recordPath, convoId]
|
|
24
|
+
/// Create new unique index, drop old constraint, rename new index
|
|
25
|
+
/// Avoids gap without any constraints
|
|
26
|
+
await sql`
|
|
27
|
+
CREATE UNIQUE INDEX "moderation_status_unique_idx_new"
|
|
28
|
+
ON moderation_subject_status(did, "recordPath", "convoId")
|
|
29
|
+
`.execute(db)
|
|
30
|
+
await db.schema
|
|
31
|
+
.alterTable('moderation_subject_status')
|
|
32
|
+
.dropConstraint('moderation_status_unique_idx')
|
|
33
|
+
.execute()
|
|
34
|
+
await db.schema.dropIndex('moderation_status_unique_idx').ifExists().execute()
|
|
35
|
+
await sql`
|
|
36
|
+
ALTER INDEX "moderation_status_unique_idx_new"
|
|
37
|
+
RENAME TO "moderation_status_unique_idx"
|
|
38
|
+
`.execute(db)
|
|
39
|
+
await sql`
|
|
40
|
+
ALTER TABLE moderation_subject_status
|
|
41
|
+
ADD CONSTRAINT "moderation_status_unique_idx"
|
|
42
|
+
UNIQUE USING INDEX "moderation_status_unique_idx"
|
|
43
|
+
`.execute(db)
|
|
44
|
+
|
|
45
|
+
// expiring_tag
|
|
46
|
+
await db.schema
|
|
47
|
+
.alterTable('expiring_tag')
|
|
48
|
+
.addColumn('convoId', 'varchar', (col) => col.notNull().defaultTo(''))
|
|
49
|
+
.execute()
|
|
50
|
+
await db.schema
|
|
51
|
+
.createIndex('idx_expiring_tag_did_record_path_convo_id')
|
|
52
|
+
.on('expiring_tag')
|
|
53
|
+
.columns(['did', 'recordPath', 'convoId'])
|
|
54
|
+
.execute()
|
|
55
|
+
|
|
56
|
+
// report table
|
|
57
|
+
await db.schema
|
|
58
|
+
.alterTable('report')
|
|
59
|
+
.addColumn('subjectConvoId', 'varchar')
|
|
60
|
+
.execute()
|
|
61
|
+
await db.schema.dropIndex('idx_report_unassigned_id').execute()
|
|
62
|
+
await sql`CREATE INDEX idx_report_unassigned_id ON report
|
|
63
|
+
(id)
|
|
64
|
+
INCLUDE (status, "reportType", "recordPath", "subjectMessageId", "subjectConvoId")
|
|
65
|
+
WHERE ("queueId" IS NULL AND status != 'closed')`.execute(db)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
69
|
+
// report
|
|
70
|
+
await db.schema.dropIndex('idx_report_unassigned_id').execute()
|
|
71
|
+
await sql`CREATE INDEX idx_report_unassigned_id ON report
|
|
72
|
+
(id)
|
|
73
|
+
INCLUDE (status, "reportType", "recordPath", "subjectMessageId")
|
|
74
|
+
WHERE ("queueId" IS NULL AND status != 'closed')`.execute(db)
|
|
75
|
+
await db.schema.alterTable('report').dropColumn('subjectConvoId').execute()
|
|
76
|
+
|
|
77
|
+
// expiring_tag
|
|
78
|
+
await db.schema
|
|
79
|
+
.dropIndex('idx_expiring_tag_did_record_path_convo_id')
|
|
80
|
+
.execute()
|
|
81
|
+
await db.schema.alterTable('expiring_tag').dropColumn('convoId').execute()
|
|
82
|
+
|
|
83
|
+
// moderation_subject_status
|
|
84
|
+
/// Reverse the unique constraint change: [did, recordPath, convoId] -> [did, recordPath]
|
|
85
|
+
await sql`
|
|
86
|
+
CREATE UNIQUE INDEX "moderation_status_unique_idx_old"
|
|
87
|
+
ON moderation_subject_status(did, "recordPath")
|
|
88
|
+
`.execute(db)
|
|
89
|
+
await db.schema
|
|
90
|
+
.alterTable('moderation_subject_status')
|
|
91
|
+
.dropConstraint('moderation_status_unique_idx')
|
|
92
|
+
.execute()
|
|
93
|
+
await db.schema.dropIndex('moderation_status_unique_idx').ifExists().execute()
|
|
94
|
+
await sql`
|
|
95
|
+
ALTER INDEX "moderation_status_unique_idx_old"
|
|
96
|
+
RENAME TO "moderation_status_unique_idx"
|
|
97
|
+
`.execute(db)
|
|
98
|
+
await sql`
|
|
99
|
+
ALTER TABLE moderation_subject_status
|
|
100
|
+
ADD CONSTRAINT "moderation_status_unique_idx"
|
|
101
|
+
UNIQUE USING INDEX "moderation_status_unique_idx"
|
|
102
|
+
`.execute(db)
|
|
103
|
+
await db.schema
|
|
104
|
+
.alterTable('moderation_subject_status')
|
|
105
|
+
.dropColumn('convoId')
|
|
106
|
+
.execute()
|
|
107
|
+
|
|
108
|
+
// moderation_event
|
|
109
|
+
await db.schema.dropIndex('moderation_event_convo_idx').execute()
|
|
110
|
+
await db.schema
|
|
111
|
+
.alterTable('moderation_event')
|
|
112
|
+
.dropColumn('subjectConvoId')
|
|
113
|
+
.execute()
|
|
114
|
+
}
|
|
@@ -40,3 +40,4 @@ export * as _20260225T000000000Z from './20260225T000000000Z-add-report-queue-ta
|
|
|
40
40
|
export * as _20260313T000000000Z from './20260313T000000000Z-add-report-activity-table.js'
|
|
41
41
|
export * as _20260318T152058935Z from './20260318T152058935Z-add-report-stat.js'
|
|
42
42
|
export * as _20260428T000000000Z from './20260428T000000000Z-add-expiring-tag-table.js'
|
|
43
|
+
export * as _20260513T202941104Z from './20260513T202941104Z-add-subject-convo-id.js'
|
|
@@ -31,10 +31,12 @@ export interface ModerationEvent {
|
|
|
31
31
|
| 'com.atproto.admin.defs#repoRef'
|
|
32
32
|
| 'com.atproto.repo.strongRef'
|
|
33
33
|
| 'chat.bsky.convo.defs#messageRef'
|
|
34
|
+
| 'chat.bsky.convo.defs#convoRef'
|
|
34
35
|
subjectDid: string
|
|
35
36
|
subjectUri: string | null
|
|
36
37
|
subjectCid: string | null
|
|
37
38
|
subjectBlobCids: string[] | null
|
|
39
|
+
subjectConvoId: string | null
|
|
38
40
|
subjectMessageId: string | null
|
|
39
41
|
createLabelVals: string | null
|
|
40
42
|
negateLabelVals: string | null
|
|
@@ -10,8 +10,12 @@ export const subjectStatusTableName = 'moderation_subject_status'
|
|
|
10
10
|
|
|
11
11
|
export interface ModerationSubjectStatus {
|
|
12
12
|
id: Generated<number>
|
|
13
|
+
|
|
14
|
+
// unique columns
|
|
13
15
|
did: string
|
|
14
16
|
recordPath: string
|
|
17
|
+
convoId: string
|
|
18
|
+
|
|
15
19
|
recordCid: string | null
|
|
16
20
|
blobCids: string[] | null
|
|
17
21
|
reviewState:
|
package/src/db/schema/report.ts
CHANGED
|
@@ -13,8 +13,9 @@ export interface Report {
|
|
|
13
13
|
status: string // 'open', 'closed', 'escalated', 'queued', 'assigned'
|
|
14
14
|
reportType: string // Denormalized from moderation_event.meta.reportType
|
|
15
15
|
did: string // Denormalized from moderation_event.subjectDid
|
|
16
|
-
recordPath: string // '' = account/message, 'collection/rkey' = record
|
|
16
|
+
recordPath: string // '' = account/message/conversation, 'collection/rkey' = record
|
|
17
17
|
subjectMessageId: string | null // Denormalized from moderation_event.subjectMessageId
|
|
18
|
+
subjectConvoId: string | null // Denormalized from moderation_event.subjectConvoId
|
|
18
19
|
createdAt: string
|
|
19
20
|
updatedAt: string
|
|
20
21
|
assignedTo: string | null // DID of permanently assigned moderator, null if unassigned
|
|
@@ -7,6 +7,7 @@ export type ExpiringTagRow = Selectable<ExpiringTag>
|
|
|
7
7
|
export type ExpiringTagGroup = {
|
|
8
8
|
did: string
|
|
9
9
|
recordPath: string
|
|
10
|
+
convoId: string
|
|
10
11
|
createdBy: string
|
|
11
12
|
tags: string[]
|
|
12
13
|
ids: number[]
|
|
@@ -18,6 +19,7 @@ export async function insertExpiringTags(
|
|
|
18
19
|
eventId: number
|
|
19
20
|
did: string
|
|
20
21
|
recordPath: string
|
|
22
|
+
convoId: string
|
|
21
23
|
tags: string[]
|
|
22
24
|
expiresAt: string
|
|
23
25
|
createdBy: string
|
|
@@ -30,6 +32,7 @@ export async function insertExpiringTags(
|
|
|
30
32
|
eventId: params.eventId,
|
|
31
33
|
did: params.did,
|
|
32
34
|
recordPath: params.recordPath,
|
|
35
|
+
convoId: params.convoId,
|
|
33
36
|
tag,
|
|
34
37
|
expiresAt: params.expiresAt,
|
|
35
38
|
createdBy: params.createdBy,
|
|
@@ -43,6 +46,7 @@ export async function removeExpiringTags(
|
|
|
43
46
|
params: {
|
|
44
47
|
did: string
|
|
45
48
|
recordPath: string
|
|
49
|
+
convoId: string
|
|
46
50
|
tags: string[]
|
|
47
51
|
},
|
|
48
52
|
): Promise<void> {
|
|
@@ -50,6 +54,7 @@ export async function removeExpiringTags(
|
|
|
50
54
|
.deleteFrom('expiring_tag')
|
|
51
55
|
.where('did', '=', params.did)
|
|
52
56
|
.where('recordPath', '=', params.recordPath)
|
|
57
|
+
.where('convoId', '=', params.convoId)
|
|
53
58
|
.where('tag', 'in', params.tags)
|
|
54
59
|
.execute()
|
|
55
60
|
}
|
|
@@ -73,15 +78,16 @@ export async function getExpiredTags(
|
|
|
73
78
|
|
|
74
79
|
if (!rows.length) return []
|
|
75
80
|
|
|
76
|
-
// Group by (did, recordPath, createdBy) so each reversal event has the correct author
|
|
81
|
+
// Group by (did, recordPath, convoId, createdBy) so each reversal event has the correct author
|
|
77
82
|
const grouped = new Map<string, ExpiringTagGroup>()
|
|
78
83
|
for (const row of rows) {
|
|
79
|
-
const key = `${row.did}|${row.recordPath}|${row.createdBy}`
|
|
84
|
+
const key = `${row.did}|${row.recordPath}|${row.convoId}|${row.createdBy}`
|
|
80
85
|
let group = grouped.get(key)
|
|
81
86
|
if (!group) {
|
|
82
87
|
group = {
|
|
83
88
|
did: row.did,
|
|
84
89
|
recordPath: row.recordPath,
|
|
90
|
+
convoId: row.convoId,
|
|
85
91
|
createdBy: row.createdBy,
|
|
86
92
|
tags: [],
|
|
87
93
|
ids: [],
|
package/src/mod-service/index.ts
CHANGED
|
@@ -235,21 +235,34 @@ export class ModerationService {
|
|
|
235
235
|
|
|
236
236
|
if (subject) {
|
|
237
237
|
const isSubjectAtUri = subject.startsWith('at://')
|
|
238
|
+
const subjectAtUri = isSubjectAtUri ? new AtUri(subject) : null
|
|
238
239
|
const subjectDid = isSubjectAtUri ? new AtUri(subject).hostname : subject
|
|
239
240
|
const subjectUri = isSubjectAtUri ? subject : null
|
|
240
241
|
// regardless of subjectUri check, we always want to query against subjectDid column since that's indexed
|
|
241
242
|
builder = builder.where('subjectDid', '=', subjectDid)
|
|
242
243
|
|
|
243
|
-
//
|
|
244
|
+
// subjectUri or subjectConvoId
|
|
244
245
|
if (!includeAllUserRecords) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
246
|
+
if (subjectAtUri?.collection === 'chat.bsky.convo') {
|
|
247
|
+
builder = builder.where('subjectConvoId', '=', subjectAtUri.rkey)
|
|
248
|
+
} else if (subjectUri) {
|
|
249
|
+
builder = builder.where('subjectUri', '=', subjectUri)
|
|
250
|
+
} else {
|
|
251
|
+
// Account-level: subjectUri IS NULL also matches conversation events,
|
|
252
|
+
// so explicitly exclude them.
|
|
253
|
+
builder = builder
|
|
254
|
+
.where('subjectUri', 'is', null)
|
|
255
|
+
.where('subjectConvoId', 'is', null)
|
|
256
|
+
}
|
|
248
257
|
}
|
|
249
258
|
} else if (subjectType === 'account') {
|
|
250
|
-
builder = builder
|
|
259
|
+
builder = builder
|
|
260
|
+
.where('subjectUri', 'is', null)
|
|
261
|
+
.where('subjectConvoId', 'is', null)
|
|
251
262
|
} else if (subjectType === 'record') {
|
|
252
263
|
builder = builder.where('subjectUri', 'is not', null)
|
|
264
|
+
} else if (subjectType === 'conversation') {
|
|
265
|
+
builder = builder.where('subjectConvoId', 'is not', null)
|
|
253
266
|
}
|
|
254
267
|
|
|
255
268
|
// If subjectType is set to 'account' let that take priority and ignore collections filter
|
|
@@ -433,7 +446,9 @@ export class ModerationService {
|
|
|
433
446
|
const subjectsToBeResolved = await this.db.db
|
|
434
447
|
.selectFrom('moderation_subject_status')
|
|
435
448
|
.where('did', '=', did)
|
|
436
|
-
.where(
|
|
449
|
+
.where((qb) =>
|
|
450
|
+
qb.where('recordPath', '!=', '').orWhere('convoId', '!=', ''),
|
|
451
|
+
)
|
|
437
452
|
.where('reviewState', 'in', [REVIEWESCALATED, REVIEWOPEN])
|
|
438
453
|
.selectAll()
|
|
439
454
|
.execute()
|
|
@@ -686,6 +701,7 @@ export class ModerationService {
|
|
|
686
701
|
subjectCid: subjectInfo.subjectCid,
|
|
687
702
|
subjectBlobCids: jsonb(subjectInfo.subjectBlobCids),
|
|
688
703
|
subjectMessageId: subjectInfo.subjectMessageId,
|
|
704
|
+
subjectConvoId: subjectInfo.subjectConvoId,
|
|
689
705
|
modTool: modTool ? jsonb(modTool) : null,
|
|
690
706
|
externalId: externalId ?? null,
|
|
691
707
|
severityLevel,
|
|
@@ -712,6 +728,7 @@ export class ModerationService {
|
|
|
712
728
|
eventId: modEvent.id,
|
|
713
729
|
did: subjectInfo.subjectDid,
|
|
714
730
|
recordPath: subjectInfo.subjectUri ?? '',
|
|
731
|
+
convoId: subjectInfo.subjectConvoId ?? '',
|
|
715
732
|
tags: event.add,
|
|
716
733
|
expiresAt,
|
|
717
734
|
createdBy,
|
|
@@ -721,6 +738,7 @@ export class ModerationService {
|
|
|
721
738
|
await removeExpiringTags(this.db, {
|
|
722
739
|
did: subjectInfo.subjectDid,
|
|
723
740
|
recordPath: subjectInfo.subjectUri ?? '',
|
|
741
|
+
convoId: subjectInfo.subjectConvoId ?? '',
|
|
724
742
|
tags: event.remove,
|
|
725
743
|
})
|
|
726
744
|
}
|
|
@@ -815,6 +833,7 @@ export class ModerationService {
|
|
|
815
833
|
.selectFrom('moderation_subject_status')
|
|
816
834
|
.where('did', '=', did)
|
|
817
835
|
.where('recordPath', '=', '')
|
|
836
|
+
.where('convoId', '=', '')
|
|
818
837
|
.where('suspendUntil', '>', new Date().toISOString())
|
|
819
838
|
.select('did')
|
|
820
839
|
.limit(1)
|
|
@@ -1135,20 +1154,34 @@ export class ModerationService {
|
|
|
1135
1154
|
)
|
|
1136
1155
|
|
|
1137
1156
|
if (!includeAllUserRecords) {
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1157
|
+
if (subjectInfo.convoId) {
|
|
1158
|
+
builder = builder.where(
|
|
1159
|
+
'moderation_subject_status.convoId',
|
|
1160
|
+
'=',
|
|
1161
|
+
subjectInfo.convoId,
|
|
1162
|
+
)
|
|
1163
|
+
} else if (subjectInfo.recordPath) {
|
|
1164
|
+
builder = builder.where(
|
|
1165
|
+
'moderation_subject_status.recordPath',
|
|
1166
|
+
'=',
|
|
1167
|
+
subjectInfo.recordPath,
|
|
1168
|
+
)
|
|
1169
|
+
} else {
|
|
1170
|
+
// Account-level: recordPath = '' also matches conversation statuses,
|
|
1171
|
+
// so explicitly exclude them.
|
|
1172
|
+
builder = builder
|
|
1173
|
+
.where('moderation_subject_status.recordPath', '=', '')
|
|
1174
|
+
.where('moderation_subject_status.convoId', '=', '')
|
|
1175
|
+
}
|
|
1147
1176
|
}
|
|
1148
1177
|
} else if (subjectType === 'account') {
|
|
1149
|
-
builder = builder
|
|
1178
|
+
builder = builder
|
|
1179
|
+
.where('moderation_subject_status.recordPath', '=', '')
|
|
1180
|
+
.where('moderation_subject_status.convoId', '=', '')
|
|
1150
1181
|
} else if (subjectType === 'record') {
|
|
1151
1182
|
builder = builder.where('moderation_subject_status.recordPath', '!=', '')
|
|
1183
|
+
} else if (subjectType === 'conversation') {
|
|
1184
|
+
builder = builder.where('moderation_subject_status.convoId', '!=', '')
|
|
1152
1185
|
}
|
|
1153
1186
|
|
|
1154
1187
|
// Only fetch items that belongs to the specified queue when specified
|
|
@@ -1438,6 +1471,7 @@ export class ModerationService {
|
|
|
1438
1471
|
.selectFrom('moderation_subject_status')
|
|
1439
1472
|
.where('did', '=', subject.did)
|
|
1440
1473
|
.where('recordPath', '=', subject.recordPath ?? '')
|
|
1474
|
+
.where('convoId', '=', subject.convoId ?? '')
|
|
1441
1475
|
.selectAll()
|
|
1442
1476
|
.executeTakeFirst()
|
|
1443
1477
|
return result ?? null
|
|
@@ -1450,6 +1484,7 @@ export class ModerationService {
|
|
|
1450
1484
|
.selectFrom('moderation_subject_status')
|
|
1451
1485
|
.where('did', '=', did)
|
|
1452
1486
|
.where('recordPath', '=', '')
|
|
1487
|
+
.where('convoId', '=', '')
|
|
1453
1488
|
.where('muteReportingUntil', '>', new Date().toISOString())
|
|
1454
1489
|
.select(sql`true`.as('status'))
|
|
1455
1490
|
.executeTakeFirst()
|
|
@@ -1463,6 +1498,7 @@ export class ModerationService {
|
|
|
1463
1498
|
.selectFrom('moderation_subject_status')
|
|
1464
1499
|
.where('did', '=', did)
|
|
1465
1500
|
.where('recordPath', '=', '')
|
|
1501
|
+
.where('convoId', '=', '')
|
|
1466
1502
|
.where('muteUntil', '>', new Date().toISOString())
|
|
1467
1503
|
.select(sql`true`.as('status'))
|
|
1468
1504
|
.executeTakeFirst()
|
|
@@ -283,6 +283,7 @@ export const adjustModerationSubjectStatus = async (
|
|
|
283
283
|
subjectDid,
|
|
284
284
|
subjectUri,
|
|
285
285
|
subjectCid,
|
|
286
|
+
subjectConvoId,
|
|
286
287
|
createdBy,
|
|
287
288
|
meta,
|
|
288
289
|
addedTags,
|
|
@@ -292,7 +293,10 @@ export const adjustModerationSubjectStatus = async (
|
|
|
292
293
|
} = moderationEvent
|
|
293
294
|
|
|
294
295
|
// If subjectUri exists, it's not a repoRef so pass along the uri to get identifier back
|
|
295
|
-
const identifier = getStatusIdentifierFromSubject(
|
|
296
|
+
const identifier = getStatusIdentifierFromSubject(
|
|
297
|
+
subjectUri || subjectDid,
|
|
298
|
+
subjectConvoId,
|
|
299
|
+
)
|
|
296
300
|
|
|
297
301
|
db.assertTransaction()
|
|
298
302
|
|
|
@@ -301,6 +305,7 @@ export const adjustModerationSubjectStatus = async (
|
|
|
301
305
|
.selectFrom('moderation_subject_status')
|
|
302
306
|
.where('did', '=', identifier.did)
|
|
303
307
|
.where('recordPath', '=', identifier.recordPath)
|
|
308
|
+
.where('convoId', '=', identifier.convoId)
|
|
304
309
|
// Make sure we respect other updates that may be happening at the same time
|
|
305
310
|
.forUpdate()
|
|
306
311
|
.selectAll()
|
|
@@ -504,14 +509,20 @@ export const adjustModerationSubjectStatus = async (
|
|
|
504
509
|
return status || null
|
|
505
510
|
}
|
|
506
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Get moderation_subject_status identifier (did, recordPath, convoId).
|
|
514
|
+
* @note Supports addressing conversations explicitly (via convoId) and implicitly (via properly formed at-uri)
|
|
515
|
+
*/
|
|
507
516
|
export const getStatusIdentifierFromSubject = (
|
|
508
517
|
subject: string | AtUri,
|
|
509
|
-
|
|
518
|
+
convoId?: string | null,
|
|
519
|
+
): { did: string; recordPath: string; convoId: string } => {
|
|
510
520
|
const isSubjectString = typeof subject === 'string'
|
|
511
521
|
if (isSubjectString && subject.startsWith('did:')) {
|
|
512
522
|
return {
|
|
513
523
|
did: subject,
|
|
514
524
|
recordPath: '',
|
|
525
|
+
convoId: convoId || '',
|
|
515
526
|
}
|
|
516
527
|
}
|
|
517
528
|
|
|
@@ -520,8 +531,19 @@ export const getStatusIdentifierFromSubject = (
|
|
|
520
531
|
}
|
|
521
532
|
|
|
522
533
|
const uri = isSubjectString ? new AtUri(subject) : subject
|
|
534
|
+
|
|
535
|
+
// Handle conversation URIs
|
|
536
|
+
if (uri.collection === 'chat.bsky.convo') {
|
|
537
|
+
return {
|
|
538
|
+
did: uri.host,
|
|
539
|
+
recordPath: '',
|
|
540
|
+
convoId: uri.rkey,
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
523
544
|
return {
|
|
524
545
|
did: uri.host,
|
|
525
546
|
recordPath: `${uri.collection}/${uri.rkey}`,
|
|
547
|
+
convoId: '',
|
|
526
548
|
}
|
|
527
549
|
}
|