bsky-app/src/lib/labeling/helpers.ts
Paul Frazee b756a27958
[APP-639] Improve nsfw handling & force hidden on iOS (#605)
* Identify adult content labels and handle them more specifically

* Change adult content defaults to more conservative settings

* Add an adultcontentenabled override that prohibits access on iOS

* Improve usability of the content hider

* Fix lint
2023-05-09 00:43:20 -05:00

409 lines
9.6 KiB
TypeScript

import {
AppBskyActorDefs,
AppBskyEmbedRecordWithMedia,
AppBskyEmbedRecord,
AppBskyEmbedImages,
AppBskyEmbedExternal,
} from '@atproto/api'
import {
CONFIGURABLE_LABEL_GROUPS,
ILLEGAL_LABEL_GROUP,
ALWAYS_FILTER_LABEL_GROUP,
ALWAYS_WARN_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
for (id in CONFIGURABLE_LABEL_GROUPS) {
if (ILLEGAL_LABEL_GROUP.values.includes(labelVal)) {
return ILLEGAL_LABEL_GROUP
}
if (ALWAYS_FILTER_LABEL_GROUP.values.includes(labelVal)) {
return ALWAYS_FILTER_LABEL_GROUP
}
if (ALWAYS_WARN_LABEL_GROUP.values.includes(labelVal)) {
return ALWAYS_WARN_LABEL_GROUP
}
if (CONFIGURABLE_LABEL_GROUPS[id].values.includes(labelVal)) {
return CONFIGURABLE_LABEL_GROUPS[id]
}
}
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:
postInfo.isBlocking ||
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 (postInfo.isBlocking) {
return {
avatar,
list: hide('Post from an account you blocked.'),
thread: hide('Post from an account you blocked.'),
view: warn('Post from an account you blocked.'),
}
}
if (postInfo.isBlockedBy) {
return {
avatar,
list: hide('Post from an account that has blocked you.'),
thread: hide('Post from an account that has blocked you.'),
view: warn('Post from an account that has blocked you.'),
}
}
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.isAdultImagery) {
return {
avatar,
list: warnImages(postPref.desc.warning),
thread: warnImages(postPref.desc.warning),
view: warnImages(postPref.desc.warning),
}
}
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 mergePostModerations(
moderations: PostModeration[],
): PostModeration {
const merged: PostModeration = {
avatar: {warn: false, blur: false},
list: show(),
thread: show(),
view: show(),
}
for (const mod of moderations) {
if (mod.list.behavior === ModerationBehaviorCode.Hide) {
merged.list = mod.list
}
if (mod.thread.behavior === ModerationBehaviorCode.Hide) {
merged.thread = mod.thread
}
if (mod.view.behavior === ModerationBehaviorCode.Hide) {
merged.view = mod.view
}
}
return merged
}
export function getProfileModeration(
store: RootStoreModel,
profileInfo: ProfileLabelInfo,
): ProfileModeration {
const accountPref = store.preferences.getLabelPreference(
profileInfo.accountLabels,
)
const profilePref = store.preferences.getLabelPreference(
profileInfo.profileLabels,
)
// avatar
let avatar = {
warn: accountPref.pref === 'hide' || accountPref.pref === 'warn',
blur:
profileInfo.isBlocking ||
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:
profileInfo.isBlocking || profileInfo.isBlockedBy
? hide('Blocked account')
: 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: profileInfo.isBlocking ? hide('Blocked account') : show(),
view: show(),
}
}
export function getProfileViewBasicLabelInfo(
profile: AppBskyActorDefs.ProfileViewBasic,
): ProfileLabelInfo {
return {
accountLabels: filterAccountLabels(profile.labels),
profileLabels: filterProfileLabels(profile.labels),
isMuted: profile.viewer?.muted || false,
isBlocking: !!profile.viewer?.blocking || false,
}
}
export function getEmbedLabels(embed?: Embed): Label[] {
if (!embed) {
return []
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return embed.record.labels || []
}
return []
}
export function getEmbedMuted(embed?: Embed): boolean {
if (!embed) {
return false
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return !!embed.record.author.viewer?.muted
}
return false
}
export function getEmbedBlocking(embed?: Embed): boolean {
if (!embed) {
return false
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return !!embed.record.author.viewer?.blocking
}
return false
}
export function getEmbedBlockedBy(embed?: Embed): boolean {
if (!embed) {
return false
}
if (
AppBskyEmbedRecord.isView(embed) &&
AppBskyEmbedRecord.isViewRecord(embed.record)
) {
return !!embed.record.author.viewer?.blockedBy
}
return false
}
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,
}
}