Skip to content

Commit

Permalink
Implement quote-post moderation handling
Browse files Browse the repository at this point in the history
  • Loading branch information
pfrazee committed Mar 10, 2024
1 parent 4dca2a8 commit 2b05b41
Show file tree
Hide file tree
Showing 10 changed files with 446 additions and 76 deletions.
93 changes: 55 additions & 38 deletions packages/api/src/moderation/decision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,25 @@ export class ModerationDecision {
static merge(
...decisions: (ModerationDecision | undefined)[]
): ModerationDecision {
const firmDecisions: ModerationDecision[] = decisions.filter(
const decisionsFiltered: ModerationDecision[] = decisions.filter(
(v) => !!v,
) as ModerationDecision[]
const decision = new ModerationDecision()
if (firmDecisions[0]) {
decision.did = firmDecisions[0].did
decision.isMe = firmDecisions[0].isMe
if (decisionsFiltered[0]) {
decision.did = decisionsFiltered[0].did
decision.isMe = decisionsFiltered[0].isMe
}
decision.causes = firmDecisions.flatMap((d) => d.causes)
decision.causes = decisionsFiltered.flatMap((d) => d.causes)
return decision
}

downgrade() {
for (const cause of this.causes) {
cause.downgraded = true
}
return this
}

get blocked() {
return !!this.blockCause
}
Expand Down Expand Up @@ -83,46 +90,54 @@ export class ModerationDecision {
if (context === 'profileList' || context === 'contentList') {
ui.filters.push(cause)
}
if (BLOCK_BEHAVIOR[context] === 'blur') {
ui.noOverride = true
ui.blurs.push(cause)
} else if (BLOCK_BEHAVIOR[context] === 'alert') {
ui.alerts.push(cause)
} else if (BLOCK_BEHAVIOR[context] === 'inform') {
ui.informs.push(cause)
if (!cause.downgraded) {
if (BLOCK_BEHAVIOR[context] === 'blur') {
ui.noOverride = true
ui.blurs.push(cause)
} else if (BLOCK_BEHAVIOR[context] === 'alert') {
ui.alerts.push(cause)
} else if (BLOCK_BEHAVIOR[context] === 'inform') {
ui.informs.push(cause)
}
}
} else if (cause.type === 'muted') {
if (context === 'profileList' || context === 'contentList') {
ui.filters.push(cause)
}
if (MUTE_BEHAVIOR[context] === 'blur') {
ui.blurs.push(cause)
} else if (MUTE_BEHAVIOR[context] === 'alert') {
ui.alerts.push(cause)
} else if (MUTE_BEHAVIOR[context] === 'inform') {
ui.informs.push(cause)
if (!cause.downgraded) {
if (MUTE_BEHAVIOR[context] === 'blur') {
ui.blurs.push(cause)
} else if (MUTE_BEHAVIOR[context] === 'alert') {
ui.alerts.push(cause)
} else if (MUTE_BEHAVIOR[context] === 'inform') {
ui.informs.push(cause)
}
}
} else if (cause.type === 'mute-word') {
if (context === 'contentList') {
ui.filters.push(cause)
}
if (MUTEWORD_BEHAVIOR[context] === 'blur') {
ui.blurs.push(cause)
} else if (MUTEWORD_BEHAVIOR[context] === 'alert') {
ui.alerts.push(cause)
} else if (MUTEWORD_BEHAVIOR[context] === 'inform') {
ui.informs.push(cause)
if (!cause.downgraded) {
if (MUTEWORD_BEHAVIOR[context] === 'blur') {
ui.blurs.push(cause)
} else if (MUTEWORD_BEHAVIOR[context] === 'alert') {
ui.alerts.push(cause)
} else if (MUTEWORD_BEHAVIOR[context] === 'inform') {
ui.informs.push(cause)
}
}
} else if (cause.type === 'hidden') {
if (context === 'profileList' || context === 'contentList') {
ui.filters.push(cause)
}
if (HIDE_BEHAVIOR[context] === 'blur') {
ui.blurs.push(cause)
} else if (HIDE_BEHAVIOR[context] === 'alert') {
ui.alerts.push(cause)
} else if (HIDE_BEHAVIOR[context] === 'inform') {
ui.informs.push(cause)
if (!cause.downgraded) {
if (HIDE_BEHAVIOR[context] === 'blur') {
ui.blurs.push(cause)
} else if (HIDE_BEHAVIOR[context] === 'alert') {
ui.alerts.push(cause)
} else if (HIDE_BEHAVIOR[context] === 'inform') {
ui.informs.push(cause)
}
}
} else if (cause.type === 'label') {
if (context === 'profileList' && cause.target === 'account') {
Expand All @@ -137,15 +152,17 @@ export class ModerationDecision {
ui.filters.push(cause)
}
}
if (cause.behavior[context] === 'blur') {
ui.blurs.push(cause)
if (cause.noOverride) {
ui.noOverride = true
if (!cause.downgraded) {
if (cause.behavior[context] === 'blur') {
ui.blurs.push(cause)
if (cause.noOverride) {
ui.noOverride = true
}
} else if (cause.behavior[context] === 'alert') {
ui.alerts.push(cause)
} else if (cause.behavior[context] === 'inform') {
ui.informs.push(cause)
}
} else if (cause.behavior[context] === 'alert') {
ui.alerts.push(cause)
} else if (cause.behavior[context] === 'inform') {
ui.informs.push(cause)
}
}
}
Expand Down
28 changes: 4 additions & 24 deletions packages/api/src/moderation/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { AppBskyActorDefs } from '../client/index'
import {
ModerationSubjectProfile,
ModerationSubjectPost,
Expand Down Expand Up @@ -37,45 +36,26 @@ export function moderatePost(
subject: ModerationSubjectPost,
opts: ModerationOpts,
): ModerationDecision {
return ModerationDecision.merge(
decidePost(subject, opts),
decideAccount(subject.author, opts),
decideProfile(subject.author, opts),
)
return decidePost(subject, opts)
}

export function moderateNotification(
subject: ModerationSubjectNotification,
opts: ModerationOpts,
): ModerationDecision {
return ModerationDecision.merge(
decideNotification(subject, opts),
decideAccount(subject.author, opts),
decideProfile(subject.author, opts),
)
return decideNotification(subject, opts)
}

export function moderateFeedGenerator(
subject: ModerationSubjectFeedGenerator,
opts: ModerationOpts,
): ModerationDecision {
return ModerationDecision.merge(
decideFeedGenerator(subject, opts),
decideAccount(subject.creator, opts),
decideProfile(subject.creator, opts),
)
return decideFeedGenerator(subject, opts)
}

export function moderateUserList(
subject: ModerationSubjectUserList,
opts: ModerationOpts,
): ModerationDecision {
const userList = decideUserList(subject, opts)
const account = AppBskyActorDefs.isProfileViewBasic(subject.creator)
? decideAccount(subject.creator, opts)
: new ModerationDecision()
const profile = AppBskyActorDefs.isProfileViewBasic(subject.creator)
? decideProfile(subject.creator, opts)
: new ModerationDecision()
return ModerationDecision.merge(userList, account, profile)
return decideUserList(subject, opts)
}
11 changes: 8 additions & 3 deletions packages/api/src/moderation/subjects/feed-generator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { ModerationDecision } from '../decision'
import { ModerationSubjectFeedGenerator, ModerationOpts } from '../types'
import { decideAccount } from './account'
import { decideProfile } from './profile'

export function decideFeedGenerator(
_subject: ModerationSubjectFeedGenerator,
_opts: ModerationOpts,
subject: ModerationSubjectFeedGenerator,
opts: ModerationOpts,
): ModerationDecision {
// TODO handle labels applied on the feed generator itself
return new ModerationDecision()
return ModerationDecision.merge(
decideAccount(subject.creator, opts),
decideProfile(subject.creator, opts),
)
}
8 changes: 7 additions & 1 deletion packages/api/src/moderation/subjects/notification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ModerationDecision } from '../decision'
import { ModerationSubjectNotification, ModerationOpts } from '../types'
import { decideAccount } from './account'
import { decideProfile } from './profile'

export function decideNotification(
subject: ModerationSubjectNotification,
Expand All @@ -15,5 +17,9 @@ export function decideNotification(
}
}

return acc
return ModerationDecision.merge(
acc,
decideAccount(subject.author, opts),
decideProfile(subject.author, opts),
)
}
42 changes: 41 additions & 1 deletion packages/api/src/moderation/subjects/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
} from '../../client'
import { ModerationSubjectPost, ModerationOpts } from '../types'
import { hasMutedWord } from '../mutewords'
import { decideAccount } from './account'
import { decideProfile } from './profile'

export function decidePost(
subject: ModerationSubjectPost,
Expand All @@ -28,7 +30,45 @@ export function decidePost(
acc.addMutedWord(checkMutedWords(subject, opts.prefs.mutedWords))
}

return acc
let embedAcc
if (subject.embed) {
if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) {
// quote post
embedAcc = decideQuotedPost(subject.embed.record, opts)
} else if (
AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
AppBskyEmbedRecord.isViewRecord(subject.embed.record.record)
) {
// quoted post with media
embedAcc = decideQuotedPost(subject.embed.record.record, opts)
}
}

return ModerationDecision.merge(
acc,
embedAcc?.downgrade(),
decideAccount(subject.author, opts),
decideProfile(subject.author, opts),
)
}

function decideQuotedPost(
subject: AppBskyEmbedRecord.ViewRecord,
opts: ModerationOpts,
) {
const acc = new ModerationDecision()
acc.setDid(subject.author.did)
acc.setIsMe(subject.author.did === opts.userDid)
if (subject.labels?.length) {
for (const label of subject.labels) {
acc.addLabel('content', label, opts)
}
}
return ModerationDecision.merge(
acc,
decideAccount(subject.author, opts),
decideProfile(subject.author, opts),
)
}

function checkHiddenPost(
Expand Down
15 changes: 12 additions & 3 deletions packages/api/src/moderation/subjects/user-list.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { AppBskyActorDefs } from '../../client/index'
import { ModerationDecision } from '../decision'
import { ModerationSubjectUserList, ModerationOpts } from '../types'
import { decideAccount } from './account'
import { decideProfile } from './profile'

export function decideUserList(
_subject: ModerationSubjectUserList,
_opts: ModerationOpts,
subject: ModerationSubjectUserList,
opts: ModerationOpts,
): ModerationDecision {
// TODO handle labels applied on the list itself
return new ModerationDecision()
const account = AppBskyActorDefs.isProfileViewBasic(subject.creator)
? decideAccount(subject.creator, opts)
: new ModerationDecision()
const profile = AppBskyActorDefs.isProfileViewBasic(subject.creator)
? decideProfile(subject.creator, opts)
: new ModerationDecision()
return ModerationDecision.merge(account, profile)
}
43 changes: 37 additions & 6 deletions packages/api/src/moderation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,24 @@ export type ModerationCauseSource =
| { type: 'labeler'; did: string }

export type ModerationCause =
| { type: 'blocking'; source: ModerationCauseSource; priority: 3 }
| { type: 'blocked-by'; source: ModerationCauseSource; priority: 4 }
| { type: 'block-other'; source: ModerationCauseSource; priority: 4 }
| {
type: 'blocking'
source: ModerationCauseSource
priority: 3
downgraded?: boolean
}
| {
type: 'blocked-by'
source: ModerationCauseSource
priority: 4
downgraded?: boolean
}
| {
type: 'block-other'
source: ModerationCauseSource
priority: 4
downgraded?: boolean
}
| {
type: 'label'
source: ModerationCauseSource
Expand All @@ -128,10 +143,26 @@ export type ModerationCause =
behavior: ModerationBehavior
noOverride: boolean
priority: 1 | 2 | 5 | 7 | 8
downgraded?: boolean
}
| {
type: 'muted'
source: ModerationCauseSource
priority: 6
downgraded?: boolean
}
| {
type: 'mute-word'
source: ModerationCauseSource
priority: 6
downgraded?: boolean
}
| {
type: 'hidden'
source: ModerationCauseSource
priority: 6
downgraded?: boolean
}
| { type: 'muted'; source: ModerationCauseSource; priority: 6 }
| { type: 'mute-word'; source: ModerationCauseSource; priority: 6 }
| { type: 'hidden'; source: ModerationCauseSource; priority: 6 }

export interface ModerationPrefsLabeler {
did: string
Expand Down
3 changes: 3 additions & 0 deletions packages/api/tests/moderation-custom-labels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ function modOpts(blurs: string, severity: string): ModerationOpts {
labels: { custom: 'hide' },
},
],
mutedWords: [],
hiddenPosts: [],
},
labelDefs: {
'did:web:labeler.test': [makeCustomLabel(blurs, severity)],
Expand All @@ -323,6 +325,7 @@ function makeCustomLabel(
identifier: 'custom',
blurs,
severity,
defaultSetting: 'warn',
locales: [],
},
'did:web:labeler.test',
Expand Down
Loading

0 comments on commit 2b05b41

Please sign in to comment.