bsky_sdk/moderation/
types.rs

1use super::decision::{DecisionContext, Priority};
2use super::error::Error;
3use atrium_api::agent::bluesky::BSKY_LABELER_DID;
4use atrium_api::app::bsky::actor::defs::{
5    MutedWord, ProfileView, ProfileViewBasic, ProfileViewDetailed, ViewerState,
6};
7use atrium_api::app::bsky::graph::defs::{ListView, ListViewBasic};
8use atrium_api::com::atproto::label::defs::{Label, LabelValueDefinitionStrings};
9use atrium_api::types::string::Did;
10use serde::{Deserialize, Deserializer, Serialize};
11use std::{collections::HashMap, str::FromStr};
12
13// behaviors
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub(crate) enum BehaviorValue {
17    Blur,
18    Alert,
19    Inform,
20}
21
22/// Moderation behaviors for different contexts.
23#[derive(Debug, Default, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct ModerationBehavior {
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub profile_list: Option<ProfileListBehavior>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub profile_view: Option<ProfileViewBehavior>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub avatar: Option<AvatarBehavior>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub banner: Option<BannerBehavior>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub display_name: Option<DisplayNameBehavior>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub content_list: Option<ContentListBehavior>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub content_view: Option<ContentViewBehavior>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub content_media: Option<ContentMediaBehavior>,
42}
43
44impl ModerationBehavior {
45    pub(crate) const BLOCK_BEHAVIOR: Self = Self {
46        profile_list: Some(ProfileListBehavior::Blur),
47        profile_view: Some(ProfileViewBehavior::Alert),
48        avatar: Some(AvatarBehavior::Blur),
49        banner: Some(BannerBehavior::Blur),
50        display_name: None,
51        content_list: Some(ContentListBehavior::Blur),
52        content_view: Some(ContentViewBehavior::Blur),
53        content_media: None,
54    };
55    pub(crate) const MUTE_BEHAVIOR: Self = Self {
56        profile_list: Some(ProfileListBehavior::Inform),
57        profile_view: Some(ProfileViewBehavior::Alert),
58        avatar: None,
59        banner: None,
60        display_name: None,
61        content_list: Some(ContentListBehavior::Blur),
62        content_view: Some(ContentViewBehavior::Inform),
63        content_media: None,
64    };
65    pub(crate) const MUTEWORD_BEHAVIOR: Self = Self {
66        profile_list: None,
67        profile_view: None,
68        avatar: None,
69        banner: None,
70        display_name: None,
71        content_list: Some(ContentListBehavior::Blur),
72        content_view: Some(ContentViewBehavior::Blur),
73        content_media: None,
74    };
75    pub(crate) const HIDE_BEHAVIOR: Self = Self {
76        profile_list: None,
77        profile_view: None,
78        avatar: None,
79        banner: None,
80        display_name: None,
81        content_list: Some(ContentListBehavior::Blur),
82        content_view: Some(ContentViewBehavior::Blur),
83        content_media: None,
84    };
85    pub(crate) fn behavior_for(&self, context: DecisionContext) -> Option<BehaviorValue> {
86        match context {
87            DecisionContext::ProfileList => self.profile_list.clone().map(Into::into),
88            DecisionContext::ProfileView => self.profile_view.clone().map(Into::into),
89            DecisionContext::Avatar => self.avatar.clone().map(Into::into),
90            DecisionContext::Banner => self.banner.clone().map(Into::into),
91            DecisionContext::DisplayName => self.display_name.clone().map(Into::into),
92            DecisionContext::ContentList => self.content_list.clone().map(Into::into),
93            DecisionContext::ContentView => self.content_view.clone().map(Into::into),
94            DecisionContext::ContentMedia => self.content_media.clone().map(Into::into),
95        }
96    }
97}
98
99/// Moderation behaviors for the profile list.
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "lowercase")]
102pub enum ProfileListBehavior {
103    Blur,
104    Alert,
105    Inform,
106}
107
108impl From<ProfileListBehavior> for BehaviorValue {
109    fn from(b: ProfileListBehavior) -> Self {
110        match b {
111            ProfileListBehavior::Blur => Self::Blur,
112            ProfileListBehavior::Alert => Self::Alert,
113            ProfileListBehavior::Inform => Self::Inform,
114        }
115    }
116}
117
118impl TryFrom<BehaviorValue> for ProfileListBehavior {
119    type Error = Error;
120
121    fn try_from(b: BehaviorValue) -> Result<Self, Self::Error> {
122        match b {
123            BehaviorValue::Blur => Ok(Self::Blur),
124            BehaviorValue::Alert => Ok(Self::Alert),
125            BehaviorValue::Inform => Ok(Self::Inform),
126        }
127    }
128}
129
130/// Moderation behaviors for the profile view.
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(rename_all = "lowercase")]
133pub enum ProfileViewBehavior {
134    Blur,
135    Alert,
136    Inform,
137}
138
139impl From<ProfileViewBehavior> for BehaviorValue {
140    fn from(b: ProfileViewBehavior) -> Self {
141        match b {
142            ProfileViewBehavior::Blur => Self::Blur,
143            ProfileViewBehavior::Alert => Self::Alert,
144            ProfileViewBehavior::Inform => Self::Inform,
145        }
146    }
147}
148
149impl TryFrom<BehaviorValue> for ProfileViewBehavior {
150    type Error = Error;
151
152    fn try_from(b: BehaviorValue) -> Result<Self, Self::Error> {
153        match b {
154            BehaviorValue::Blur => Ok(Self::Blur),
155            BehaviorValue::Alert => Ok(Self::Alert),
156            BehaviorValue::Inform => Ok(Self::Inform),
157        }
158    }
159}
160
161/// Moderation behaviors for the user's avatar.
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "lowercase")]
164pub enum AvatarBehavior {
165    Blur,
166    Alert,
167}
168
169impl From<AvatarBehavior> for BehaviorValue {
170    fn from(b: AvatarBehavior) -> Self {
171        match b {
172            AvatarBehavior::Blur => Self::Blur,
173            AvatarBehavior::Alert => Self::Alert,
174        }
175    }
176}
177
178impl TryFrom<BehaviorValue> for AvatarBehavior {
179    type Error = Error;
180
181    fn try_from(b: BehaviorValue) -> Result<Self, Self::Error> {
182        match b {
183            BehaviorValue::Blur => Ok(Self::Blur),
184            BehaviorValue::Alert => Ok(Self::Alert),
185            BehaviorValue::Inform => Err(Error::BehaviorValue),
186        }
187    }
188}
189
190/// Moderation behaviors for the user's banner.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "lowercase")]
193pub enum BannerBehavior {
194    Blur,
195}
196
197impl From<BannerBehavior> for BehaviorValue {
198    fn from(b: BannerBehavior) -> Self {
199        match b {
200            BannerBehavior::Blur => Self::Blur,
201        }
202    }
203}
204
205impl TryFrom<BehaviorValue> for BannerBehavior {
206    type Error = Error;
207
208    fn try_from(b: BehaviorValue) -> Result<Self, Self::Error> {
209        match b {
210            BehaviorValue::Blur => Ok(Self::Blur),
211            _ => Err(Error::BehaviorValue),
212        }
213    }
214}
215
216/// Moderation behaviors for the user's display name.
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "lowercase")]
219pub enum DisplayNameBehavior {
220    Blur,
221}
222
223impl From<DisplayNameBehavior> for BehaviorValue {
224    fn from(b: DisplayNameBehavior) -> Self {
225        match b {
226            DisplayNameBehavior::Blur => Self::Blur,
227        }
228    }
229}
230
231impl TryFrom<BehaviorValue> for DisplayNameBehavior {
232    type Error = Error;
233
234    fn try_from(b: BehaviorValue) -> Result<Self, Self::Error> {
235        match b {
236            BehaviorValue::Blur => Ok(Self::Blur),
237            _ => Err(Error::BehaviorValue),
238        }
239    }
240}
241
242/// Moderation behaviors for the content list.
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "lowercase")]
245pub enum ContentListBehavior {
246    Blur,
247    Alert,
248    Inform,
249}
250
251impl From<ContentListBehavior> for BehaviorValue {
252    fn from(b: ContentListBehavior) -> Self {
253        match b {
254            ContentListBehavior::Blur => Self::Blur,
255            ContentListBehavior::Alert => Self::Alert,
256            ContentListBehavior::Inform => Self::Inform,
257        }
258    }
259}
260
261impl TryFrom<BehaviorValue> for ContentListBehavior {
262    type Error = Error;
263
264    fn try_from(b: BehaviorValue) -> Result<Self, Self::Error> {
265        match b {
266            BehaviorValue::Blur => Ok(Self::Blur),
267            BehaviorValue::Alert => Ok(Self::Alert),
268            BehaviorValue::Inform => Ok(Self::Inform),
269        }
270    }
271}
272
273/// Moderation behaviors for the content view.
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275#[serde(rename_all = "lowercase")]
276pub enum ContentViewBehavior {
277    Blur,
278    Alert,
279    Inform,
280}
281
282impl From<ContentViewBehavior> for BehaviorValue {
283    fn from(b: ContentViewBehavior) -> Self {
284        match b {
285            ContentViewBehavior::Blur => Self::Blur,
286            ContentViewBehavior::Alert => Self::Alert,
287            ContentViewBehavior::Inform => Self::Inform,
288        }
289    }
290}
291
292impl TryFrom<BehaviorValue> for ContentViewBehavior {
293    type Error = Error;
294
295    fn try_from(b: BehaviorValue) -> Result<Self, Self::Error> {
296        match b {
297            BehaviorValue::Blur => Ok(Self::Blur),
298            BehaviorValue::Alert => Ok(Self::Alert),
299            BehaviorValue::Inform => Ok(Self::Inform),
300        }
301    }
302}
303
304/// Moderation behaviors for the content media.
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
306#[serde(rename_all = "lowercase")]
307pub enum ContentMediaBehavior {
308    Blur,
309}
310
311impl From<ContentMediaBehavior> for BehaviorValue {
312    fn from(b: ContentMediaBehavior) -> Self {
313        match b {
314            ContentMediaBehavior::Blur => Self::Blur,
315        }
316    }
317}
318
319impl TryFrom<BehaviorValue> for ContentMediaBehavior {
320    type Error = Error;
321
322    fn try_from(b: BehaviorValue) -> Result<Self, Self::Error> {
323        match b {
324            BehaviorValue::Blur => Ok(Self::Blur),
325            _ => Err(Error::BehaviorValue),
326        }
327    }
328}
329
330// labels
331
332/// The target of a label.
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub enum LabelTarget {
335    Account,
336    Profile,
337    Content,
338}
339
340/// The preference for a label.
341#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
342#[serde(rename_all = "lowercase")]
343pub enum LabelPreference {
344    Ignore,
345    Warn,
346    Hide,
347}
348
349impl AsRef<str> for LabelPreference {
350    fn as_ref(&self) -> &str {
351        match self {
352            Self::Ignore => "ignore",
353            Self::Warn => "warn",
354            Self::Hide => "hide",
355        }
356    }
357}
358
359impl FromStr for LabelPreference {
360    type Err = Error;
361
362    fn from_str(s: &str) -> Result<Self, Self::Err> {
363        match s {
364            "ignore" => Ok(Self::Ignore),
365            "warn" => Ok(Self::Warn),
366            "hide" => Ok(Self::Hide),
367            _ => Err(Error::LabelPreference),
368        }
369    }
370}
371
372/// A flag for a label value definition.
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
374#[serde(rename_all = "kebab-case")]
375pub enum LabelValueDefinitionFlag {
376    NoOverride,
377    Adult,
378    Unauthed,
379    NoSelf,
380}
381
382/// The blurs for a label value definition.
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(rename_all = "lowercase")]
385pub enum LabelValueDefinitionBlurs {
386    Content,
387    Media,
388    None,
389}
390
391impl AsRef<str> for LabelValueDefinitionBlurs {
392    fn as_ref(&self) -> &str {
393        match self {
394            Self::Content => "content",
395            Self::Media => "media",
396            Self::None => "none",
397        }
398    }
399}
400
401impl FromStr for LabelValueDefinitionBlurs {
402    type Err = Error;
403
404    fn from_str(s: &str) -> Result<Self, Self::Err> {
405        match s {
406            "content" => Ok(Self::Content),
407            "media" => Ok(Self::Media),
408            "none" => Ok(Self::None),
409            _ => Err(Error::LabelValueDefinitionBlurs),
410        }
411    }
412}
413
414/// The severity for a label value definition.
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "lowercase")]
417pub enum LabelValueDefinitionSeverity {
418    Inform,
419    Alert,
420    None,
421}
422
423impl AsRef<str> for LabelValueDefinitionSeverity {
424    fn as_ref(&self) -> &str {
425        match self {
426            Self::Inform => "inform",
427            Self::Alert => "alert",
428            Self::None => "none",
429        }
430    }
431}
432
433impl FromStr for LabelValueDefinitionSeverity {
434    type Err = Error;
435
436    fn from_str(s: &str) -> Result<Self, Self::Err> {
437        match s {
438            "inform" => Ok(Self::Inform),
439            "alert" => Ok(Self::Alert),
440            "none" => Ok(Self::None),
441            _ => Err(Error::LabelValueDefinitionSeverity),
442        }
443    }
444}
445
446/// A label value definition.
447#[derive(Debug, Clone, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct InterpretedLabelValueDefinition {
450    // from com.atproto.label/defs#labelValueDefinition, with type narrowing
451    pub adult_only: bool,
452    pub blurs: LabelValueDefinitionBlurs,
453    pub default_setting: LabelPreference,
454    pub identifier: String,
455    pub locales: Vec<LabelValueDefinitionStrings>,
456    pub severity: LabelValueDefinitionSeverity,
457    // others
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub defined_by: Option<Did>,
460    pub configurable: bool,
461    pub flags: Vec<LabelValueDefinitionFlag>,
462    pub behaviors: InterpretedLabelValueDefinitionBehaviors,
463}
464
465/// The behaviors for a label value definition.
466#[derive(Debug, Default, Clone, Serialize, Deserialize)]
467pub struct InterpretedLabelValueDefinitionBehaviors {
468    pub account: ModerationBehavior,
469    pub profile: ModerationBehavior,
470    pub content: ModerationBehavior,
471}
472
473impl InterpretedLabelValueDefinitionBehaviors {
474    pub(crate) fn behavior_for(&self, target: LabelTarget) -> ModerationBehavior {
475        match target {
476            LabelTarget::Account => self.account.clone(),
477            LabelTarget::Profile => self.profile.clone(),
478            LabelTarget::Content => self.content.clone(),
479        }
480    }
481}
482
483// subjects
484
485/// A subject profile.
486#[derive(Debug)]
487pub enum SubjectProfile {
488    ProfileViewBasic(Box<ProfileViewBasic>),
489    ProfileView(Box<ProfileView>),
490    ProfileViewDetailed(Box<ProfileViewDetailed>),
491}
492
493impl SubjectProfile {
494    pub(crate) fn did(&self) -> &Did {
495        match self {
496            Self::ProfileViewBasic(p) => &p.did,
497            Self::ProfileView(p) => &p.did,
498            Self::ProfileViewDetailed(p) => &p.did,
499        }
500    }
501    pub(crate) fn labels(&self) -> &Option<Vec<Label>> {
502        match self {
503            Self::ProfileViewBasic(p) => &p.labels,
504            Self::ProfileView(p) => &p.labels,
505            Self::ProfileViewDetailed(p) => &p.labels,
506        }
507    }
508    pub(crate) fn viewer(&self) -> &Option<ViewerState> {
509        match self {
510            Self::ProfileViewBasic(p) => &p.viewer,
511            Self::ProfileView(p) => &p.viewer,
512            Self::ProfileViewDetailed(p) => &p.viewer,
513        }
514    }
515}
516
517impl From<ProfileViewBasic> for SubjectProfile {
518    fn from(p: ProfileViewBasic) -> Self {
519        Self::ProfileViewBasic(Box::new(p))
520    }
521}
522
523impl From<ProfileView> for SubjectProfile {
524    fn from(p: ProfileView) -> Self {
525        Self::ProfileView(Box::new(p))
526    }
527}
528
529impl From<ProfileViewDetailed> for SubjectProfile {
530    fn from(p: ProfileViewDetailed) -> Self {
531        Self::ProfileViewDetailed(Box::new(p))
532    }
533}
534
535/// A subject post.
536pub type SubjectPost = atrium_api::app::bsky::feed::defs::PostView;
537
538/// A subject notification.
539pub type SubjectNotification =
540    atrium_api::app::bsky::notification::list_notifications::Notification;
541
542/// A subject feed generator.
543pub type SubjectFeedGenerator = atrium_api::app::bsky::feed::defs::GeneratorView;
544
545/// A subject user list.
546#[derive(Debug)]
547pub enum SubjectUserList {
548    ListView(Box<ListView>),
549    ListViewBasic(Box<ListViewBasic>),
550}
551
552impl From<ListView> for SubjectUserList {
553    fn from(list_view: ListView) -> Self {
554        Self::ListView(Box::new(list_view))
555    }
556}
557
558impl From<ListViewBasic> for SubjectUserList {
559    fn from(list_view_basic: ListViewBasic) -> Self {
560        Self::ListViewBasic(Box::new(list_view_basic))
561    }
562}
563
564/// A cause for moderation decisions.
565#[derive(Debug, Clone)]
566pub enum ModerationCause {
567    Blocking(Box<ModerationCauseOther>),
568    BlockedBy(Box<ModerationCauseOther>),
569    // BlockOther(Box<ModerationCauseOther>),
570    Label(Box<ModerationCauseLabel>),
571    Muted(Box<ModerationCauseOther>),
572    MuteWord(Box<ModerationCauseOther>),
573    Hidden(Box<ModerationCauseOther>),
574}
575
576impl ModerationCause {
577    pub fn priority(&self) -> u8 {
578        match self {
579            Self::Blocking(_) => *Priority::Priority3.as_ref(),
580            Self::BlockedBy(_) => *Priority::Priority4.as_ref(),
581            Self::Label(label) => *label.priority.as_ref(),
582            Self::Muted(_) => *Priority::Priority6.as_ref(),
583            Self::MuteWord(_) => *Priority::Priority6.as_ref(),
584            Self::Hidden(_) => *Priority::Priority6.as_ref(),
585        }
586    }
587    pub fn downgrade(&mut self) {
588        match self {
589            Self::Blocking(blocking) => blocking.downgraded = true,
590            Self::BlockedBy(blocked_by) => blocked_by.downgraded = true,
591            Self::Label(label) => label.downgraded = true,
592            Self::Muted(muted) => muted.downgraded = true,
593            Self::MuteWord(mute_word) => mute_word.downgraded = true,
594            Self::Hidden(hidden) => hidden.downgraded = true,
595        }
596    }
597}
598
599/// The source of a moderation cause.
600#[derive(Debug, Clone)]
601pub enum ModerationCauseSource {
602    User,
603    List(Box<ListViewBasic>),
604    Labeler(Did),
605}
606
607/// A label moderation cause.
608#[derive(Debug, Clone)]
609pub struct ModerationCauseLabel {
610    pub source: ModerationCauseSource,
611    pub label: Label,
612    pub label_def: InterpretedLabelValueDefinition,
613    pub target: LabelTarget,
614    pub setting: LabelPreference,
615    pub behavior: ModerationBehavior,
616    pub no_override: bool,
617    pub(crate) priority: Priority,
618    pub downgraded: bool,
619}
620
621/// An other moderation cause.
622#[derive(Debug, Clone)]
623pub struct ModerationCauseOther {
624    pub source: ModerationCauseSource,
625    pub downgraded: bool,
626}
627
628// moderation preferences
629
630/// The labeler preferences for moderation.
631#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
632pub struct ModerationPrefsLabeler {
633    pub did: Did,
634    pub labels: HashMap<String, LabelPreference>,
635    #[serde(skip_serializing, skip_deserializing)]
636    pub is_default_labeler: bool,
637}
638
639impl Default for ModerationPrefsLabeler {
640    fn default() -> Self {
641        Self {
642            did: BSKY_LABELER_DID.parse().expect("invalid did"),
643            labels: HashMap::default(),
644            is_default_labeler: true,
645        }
646    }
647}
648
649/// The moderation preferences.
650#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
651#[serde(rename_all = "camelCase")]
652pub struct ModerationPrefs {
653    pub adult_content_enabled: bool,
654    pub labels: HashMap<String, LabelPreference>,
655    #[serde(deserialize_with = "deserialize_labelers")]
656    pub labelers: Vec<ModerationPrefsLabeler>,
657    pub muted_words: Vec<MutedWord>,
658    pub hidden_posts: Vec<String>,
659}
660
661fn deserialize_labelers<'de, D>(deserializer: D) -> Result<Vec<ModerationPrefsLabeler>, D::Error>
662where
663    D: Deserializer<'de>,
664{
665    let mut labelers: Vec<ModerationPrefsLabeler> = Deserialize::deserialize(deserializer)?;
666    for labeler in labelers.iter_mut() {
667        if labeler.did.as_str() == BSKY_LABELER_DID {
668            labeler.is_default_labeler = true;
669        }
670    }
671    Ok(labelers)
672}
673
674impl Default for ModerationPrefs {
675    fn default() -> Self {
676        Self {
677            adult_content_enabled: false,
678            labels: HashMap::from_iter([
679                (String::from("porn"), LabelPreference::Hide),
680                (String::from("sexual"), LabelPreference::Warn),
681                (String::from("nudity"), LabelPreference::Ignore),
682                (String::from("graphic-media"), LabelPreference::Warn),
683            ]),
684            labelers: Vec::default(),
685            muted_words: Vec::default(),
686            hidden_posts: Vec::default(),
687        }
688    }
689}