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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub(crate) enum BehaviorValue {
17 Blur,
18 Alert,
19 Inform,
20}
21
22#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334pub enum LabelTarget {
335 Account,
336 Profile,
337 Content,
338}
339
340#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct InterpretedLabelValueDefinition {
450 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 #[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#[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#[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
535pub type SubjectPost = atrium_api::app::bsky::feed::defs::PostView;
537
538pub type SubjectNotification =
540 atrium_api::app::bsky::notification::list_notifications::Notification;
541
542pub type SubjectFeedGenerator = atrium_api::app::bsky::feed::defs::GeneratorView;
544
545#[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#[derive(Debug, Clone)]
566pub enum ModerationCause {
567 Blocking(Box<ModerationCauseOther>),
568 BlockedBy(Box<ModerationCauseOther>),
569 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#[derive(Debug, Clone)]
601pub enum ModerationCauseSource {
602 User,
603 List(Box<ListViewBasic>),
604 Labeler(Did),
605}
606
607#[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#[derive(Debug, Clone)]
623pub struct ModerationCauseOther {
624 pub source: ModerationCauseSource,
625 pub downgraded: bool,
626}
627
628#[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#[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}