Skip to main content

atrg_label/
types.rs

1//! Label types matching the `com.atproto.label.defs` schema.
2
3use serde::{Deserialize, Serialize};
4
5/// Configuration for the labeler service.
6#[derive(Debug, Clone, Deserialize)]
7pub struct LabelerConfig {
8    /// DID of the labeler service.
9    pub did: String,
10    /// Path to the signing key file (PEM format).
11    pub signing_key_path: Option<String>,
12    /// Inline signing key (base64-encoded, for env var injection).
13    pub signing_key_base64: Option<String>,
14}
15
16/// A label as defined by `com.atproto.label.defs#label`.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Label {
19    /// Version of the label format (currently 1).
20    #[serde(default = "default_version")]
21    pub ver: i32,
22    /// DID of the labeler that created this label.
23    pub src: String,
24    /// AT-URI of the subject being labeled.
25    pub uri: String,
26    /// CID of the subject (optional, for specific record versions).
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub cid: Option<String>,
29    /// The label value (e.g. "porn", "spam", "misleading").
30    pub val: String,
31    /// Whether this is a negation (removal) of a previous label.
32    #[serde(default)]
33    pub neg: bool,
34    /// Timestamp when the label was created (ISO 8601).
35    pub cts: String,
36    /// Expiration timestamp (optional).
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub exp: Option<String>,
39}
40
41fn default_version() -> i32 {
42    1
43}
44
45/// A label with its cryptographic signature.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SignedLabel {
48    /// The label data.
49    #[serde(flatten)]
50    pub label: Label,
51    /// Base64-encoded signature over the CBOR-serialized label.
52    pub sig: String,
53}
54
55/// Enumeration of well-known label values.
56///
57/// This is not exhaustive — labelers can define custom values.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum LabelValue {
60    /// Adult content — pornography.
61    Porn,
62    /// Adult content — sexual.
63    Sexual,
64    /// Adult content — nudity.
65    Nudity,
66    /// Graphic media.
67    GraphicMedia,
68    /// Spam content.
69    Spam,
70    /// Impersonation.
71    Impersonation,
72    /// Custom label value.
73    Custom(String),
74}
75
76impl LabelValue {
77    /// Convert to the string representation used in the protocol.
78    pub fn as_str(&self) -> &str {
79        match self {
80            Self::Porn => "porn",
81            Self::Sexual => "sexual",
82            Self::Nudity => "nudity",
83            Self::GraphicMedia => "graphic-media",
84            Self::Spam => "spam",
85            Self::Impersonation => "impersonation",
86            Self::Custom(s) => s.as_str(),
87        }
88    }
89}
90
91impl std::fmt::Display for LabelValue {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        f.write_str(self.as_str())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn label_value_as_str() {
103        assert_eq!(LabelValue::Porn.as_str(), "porn");
104        assert_eq!(LabelValue::Sexual.as_str(), "sexual");
105        assert_eq!(LabelValue::Nudity.as_str(), "nudity");
106        assert_eq!(LabelValue::GraphicMedia.as_str(), "graphic-media");
107        assert_eq!(LabelValue::Spam.as_str(), "spam");
108        assert_eq!(LabelValue::Impersonation.as_str(), "impersonation");
109        assert_eq!(
110            LabelValue::Custom("custom-val".into()).as_str(),
111            "custom-val"
112        );
113    }
114
115    #[test]
116    fn label_value_display() {
117        assert_eq!(format!("{}", LabelValue::Porn), "porn");
118        assert_eq!(format!("{}", LabelValue::Custom("test".into())), "test");
119    }
120
121    #[test]
122    fn label_serde_roundtrip() {
123        let label = Label {
124            ver: 1,
125            src: "did:plc:labeler".to_string(),
126            uri: "at://did:plc:user/app.bsky.feed.post/abc".to_string(),
127            cid: Some("bafyreib".to_string()),
128            val: "spam".to_string(),
129            neg: false,
130            cts: "2024-01-01T00:00:00Z".to_string(),
131            exp: None,
132        };
133
134        let json = serde_json::to_string(&label).unwrap();
135        let parsed: Label = serde_json::from_str(&json).unwrap();
136
137        assert_eq!(parsed.ver, 1);
138        assert_eq!(parsed.src, "did:plc:labeler");
139        assert_eq!(parsed.uri, "at://did:plc:user/app.bsky.feed.post/abc");
140        assert_eq!(parsed.cid.as_deref(), Some("bafyreib"));
141        assert_eq!(parsed.val, "spam");
142        assert!(!parsed.neg);
143        assert_eq!(parsed.cts, "2024-01-01T00:00:00Z");
144        assert!(parsed.exp.is_none());
145    }
146
147    #[test]
148    fn label_serde_optional_fields_omitted() {
149        let label = Label {
150            ver: 1,
151            src: "did:plc:labeler".to_string(),
152            uri: "at://did:plc:user/app.bsky.feed.post/abc".to_string(),
153            cid: None,
154            val: "spam".to_string(),
155            neg: false,
156            cts: "2024-01-01T00:00:00Z".to_string(),
157            exp: None,
158        };
159
160        let json = serde_json::to_string(&label).unwrap();
161        assert!(!json.contains("cid"));
162        assert!(!json.contains("exp"));
163    }
164
165    #[test]
166    fn signed_label_flattens() {
167        let signed = SignedLabel {
168            label: Label {
169                ver: 1,
170                src: "did:plc:labeler".to_string(),
171                uri: "at://did:plc:user/post/1".to_string(),
172                cid: None,
173                val: "porn".to_string(),
174                neg: false,
175                cts: "2024-01-01T00:00:00Z".to_string(),
176                exp: None,
177            },
178            sig: "c2lnbmF0dXJl".to_string(),
179        };
180
181        let json = serde_json::to_string(&signed).unwrap();
182        let v: serde_json::Value = serde_json::from_str(&json).unwrap();
183        assert_eq!(v["src"], "did:plc:labeler");
184        assert_eq!(v["sig"], "c2lnbmF0dXJl");
185    }
186}