about summary refs log tree commit diff
path: root/src/lib/labeling
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/labeling')
-rw-r--r--src/lib/labeling/const.ts20
-rw-r--r--src/lib/labeling/helpers.ts303
-rw-r--r--src/lib/labeling/types.ts58
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
+}