1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum LabelPreference {
9 Ignore,
10 Warn,
11 Hide,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum UiContext {
17 ProfileList,
18 ProfileView,
19 ContentList,
20 ContentView,
21 ContentMedia,
22 Avatar,
23 Banner,
24 DisplayName,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum LabelBlurs {
31 Content,
32 Media,
33 None,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum LabelSeverity {
40 Alert,
41 Inform,
42 None,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum LabelFlag {
48 NoOverride,
49 Adult,
50 Unauthed,
51 NoSelf,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum BehaviorValue {
57 Blur,
58 Alert,
59 Inform,
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct ModerationBehavior {
65 pub profile_list: Option<BehaviorValue>,
66 pub profile_view: Option<BehaviorValue>,
67 pub content_list: Option<BehaviorValue>,
68 pub content_view: Option<BehaviorValue>,
69 pub content_media: Option<BehaviorValue>,
70 pub avatar: Option<BehaviorValue>,
71 pub banner: Option<BehaviorValue>,
72 pub display_name: Option<BehaviorValue>,
73}
74
75impl ModerationBehavior {
76 #[must_use]
77 pub const fn get(&self, ctx: UiContext) -> Option<BehaviorValue> {
78 match ctx {
79 UiContext::ProfileList => self.profile_list,
80 UiContext::ProfileView => self.profile_view,
81 UiContext::ContentList => self.content_list,
82 UiContext::ContentView => self.content_view,
83 UiContext::ContentMedia => self.content_media,
84 UiContext::Avatar => self.avatar,
85 UiContext::Banner => self.banner,
86 UiContext::DisplayName => self.display_name,
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct LabelValueDefinition {
94 pub identifier: String,
95 pub configurable: bool,
96 pub default_setting: LabelPreference,
97 pub flags: Vec<LabelFlag>,
98 pub severity: LabelSeverity,
99 pub blurs: LabelBlurs,
100 pub behaviors: LabelBehaviors,
101}
102
103#[derive(Debug, Clone)]
105pub struct LabelBehaviors {
106 pub account: ModerationBehavior,
107 pub profile: ModerationBehavior,
108 pub content: ModerationBehavior,
109}
110
111#[derive(Debug, Clone)]
113pub enum ModerationCauseSource {
114 User,
115 List { uri: String, name: String },
116 Labeler { did: String },
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum LabelTarget {
122 Account,
123 Profile,
124 Content,
125}
126
127#[derive(Debug, Clone)]
129pub enum ModerationCause {
130 Blocking {
131 source: ModerationCauseSource,
132 priority: u8,
133 downgraded: bool,
134 },
135 BlockedBy {
136 source: ModerationCauseSource,
137 priority: u8,
138 downgraded: bool,
139 },
140 BlockOther {
141 source: ModerationCauseSource,
142 priority: u8,
143 downgraded: bool,
144 },
145 Label {
146 source: ModerationCauseSource,
147 label: LabelData,
148 label_def: LabelValueDefinition,
149 target: LabelTarget,
150 setting: LabelPreference,
151 no_override: bool,
152 priority: u8,
153 downgraded: bool,
154 },
155 Muted {
156 source: ModerationCauseSource,
157 priority: u8,
158 downgraded: bool,
159 },
160 MuteWord {
161 source: ModerationCauseSource,
162 priority: u8,
163 downgraded: bool,
164 },
165 Hidden {
166 source: ModerationCauseSource,
167 priority: u8,
168 downgraded: bool,
169 },
170}
171
172impl ModerationCause {
173 #[must_use]
174 pub const fn priority(&self) -> u8 {
175 match self {
176 Self::Blocking { priority, .. }
177 | Self::BlockedBy { priority, .. }
178 | Self::BlockOther { priority, .. }
179 | Self::Label { priority, .. }
180 | Self::Muted { priority, .. }
181 | Self::MuteWord { priority, .. }
182 | Self::Hidden { priority, .. } => *priority,
183 }
184 }
185
186 #[must_use]
187 pub const fn is_downgraded(&self) -> bool {
188 match self {
189 Self::Blocking { downgraded, .. }
190 | Self::BlockedBy { downgraded, .. }
191 | Self::BlockOther { downgraded, .. }
192 | Self::Label { downgraded, .. }
193 | Self::Muted { downgraded, .. }
194 | Self::MuteWord { downgraded, .. }
195 | Self::Hidden { downgraded, .. } => *downgraded,
196 }
197 }
198
199 pub const fn set_downgraded(&mut self) {
200 match self {
201 Self::Blocking { downgraded, .. }
202 | Self::BlockedBy { downgraded, .. }
203 | Self::BlockOther { downgraded, .. }
204 | Self::Label { downgraded, .. }
205 | Self::Muted { downgraded, .. }
206 | Self::MuteWord { downgraded, .. }
207 | Self::Hidden { downgraded, .. } => *downgraded = true,
208 }
209 }
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct LabelData {
215 pub src: String,
216 pub uri: String,
217 pub val: String,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub neg: Option<bool>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct MutedWord {
226 pub value: String,
227 pub targets: Vec<String>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub actor_target: Option<String>,
230 #[serde(skip_serializing_if = "Option::is_none")]
231 pub expires_at: Option<String>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct ModerationPrefsLabeler {
237 pub did: String,
238 pub labels: std::collections::HashMap<String, LabelPreference>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(rename_all = "camelCase")]
244pub struct ModerationPrefs {
245 pub adult_content_enabled: bool,
246 pub labels: std::collections::HashMap<String, LabelPreference>,
247 pub labelers: Vec<ModerationPrefsLabeler>,
248 pub muted_words: Vec<MutedWord>,
249 pub hidden_posts: Vec<String>,
250}
251
252#[derive(Debug, Clone)]
254pub struct ModerationOpts {
255 pub user_did: Option<String>,
256 pub prefs: ModerationPrefs,
257 pub label_defs: std::collections::HashMap<String, Vec<LabelValueDefinition>>,
258}
259
260#[derive(Debug, Clone, Default)]
262pub struct ModerationUi {
263 pub no_override: bool,
264 pub filters: Vec<ModerationCause>,
265 pub blurs: Vec<ModerationCause>,
266 pub alerts: Vec<ModerationCause>,
267 pub informs: Vec<ModerationCause>,
268}
269
270impl ModerationUi {
271 #[must_use]
272 pub fn filter(&self) -> bool {
273 !self.filters.is_empty()
274 }
275 #[must_use]
276 pub fn blur(&self) -> bool {
277 !self.blurs.is_empty()
278 }
279 #[must_use]
280 pub fn alert(&self) -> bool {
281 !self.alerts.is_empty()
282 }
283 #[must_use]
284 pub fn inform(&self) -> bool {
285 !self.informs.is_empty()
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn label_preference_serde() {
295 let pref = LabelPreference::Hide;
296 let json = serde_json::to_string(&pref).unwrap();
297 assert_eq!(json, "\"hide\"");
298 let parsed: LabelPreference = serde_json::from_str(&json).unwrap();
299 assert_eq!(parsed, LabelPreference::Hide);
300 }
301
302 #[test]
303 fn moderation_ui_helpers() {
304 let mut ui = ModerationUi::default();
305 assert!(!ui.filter());
306 assert!(!ui.blur());
307 assert!(!ui.alert());
308 assert!(!ui.inform());
309
310 ui.filters.push(ModerationCause::Hidden {
311 source: ModerationCauseSource::User,
312 priority: 6,
313 downgraded: false,
314 });
315 assert!(ui.filter());
316 }
317
318 #[test]
319 fn cause_priority_and_downgrade() {
320 let mut cause = ModerationCause::Muted {
321 source: ModerationCauseSource::User,
322 priority: 6,
323 downgraded: false,
324 };
325 assert_eq!(cause.priority(), 6);
326 assert!(!cause.is_downgraded());
327 cause.set_downgraded();
328 assert!(cause.is_downgraded());
329 }
330
331 #[test]
332 fn muted_word_serde() {
333 let word = MutedWord {
334 value: "test".into(),
335 targets: vec!["content".into()],
336 actor_target: Some("exclude-following".into()),
337 expires_at: None,
338 };
339 let json = serde_json::to_string(&word).unwrap();
340 assert!(json.contains("\"actorTarget\""));
341 let parsed: MutedWord = serde_json::from_str(&json).unwrap();
342 assert_eq!(parsed.value, "test");
343 }
344
345 #[test]
346 fn behavior_get_context() {
347 let behavior = ModerationBehavior {
348 content_list: Some(BehaviorValue::Blur),
349 content_view: Some(BehaviorValue::Alert),
350 ..Default::default()
351 };
352 assert_eq!(
353 behavior.get(UiContext::ContentList),
354 Some(BehaviorValue::Blur)
355 );
356 assert_eq!(
357 behavior.get(UiContext::ContentView),
358 Some(BehaviorValue::Alert)
359 );
360 assert_eq!(behavior.get(UiContext::Avatar), None);
361 }
362}