Skip to content

Commit

Permalink
✨ Add a policy property to takedown events (#3271)
Browse files Browse the repository at this point in the history
* ✨ Add a policy property to takedown events

* ✨ Add policy list setting validation

* ✨ Make multiple policies possible for takedown and event search

* 📝 Add changeset

* ✨ Use , as policies separator
  • Loading branch information
foysalit authored Jan 3, 2025
1 parent f70d159 commit 53621f8
Show file tree
Hide file tree
Showing 19 changed files with 204 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/smooth-steaks-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@atproto/dev-env": patch
"@atproto/ozone": patch
"@atproto/api": patch
---

Allow setting policy names with takedown actions and when querying events
6 changes: 6 additions & 0 deletions lexicons/tools/ozone/moderation/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@
"acknowledgeAccountSubjects": {
"type": "boolean",
"description": "If true, all other reports on content authored by this account will be resolved (acknowledged)."
},
"policies": {
"type": "array",
"maxLength": 5,
"items": { "type": "string" },
"description": "Names/Keywords of the policies that drove the decision."
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions lexicons/tools/ozone/moderation/queryEvents.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@
"type": "string"
}
},
"policies": {
"type": "array",
"items": {
"type": "string",
"description": "If specified, only events where the action policies match any of the given policies are returned"
}
},
"cursor": {
"type": "string"
}
Expand Down
17 changes: 17 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11309,6 +11309,15 @@ export const schemaDict = {
description:
'If true, all other reports on content authored by this account will be resolved (acknowledged).',
},
policies: {
type: 'array',
maxLength: 5,
items: {
type: 'string',
},
description:
'Names/Keywords of the policies that drove the decision.',
},
},
},
modEventReverseTakedown: {
Expand Down Expand Up @@ -12345,6 +12354,14 @@ export const schemaDict = {
type: 'string',
},
},
policies: {
type: 'array',
items: {
type: 'string',
description:
'If specified, only events where the policy matches the given policy are returned',
},
},
cursor: {
type: 'string',
},
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/client/types/tools/ozone/moderation/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ export interface ModEventTakedown {
durationInHours?: number
/** If true, all other reports on content authored by this account will be resolved (acknowledged). */
acknowledgeAccountSubjects?: boolean
/** Names/Keywords of the policies that drove the decision. */
policies?: string[]
[k: string]: unknown
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface QueryParams {
/** If specified, only events where all of these tags were removed are returned */
removedTags?: string[]
reportTypes?: string[]
policies?: string[]
cursor?: string
}

Expand Down
5 changes: 4 additions & 1 deletion packages/dev-env/src/moderator-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,19 @@ export class ModeratorClient {
durationInHours?: number
acknowledgeAccountSubjects?: boolean
reason?: string
policies?: string[]
},
role?: ModLevel,
) {
const { durationInHours, acknowledgeAccountSubjects, ...rest } = opts
const { durationInHours, acknowledgeAccountSubjects, policies, ...rest } =
opts
return this.emitEvent(
{
event: {
$type: 'tools.ozone.moderation.defs#modEventTakedown',
acknowledgeAccountSubjects,
durationInHours,
policies,
},
...rest,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/ozone/src/api/moderation/queryEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default function (server: Server, ctx: AppContext) {
reportTypes,
collections = [],
subjectType,
policies,
} = params
const db = ctx.db
const modService = ctx.modService(db)
Expand All @@ -47,6 +48,7 @@ export default function (server: Server, ctx: AppContext) {
reportTypes,
collections,
subjectType,
policies,
})
return {
encoding: 'application/json',
Expand Down
17 changes: 17 additions & 0 deletions packages/ozone/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11309,6 +11309,15 @@ export const schemaDict = {
description:
'If true, all other reports on content authored by this account will be resolved (acknowledged).',
},
policies: {
type: 'array',
maxLength: 5,
items: {
type: 'string',
},
description:
'Names/Keywords of the policies that drove the decision.',
},
},
},
modEventReverseTakedown: {
Expand Down Expand Up @@ -12345,6 +12354,14 @@ export const schemaDict = {
type: 'string',
},
},
policies: {
type: 'array',
items: {
type: 'string',
description:
'If specified, only events where the policy matches the given policy are returned',
},
},
cursor: {
type: 'string',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ export interface ModEventTakedown {
durationInHours?: number
/** If true, all other reports on content authored by this account will be resolved (acknowledged). */
acknowledgeAccountSubjects?: boolean
/** Names/Keywords of the policies that drove the decision. */
policies?: string[]
[k: string]: unknown
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface QueryParams {
/** If specified, only events where all of these tags were removed are returned */
removedTags?: string[]
reportTypes?: string[]
policies?: string[]
cursor?: string
}

Expand Down
14 changes: 14 additions & 0 deletions packages/ozone/src/mod-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export class ModerationService {
reportTypes?: string[]
collections: string[]
subjectType?: string
policies?: string[]
}): Promise<{ cursor?: string; events: ModerationEventRow[] }> {
const {
subject,
Expand All @@ -172,6 +173,7 @@ export class ModerationService {
reportTypes,
collections,
subjectType,
policies,
} = opts
const { ref } = this.db.db.dynamic
let builder = this.db.db.selectFrom('moderation_event').selectAll()
Expand Down Expand Up @@ -264,6 +266,14 @@ export class ModerationService {
if (reportTypes?.length) {
builder = builder.where(sql`meta->>'reportType'`, 'in', reportTypes)
}
if (policies?.length) {
builder = builder.where((qb) => {
policies.forEach((policy) => {
qb = qb.orWhere(sql`meta->>'policies'`, 'ilike', `%${policy}%`)
})
return qb
})
}

const keyset = new TimeIdKeyset(
ref(`moderation_event.createdAt`),
Expand Down Expand Up @@ -435,6 +445,10 @@ export class ModerationService {
meta.acknowledgeAccountSubjects = true
}

if (isModEventTakedown(event) && event.policies?.length) {
meta.policies = event.policies.join(',')
}

// Keep trace of reports that came in while the reporter was in muted stated
if (isModEventReport(event)) {
const isReportingMuted = await this.isReportingMutedForSubject(createdBy)
Expand Down
11 changes: 11 additions & 0 deletions packages/ozone/src/mod-service/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ export class ModerationViews {
}
}

if (
event.action === 'tools.ozone.moderation.defs#modEventTakedown' &&
typeof event.meta?.policies === 'string' &&
event.meta.policies.length > 0
) {
eventView.event = {
...eventView.event,
policies: event.meta.policies.split(','),
}
}

if (event.action === 'tools.ozone.moderation.defs#modEventLabel') {
eventView.event = {
...eventView.event,
Expand Down
1 change: 1 addition & 0 deletions packages/ozone/src/setting/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const ProtectedTagSettingKey = 'tools.ozone.setting.protectedTags'
export const PolicyListSettingKey = 'tools.ozone.setting.policyList'
29 changes: 28 additions & 1 deletion packages/ozone/src/setting/validators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Selectable } from 'kysely'
import { Setting } from '../db/schema/setting'
import { ProtectedTagSettingKey } from './constants'
import { PolicyListSettingKey, ProtectedTagSettingKey } from './constants'
import { InvalidRequestError } from '@atproto/xrpc-server'

export const settingValidators = new Map<
Expand Down Expand Up @@ -58,4 +58,31 @@ export const settingValidators = new Map<
}
},
],
[
PolicyListSettingKey,
async (setting: Partial<Selectable<Setting>>) => {
if (setting.managerRole !== 'tools.ozone.team.defs#roleAdmin') {
throw new InvalidRequestError(
'Only admins should be able to manage policy list',
)
}

if (typeof setting.value !== 'object') {
throw new InvalidRequestError('Invalid value')
}
for (const [key, val] of Object.entries(setting.value)) {
if (!val || typeof val !== 'object') {
throw new InvalidRequestError(
`Invalid configuration for policy ${key}`,
)
}

if (!val['name'] || !val['description']) {
throw new InvalidRequestError(
`Must define a name and description for policy ${key}`,
)
}
}
},
],
])
64 changes: 64 additions & 0 deletions packages/ozone/tests/takedown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
TestNetwork,
TestOzone,
SeedClient,
basicSeed,
ModeratorClient,
} from '@atproto/dev-env'
import { AtpAgent } from '@atproto/api'

describe('moderation', () => {
let network: TestNetwork
let ozone: TestOzone
let agent: AtpAgent
let bskyAgent: AtpAgent
let pdsAgent: AtpAgent
let sc: SeedClient
let modClient: ModeratorClient

const repoSubject = (did: string) => ({
$type: 'com.atproto.admin.defs#repoRef',
did,
})

beforeAll(async () => {
network = await TestNetwork.create({
dbPostgresSchema: 'ozone_takedown',
})
ozone = network.ozone
agent = network.ozone.getClient()
bskyAgent = network.bsky.getClient()
pdsAgent = network.pds.getClient()
sc = network.getSeedClient()
modClient = network.ozone.getModClient()
await basicSeed(sc)
await network.processAll()
})

afterAll(async () => {
await network.close()
})

it('allows specifying policy for takedown actions.', async () => {
await modClient.performTakedown({
subject: repoSubject(sc.dids.bob),
policies: ['trolling'],
})

// Verify that that the takedown even exposes the policy specified for it
const { events } = await modClient.queryEvents({
subject: sc.dids.bob,
types: ['tools.ozone.moderation.defs#modEventTakedown'],
})

expect(events[0].event.policies?.[0]).toEqual('trolling')

// Verify that event stream can be filtered by policy
const { events: filteredEvents } = await modClient.queryEvents({
subject: sc.dids.bob,
policies: ['trolling'],
})

expect(filteredEvents[0].subject.did).toEqual(sc.dids.bob)
})
})
17 changes: 17 additions & 0 deletions packages/pds/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11309,6 +11309,15 @@ export const schemaDict = {
description:
'If true, all other reports on content authored by this account will be resolved (acknowledged).',
},
policies: {
type: 'array',
maxLength: 5,
items: {
type: 'string',
},
description:
'Names/Keywords of the policies that drove the decision.',
},
},
},
modEventReverseTakedown: {
Expand Down Expand Up @@ -12345,6 +12354,14 @@ export const schemaDict = {
type: 'string',
},
},
policies: {
type: 'array',
items: {
type: 'string',
description:
'If specified, only events where the policy matches the given policy are returned',
},
},
cursor: {
type: 'string',
},
Expand Down
2 changes: 2 additions & 0 deletions packages/pds/src/lexicon/types/tools/ozone/moderation/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ export interface ModEventTakedown {
durationInHours?: number
/** If true, all other reports on content authored by this account will be resolved (acknowledged). */
acknowledgeAccountSubjects?: boolean
/** Names/Keywords of the policies that drove the decision. */
policies?: string[]
[k: string]: unknown
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface QueryParams {
/** If specified, only events where all of these tags were removed are returned */
removedTags?: string[]
reportTypes?: string[]
policies?: string[]
cursor?: string
}

Expand Down

0 comments on commit 53621f8

Please sign in to comment.