diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-04-27 12:38:23 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-27 12:38:23 -0500 |
commit | 1d50ddb378d5c6954d4cf8a6145b4486b9497107 (patch) | |
tree | 85a55e9aef6692c304cc31d7c3bb239c186f7951 /src/lib/labeling | |
parent | 51be8474db5e8074b1af233609b5eb455af31692 (diff) | |
download | voidsky-1d50ddb378d5c6954d4cf8a6145b4486b9497107.tar.zst |
Refactor moderation to apply to accounts, profiles, and posts correctly (#548)
* Add ScreenHider component * Add blur attribute to UserAvatar and UserBanner * Remove dead suggested posts component and model * Bump @atproto/api@0.2.10 * Rework moderation tooling to give a more precise DSL * Add label mocks * Apply finer grained moderation controls * Refactor ProfileCard to just take the profile object * Apply moderation to user listings and banner * Apply moderation to notifications * Fix lint * Tune avatar & banner blur settings per platform * 1.24
Diffstat (limited to 'src/lib/labeling')
-rw-r--r-- | src/lib/labeling/const.ts | 20 | ||||
-rw-r--r-- | src/lib/labeling/helpers.ts | 303 | ||||
-rw-r--r-- | src/lib/labeling/types.ts | 58 |
3 files changed, 372 insertions, 9 deletions
diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts index f68353222..6670e5413 100644 --- a/src/lib/labeling/const.ts +++ b/src/lib/labeling/const.ts @@ -1,23 +1,20 @@ import {LabelPreferencesModel} from 'state/models/ui/preferences' - -export interface LabelValGroup { - id: keyof LabelPreferencesModel | 'illegal' | 'unknown' - title: string - subtitle?: string - warning?: string - values: string[] -} +import {LabelValGroup} from './types' export const ILLEGAL_LABEL_GROUP: LabelValGroup = { id: 'illegal', title: 'Illegal Content', + warning: 'Illegal Content', values: ['csam', 'dmca-violation', 'nudity-nonconsentual'], + imagesOnly: false, // not applicable } export const UNKNOWN_LABEL_GROUP: LabelValGroup = { id: 'unknown', title: 'Unknown Label', + warning: 'Content Warning', values: [], + imagesOnly: false, } export const CONFIGURABLE_LABEL_GROUPS: Record< @@ -30,6 +27,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'i.e. Pornography', warning: 'Sexually Explicit', values: ['porn'], + imagesOnly: false, // apply to whole thing }, nudity: { id: 'nudity', @@ -37,6 +35,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Including non-sexual and artistic', warning: 'Nudity', values: ['nudity'], + imagesOnly: true, }, suggestive: { id: 'suggestive', @@ -44,6 +43,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Does not include nudity', warning: 'Sexually Suggestive', values: ['sexual'], + imagesOnly: true, }, gore: { id: 'gore', @@ -51,12 +51,14 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Gore, self-harm, torture', warning: 'Violence', values: ['gore', 'self-harm', 'torture'], + imagesOnly: true, }, hate: { id: 'hate', title: 'Political Hate-Groups', warning: 'Hate', values: ['icon-kkk', 'icon-nazi'], + imagesOnly: false, }, spam: { id: 'spam', @@ -64,6 +66,7 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Excessive low-quality posts', warning: 'Spam', values: ['spam'], + imagesOnly: false, }, impersonation: { id: 'impersonation', @@ -71,5 +74,6 @@ export const CONFIGURABLE_LABEL_GROUPS: Record< subtitle: 'Accounts falsely claiming to be people or orgs', warning: 'Impersonation', values: ['impersonation'], + imagesOnly: false, }, } diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts index b2057ff18..bac98c6a2 100644 --- a/src/lib/labeling/helpers.ts +++ b/src/lib/labeling/helpers.ts @@ -1,9 +1,33 @@ import { - LabelValGroup, + AppBskyActorDefs, + AppBskyEmbedRecordWithMedia, + AppBskyEmbedRecord, + AppBskyFeedPost, + AppBskyEmbedImages, + AppBskyEmbedExternal, +} from '@atproto/api' +import { CONFIGURABLE_LABEL_GROUPS, ILLEGAL_LABEL_GROUP, UNKNOWN_LABEL_GROUP, } from './const' +import { + Label, + LabelValGroup, + ModerationBehaviorCode, + PostModeration, + ProfileModeration, + PostLabelInfo, + ProfileLabelInfo, +} from './types' +import {RootStoreModel} from 'state/index' + +type Embed = + | AppBskyEmbedRecord.View + | AppBskyEmbedImages.View + | AppBskyEmbedExternal.View + | AppBskyEmbedRecordWithMedia.View + | {$type: string; [k: string]: unknown} export function getLabelValueGroup(labelVal: string): LabelValGroup { let id: keyof typeof CONFIGURABLE_LABEL_GROUPS @@ -17,3 +41,280 @@ export function getLabelValueGroup(labelVal: string): LabelValGroup { } return UNKNOWN_LABEL_GROUP } + +export function getPostModeration( + store: RootStoreModel, + postInfo: PostLabelInfo, +): PostModeration { + const accountPref = store.preferences.getLabelPreference( + postInfo.accountLabels, + ) + const profilePref = store.preferences.getLabelPreference( + postInfo.profileLabels, + ) + const postPref = store.preferences.getLabelPreference(postInfo.postLabels) + + // avatar + let avatar = { + warn: accountPref.pref === 'hide' || accountPref.pref === 'warn', + blur: + accountPref.pref === 'hide' || + accountPref.pref === 'warn' || + profilePref.pref === 'hide' || + profilePref.pref === 'warn', + } + + // hide no-override cases + if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') { + return hidePostNoOverride(accountPref.desc.warning) + } + if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') { + return hidePostNoOverride(profilePref.desc.warning) + } + if (postPref.pref === 'hide' && postPref.desc.id === 'illegal') { + return hidePostNoOverride(postPref.desc.warning) + } + + // hide cases + if (accountPref.pref === 'hide') { + return { + avatar, + list: hide(accountPref.desc.warning), + thread: hide(accountPref.desc.warning), + view: warn(accountPref.desc.warning), + } + } + if (profilePref.pref === 'hide') { + return { + avatar, + list: hide(profilePref.desc.warning), + thread: hide(profilePref.desc.warning), + view: warn(profilePref.desc.warning), + } + } + if (postPref.pref === 'hide') { + return { + avatar, + list: hide(postPref.desc.warning), + thread: hide(postPref.desc.warning), + view: warn(postPref.desc.warning), + } + } + + // muting + if (postInfo.isMuted) { + return { + avatar, + list: hide('Post from an account you muted.'), + thread: warn('Post from an account you muted.'), + view: warn('Post from an account you muted.'), + } + } + + // warning cases + if (postPref.pref === 'warn') { + if (postPref.desc.imagesOnly) { + return { + avatar, + list: warnContent(postPref.desc.warning), // TODO make warnImages when there's time + thread: warnContent(postPref.desc.warning), // TODO make warnImages when there's time + view: warnContent(postPref.desc.warning), // TODO make warnImages when there's time + } + } + return { + avatar, + list: warnContent(postPref.desc.warning), + thread: warnContent(postPref.desc.warning), + view: warnContent(postPref.desc.warning), + } + } + if (accountPref.pref === 'warn') { + return { + avatar, + list: warnContent(accountPref.desc.warning), + thread: warnContent(accountPref.desc.warning), + view: warnContent(accountPref.desc.warning), + } + } + + return { + avatar, + list: show(), + thread: show(), + view: show(), + } +} + +export function getProfileModeration( + store: RootStoreModel, + profileLabels: ProfileLabelInfo, +): ProfileModeration { + const accountPref = store.preferences.getLabelPreference( + profileLabels.accountLabels, + ) + const profilePref = store.preferences.getLabelPreference( + profileLabels.profileLabels, + ) + + // avatar + let avatar = { + warn: accountPref.pref === 'hide' || accountPref.pref === 'warn', + blur: + accountPref.pref === 'hide' || + accountPref.pref === 'warn' || + profilePref.pref === 'hide' || + profilePref.pref === 'warn', + } + + // hide no-override cases + if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') { + return hideProfileNoOverride(accountPref.desc.warning) + } + if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') { + return hideProfileNoOverride(profilePref.desc.warning) + } + + // hide cases + if (accountPref.pref === 'hide') { + return { + avatar, + list: hide(accountPref.desc.warning), + view: hide(accountPref.desc.warning), + } + } + if (profilePref.pref === 'hide') { + return { + avatar, + list: hide(profilePref.desc.warning), + view: hide(profilePref.desc.warning), + } + } + + // warn cases + if (accountPref.pref === 'warn') { + return { + avatar, + list: warn(accountPref.desc.warning), + view: warn(accountPref.desc.warning), + } + } + // we don't warn for this + // if (profilePref.pref === 'warn') { + // return { + // avatar, + // list: warn(profilePref.desc.warning), + // view: warn(profilePref.desc.warning), + // } + // } + + return { + avatar, + list: show(), + view: show(), + } +} + +export function getProfileViewBasicLabelInfo( + profile: AppBskyActorDefs.ProfileViewBasic, +): ProfileLabelInfo { + return { + accountLabels: filterAccountLabels(profile.labels), + profileLabels: filterProfileLabels(profile.labels), + isMuted: profile.viewer?.muted || false, + } +} + +export function getEmbedLabels(embed?: Embed): Label[] { + if (!embed) { + return [] + } + if ( + AppBskyEmbedRecordWithMedia.isView(embed) && + AppBskyEmbedRecord.isViewRecord(embed.record.record) && + AppBskyFeedPost.isRecord(embed.record.record.value) && + AppBskyFeedPost.validateRecord(embed.record.record.value).success + ) { + return embed.record.record.labels || [] + } + return [] +} + +export function filterAccountLabels(labels?: Label[]): Label[] { + if (!labels) { + return [] + } + return labels.filter( + label => !label.uri.endsWith('/app.bsky.actor.profile/self'), + ) +} + +export function filterProfileLabels(labels?: Label[]): Label[] { + if (!labels) { + return [] + } + return labels.filter(label => + label.uri.endsWith('/app.bsky.actor.profile/self'), + ) +} + +// internal methods +// = + +function show() { + return { + behavior: ModerationBehaviorCode.Show, + } +} + +function hidePostNoOverride(reason: string) { + return { + avatar: {warn: true, blur: true}, + list: hideNoOverride(reason), + thread: hideNoOverride(reason), + view: hideNoOverride(reason), + } +} + +function hideProfileNoOverride(reason: string) { + return { + avatar: {warn: true, blur: true}, + list: hideNoOverride(reason), + view: hideNoOverride(reason), + } +} + +function hideNoOverride(reason: string) { + return { + behavior: ModerationBehaviorCode.Hide, + reason, + noOverride: true, + } +} + +function hide(reason: string) { + return { + behavior: ModerationBehaviorCode.Hide, + reason, + } +} + +function warn(reason: string) { + return { + behavior: ModerationBehaviorCode.Warn, + reason, + } +} + +function warnContent(reason: string) { + return { + behavior: ModerationBehaviorCode.WarnContent, + reason, + } +} + +function warnImages(reason: string) { + return { + behavior: ModerationBehaviorCode.WarnImages, + reason, + } +} diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts new file mode 100644 index 000000000..d4efb499a --- /dev/null +++ b/src/lib/labeling/types.ts @@ -0,0 +1,58 @@ +import {ComAtprotoLabelDefs} from '@atproto/api' +import {LabelPreferencesModel} from 'state/models/ui/preferences' + +export type Label = ComAtprotoLabelDefs.Label + +export interface LabelValGroup { + id: keyof LabelPreferencesModel | 'illegal' | 'unknown' + title: string + imagesOnly: boolean + subtitle?: string + warning: string + values: string[] +} + +export interface PostLabelInfo { + postLabels: Label[] + accountLabels: Label[] + profileLabels: Label[] + isMuted: boolean +} + +export interface ProfileLabelInfo { + accountLabels: Label[] + profileLabels: Label[] + isMuted: boolean +} + +export enum ModerationBehaviorCode { + Show, + Hide, + Warn, + WarnContent, + WarnImages, +} + +export interface ModerationBehavior { + behavior: ModerationBehaviorCode + noOverride?: boolean + reason?: string +} + +export interface AvatarModeration { + warn: boolean + blur: boolean +} + +export interface PostModeration { + avatar: AvatarModeration + list: ModerationBehavior + thread: ModerationBehavior + view: ModerationBehavior +} + +export interface ProfileModeration { + avatar: AvatarModeration + list: ModerationBehavior + view: ModerationBehavior +} |