@atproto/ozone 0.2.8 → 0.2.11
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 +33 -0
- package/package.json +25 -21
- package/bin/migration-create.ts +0 -38
- package/jest.config.cjs +0 -22
- package/src/api/chat/getActorMetadata.ts +0 -23
- package/src/api/chat/getConvo.ts +0 -23
- package/src/api/chat/getConvoMembers.ts +0 -23
- package/src/api/chat/getConvos.ts +0 -23
- package/src/api/chat/getMessageContext.ts +0 -42
- package/src/api/chat/index.ts +0 -16
- package/src/api/communication/createTemplate.ts +0 -51
- package/src/api/communication/deleteTemplate.ts +0 -23
- package/src/api/communication/listTemplates.ts +0 -31
- package/src/api/communication/updateTemplate.ts +0 -51
- package/src/api/health.ts +0 -27
- package/src/api/index.ts +0 -146
- package/src/api/label/fetchLabels.ts +0 -32
- package/src/api/label/queryLabels.ts +0 -57
- package/src/api/label/subscribeLabels.ts +0 -25
- package/src/api/moderation/cancelScheduledActions.ts +0 -72
- package/src/api/moderation/emitEvent.ts +0 -475
- package/src/api/moderation/getAccountTimeline.ts +0 -160
- package/src/api/moderation/getEvent.ts +0 -19
- package/src/api/moderation/getRecord.ts +0 -40
- package/src/api/moderation/getRecords.ts +0 -50
- package/src/api/moderation/getRepo.ts +0 -34
- package/src/api/moderation/getReporterStats.ts +0 -18
- package/src/api/moderation/getRepos.ts +0 -41
- package/src/api/moderation/getSubjects.ts +0 -101
- package/src/api/moderation/listScheduledActions.ts +0 -45
- package/src/api/moderation/queryEvents.ts +0 -72
- package/src/api/moderation/queryStatuses.ts +0 -23
- package/src/api/moderation/scheduleAction.ts +0 -129
- package/src/api/moderation/searchRepos.ts +0 -46
- package/src/api/moderation/util.ts +0 -96
- package/src/api/proxied.ts +0 -327
- package/src/api/queue/assignModerator.ts +0 -31
- package/src/api/queue/createQueue.ts +0 -62
- package/src/api/queue/deleteQueue.ts +0 -56
- package/src/api/queue/getAssignments.ts +0 -19
- package/src/api/queue/listQueues.ts +0 -39
- package/src/api/queue/routeReports.ts +0 -44
- package/src/api/queue/unassignModerator.ts +0 -26
- package/src/api/queue/updateQueue.ts +0 -54
- package/src/api/report/assignModerator.ts +0 -36
- package/src/api/report/createActivity.ts +0 -57
- package/src/api/report/createReport.ts +0 -93
- package/src/api/report/getAssignments.ts +0 -20
- package/src/api/report/getHistoricalStats.ts +0 -41
- package/src/api/report/getLatestReport.ts +0 -44
- package/src/api/report/getLiveStats.ts +0 -26
- package/src/api/report/getReport.ts +0 -55
- package/src/api/report/listActivities.ts +0 -37
- package/src/api/report/queryActivities.ts +0 -64
- package/src/api/report/queryReports.ts +0 -44
- package/src/api/report/reassignQueue.ts +0 -68
- package/src/api/report/refreshStats.ts +0 -27
- package/src/api/report/unassignModerator.ts +0 -21
- package/src/api/safelink/addRule.ts +0 -48
- package/src/api/safelink/queryEvents.ts +0 -32
- package/src/api/safelink/queryRules.ts +0 -58
- package/src/api/safelink/removeRule.ts +0 -42
- package/src/api/safelink/updateRule.ts +0 -48
- package/src/api/server/getConfig.ts +0 -35
- package/src/api/set/addValues.ts +0 -28
- package/src/api/set/deleteSet.ts +0 -34
- package/src/api/set/deleteValues.ts +0 -31
- package/src/api/set/getValues.ts +0 -42
- package/src/api/set/querySets.ts +0 -36
- package/src/api/set/upsertSet.ts +0 -38
- package/src/api/setting/listOptions.ts +0 -44
- package/src/api/setting/removeOptions.ts +0 -64
- package/src/api/setting/upsertOption.ts +0 -156
- package/src/api/team/addMember.ts +0 -51
- package/src/api/team/deleteMember.ts +0 -29
- package/src/api/team/listMembers.ts +0 -20
- package/src/api/team/updateMember.ts +0 -47
- package/src/api/util.ts +0 -265
- package/src/api/verification/grantVerifications.ts +0 -90
- package/src/api/verification/listVerifications.ts +0 -44
- package/src/api/verification/revokeVerifications.ts +0 -43
- package/src/api/well-known.ts +0 -46
- package/src/assignment/index.ts +0 -728
- package/src/auth-verifier.ts +0 -227
- package/src/background.ts +0 -183
- package/src/communication-service/template.ts +0 -110
- package/src/communication-service/util.ts +0 -8
- package/src/config/config.ts +0 -211
- package/src/config/env.ts +0 -95
- package/src/config/index.ts +0 -3
- package/src/config/secrets.ts +0 -17
- package/src/context.ts +0 -399
- package/src/daemon/blob-diverter.ts +0 -186
- package/src/daemon/context.ts +0 -247
- package/src/daemon/event-pusher.ts +0 -363
- package/src/daemon/event-reverser.ts +0 -128
- package/src/daemon/index.ts +0 -33
- package/src/daemon/job-cursor.ts +0 -33
- package/src/daemon/materialized-view-refresher.ts +0 -33
- package/src/daemon/queue-router.ts +0 -101
- package/src/daemon/scheduled-action-processor.ts +0 -304
- package/src/daemon/stats-computer.ts +0 -101
- package/src/daemon/strike-expiry-processor.ts +0 -95
- package/src/daemon/team-profile-synchronizer.ts +0 -15
- package/src/daemon/verification-listener.ts +0 -169
- package/src/db/index.ts +0 -203
- package/src/db/migrations/20231219T205730722Z-init.ts +0 -170
- package/src/db/migrations/20240116T085607200Z-communication-template.ts +0 -23
- package/src/db/migrations/20240201T051104136Z-mod-event-blobs.ts +0 -15
- package/src/db/migrations/20240208T213404429Z-add-tags-column-to-moderation-subject.ts +0 -31
- package/src/db/migrations/20240228T003647759Z-add-label-sigs.ts +0 -25
- package/src/db/migrations/20240408T192432676Z-mute-reporting.ts +0 -15
- package/src/db/migrations/20240506T225055595Z-message-subject.ts +0 -21
- package/src/db/migrations/20240521T211332580Z-member.ts +0 -17
- package/src/db/migrations/20240814T003647759Z-event-created-at-index.ts +0 -13
- package/src/db/migrations/20240903T205730722Z-add-template-lang.ts +0 -12
- package/src/db/migrations/20240904T205730722Z-add-subject-did-index.ts +0 -13
- package/src/db/migrations/20241001T205730722Z-subject-status-review-state-index.ts +0 -15
- package/src/db/migrations/20241008T205730722Z-sets.ts +0 -53
- package/src/db/migrations/20241018T205730722Z-setting.ts +0 -27
- package/src/db/migrations/20241026T205730722Z-add-hosting-status-to-subject-status.ts +0 -57
- package/src/db/migrations/20241220T144630860Z-stats-materialized-views.ts +0 -215
- package/src/db/migrations/20250204T003647759Z-add-subject-priority-score.ts +0 -22
- package/src/db/migrations/20250211T003647759Z-add-reporter-stats-index.ts +0 -38
- package/src/db/migrations/20250211T132135150Z-moderation-event-message-partial-idx.ts +0 -26
- package/src/db/migrations/20250221T132135150Z-member-details.ts +0 -14
- package/src/db/migrations/20250404T201720309Z-subject-status-sort-idxs.ts +0 -18
- package/src/db/migrations/20250415T201720309Z-verification.ts +0 -34
- package/src/db/migrations/20250417T201720309Z-firehose-cursor.ts +0 -16
- package/src/db/migrations/20250609T110704000Z-safelink.ts +0 -53
- package/src/db/migrations/20250618T180246000Z-add-mod-tool-to-moderation-event.ts +0 -18
- package/src/db/migrations/20250701T000000000Z-add-age-assurance-state.ts +0 -25
- package/src/db/migrations/20250715T000000000Z-add-mod-event-external-id.ts +0 -15
- package/src/db/migrations/20250718T150931000Z-update-appeal-reason-stats.ts +0 -310
- package/src/db/migrations/20250813T000000000Z-mod-tool-batch-id-index.ts +0 -14
- package/src/db/migrations/20250923T000000000Z-scheduled-actions.ts +0 -56
- package/src/db/migrations/20251008T120000000Z-add-strike-system.ts +0 -87
- package/src/db/migrations/20260210T154806448Z-mod-event-created-by-indexes.ts +0 -22
- package/src/db/migrations/20260219T164523000Z-create-report-table.ts +0 -155
- package/src/db/migrations/20260219T165302248Z-moderator-assignment.ts +0 -42
- package/src/db/migrations/20260225T000000000Z-add-report-queue-table.ts +0 -41
- package/src/db/migrations/20260313T000000000Z-add-report-activity-table.ts +0 -48
- package/src/db/migrations/20260318T152058935Z-add-report-stat.ts +0 -35
- package/src/db/migrations/20260428T000000000Z-add-expiring-tag-table.ts +0 -32
- package/src/db/migrations/20260513T202941104Z-add-subject-convo-id.ts +0 -114
- package/src/db/migrations/20260602T120000000Z-add-report-activity-created-index.ts +0 -17
- package/src/db/migrations/index.ts +0 -44
- package/src/db/migrations/provider.ts +0 -26
- package/src/db/pagination.ts +0 -335
- package/src/db/schema/account_events_stats.ts +0 -16
- package/src/db/schema/account_record_events_stats.ts +0 -15
- package/src/db/schema/account_record_status_stats.ts +0 -15
- package/src/db/schema/account_strike.ts +0 -13
- package/src/db/schema/blob_push_event.ts +0 -21
- package/src/db/schema/communication_template.ts +0 -19
- package/src/db/schema/expiring_tag.ts +0 -18
- package/src/db/schema/firehose_cursor.ts +0 -13
- package/src/db/schema/index.ts +0 -60
- package/src/db/schema/job_cursor.ts +0 -13
- package/src/db/schema/label.ts +0 -22
- package/src/db/schema/member.ts +0 -22
- package/src/db/schema/moderation_event.ts +0 -61
- package/src/db/schema/moderation_subject_status.ts +0 -52
- package/src/db/schema/moderator_assignment.ts +0 -16
- package/src/db/schema/ozone_set.ts +0 -24
- package/src/db/schema/record_events_stats.ts +0 -15
- package/src/db/schema/record_push_event.ts +0 -21
- package/src/db/schema/repo_push_event.ts +0 -19
- package/src/db/schema/report.ts +0 -28
- package/src/db/schema/report_activity.ts +0 -22
- package/src/db/schema/report_queue.ts +0 -21
- package/src/db/schema/report_stat.ts +0 -27
- package/src/db/schema/safelink.ts +0 -39
- package/src/db/schema/scheduled-action.ts +0 -25
- package/src/db/schema/setting.ts +0 -24
- package/src/db/schema/signing_key.ts +0 -10
- package/src/db/schema/verification.ts +0 -21
- package/src/db/types.ts +0 -24
- package/src/error.ts +0 -12
- package/src/image-invalidator.ts +0 -7
- package/src/index.ts +0 -154
- package/src/jetstream/service.ts +0 -107
- package/src/logger.ts +0 -29
- package/src/mod-service/expiring-tags.ts +0 -104
- package/src/mod-service/index.ts +0 -1842
- package/src/mod-service/profile.ts +0 -139
- package/src/mod-service/report.ts +0 -429
- package/src/mod-service/status.ts +0 -549
- package/src/mod-service/strike.ts +0 -96
- package/src/mod-service/subject.ts +0 -311
- package/src/mod-service/types.ts +0 -96
- package/src/mod-service/util.ts +0 -99
- package/src/mod-service/views.ts +0 -912
- package/src/queue/service.ts +0 -603
- package/src/report/activity.ts +0 -281
- package/src/report/handle-report-update.ts +0 -209
- package/src/report/reassign.ts +0 -109
- package/src/report/stats.ts +0 -852
- package/src/report/views.ts +0 -239
- package/src/safelink/service.ts +0 -304
- package/src/scheduled-action/service.ts +0 -281
- package/src/scheduled-action/types.ts +0 -17
- package/src/sequencer/index.ts +0 -2
- package/src/sequencer/outbox.ts +0 -123
- package/src/sequencer/sequencer.ts +0 -147
- package/src/set/service.ts +0 -230
- package/src/setting/constants.ts +0 -3
- package/src/setting/service.ts +0 -148
- package/src/setting/types.ts +0 -3
- package/src/setting/validators.ts +0 -333
- package/src/tag-service/content-tagger.ts +0 -30
- package/src/tag-service/embed-tagger.ts +0 -70
- package/src/tag-service/index.ts +0 -70
- package/src/tag-service/language-data.ts +0 -561
- package/src/tag-service/language-tagger.ts +0 -101
- package/src/tag-service/util.ts +0 -13
- package/src/team/index.ts +0 -296
- package/src/util.ts +0 -230
- package/src/verification/issuer.ts +0 -146
- package/src/verification/service.ts +0 -208
- package/src/verification/util.ts +0 -53
- package/test.env +0 -2
- package/tests/3p-labeler.test.ts +0 -288
- package/tests/__snapshots__/account-strikes.test.ts.snap +0 -159
- package/tests/__snapshots__/age-assurance.test.ts.snap +0 -66
- package/tests/__snapshots__/blob-divert.test.ts.snap +0 -219
- package/tests/__snapshots__/get-account-timeline.test.ts.snap +0 -36
- package/tests/__snapshots__/get-record.test.ts.snap +0 -271
- package/tests/__snapshots__/get-records.test.ts.snap +0 -175
- package/tests/__snapshots__/get-repo.test.ts.snap +0 -91
- package/tests/__snapshots__/get-repos.test.ts.snap +0 -127
- package/tests/__snapshots__/get-starter-pack.test.ts.snap +0 -535
- package/tests/__snapshots__/get-subjects.test.ts.snap +0 -529
- package/tests/__snapshots__/moderation-events.test.ts.snap +0 -347
- package/tests/__snapshots__/moderation-statuses.test.ts.snap +0 -276
- package/tests/__snapshots__/moderation.test.ts.snap +0 -85
- package/tests/__snapshots__/report-reason.test.ts.snap +0 -14
- package/tests/__snapshots__/safelink.test.ts.snap +0 -179
- package/tests/__snapshots__/scheduled-action.test.ts.snap +0 -61
- package/tests/__snapshots__/sets.test.ts.snap +0 -46
- package/tests/__snapshots__/settings.test.ts.snap +0 -52
- package/tests/__snapshots__/team.test.ts.snap +0 -374
- package/tests/__snapshots__/verification-listener.test.ts.snap +0 -152
- package/tests/__snapshots__/verification.test.ts.snap +0 -302
- package/tests/_util.ts +0 -242
- package/tests/account-strikes.test.ts +0 -184
- package/tests/ack-all-subjects-of-account.test.ts +0 -177
- package/tests/age-assurance.test.ts +0 -372
- package/tests/blob-divert.test.ts +0 -106
- package/tests/communication-templates.test.ts +0 -149
- package/tests/content-tagger.test.ts +0 -170
- package/tests/db.test.ts +0 -184
- package/tests/expiring-label.test.ts +0 -72
- package/tests/expiring-tags.test.ts +0 -232
- package/tests/get-account-timeline.test.ts +0 -85
- package/tests/get-config.test.ts +0 -55
- package/tests/get-lists.test.ts +0 -111
- package/tests/get-profiles.test.ts +0 -70
- package/tests/get-record.test.ts +0 -130
- package/tests/get-records.test.ts +0 -91
- package/tests/get-repo.test.ts +0 -171
- package/tests/get-report.test.ts +0 -136
- package/tests/get-reporter-stats.test.ts +0 -132
- package/tests/get-repos.test.ts +0 -91
- package/tests/get-starter-pack.test.ts +0 -115
- package/tests/get-subjects.test.ts +0 -81
- package/tests/mod-tool.test.ts +0 -268
- package/tests/moderation-appeals.test.ts +0 -260
- package/tests/moderation-events.test.ts +0 -756
- package/tests/moderation-status-tags.test.ts +0 -140
- package/tests/moderation-statuses.test.ts +0 -495
- package/tests/moderation.test.ts +0 -992
- package/tests/protected-tags.test.ts +0 -218
- package/tests/query-labels.test.ts +0 -238
- package/tests/query-reports.test.ts +0 -608
- package/tests/queue-assignment.test.ts +0 -428
- package/tests/queue-router.test.ts +0 -306
- package/tests/queues.test.ts +0 -690
- package/tests/record-and-account-events.test.ts +0 -197
- package/tests/repo-search.test.ts +0 -136
- package/tests/report-action.test.ts +0 -308
- package/tests/report-activity.test.ts +0 -711
- package/tests/report-assignment.test.ts +0 -517
- package/tests/report-muting.test.ts +0 -100
- package/tests/report-reason.test.ts +0 -154
- package/tests/report-reassign-queue.test.ts +0 -340
- package/tests/report-routing.test.ts +0 -245
- package/tests/report-stats.test.ts +0 -545
- package/tests/revoke-account-credentials.test.ts +0 -54
- package/tests/safelink.test.ts +0 -534
- package/tests/scheduled-action-processor.test.ts +0 -488
- package/tests/scheduled-action.test.ts +0 -334
- package/tests/sequencer.test.ts +0 -227
- package/tests/server.test.ts +0 -62
- package/tests/sets.test.ts +0 -246
- package/tests/settings.test.ts +0 -308
- package/tests/strike-expiry-processor.test.ts +0 -299
- package/tests/subject-priority-score.test.ts +0 -96
- package/tests/takedown.test.ts +0 -105
- package/tests/team.test.ts +0 -216
- package/tests/verification-listener.test.ts +0 -129
- package/tests/verification.test.ts +0 -186
- package/tsconfig.build.json +0 -9
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
package/src/queue/service.ts
DELETED
|
@@ -1,603 +0,0 @@
|
|
|
1
|
-
import { Selectable, sql } from 'kysely'
|
|
2
|
-
import { ToolsOzoneQueueDefs } from '@atproto/api'
|
|
3
|
-
import { AtUri } from '@atproto/syntax'
|
|
4
|
-
import { InvalidRequestError } from '@atproto/xrpc-server'
|
|
5
|
-
import { Database } from '../db/index.js'
|
|
6
|
-
import { TimeIdKeyset, paginate } from '../db/pagination.js'
|
|
7
|
-
import { ReportQueue } from '../db/schema/report_queue.js'
|
|
8
|
-
import { jsonb } from '../db/types.js'
|
|
9
|
-
import { handleReportUpdate } from '../report/handle-report-update.js'
|
|
10
|
-
import { ReportStatsService } from '../report/stats.js'
|
|
11
|
-
import { viewQueueStats } from '../report/views.js'
|
|
12
|
-
|
|
13
|
-
const MOD_EVENT_REPORT_ACTION = 'tools.ozone.moderation.defs#modEventReport'
|
|
14
|
-
const REASON_OTHER = 'com.atproto.moderation.defs#reasonOther'
|
|
15
|
-
|
|
16
|
-
type SubjectType = 'account' | 'record' | 'message' | 'conversation'
|
|
17
|
-
|
|
18
|
-
type ResolvedAssignment = {
|
|
19
|
-
queueId: number
|
|
20
|
-
queuedAt: string | null
|
|
21
|
-
status: 'queued' | 'open'
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function resolveAssignment(
|
|
25
|
-
subjectType: SubjectType,
|
|
26
|
-
collection: string | null,
|
|
27
|
-
reportType: string,
|
|
28
|
-
queues: Selectable<ReportQueue>[],
|
|
29
|
-
now: string,
|
|
30
|
-
): ResolvedAssignment {
|
|
31
|
-
const matched = findMatchingQueue(queues, subjectType, collection, reportType)
|
|
32
|
-
if (matched) return { queueId: matched.id, queuedAt: now, status: 'queued' }
|
|
33
|
-
return { queueId: -1, queuedAt: null, status: 'open' }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type QueueServiceCreator = (db: Database) => QueueService
|
|
37
|
-
|
|
38
|
-
export class QueueService {
|
|
39
|
-
constructor(public db: Database) {}
|
|
40
|
-
|
|
41
|
-
static creator() {
|
|
42
|
-
return (db: Database) => new QueueService(db)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async checkConflict({
|
|
46
|
-
subjectTypes,
|
|
47
|
-
collection,
|
|
48
|
-
reportTypes,
|
|
49
|
-
excludeId,
|
|
50
|
-
}: {
|
|
51
|
-
subjectTypes: string[]
|
|
52
|
-
collection?: string | null
|
|
53
|
-
reportTypes: string[]
|
|
54
|
-
excludeId?: number
|
|
55
|
-
}): Promise<void> {
|
|
56
|
-
// It's not ideal to load all rows and perform in memory checks in case we end up with a LOT of queues
|
|
57
|
-
// but we are not foreseeing a lot of queue rows so this should be fine for the
|
|
58
|
-
let qb = this.db.db
|
|
59
|
-
.selectFrom('report_queue')
|
|
60
|
-
.selectAll()
|
|
61
|
-
.where('deletedAt', 'is', null)
|
|
62
|
-
|
|
63
|
-
if (excludeId !== undefined) {
|
|
64
|
-
qb = qb.where('id', '!=', excludeId)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const existingQueues = await qb.execute()
|
|
68
|
-
|
|
69
|
-
for (const existing of existingQueues) {
|
|
70
|
-
const subjectTypesOverlap = subjectTypes.some((st) =>
|
|
71
|
-
existing.subjectTypes.includes(st),
|
|
72
|
-
)
|
|
73
|
-
const collectionMatch = (collection ?? null) === existing.collection
|
|
74
|
-
const reportTypesOverlap = reportTypes.some((rt) =>
|
|
75
|
-
existing.reportTypes.includes(rt),
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
if (subjectTypesOverlap && collectionMatch && reportTypesOverlap) {
|
|
79
|
-
throw new InvalidRequestError(
|
|
80
|
-
`Queue configuration conflicts with existing queue: ${existing.name}`,
|
|
81
|
-
'ConflictingQueue',
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async create({
|
|
88
|
-
name,
|
|
89
|
-
subjectTypes,
|
|
90
|
-
collection,
|
|
91
|
-
reportTypes,
|
|
92
|
-
description,
|
|
93
|
-
createdBy,
|
|
94
|
-
}: {
|
|
95
|
-
name: string
|
|
96
|
-
subjectTypes: string[]
|
|
97
|
-
collection?: string | null
|
|
98
|
-
reportTypes: string[]
|
|
99
|
-
description?: string | null
|
|
100
|
-
createdBy: string
|
|
101
|
-
}): Promise<Selectable<ReportQueue>> {
|
|
102
|
-
const now = new Date().toISOString()
|
|
103
|
-
return await this.db.db
|
|
104
|
-
.insertInto('report_queue')
|
|
105
|
-
.values({
|
|
106
|
-
name,
|
|
107
|
-
subjectTypes: jsonb(subjectTypes),
|
|
108
|
-
collection: collection ?? null,
|
|
109
|
-
reportTypes: jsonb(reportTypes),
|
|
110
|
-
description: description ?? null,
|
|
111
|
-
createdBy,
|
|
112
|
-
enabled: true,
|
|
113
|
-
createdAt: now,
|
|
114
|
-
updatedAt: now,
|
|
115
|
-
})
|
|
116
|
-
.returningAll()
|
|
117
|
-
.executeTakeFirstOrThrow()
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async getById(id: number): Promise<Selectable<ReportQueue> | undefined> {
|
|
121
|
-
return await this.db.db
|
|
122
|
-
.selectFrom('report_queue')
|
|
123
|
-
.selectAll()
|
|
124
|
-
.where('id', '=', id)
|
|
125
|
-
.where('deletedAt', 'is', null)
|
|
126
|
-
.executeTakeFirst()
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async getViewsByIds(
|
|
130
|
-
ids: number[],
|
|
131
|
-
): Promise<Map<number, ToolsOzoneQueueDefs.QueueView>> {
|
|
132
|
-
if (!ids.length) return new Map()
|
|
133
|
-
const rows = await this.db.db
|
|
134
|
-
.selectFrom('report_queue')
|
|
135
|
-
.selectAll()
|
|
136
|
-
.where('id', 'in', ids)
|
|
137
|
-
.execute()
|
|
138
|
-
return new Map(rows.map((r) => [r.id, this.view(r)]))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async update(
|
|
142
|
-
id: number,
|
|
143
|
-
updates: { name?: string; enabled?: boolean; description?: string },
|
|
144
|
-
): Promise<Selectable<ReportQueue>> {
|
|
145
|
-
const now = new Date().toISOString()
|
|
146
|
-
return await this.db.db
|
|
147
|
-
.updateTable('report_queue')
|
|
148
|
-
.set({ ...updates, updatedAt: now })
|
|
149
|
-
.where('id', '=', id)
|
|
150
|
-
.returningAll()
|
|
151
|
-
.executeTakeFirstOrThrow()
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async delete(id: number): Promise<void> {
|
|
155
|
-
const now = new Date().toISOString()
|
|
156
|
-
await this.db.db
|
|
157
|
-
.updateTable('report_queue')
|
|
158
|
-
.set({ deletedAt: now })
|
|
159
|
-
.where('id', '=', id)
|
|
160
|
-
.execute()
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async migrateReports(
|
|
164
|
-
fromQueueId: number,
|
|
165
|
-
toQueueId?: number,
|
|
166
|
-
): Promise<number> {
|
|
167
|
-
const now = new Date().toISOString()
|
|
168
|
-
const results = await this.db.db
|
|
169
|
-
.updateTable('report')
|
|
170
|
-
.set({
|
|
171
|
-
queueId: toQueueId ?? -1,
|
|
172
|
-
queuedAt: toQueueId ? now : null,
|
|
173
|
-
updatedAt: now,
|
|
174
|
-
})
|
|
175
|
-
.where('queueId', '=', fromQueueId)
|
|
176
|
-
.where('status', '!=', 'closed')
|
|
177
|
-
.execute()
|
|
178
|
-
return results.reduce((sum, r) => sum + Number(r.numUpdatedRows), 0)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async list({
|
|
182
|
-
limit,
|
|
183
|
-
cursor,
|
|
184
|
-
enabled,
|
|
185
|
-
subjectType,
|
|
186
|
-
collection,
|
|
187
|
-
reportTypes,
|
|
188
|
-
}: {
|
|
189
|
-
limit: number
|
|
190
|
-
cursor?: string
|
|
191
|
-
enabled?: boolean
|
|
192
|
-
subjectType?: string
|
|
193
|
-
collection?: string
|
|
194
|
-
reportTypes?: string[]
|
|
195
|
-
}): Promise<{ queues: Selectable<ReportQueue>[]; cursor?: string }> {
|
|
196
|
-
const { ref } = this.db.db.dynamic
|
|
197
|
-
let qb = this.db.db
|
|
198
|
-
.selectFrom('report_queue')
|
|
199
|
-
.selectAll()
|
|
200
|
-
.where('deletedAt', 'is', null)
|
|
201
|
-
|
|
202
|
-
if (enabled !== undefined) {
|
|
203
|
-
qb = qb.where('enabled', '=', enabled)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (subjectType !== undefined) {
|
|
207
|
-
qb = qb.where(sql<boolean>`"subjectTypes" @> ${jsonb([subjectType])}`)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (collection !== undefined) {
|
|
211
|
-
qb = qb.where('collection', '=', collection)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (reportTypes && reportTypes.length > 0) {
|
|
215
|
-
const conditions = reportTypes.map(
|
|
216
|
-
(t) => sql`"reportTypes" @> ${jsonb([t])}`,
|
|
217
|
-
)
|
|
218
|
-
qb = qb.where(sql<boolean>`(${sql.join(conditions, sql` OR `)})`)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const keyset = new TimeIdKeyset(ref('createdAt'), ref('id'))
|
|
222
|
-
const paginatedBuilder = paginate(qb, {
|
|
223
|
-
limit,
|
|
224
|
-
cursor,
|
|
225
|
-
keyset,
|
|
226
|
-
direction: 'asc',
|
|
227
|
-
tryIndex: true,
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
const queues = await paginatedBuilder.execute()
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
queues,
|
|
234
|
-
cursor: keyset.packFromResult(queues),
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
view(queue: Selectable<ReportQueue>): ToolsOzoneQueueDefs.QueueView {
|
|
239
|
-
return {
|
|
240
|
-
id: queue.id,
|
|
241
|
-
name: queue.name,
|
|
242
|
-
subjectTypes: queue.subjectTypes,
|
|
243
|
-
collection: queue.collection ?? undefined,
|
|
244
|
-
reportTypes: queue.reportTypes,
|
|
245
|
-
description: queue.description ?? undefined,
|
|
246
|
-
createdBy: queue.createdBy,
|
|
247
|
-
createdAt: queue.createdAt,
|
|
248
|
-
updatedAt: queue.updatedAt,
|
|
249
|
-
enabled: queue.enabled,
|
|
250
|
-
deletedAt: queue.deletedAt ?? undefined,
|
|
251
|
-
stats: {
|
|
252
|
-
pendingCount: 0,
|
|
253
|
-
actionedCount: 0,
|
|
254
|
-
escalatedCount: 0,
|
|
255
|
-
inboundCount: 0,
|
|
256
|
-
actionRate: 0,
|
|
257
|
-
},
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async viewsWithStats(
|
|
262
|
-
queues: Selectable<ReportQueue>[],
|
|
263
|
-
): Promise<ToolsOzoneQueueDefs.QueueView[]> {
|
|
264
|
-
const statsService = new ReportStatsService(this.db)
|
|
265
|
-
const queueIds = queues.map((q) => q.id)
|
|
266
|
-
const statsMap = await statsService.getLiveStatsForQueues(queueIds)
|
|
267
|
-
|
|
268
|
-
return queues.map((queue) => {
|
|
269
|
-
const view = this.view(queue)
|
|
270
|
-
view.stats = viewQueueStats(statsMap.get(queue.id))
|
|
271
|
-
return view
|
|
272
|
-
})
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Re-route a range of existing reports against the current queue config.
|
|
277
|
-
* Used by the manual `tools.ozone.queue.routeReports` endpoint to pick up
|
|
278
|
-
* reports after queues are created or modified. New reports are inserted
|
|
279
|
-
* by the daemon via `insertReportsFromEvents`, not here.
|
|
280
|
-
*/
|
|
281
|
-
async assignReportBatch(
|
|
282
|
-
params: { start: number; end: number; limit: number },
|
|
283
|
-
opts?: { includeUnmatched?: boolean; serviceDid?: string },
|
|
284
|
-
): Promise<{
|
|
285
|
-
processed: number
|
|
286
|
-
assigned: number
|
|
287
|
-
unmatched: number
|
|
288
|
-
maxId: number
|
|
289
|
-
}> {
|
|
290
|
-
const { queues } = await this.list({ limit: 1000, enabled: true })
|
|
291
|
-
|
|
292
|
-
if (!queues.length) {
|
|
293
|
-
return { processed: 0, assigned: 0, unmatched: 0, maxId: 0 }
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let query = this.db.db
|
|
297
|
-
.selectFrom('report as r')
|
|
298
|
-
.select([
|
|
299
|
-
'r.id',
|
|
300
|
-
'r.status',
|
|
301
|
-
'r.reportType',
|
|
302
|
-
'r.recordPath',
|
|
303
|
-
'r.subjectMessageId',
|
|
304
|
-
])
|
|
305
|
-
.where('r.status', '!=', 'closed')
|
|
306
|
-
.where('r.id', '>=', params.start)
|
|
307
|
-
.where('r.id', '<=', params.end)
|
|
308
|
-
.orderBy('r.id', 'asc')
|
|
309
|
-
.limit(params.limit)
|
|
310
|
-
|
|
311
|
-
if (opts?.includeUnmatched) {
|
|
312
|
-
query = query.where((eb) =>
|
|
313
|
-
eb.or([eb('r.queueId', 'is', null), eb('r.queueId', '=', -1)]),
|
|
314
|
-
)
|
|
315
|
-
} else {
|
|
316
|
-
query = query.where('r.queueId', 'is', null)
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const reports = await query.execute()
|
|
320
|
-
|
|
321
|
-
if (!reports.length) {
|
|
322
|
-
return { processed: 0, assigned: 0, unmatched: 0, maxId: 0 }
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const now = new Date().toISOString()
|
|
326
|
-
|
|
327
|
-
// Resolve each report's destination in memory — no DB calls in this loop
|
|
328
|
-
type MatchedEntry = {
|
|
329
|
-
id: number
|
|
330
|
-
queueId: number
|
|
331
|
-
nextStatus: string | null
|
|
332
|
-
activity: { activityType: string; previousStatus: string } | null
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const matchedByQueue = new Map<number, MatchedEntry[]>()
|
|
336
|
-
const unmatchedIds: number[] = []
|
|
337
|
-
let maxReportId = 0
|
|
338
|
-
|
|
339
|
-
for (const report of reports) {
|
|
340
|
-
const subjectType: SubjectType = report.subjectMessageId
|
|
341
|
-
? 'message'
|
|
342
|
-
: report.recordPath
|
|
343
|
-
? 'record'
|
|
344
|
-
: 'account'
|
|
345
|
-
|
|
346
|
-
// recordPath is 'collection/rkey' for records, '' for accounts
|
|
347
|
-
const slashIdx = report.recordPath.indexOf('/')
|
|
348
|
-
const collection =
|
|
349
|
-
slashIdx > 0 ? report.recordPath.slice(0, slashIdx) : null
|
|
350
|
-
|
|
351
|
-
const assignment = resolveAssignment(
|
|
352
|
-
subjectType,
|
|
353
|
-
collection,
|
|
354
|
-
report.reportType,
|
|
355
|
-
queues,
|
|
356
|
-
now,
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
if (assignment.queueId !== -1) {
|
|
360
|
-
// Existing-row UPDATE path uses handleReportUpdate so that already
|
|
361
|
-
// escalated/closed/etc. reports keep their status — only open → queued
|
|
362
|
-
// transitions emit a status change and an activity row.
|
|
363
|
-
const result = handleReportUpdate(report.status, { type: 'queue' })
|
|
364
|
-
const group = matchedByQueue.get(assignment.queueId) ?? []
|
|
365
|
-
group.push({
|
|
366
|
-
id: report.id,
|
|
367
|
-
queueId: assignment.queueId,
|
|
368
|
-
nextStatus: result.nextStatus,
|
|
369
|
-
activity: result.activity,
|
|
370
|
-
})
|
|
371
|
-
matchedByQueue.set(assignment.queueId, group)
|
|
372
|
-
} else {
|
|
373
|
-
unmatchedIds.push(report.id)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (report.id > maxReportId) maxReportId = report.id
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Bulk UPDATE matched reports — split by whether status should change.
|
|
380
|
-
// handleReportUpdate returns nextStatus only for open → queued;
|
|
381
|
-
// other statuses keep their current status but still get routed.
|
|
382
|
-
for (const [queueId, group] of matchedByQueue) {
|
|
383
|
-
const withTransition = group
|
|
384
|
-
.filter((r) => r.nextStatus !== null)
|
|
385
|
-
.map((r) => r.id)
|
|
386
|
-
const withoutTransition = group
|
|
387
|
-
.filter((r) => r.nextStatus === null)
|
|
388
|
-
.map((r) => r.id)
|
|
389
|
-
|
|
390
|
-
if (withTransition.length) {
|
|
391
|
-
await this.db.db
|
|
392
|
-
.updateTable('report')
|
|
393
|
-
.set({ queueId, queuedAt: now, status: 'queued', updatedAt: now })
|
|
394
|
-
.where('id', 'in', withTransition)
|
|
395
|
-
.execute()
|
|
396
|
-
}
|
|
397
|
-
if (withoutTransition.length) {
|
|
398
|
-
await this.db.db
|
|
399
|
-
.updateTable('report')
|
|
400
|
-
.set({ queueId, queuedAt: now, updatedAt: now })
|
|
401
|
-
.where('id', 'in', withoutTransition)
|
|
402
|
-
.execute()
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Bulk UPDATE unmatched reports — status stays unchanged
|
|
407
|
-
if (unmatchedIds.length) {
|
|
408
|
-
await this.db.db
|
|
409
|
-
.updateTable('report')
|
|
410
|
-
.set({ queueId: -1, queuedAt: null, updatedAt: now })
|
|
411
|
-
.where('id', 'in', unmatchedIds)
|
|
412
|
-
.execute()
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Bulk INSERT activities for matched reports that changed status.
|
|
416
|
-
if (opts?.serviceDid) {
|
|
417
|
-
const withActivities = [...matchedByQueue.values()]
|
|
418
|
-
.flat()
|
|
419
|
-
.filter((r) => r.activity !== null)
|
|
420
|
-
if (withActivities.length) {
|
|
421
|
-
await this.db.db
|
|
422
|
-
.insertInto('report_activity')
|
|
423
|
-
.values(
|
|
424
|
-
withActivities.map((r) => ({
|
|
425
|
-
reportId: r.id,
|
|
426
|
-
activityType: r.activity!.activityType,
|
|
427
|
-
previousStatus: r.activity!.previousStatus,
|
|
428
|
-
internalNote: null,
|
|
429
|
-
publicNote: null,
|
|
430
|
-
meta: null,
|
|
431
|
-
isAutomated: true,
|
|
432
|
-
createdBy: opts.serviceDid!,
|
|
433
|
-
createdAt: now,
|
|
434
|
-
})),
|
|
435
|
-
)
|
|
436
|
-
.execute()
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const assigned = [...matchedByQueue.values()].reduce(
|
|
441
|
-
(sum, g) => sum + g.length,
|
|
442
|
-
0,
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
return {
|
|
446
|
-
processed: reports.length,
|
|
447
|
-
assigned,
|
|
448
|
-
unmatched: unmatchedIds.length,
|
|
449
|
-
maxId: maxReportId,
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Read newly-created modEventReport rows from `moderation_event` and
|
|
455
|
-
* insert corresponding `report` rows with `queueId` already resolved.
|
|
456
|
-
* Used by the queue-router daemon. Idempotent via `ON CONFLICT (eventId)
|
|
457
|
-
* DO NOTHING` — safe to re-run on the same range.
|
|
458
|
-
*
|
|
459
|
-
* Even when no queues are configured, report rows are still inserted with
|
|
460
|
-
* `queueId = -1` so the invariant "every modEventReport has a `report` row"
|
|
461
|
-
* holds.
|
|
462
|
-
*/
|
|
463
|
-
async insertReportsFromEvents(params: {
|
|
464
|
-
cursor: number | null
|
|
465
|
-
limit: number
|
|
466
|
-
}): Promise<{
|
|
467
|
-
processed: number
|
|
468
|
-
assigned: number
|
|
469
|
-
unmatched: number
|
|
470
|
-
maxEventId: number
|
|
471
|
-
}> {
|
|
472
|
-
const { queues } = await this.list({ limit: 1000, enabled: true })
|
|
473
|
-
|
|
474
|
-
let query = this.db.db
|
|
475
|
-
.selectFrom('moderation_event')
|
|
476
|
-
.select([
|
|
477
|
-
'id',
|
|
478
|
-
'subjectDid',
|
|
479
|
-
'subjectUri',
|
|
480
|
-
'subjectMessageId',
|
|
481
|
-
'subjectConvoId',
|
|
482
|
-
'meta',
|
|
483
|
-
'createdAt',
|
|
484
|
-
])
|
|
485
|
-
.where('action', '=', MOD_EVENT_REPORT_ACTION)
|
|
486
|
-
.orderBy('id', 'asc')
|
|
487
|
-
.limit(params.limit)
|
|
488
|
-
|
|
489
|
-
if (params.cursor !== null) {
|
|
490
|
-
query = query.where('id', '>', params.cursor)
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const events = await query.execute()
|
|
494
|
-
|
|
495
|
-
if (!events.length) {
|
|
496
|
-
return { processed: 0, assigned: 0, unmatched: 0, maxEventId: 0 }
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const now = new Date().toISOString()
|
|
500
|
-
let maxEventId = 0
|
|
501
|
-
let assigned = 0
|
|
502
|
-
let unmatched = 0
|
|
503
|
-
|
|
504
|
-
const rows = events.map((event) => {
|
|
505
|
-
const subjectType: SubjectType = event.subjectMessageId
|
|
506
|
-
? 'message'
|
|
507
|
-
: event.subjectConvoId
|
|
508
|
-
? 'conversation'
|
|
509
|
-
: event.subjectUri
|
|
510
|
-
? 'record'
|
|
511
|
-
: 'account'
|
|
512
|
-
|
|
513
|
-
let collection: string | null = null
|
|
514
|
-
let recordPath = ''
|
|
515
|
-
if (event.subjectUri) {
|
|
516
|
-
const uri = new AtUri(event.subjectUri)
|
|
517
|
-
collection = uri.collection
|
|
518
|
-
recordPath = `${uri.collection}/${uri.rkey}`
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const reportType =
|
|
522
|
-
(event.meta?.reportType as string | undefined) ?? REASON_OTHER
|
|
523
|
-
|
|
524
|
-
const assignment = resolveAssignment(
|
|
525
|
-
subjectType,
|
|
526
|
-
collection,
|
|
527
|
-
reportType,
|
|
528
|
-
queues,
|
|
529
|
-
now,
|
|
530
|
-
)
|
|
531
|
-
|
|
532
|
-
if (assignment.queueId === -1) unmatched++
|
|
533
|
-
else assigned++
|
|
534
|
-
if (event.id > maxEventId) maxEventId = event.id
|
|
535
|
-
|
|
536
|
-
const isMuted =
|
|
537
|
-
!!event.meta?.isReporterMuted || !!event.meta?.isSubjectMuted
|
|
538
|
-
|
|
539
|
-
return {
|
|
540
|
-
eventId: event.id,
|
|
541
|
-
queueId: assignment.queueId,
|
|
542
|
-
queuedAt: assignment.queuedAt,
|
|
543
|
-
actionEventIds: null,
|
|
544
|
-
actionNote: null,
|
|
545
|
-
isMuted,
|
|
546
|
-
status: assignment.status,
|
|
547
|
-
reportType,
|
|
548
|
-
did: event.subjectDid,
|
|
549
|
-
recordPath,
|
|
550
|
-
subjectMessageId: event.subjectMessageId,
|
|
551
|
-
subjectConvoId: event.subjectConvoId,
|
|
552
|
-
createdAt: now,
|
|
553
|
-
updatedAt: now,
|
|
554
|
-
}
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
// ON CONFLICT (eventId) DO NOTHING covers any race where a report row
|
|
558
|
-
// already exists for the event (e.g. transitional code paths or retries
|
|
559
|
-
// after a crash mid-batch).
|
|
560
|
-
await this.db.db
|
|
561
|
-
.insertInto('report')
|
|
562
|
-
.values(rows)
|
|
563
|
-
.onConflict((oc) => oc.column('eventId').doNothing())
|
|
564
|
-
.execute()
|
|
565
|
-
|
|
566
|
-
// Activity rows are intentionally not emitted: a freshly-inserted report
|
|
567
|
-
// has no prior state to "transition" from. Activity rows record state
|
|
568
|
-
// changes, and being born already-queued is not a state change. This
|
|
569
|
-
// matches `handleReportUpdate`'s design where activity is only emitted
|
|
570
|
-
// on real transitions.
|
|
571
|
-
|
|
572
|
-
return {
|
|
573
|
-
processed: events.length,
|
|
574
|
-
assigned,
|
|
575
|
-
unmatched,
|
|
576
|
-
maxEventId,
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
export function findMatchingQueue(
|
|
582
|
-
queues: Selectable<ReportQueue>[],
|
|
583
|
-
subjectType: string,
|
|
584
|
-
collection: string | null,
|
|
585
|
-
reportType: string | undefined,
|
|
586
|
-
): Selectable<ReportQueue> | null {
|
|
587
|
-
if (!reportType) return null
|
|
588
|
-
|
|
589
|
-
for (const queue of queues) {
|
|
590
|
-
const subjectTypeMatch = queue.subjectTypes.includes(subjectType)
|
|
591
|
-
const collectionMatch =
|
|
592
|
-
subjectType === 'record' && queue.collection !== null
|
|
593
|
-
? (collection ?? null) === queue.collection
|
|
594
|
-
: true
|
|
595
|
-
const reportTypeMatch = queue.reportTypes.includes(reportType)
|
|
596
|
-
|
|
597
|
-
if (subjectTypeMatch && collectionMatch && reportTypeMatch) {
|
|
598
|
-
return queue
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
return null
|
|
603
|
-
}
|