Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add a policy property to takedown events #3271

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lexicons/tools/ozone/moderation/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@
"acknowledgeAccountSubjects": {
"type": "boolean",
"description": "If true, all other reports on content authored by this account will be resolved (acknowledged)."
},
"policy": {
"type": "string",
"description": "Name/Keyword of the policy that drove the decision."
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions lexicons/tools/ozone/moderation/queryEvents.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@
"type": "string"
}
},
"policy": {
"type": "string",
"description": "If specified, only events where the policy matches the given policy are returned"
},
"cursor": {
"type": "string"
}
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11304,6 +11304,10 @@ export const schemaDict = {
description:
'If true, all other reports on content authored by this account will be resolved (acknowledged).',
},
policy: {
type: 'string',
description: 'Name/Keyword of the policy that drove the decision.',
},
},
},
modEventReverseTakedown: {
Expand Down Expand Up @@ -12340,6 +12344,11 @@ export const schemaDict = {
type: 'string',
},
},
policy: {
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
/** Name/Keyword of the policy that drove the decision. */
policy?: string
[k: string]: unknown
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export interface QueryParams {
/** If specified, only events where all of these tags were removed are returned */
removedTags?: string[]
reportTypes?: string[]
/** If specified, only events where the policy matches the given policy are returned */
policy?: 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
policy?: string
},
role?: ModLevel,
) {
const { durationInHours, acknowledgeAccountSubjects, ...rest } = opts
const { durationInHours, acknowledgeAccountSubjects, policy, ...rest } =
opts
return this.emitEvent(
{
event: {
$type: 'tools.ozone.moderation.defs#modEventTakedown',
acknowledgeAccountSubjects,
durationInHours,
policy,
},
...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,
policy,
} = 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,
policy,
})
return {
encoding: 'application/json',
Expand Down
9 changes: 9 additions & 0 deletions packages/ozone/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11304,6 +11304,10 @@ export const schemaDict = {
description:
'If true, all other reports on content authored by this account will be resolved (acknowledged).',
},
policy: {
type: 'string',
description: 'Name/Keyword of the policy that drove the decision.',
},
},
},
modEventReverseTakedown: {
Expand Down Expand Up @@ -12340,6 +12344,11 @@ export const schemaDict = {
type: 'string',
},
},
policy: {
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
/** Name/Keyword of the policy that drove the decision. */
policy?: string
[k: string]: unknown
}

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

Expand Down
9 changes: 9 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
policy?: string
}): Promise<{ cursor?: string; events: ModerationEventRow[] }> {
const {
subject,
Expand All @@ -172,6 +173,7 @@ export class ModerationService {
reportTypes,
collections,
subjectType,
policy,
} = opts
const { ref } = this.db.db.dynamic
let builder = this.db.db.selectFrom('moderation_event').selectAll()
Expand Down Expand Up @@ -264,6 +266,9 @@ export class ModerationService {
if (reportTypes?.length) {
builder = builder.where(sql`meta->>'reportType'`, 'in', reportTypes)
}
if (policy) {
builder = builder.where(sql`meta->>'policy'`, '=', policy)
}

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

if (isModEventTakedown(event) && event.policy) {
meta.policy = event.policy
}

// 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
10 changes: 10 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,16 @@ export class ModerationViews {
}
}

if (
event.action === 'tools.ozone.moderation.defs#modEventTakedown' &&
event.meta?.policy
) {
eventView.event = {
...eventView.event,
policy: event.meta.policy,
}
}

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),
policy: '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.policy).toEqual('trolling')

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

expect(filteredEvents[0].subject.did).toEqual(sc.dids.bob)
})
})
9 changes: 9 additions & 0 deletions packages/pds/src/lexicon/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11304,6 +11304,10 @@ export const schemaDict = {
description:
'If true, all other reports on content authored by this account will be resolved (acknowledged).',
},
policy: {
type: 'string',
description: 'Name/Keyword of the policy that drove the decision.',
},
},
},
modEventReverseTakedown: {
Expand Down Expand Up @@ -12340,6 +12344,11 @@ export const schemaDict = {
type: 'string',
},
},
policy: {
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
/** Name/Keyword of the policy that drove the decision. */
policy?: string
[k: string]: unknown
}

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

Expand Down
Loading