@atproto/ozone 0.1.133 → 0.1.134
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 +10 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +2 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/moderation/getAccountTimeline.d.ts +4 -0
- package/dist/api/moderation/getAccountTimeline.d.ts.map +1 -0
- package/dist/api/moderation/getAccountTimeline.js +139 -0
- package/dist/api/moderation/getAccountTimeline.js.map +1 -0
- package/dist/api/util.d.ts +1 -1
- package/dist/lexicon/index.d.ts +5 -0
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +7 -0
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +168 -0
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +116 -0
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts +6 -0
- package/dist/lexicon/types/tools/ozone/moderation/defs.d.ts.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js +7 -1
- package/dist/lexicon/types/tools/ozone/moderation/defs.js.map +1 -1
- package/dist/lexicon/types/tools/ozone/moderation/getAccountTimeline.d.ts +41 -0
- package/dist/lexicon/types/tools/ozone/moderation/getAccountTimeline.d.ts.map +1 -0
- package/dist/lexicon/types/tools/ozone/moderation/getAccountTimeline.js +25 -0
- package/dist/lexicon/types/tools/ozone/moderation/getAccountTimeline.js.map +1 -0
- package/dist/mod-service/index.d.ts +7 -1
- package/dist/mod-service/index.d.ts.map +1 -1
- package/dist/mod-service/index.js +20 -0
- package/dist/mod-service/index.js.map +1 -1
- package/dist/mod-service/util.d.ts +3 -0
- package/dist/mod-service/util.d.ts.map +1 -1
- package/dist/mod-service/util.js +11 -1
- package/dist/mod-service/util.js.map +1 -1
- package/package.json +4 -4
- package/src/api/index.ts +2 -0
- package/src/api/moderation/getAccountTimeline.ts +160 -0
- package/src/lexicon/index.ts +19 -0
- package/src/lexicon/lexicons.ts +121 -0
- package/src/lexicon/types/tools/ozone/moderation/defs.ts +7 -0
- package/src/lexicon/types/tools/ozone/moderation/getAccountTimeline.ts +102 -0
- package/src/mod-service/index.ts +24 -0
- package/src/mod-service/util.ts +11 -0
- package/tests/__snapshots__/get-account-timeline.test.ts.snap +36 -0
- package/tests/get-account-timeline.test.ts +85 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/ozone",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.134",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Backend service for moderating the Bluesky network.",
|
|
6
6
|
"keywords": [
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"uint8arrays": "3.0.0",
|
|
36
36
|
"undici": "^6.14.1",
|
|
37
37
|
"ws": "^8.12.0",
|
|
38
|
-
"@atproto/api": "^0.16.0",
|
|
39
38
|
"@atproto/common": "^0.4.11",
|
|
39
|
+
"@atproto/api": "^0.16.1",
|
|
40
40
|
"@atproto/crypto": "^0.4.4",
|
|
41
41
|
"@atproto/identity": "^0.4.8",
|
|
42
42
|
"@atproto/lexicon": "^0.4.12",
|
|
43
43
|
"@atproto/syntax": "^0.4.0",
|
|
44
44
|
"@atproto/xrpc": "^0.7.1",
|
|
45
|
-
"@atproto/xrpc-server": "^0.9.
|
|
45
|
+
"@atproto/xrpc-server": "^0.9.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@did-plc/server": "^0.0.1",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"ts-node": "^10.8.2",
|
|
56
56
|
"typescript": "^5.6.3",
|
|
57
57
|
"@atproto/lex-cli": "^0.9.1",
|
|
58
|
-
"@atproto/pds": "^0.4.
|
|
58
|
+
"@atproto/pds": "^0.4.164"
|
|
59
59
|
},
|
|
60
60
|
"scripts": {
|
|
61
61
|
"codegen": "lex gen-server --yes ./src/lexicon ../../lexicons/com/atproto/*/* ../../lexicons/app/bsky/*/* ../../lexicons/chat/bsky/*/* ../../lexicons/tools/ozone/*/*",
|
package/src/api/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import fetchLabels from './label/fetchLabels'
|
|
|
9
9
|
import queryLabels from './label/queryLabels'
|
|
10
10
|
import subscribeLabels from './label/subscribeLabels'
|
|
11
11
|
import emitEvent from './moderation/emitEvent'
|
|
12
|
+
import getAccountTimeline from './moderation/getAccountTimeline'
|
|
12
13
|
import getEvent from './moderation/getEvent'
|
|
13
14
|
import adminGetRecord from './moderation/getRecord'
|
|
14
15
|
import adminGetRecords from './moderation/getRecords'
|
|
@@ -92,5 +93,6 @@ export default function (server: Server, ctx: AppContext) {
|
|
|
92
93
|
removeSafelinkRule(server, ctx)
|
|
93
94
|
querySafelinkEvents(server, ctx)
|
|
94
95
|
querySafelinkRules(server, ctx)
|
|
96
|
+
getAccountTimeline(server, ctx)
|
|
95
97
|
return server
|
|
96
98
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { ToolsOzoneModerationGetAccountTimeline } from '@atproto/api'
|
|
2
|
+
import { AppContext } from '../../context'
|
|
3
|
+
import { Server } from '../../lexicon'
|
|
4
|
+
import { ids } from '../../lexicon/lexicons'
|
|
5
|
+
import { dateFromDatetime } from '../../mod-service/util'
|
|
6
|
+
|
|
7
|
+
export default function (server: Server, ctx: AppContext) {
|
|
8
|
+
server.tools.ozone.moderation.getAccountTimeline({
|
|
9
|
+
auth: ctx.authVerifier.modOrAdminToken,
|
|
10
|
+
handler: async ({ params }) => {
|
|
11
|
+
const { did } = params
|
|
12
|
+
const db = ctx.db
|
|
13
|
+
const modService = ctx.modService(db)
|
|
14
|
+
const [modEventHistory, accountHistory, plcHistory] =
|
|
15
|
+
await Promise.allSettled([
|
|
16
|
+
modService.getAccountTimeline(did),
|
|
17
|
+
getAccountHistory(ctx, did),
|
|
18
|
+
getPlcHistory(ctx, did),
|
|
19
|
+
])
|
|
20
|
+
const timelineByDay = new Map<
|
|
21
|
+
string,
|
|
22
|
+
ToolsOzoneModerationGetAccountTimeline.TimelineItemSummary[]
|
|
23
|
+
>()
|
|
24
|
+
|
|
25
|
+
if (modEventHistory.status === 'fulfilled') {
|
|
26
|
+
for (const row of modEventHistory.value) {
|
|
27
|
+
const day = timelineByDay.get(row.day)
|
|
28
|
+
const summary = {
|
|
29
|
+
eventSubjectType: row.subjectUri ? 'record' : 'account',
|
|
30
|
+
eventType: row.action,
|
|
31
|
+
count: row.count,
|
|
32
|
+
}
|
|
33
|
+
if (day) {
|
|
34
|
+
day.push(summary)
|
|
35
|
+
timelineByDay.set(row.day, day)
|
|
36
|
+
} else {
|
|
37
|
+
timelineByDay.set(row.day, [summary])
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
throw modEventHistory.reason
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (accountHistory.status === 'fulfilled') {
|
|
45
|
+
for (const [rowDay, row] of Object.entries(accountHistory.value)) {
|
|
46
|
+
const day = timelineByDay.get(rowDay)
|
|
47
|
+
const summaries: ToolsOzoneModerationGetAccountTimeline.TimelineItemSummary[] =
|
|
48
|
+
[]
|
|
49
|
+
for (const [eventType, count] of Object.entries(row)) {
|
|
50
|
+
summaries.push({
|
|
51
|
+
eventSubjectType: 'account',
|
|
52
|
+
eventType,
|
|
53
|
+
count,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
if (day) {
|
|
57
|
+
day.push(...summaries)
|
|
58
|
+
timelineByDay.set(rowDay, day)
|
|
59
|
+
} else {
|
|
60
|
+
timelineByDay.set(rowDay, summaries)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (plcHistory.status === 'fulfilled') {
|
|
66
|
+
for (const [rowDay, row] of Object.entries(plcHistory.value)) {
|
|
67
|
+
const day = timelineByDay.get(rowDay)
|
|
68
|
+
const summaries: ToolsOzoneModerationGetAccountTimeline.TimelineItemSummary[] =
|
|
69
|
+
[]
|
|
70
|
+
for (const [eventType, count] of Object.entries(row)) {
|
|
71
|
+
summaries.push({
|
|
72
|
+
eventSubjectType: 'account',
|
|
73
|
+
eventType,
|
|
74
|
+
count,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
if (day) {
|
|
78
|
+
day.push(...summaries)
|
|
79
|
+
timelineByDay.set(rowDay, day)
|
|
80
|
+
} else {
|
|
81
|
+
timelineByDay.set(rowDay, summaries)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const timeline: ToolsOzoneModerationGetAccountTimeline.TimelineItem[] = []
|
|
87
|
+
|
|
88
|
+
for (const [day, summary] of timelineByDay.entries()) {
|
|
89
|
+
timeline.push({ day, summary: summary.flat() })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
encoding: 'application/json',
|
|
94
|
+
body: { timeline },
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const getAccountHistory = async (ctx: AppContext, did: string) => {
|
|
101
|
+
const events: Record<string, Record<string, number>> = {}
|
|
102
|
+
|
|
103
|
+
if (!ctx.pdsAgent) {
|
|
104
|
+
return events
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const auth = await ctx.pdsAuth(ids.ToolsOzoneHostingGetAccountHistory)
|
|
108
|
+
let cursor: string | undefined = undefined
|
|
109
|
+
|
|
110
|
+
do {
|
|
111
|
+
const { data } = await ctx.pdsAgent.tools.ozone.hosting.getAccountHistory(
|
|
112
|
+
{ did, cursor },
|
|
113
|
+
auth,
|
|
114
|
+
)
|
|
115
|
+
cursor = data.cursor
|
|
116
|
+
for (const event of data.events) {
|
|
117
|
+
// This should never happen and the check is here only because typescript screams at us otherwise
|
|
118
|
+
if (!event.$type) {
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const day = dateFromDatetime(new Date(event.createdAt))
|
|
123
|
+
events[day] ??= {}
|
|
124
|
+
events[day][event.$type] ??= 0
|
|
125
|
+
events[day][event.$type]++
|
|
126
|
+
}
|
|
127
|
+
} while (cursor)
|
|
128
|
+
|
|
129
|
+
return events
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const PLC_OPERATION_MAP = {
|
|
133
|
+
create: 'tools.ozone.moderation.defs#timelineEventPlcCreate',
|
|
134
|
+
plc_operation: 'tools.ozone.moderation.defs#timelineEventPlcOperation',
|
|
135
|
+
plc_tombstone: 'tools.ozone.moderation.defs#timelineEventPlcTombstone',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const getPlcHistory = async (ctx: AppContext, did: string) => {
|
|
139
|
+
const events: Record<string, Record<string, number>> = {}
|
|
140
|
+
|
|
141
|
+
if (!ctx.plcClient) {
|
|
142
|
+
return events
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await ctx.plcClient.getAuditableLog(did)
|
|
146
|
+
for (const event of result) {
|
|
147
|
+
// Skip events that are not mapped, this means we will have to add correct mapping if/when new event types are introduced here
|
|
148
|
+
if (!Object.hasOwn(PLC_OPERATION_MAP, event.operation.type)) {
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
const day = dateFromDatetime(new Date(event.createdAt))
|
|
152
|
+
events[day] ??= {}
|
|
153
|
+
const eventType =
|
|
154
|
+
PLC_OPERATION_MAP[event.operation.type] || event.operation.type
|
|
155
|
+
events[day][eventType] ??= 0
|
|
156
|
+
events[day][eventType]++
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return events
|
|
160
|
+
}
|
package/src/lexicon/index.ts
CHANGED
|
@@ -201,6 +201,7 @@ import * as ToolsOzoneCommunicationListTemplates from './types/tools/ozone/commu
|
|
|
201
201
|
import * as ToolsOzoneCommunicationUpdateTemplate from './types/tools/ozone/communication/updateTemplate.js'
|
|
202
202
|
import * as ToolsOzoneHostingGetAccountHistory from './types/tools/ozone/hosting/getAccountHistory.js'
|
|
203
203
|
import * as ToolsOzoneModerationEmitEvent from './types/tools/ozone/moderation/emitEvent.js'
|
|
204
|
+
import * as ToolsOzoneModerationGetAccountTimeline from './types/tools/ozone/moderation/getAccountTimeline.js'
|
|
204
205
|
import * as ToolsOzoneModerationGetEvent from './types/tools/ozone/moderation/getEvent.js'
|
|
205
206
|
import * as ToolsOzoneModerationGetRecord from './types/tools/ozone/moderation/getRecord.js'
|
|
206
207
|
import * as ToolsOzoneModerationGetRecords from './types/tools/ozone/moderation/getRecords.js'
|
|
@@ -275,6 +276,12 @@ export const TOOLS_OZONE_MODERATION = {
|
|
|
275
276
|
DefsReviewEscalated: 'tools.ozone.moderation.defs#reviewEscalated',
|
|
276
277
|
DefsReviewClosed: 'tools.ozone.moderation.defs#reviewClosed',
|
|
277
278
|
DefsReviewNone: 'tools.ozone.moderation.defs#reviewNone',
|
|
279
|
+
DefsTimelineEventPlcCreate:
|
|
280
|
+
'tools.ozone.moderation.defs#timelineEventPlcCreate',
|
|
281
|
+
DefsTimelineEventPlcOperation:
|
|
282
|
+
'tools.ozone.moderation.defs#timelineEventPlcOperation',
|
|
283
|
+
DefsTimelineEventPlcTombstone:
|
|
284
|
+
'tools.ozone.moderation.defs#timelineEventPlcTombstone',
|
|
278
285
|
}
|
|
279
286
|
export const TOOLS_OZONE_TEAM = {
|
|
280
287
|
DefsRoleAdmin: 'tools.ozone.team.defs#roleAdmin',
|
|
@@ -2918,6 +2925,18 @@ export class ToolsOzoneModerationNS {
|
|
|
2918
2925
|
return this._server.xrpc.method(nsid, cfg)
|
|
2919
2926
|
}
|
|
2920
2927
|
|
|
2928
|
+
getAccountTimeline<A extends Auth = void>(
|
|
2929
|
+
cfg: MethodConfigOrHandler<
|
|
2930
|
+
A,
|
|
2931
|
+
ToolsOzoneModerationGetAccountTimeline.QueryParams,
|
|
2932
|
+
ToolsOzoneModerationGetAccountTimeline.HandlerInput,
|
|
2933
|
+
ToolsOzoneModerationGetAccountTimeline.HandlerOutput
|
|
2934
|
+
>,
|
|
2935
|
+
) {
|
|
2936
|
+
const nsid = 'tools.ozone.moderation.getAccountTimeline' // @ts-ignore
|
|
2937
|
+
return this._server.xrpc.method(nsid, cfg)
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2921
2940
|
getEvent<A extends Auth = void>(
|
|
2922
2941
|
cfg: MethodConfigOrHandler<
|
|
2923
2942
|
A,
|
package/src/lexicon/lexicons.ts
CHANGED
|
@@ -15006,6 +15006,21 @@ export const schemaDict = {
|
|
|
15006
15006
|
},
|
|
15007
15007
|
},
|
|
15008
15008
|
},
|
|
15009
|
+
timelineEventPlcCreate: {
|
|
15010
|
+
type: 'token',
|
|
15011
|
+
description:
|
|
15012
|
+
'Moderation event timeline event for a PLC create operation',
|
|
15013
|
+
},
|
|
15014
|
+
timelineEventPlcOperation: {
|
|
15015
|
+
type: 'token',
|
|
15016
|
+
description:
|
|
15017
|
+
'Moderation event timeline event for generic PLC operation',
|
|
15018
|
+
},
|
|
15019
|
+
timelineEventPlcTombstone: {
|
|
15020
|
+
type: 'token',
|
|
15021
|
+
description:
|
|
15022
|
+
'Moderation event timeline event for a PLC tombstone operation',
|
|
15023
|
+
},
|
|
15009
15024
|
},
|
|
15010
15025
|
},
|
|
15011
15026
|
ToolsOzoneModerationEmitEvent: {
|
|
@@ -15097,6 +15112,110 @@ export const schemaDict = {
|
|
|
15097
15112
|
},
|
|
15098
15113
|
},
|
|
15099
15114
|
},
|
|
15115
|
+
ToolsOzoneModerationGetAccountTimeline: {
|
|
15116
|
+
lexicon: 1,
|
|
15117
|
+
id: 'tools.ozone.moderation.getAccountTimeline',
|
|
15118
|
+
defs: {
|
|
15119
|
+
main: {
|
|
15120
|
+
type: 'query',
|
|
15121
|
+
description:
|
|
15122
|
+
'Get timeline of all available events of an account. This includes moderation events, account history and did history.',
|
|
15123
|
+
parameters: {
|
|
15124
|
+
type: 'params',
|
|
15125
|
+
required: ['did'],
|
|
15126
|
+
properties: {
|
|
15127
|
+
did: {
|
|
15128
|
+
type: 'string',
|
|
15129
|
+
format: 'did',
|
|
15130
|
+
},
|
|
15131
|
+
},
|
|
15132
|
+
},
|
|
15133
|
+
output: {
|
|
15134
|
+
encoding: 'application/json',
|
|
15135
|
+
schema: {
|
|
15136
|
+
type: 'object',
|
|
15137
|
+
required: ['timeline'],
|
|
15138
|
+
properties: {
|
|
15139
|
+
timeline: {
|
|
15140
|
+
type: 'array',
|
|
15141
|
+
items: {
|
|
15142
|
+
type: 'ref',
|
|
15143
|
+
ref: 'lex:tools.ozone.moderation.getAccountTimeline#timelineItem',
|
|
15144
|
+
},
|
|
15145
|
+
},
|
|
15146
|
+
},
|
|
15147
|
+
},
|
|
15148
|
+
},
|
|
15149
|
+
errors: [
|
|
15150
|
+
{
|
|
15151
|
+
name: 'RepoNotFound',
|
|
15152
|
+
},
|
|
15153
|
+
],
|
|
15154
|
+
},
|
|
15155
|
+
timelineItem: {
|
|
15156
|
+
type: 'object',
|
|
15157
|
+
required: ['day', 'summary'],
|
|
15158
|
+
properties: {
|
|
15159
|
+
day: {
|
|
15160
|
+
type: 'string',
|
|
15161
|
+
},
|
|
15162
|
+
summary: {
|
|
15163
|
+
type: 'array',
|
|
15164
|
+
items: {
|
|
15165
|
+
type: 'ref',
|
|
15166
|
+
ref: 'lex:tools.ozone.moderation.getAccountTimeline#timelineItemSummary',
|
|
15167
|
+
},
|
|
15168
|
+
},
|
|
15169
|
+
},
|
|
15170
|
+
},
|
|
15171
|
+
timelineItemSummary: {
|
|
15172
|
+
type: 'object',
|
|
15173
|
+
required: ['eventSubjectType', 'eventType', 'count'],
|
|
15174
|
+
properties: {
|
|
15175
|
+
eventSubjectType: {
|
|
15176
|
+
type: 'string',
|
|
15177
|
+
knownValues: ['account', 'record', 'chat'],
|
|
15178
|
+
},
|
|
15179
|
+
eventType: {
|
|
15180
|
+
type: 'string',
|
|
15181
|
+
knownValues: [
|
|
15182
|
+
'tools.ozone.moderation.defs#modEventTakedown',
|
|
15183
|
+
'tools.ozone.moderation.defs#modEventReverseTakedown',
|
|
15184
|
+
'tools.ozone.moderation.defs#modEventComment',
|
|
15185
|
+
'tools.ozone.moderation.defs#modEventReport',
|
|
15186
|
+
'tools.ozone.moderation.defs#modEventLabel',
|
|
15187
|
+
'tools.ozone.moderation.defs#modEventAcknowledge',
|
|
15188
|
+
'tools.ozone.moderation.defs#modEventEscalate',
|
|
15189
|
+
'tools.ozone.moderation.defs#modEventMute',
|
|
15190
|
+
'tools.ozone.moderation.defs#modEventUnmute',
|
|
15191
|
+
'tools.ozone.moderation.defs#modEventMuteReporter',
|
|
15192
|
+
'tools.ozone.moderation.defs#modEventUnmuteReporter',
|
|
15193
|
+
'tools.ozone.moderation.defs#modEventEmail',
|
|
15194
|
+
'tools.ozone.moderation.defs#modEventResolveAppeal',
|
|
15195
|
+
'tools.ozone.moderation.defs#modEventDivert',
|
|
15196
|
+
'tools.ozone.moderation.defs#modEventTag',
|
|
15197
|
+
'tools.ozone.moderation.defs#accountEvent',
|
|
15198
|
+
'tools.ozone.moderation.defs#identityEvent',
|
|
15199
|
+
'tools.ozone.moderation.defs#recordEvent',
|
|
15200
|
+
'tools.ozone.moderation.defs#modEventPriorityScore',
|
|
15201
|
+
'tools.ozone.moderation.defs#ageAssuranceEvent',
|
|
15202
|
+
'tools.ozone.moderation.defs#ageAssuranceOverrideEvent',
|
|
15203
|
+
'tools.ozone.moderation.defs#timelineEventPlcCreate',
|
|
15204
|
+
'tools.ozone.moderation.defs#timelineEventPlcOperation',
|
|
15205
|
+
'tools.ozone.moderation.defs#timelineEventPlcTombstone',
|
|
15206
|
+
'tools.ozone.hosting.getAccountHistory#accountCreated',
|
|
15207
|
+
'tools.ozone.hosting.getAccountHistory#emailConfirmed',
|
|
15208
|
+
'tools.ozone.hosting.getAccountHistory#passwordUpdated',
|
|
15209
|
+
'tools.ozone.hosting.getAccountHistory#handleUpdated',
|
|
15210
|
+
],
|
|
15211
|
+
},
|
|
15212
|
+
count: {
|
|
15213
|
+
type: 'integer',
|
|
15214
|
+
},
|
|
15215
|
+
},
|
|
15216
|
+
},
|
|
15217
|
+
},
|
|
15218
|
+
},
|
|
15100
15219
|
ToolsOzoneModerationGetEvent: {
|
|
15101
15220
|
lexicon: 1,
|
|
15102
15221
|
id: 'tools.ozone.moderation.getEvent',
|
|
@@ -17914,6 +18033,8 @@ export const ids = {
|
|
|
17914
18033
|
ToolsOzoneHostingGetAccountHistory: 'tools.ozone.hosting.getAccountHistory',
|
|
17915
18034
|
ToolsOzoneModerationDefs: 'tools.ozone.moderation.defs',
|
|
17916
18035
|
ToolsOzoneModerationEmitEvent: 'tools.ozone.moderation.emitEvent',
|
|
18036
|
+
ToolsOzoneModerationGetAccountTimeline:
|
|
18037
|
+
'tools.ozone.moderation.getAccountTimeline',
|
|
17917
18038
|
ToolsOzoneModerationGetEvent: 'tools.ozone.moderation.getEvent',
|
|
17918
18039
|
ToolsOzoneModerationGetRecord: 'tools.ozone.moderation.getRecord',
|
|
17919
18040
|
ToolsOzoneModerationGetRecords: 'tools.ozone.moderation.getRecords',
|
|
@@ -980,3 +980,10 @@ export function isModTool<V>(v: V) {
|
|
|
980
980
|
export function validateModTool<V>(v: V) {
|
|
981
981
|
return validate<ModTool & V>(v, id, hashModTool)
|
|
982
982
|
}
|
|
983
|
+
|
|
984
|
+
/** Moderation event timeline event for a PLC create operation */
|
|
985
|
+
export const TIMELINEEVENTPLCCREATE = `${id}#timelineEventPlcCreate`
|
|
986
|
+
/** Moderation event timeline event for generic PLC operation */
|
|
987
|
+
export const TIMELINEEVENTPLCOPERATION = `${id}#timelineEventPlcOperation`
|
|
988
|
+
/** Moderation event timeline event for a PLC tombstone operation */
|
|
989
|
+
export const TIMELINEEVENTPLCTOMBSTONE = `${id}#timelineEventPlcTombstone`
|
|
@@ -0,0 +1,102 @@
|
|
|
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.moderation.getAccountTimeline'
|
|
16
|
+
|
|
17
|
+
export type QueryParams = {
|
|
18
|
+
did: string
|
|
19
|
+
}
|
|
20
|
+
export type InputSchema = undefined
|
|
21
|
+
|
|
22
|
+
export interface OutputSchema {
|
|
23
|
+
timeline: TimelineItem[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type HandlerInput = void
|
|
27
|
+
|
|
28
|
+
export interface HandlerSuccess {
|
|
29
|
+
encoding: 'application/json'
|
|
30
|
+
body: OutputSchema
|
|
31
|
+
headers?: { [key: string]: string }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HandlerError {
|
|
35
|
+
status: number
|
|
36
|
+
message?: string
|
|
37
|
+
error?: 'RepoNotFound'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type HandlerOutput = HandlerError | HandlerSuccess
|
|
41
|
+
|
|
42
|
+
export interface TimelineItem {
|
|
43
|
+
$type?: 'tools.ozone.moderation.getAccountTimeline#timelineItem'
|
|
44
|
+
day: string
|
|
45
|
+
summary: TimelineItemSummary[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hashTimelineItem = 'timelineItem'
|
|
49
|
+
|
|
50
|
+
export function isTimelineItem<V>(v: V) {
|
|
51
|
+
return is$typed(v, id, hashTimelineItem)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validateTimelineItem<V>(v: V) {
|
|
55
|
+
return validate<TimelineItem & V>(v, id, hashTimelineItem)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface TimelineItemSummary {
|
|
59
|
+
$type?: 'tools.ozone.moderation.getAccountTimeline#timelineItemSummary'
|
|
60
|
+
eventSubjectType: 'account' | 'record' | 'chat' | (string & {})
|
|
61
|
+
eventType:
|
|
62
|
+
| 'tools.ozone.moderation.defs#modEventTakedown'
|
|
63
|
+
| 'tools.ozone.moderation.defs#modEventReverseTakedown'
|
|
64
|
+
| 'tools.ozone.moderation.defs#modEventComment'
|
|
65
|
+
| 'tools.ozone.moderation.defs#modEventReport'
|
|
66
|
+
| 'tools.ozone.moderation.defs#modEventLabel'
|
|
67
|
+
| 'tools.ozone.moderation.defs#modEventAcknowledge'
|
|
68
|
+
| 'tools.ozone.moderation.defs#modEventEscalate'
|
|
69
|
+
| 'tools.ozone.moderation.defs#modEventMute'
|
|
70
|
+
| 'tools.ozone.moderation.defs#modEventUnmute'
|
|
71
|
+
| 'tools.ozone.moderation.defs#modEventMuteReporter'
|
|
72
|
+
| 'tools.ozone.moderation.defs#modEventUnmuteReporter'
|
|
73
|
+
| 'tools.ozone.moderation.defs#modEventEmail'
|
|
74
|
+
| 'tools.ozone.moderation.defs#modEventResolveAppeal'
|
|
75
|
+
| 'tools.ozone.moderation.defs#modEventDivert'
|
|
76
|
+
| 'tools.ozone.moderation.defs#modEventTag'
|
|
77
|
+
| 'tools.ozone.moderation.defs#accountEvent'
|
|
78
|
+
| 'tools.ozone.moderation.defs#identityEvent'
|
|
79
|
+
| 'tools.ozone.moderation.defs#recordEvent'
|
|
80
|
+
| 'tools.ozone.moderation.defs#modEventPriorityScore'
|
|
81
|
+
| 'tools.ozone.moderation.defs#ageAssuranceEvent'
|
|
82
|
+
| 'tools.ozone.moderation.defs#ageAssuranceOverrideEvent'
|
|
83
|
+
| 'tools.ozone.moderation.defs#timelineEventPlcCreate'
|
|
84
|
+
| 'tools.ozone.moderation.defs#timelineEventPlcOperation'
|
|
85
|
+
| 'tools.ozone.moderation.defs#timelineEventPlcTombstone'
|
|
86
|
+
| 'tools.ozone.hosting.getAccountHistory#accountCreated'
|
|
87
|
+
| 'tools.ozone.hosting.getAccountHistory#emailConfirmed'
|
|
88
|
+
| 'tools.ozone.hosting.getAccountHistory#passwordUpdated'
|
|
89
|
+
| 'tools.ozone.hosting.getAccountHistory#handleUpdated'
|
|
90
|
+
| (string & {})
|
|
91
|
+
count: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hashTimelineItemSummary = 'timelineItemSummary'
|
|
95
|
+
|
|
96
|
+
export function isTimelineItemSummary<V>(v: V) {
|
|
97
|
+
return is$typed(v, id, hashTimelineItemSummary)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function validateTimelineItemSummary<V>(v: V) {
|
|
101
|
+
return validate<TimelineItemSummary & V>(v, id, hashTimelineItemSummary)
|
|
102
|
+
}
|
package/src/mod-service/index.ts
CHANGED
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
ReversibleModerationEvent,
|
|
65
65
|
} from './types'
|
|
66
66
|
import {
|
|
67
|
+
dateFromDbDatetime,
|
|
67
68
|
formatLabel,
|
|
68
69
|
formatLabelRow,
|
|
69
70
|
getPdsAgentForRepo,
|
|
@@ -1545,6 +1546,29 @@ export class ModerationService {
|
|
|
1545
1546
|
// Convert map values to an array and return
|
|
1546
1547
|
return Array.from(statsMap.values())
|
|
1547
1548
|
}
|
|
1549
|
+
|
|
1550
|
+
async getAccountTimeline(did: string) {
|
|
1551
|
+
const { ref } = this.db.db.dynamic
|
|
1552
|
+
// Without the subquery approach, pg tries to do the sort operation first which can be super expensive when a subjectDid has too many entries
|
|
1553
|
+
const result = await this.db.db
|
|
1554
|
+
.selectFrom(
|
|
1555
|
+
this.db.db
|
|
1556
|
+
.selectFrom('moderation_event')
|
|
1557
|
+
.where('subjectDid', '=', did)
|
|
1558
|
+
.select([
|
|
1559
|
+
dateFromDbDatetime(ref('createdAt')).as('day'),
|
|
1560
|
+
'subjectUri',
|
|
1561
|
+
'action',
|
|
1562
|
+
sql<number>`count(*)`.as('count'),
|
|
1563
|
+
])
|
|
1564
|
+
.groupBy(['day', 'subjectUri', 'action'])
|
|
1565
|
+
.as('results'),
|
|
1566
|
+
)
|
|
1567
|
+
.selectAll()
|
|
1568
|
+
.orderBy('day', 'desc')
|
|
1569
|
+
.execute()
|
|
1570
|
+
return result
|
|
1571
|
+
}
|
|
1548
1572
|
}
|
|
1549
1573
|
|
|
1550
1574
|
const parseTags = (tags?: string[]) =>
|
package/src/mod-service/util.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import net from 'node:net'
|
|
2
|
+
import { sql } from 'kysely'
|
|
2
3
|
import AtpAgent from '@atproto/api'
|
|
3
4
|
import { cborEncode, noUndefinedVals } from '@atproto/common'
|
|
4
5
|
import { Keypair } from '@atproto/crypto'
|
|
5
6
|
import { IdResolver } from '@atproto/identity'
|
|
6
7
|
import { LabelRow } from '../db/schema/label'
|
|
8
|
+
import { DbRef } from '../db/types'
|
|
7
9
|
import { Label } from '../lexicon/types/com/atproto/label/defs'
|
|
8
10
|
|
|
9
11
|
export type SignedLabel = Label & { sig: Uint8Array }
|
|
@@ -83,3 +85,12 @@ export const getPdsAgentForRepo = async (
|
|
|
83
85
|
|
|
84
86
|
return { url, agent: new AtpAgent({ service: url }) }
|
|
85
87
|
}
|
|
88
|
+
|
|
89
|
+
export const dateFromDatetime = (datetime: Date) => {
|
|
90
|
+
const [date] = datetime.toISOString().split('T')
|
|
91
|
+
return date
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const dateFromDbDatetime = (dateRef: DbRef) => {
|
|
95
|
+
return sql<string>`SPLIT_PART(${dateRef}, 'T', 1)`
|
|
96
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`account timeline Returns entire timeline of events for a given account 1`] = `
|
|
4
|
+
Array [
|
|
5
|
+
Object {
|
|
6
|
+
"count": 1,
|
|
7
|
+
"eventSubjectType": "record",
|
|
8
|
+
"eventType": "tools.ozone.moderation.defs#modEventReport",
|
|
9
|
+
},
|
|
10
|
+
Object {
|
|
11
|
+
"count": 1,
|
|
12
|
+
"eventSubjectType": "record",
|
|
13
|
+
"eventType": "tools.ozone.moderation.defs#modEventTag",
|
|
14
|
+
},
|
|
15
|
+
Object {
|
|
16
|
+
"count": 2,
|
|
17
|
+
"eventSubjectType": "account",
|
|
18
|
+
"eventType": "tools.ozone.moderation.defs#modEventReport",
|
|
19
|
+
},
|
|
20
|
+
Object {
|
|
21
|
+
"count": 1,
|
|
22
|
+
"eventSubjectType": "account",
|
|
23
|
+
"eventType": "tools.ozone.moderation.defs#modEventTag",
|
|
24
|
+
},
|
|
25
|
+
Object {
|
|
26
|
+
"count": 1,
|
|
27
|
+
"eventSubjectType": "account",
|
|
28
|
+
"eventType": "tools.ozone.moderation.defs#modEventTakedown",
|
|
29
|
+
},
|
|
30
|
+
Object {
|
|
31
|
+
"count": 1,
|
|
32
|
+
"eventSubjectType": "account",
|
|
33
|
+
"eventType": "tools.ozone.moderation.defs#timelineEventPlcOperation",
|
|
34
|
+
},
|
|
35
|
+
]
|
|
36
|
+
`;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import AtpAgent from '@atproto/api'
|
|
2
|
+
import {
|
|
3
|
+
ModeratorClient,
|
|
4
|
+
SeedClient,
|
|
5
|
+
TestNetwork,
|
|
6
|
+
basicSeed,
|
|
7
|
+
} from '@atproto/dev-env'
|
|
8
|
+
import { REASONSPAM } from '../dist/lexicon/types/com/atproto/moderation/defs'
|
|
9
|
+
import { ids } from '../src/lexicon/lexicons'
|
|
10
|
+
import { forSnapshot } from './_util'
|
|
11
|
+
|
|
12
|
+
describe('account timeline', () => {
|
|
13
|
+
let network: TestNetwork
|
|
14
|
+
let sc: SeedClient
|
|
15
|
+
let modClient: ModeratorClient
|
|
16
|
+
let agent: AtpAgent
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
network = await TestNetwork.create({
|
|
20
|
+
dbPostgresSchema: 'ozone_account_timeline_test',
|
|
21
|
+
})
|
|
22
|
+
sc = network.getSeedClient()
|
|
23
|
+
agent = network.ozone.getClient()
|
|
24
|
+
modClient = network.ozone.getModClient()
|
|
25
|
+
await basicSeed(sc)
|
|
26
|
+
|
|
27
|
+
// Trigger some moderation events
|
|
28
|
+
await Promise.all([
|
|
29
|
+
sc.createReport({
|
|
30
|
+
subject: {
|
|
31
|
+
$type: 'com.atproto.admin.defs#repoRef',
|
|
32
|
+
did: sc.dids.alice,
|
|
33
|
+
},
|
|
34
|
+
reasonType: REASONSPAM,
|
|
35
|
+
reportedBy: sc.dids.bob,
|
|
36
|
+
}),
|
|
37
|
+
sc.createReport({
|
|
38
|
+
subject: {
|
|
39
|
+
$type: 'com.atproto.admin.defs#repoRef',
|
|
40
|
+
did: sc.dids.alice,
|
|
41
|
+
},
|
|
42
|
+
reasonType: REASONSPAM,
|
|
43
|
+
reportedBy: sc.dids.carol,
|
|
44
|
+
}),
|
|
45
|
+
sc.createReport({
|
|
46
|
+
subject: {
|
|
47
|
+
$type: 'com.atproto.repo.strongRef',
|
|
48
|
+
uri: sc.posts[sc.dids.alice][0].ref.uriStr,
|
|
49
|
+
cid: sc.posts[sc.dids.alice][0].ref.cidStr,
|
|
50
|
+
},
|
|
51
|
+
reasonType: REASONSPAM,
|
|
52
|
+
reportedBy: sc.dids.bob,
|
|
53
|
+
}),
|
|
54
|
+
])
|
|
55
|
+
await modClient.performTakedown({
|
|
56
|
+
subject: {
|
|
57
|
+
$type: 'com.atproto.admin.defs#repoRef',
|
|
58
|
+
did: sc.dids.alice,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
await network.processAll()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
afterAll(async () => {
|
|
65
|
+
await network.close()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('Returns entire timeline of events for a given account', async () => {
|
|
69
|
+
const getAccountTimeline = async (did: string) =>
|
|
70
|
+
agent.tools.ozone.moderation.getAccountTimeline(
|
|
71
|
+
{ did },
|
|
72
|
+
{
|
|
73
|
+
headers: await network.ozone.modHeaders(
|
|
74
|
+
ids.ToolsOzoneModerationGetAccountTimeline,
|
|
75
|
+
),
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const {
|
|
80
|
+
data: { timeline },
|
|
81
|
+
} = await getAccountTimeline(sc.dids.alice)
|
|
82
|
+
|
|
83
|
+
expect(forSnapshot(timeline[0].summary)).toMatchSnapshot()
|
|
84
|
+
})
|
|
85
|
+
})
|