Skip to content

Commit

Permalink
Add defaultSetting and adultOnly to custom label value definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
pfrazee committed Mar 7, 2024
1 parent b6aae1a commit 876cdcf
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 5 deletions.
10 changes: 10 additions & 0 deletions lexicons/com/atproto/label/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@
"description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
"knownValues": ["content", "media", "none"]
},
"defaultSetting": {
"type": "string",
"description": "The default setting for this label.",
"knownValues": ["ignore", "warn", "hide"],
"default": "warn"
},
"adultOnly": {
"type": "boolean",
"description": "Does the user need to have adult content enabled in order to configure this label?"
},
"locales": {
"type": "array",
"items": { "type": "ref", "ref": "#labelValueDefinitionStrings" }
Expand Down
11 changes: 11 additions & 0 deletions packages/api/src/client/lexicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2292,6 +2292,17 @@ export const schemaDict = {
"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
knownValues: ['content', 'media', 'none'],
},
defaultSetting: {
type: 'string',
description: 'The default setting for this label.',
knownValues: ['ignore', 'warn', 'hide'],
default: 'warn',
},
adultOnly: {
type: 'boolean',
description:
'Does the user need to have adult content enabled in order to configure this label?',
},
locales: {
type: 'array',
items: {
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/client/types/com/atproto/label/defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export interface LabelValueDefinition {
severity: 'inform' | 'alert' | 'none' | (string & {})
/** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */
blurs: 'content' | 'media' | 'none' | (string & {})
/** The default setting for this label. */
defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})
/** Does the user need to have adult content enabled in order to configure this label? */
adultOnly?: boolean
locales: LabelValueDefinitionStrings[]
[k: string]: unknown
}
Expand Down
25 changes: 20 additions & 5 deletions packages/api/src/moderation/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import {
AppBskyLabelerDefs,
ComAtprotoLabelDefs,
} from '../client'
import { InterprettedLabelValueDefinition, ModerationBehavior } from './types'
import {
InterprettedLabelValueDefinition,
ModerationBehavior,
LabelPreference,
LabelValueDefinitionFlag,
} from './types'

export function isQuotedPost(embed: unknown): embed is AppBskyEmbedRecord.View {
return Boolean(embed && AppBskyEmbedRecord.isView(embed))
Expand Down Expand Up @@ -40,15 +45,15 @@ export function interpretLabelValueDefinition(
behaviors.account.profileList = alertOrInform
behaviors.account.profileView = alertOrInform
behaviors.account.contentList = 'blur'
behaviors.account.contentView = alertOrInform
behaviors.account.contentView = def.adultOnly ? 'blur' : alertOrInform
// target=profile, blurs=content
behaviors.account.profileView = alertOrInform
behaviors.profile.avatar = 'blur'
behaviors.profile.banner = 'blur'
behaviors.profile.displayName = 'blur'
// target=content, blurs=content
behaviors.content.contentList = 'blur'
behaviors.content.contentView = alertOrInform
behaviors.content.contentView = def.adultOnly ? 'blur' : alertOrInform
} else if (def.blurs === 'media') {
// target=account, blurs=media
behaviors.account.profileList = alertOrInform
Expand All @@ -75,12 +80,22 @@ export function interpretLabelValueDefinition(
behaviors.content.contentView = alertOrInform
}

let defaultSetting: LabelPreference = 'warn'
if (def.defaultSetting === 'hide' || def.defaultSetting === 'ignore') {
defaultSetting = def.defaultSetting as LabelPreference
}

const flags: LabelValueDefinitionFlag[] = ['no-self']
if (def.adultOnly) {
flags.push('adult')
}

return {
...def,
definedBy,
configurable: true,
defaultSetting: 'warn',
flags: ['no-self'],
defaultSetting,
flags,
behaviors,
}
}
Expand Down
209 changes: 209 additions & 0 deletions packages/api/tests/moderation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,4 +441,213 @@ describe('Moderation', () => {
expect(res.ui('contentView')).toBeModerationResult([])
expect(res.ui('contentMedia')).toBeModerationResult([])
})

it('Custom labels can set the default setting', () => {
const modOpts = {
userDid: 'did:web:alice.test',
prefs: {
adultContentEnabled: true,
labels: {},
labelers: [
{
did: 'did:web:labeler.test',
labels: {},
},
],
},
labelDefs: {
'did:web:labeler.test': [
interpretLabelValueDefinition(
{
identifier: 'default-hide',
blurs: 'content',
severity: 'inform',
defaultSetting: 'hide',
locales: [],
},
'did:web:labeler.test',
),
interpretLabelValueDefinition(
{
identifier: 'default-warn',
blurs: 'content',
severity: 'inform',
defaultSetting: 'warn',
locales: [],
},
'did:web:labeler.test',
),
interpretLabelValueDefinition(
{
identifier: 'default-ignore',
blurs: 'content',
severity: 'inform',
defaultSetting: 'ignore',
locales: [],
},
'did:web:labeler.test',
),
],
},
}
const res1 = moderatePost(
mock.postView({
record: {
text: 'Hello',
createdAt: new Date().toISOString(),
},
author: mock.profileViewBasic({
handle: 'bob.test',
displayName: 'Bob',
}),
labels: [
{
src: 'did:web:labeler.test',
uri: 'at://did:web:bob.test/app.bsky.post/fake',
val: 'default-hide',
cts: new Date().toISOString(),
},
],
}),
modOpts,
)

expect(res1.ui('profileList')).toBeModerationResult(['filter'])
expect(res1.ui('profileView')).toBeModerationResult([])
expect(res1.ui('avatar')).toBeModerationResult([])
expect(res1.ui('banner')).toBeModerationResult([])
expect(res1.ui('displayName')).toBeModerationResult([])
expect(res1.ui('contentList')).toBeModerationResult(['filter', 'blur'])
expect(res1.ui('contentView')).toBeModerationResult(['inform'])
expect(res1.ui('contentMedia')).toBeModerationResult([])

const res2 = moderatePost(
mock.postView({
record: {
text: 'Hello',
createdAt: new Date().toISOString(),
},
author: mock.profileViewBasic({
handle: 'bob.test',
displayName: 'Bob',
}),
labels: [
{
src: 'did:web:labeler.test',
uri: 'at://did:web:bob.test/app.bsky.post/fake',
val: 'default-warn',
cts: new Date().toISOString(),
},
],
}),
modOpts,
)

expect(res2.ui('profileList')).toBeModerationResult([])
expect(res2.ui('profileView')).toBeModerationResult([])
expect(res2.ui('avatar')).toBeModerationResult([])
expect(res2.ui('banner')).toBeModerationResult([])
expect(res2.ui('displayName')).toBeModerationResult([])
expect(res2.ui('contentList')).toBeModerationResult(['blur'])
expect(res2.ui('contentView')).toBeModerationResult(['inform'])
expect(res2.ui('contentMedia')).toBeModerationResult([])

const res3 = moderatePost(
mock.postView({
record: {
text: 'Hello',
createdAt: new Date().toISOString(),
},
author: mock.profileViewBasic({
handle: 'bob.test',
displayName: 'Bob',
}),
labels: [
{
src: 'did:web:labeler.test',
uri: 'at://did:web:bob.test/app.bsky.post/fake',
val: 'default-ignore',
cts: new Date().toISOString(),
},
],
}),
modOpts,
)

expect(res3.ui('profileList')).toBeModerationResult([])
expect(res3.ui('profileView')).toBeModerationResult([])
expect(res3.ui('avatar')).toBeModerationResult([])
expect(res3.ui('banner')).toBeModerationResult([])
expect(res3.ui('displayName')).toBeModerationResult([])
expect(res3.ui('contentList')).toBeModerationResult([])
expect(res3.ui('contentView')).toBeModerationResult([])
expect(res3.ui('contentMedia')).toBeModerationResult([])
})

it('Custom labels can require adult content to be enabled', () => {
const modOpts = {
userDid: 'did:web:alice.test',
prefs: {
adultContentEnabled: false,
labels: { adult: 'ignore' },
labelers: [
{
did: 'did:web:labeler.test',
labels: {
adult: 'ignore',
},
},
],
},
labelDefs: {
'did:web:labeler.test': [
interpretLabelValueDefinition(
{
identifier: 'adult',
blurs: 'content',
severity: 'inform',
defaultSetting: 'hide',
adultOnly: true,
locales: [],
},
'did:web:labeler.test',
),
],
},
}
const res = moderatePost(
mock.postView({
record: {
text: 'Hello',
createdAt: new Date().toISOString(),
},
author: mock.profileViewBasic({
handle: 'bob.test',
displayName: 'Bob',
}),
labels: [
{
src: 'did:web:labeler.test',
uri: 'at://did:web:bob.test/app.bsky.post/fake',
val: 'adult',
cts: new Date().toISOString(),
},
],
}),
modOpts,
)

expect(res.ui('profileList')).toBeModerationResult(['filter'])
expect(res.ui('profileView')).toBeModerationResult([])
expect(res.ui('avatar')).toBeModerationResult([])
expect(res.ui('banner')).toBeModerationResult([])
expect(res.ui('displayName')).toBeModerationResult([])
expect(res.ui('contentList')).toBeModerationResult([
'filter',
'blur',
'noOverride',
])
expect(res.ui('contentView')).toBeModerationResult(['blur', 'noOverride'])
expect(res.ui('contentMedia')).toBeModerationResult([])
})
})

0 comments on commit 876cdcf

Please sign in to comment.