Skip to main content

skyfeed/
models.rs

1use atrium_api::{
2    app::bsky::{
3        embed::record_with_media::MainMediaRefs,
4        feed::post::{RecordEmbedRefs, RecordLabelsRefs},
5    },
6    types::{BlobRef, Object, TypedBlobRef, Union},
7};
8use chrono::{DateTime, Utc};
9use log::trace;
10
11#[derive(Debug, Clone)]
12pub struct FeedRequest {
13    pub cursor: Option<String>,
14    pub feed: String,
15    pub limit: Option<u8>,
16}
17
18#[derive(Debug, Clone)]
19pub struct Post {
20    pub author_did: Did,
21    pub cid: Cid,
22    pub uri: Uri,
23    pub text: String,
24    pub labels: Vec<Label>,
25    pub langs: Vec<String>,
26    pub timestamp: DateTime<Utc>,
27    pub embed: Option<Embed>,
28}
29
30#[derive(Debug, Clone)]
31pub struct Cid(pub String);
32
33#[derive(Debug, Clone)]
34pub struct Did(pub String);
35
36#[derive(Debug, Clone)]
37pub enum Embed {
38    Images(Vec<ImageEmbed>),
39    Video(VideoEmbed),
40    External(ExternalEmbed),
41    Quote(QuoteEmbed),
42    QuoteWithMedia(QuoteEmbed, MediaEmbed),
43}
44
45#[derive(Debug, Clone)]
46pub enum MediaEmbed {
47    Images(Vec<ImageEmbed>),
48    Video(VideoEmbed),
49    External(ExternalEmbed),
50}
51
52#[derive(Debug, Clone)]
53pub struct ImageEmbed {
54    pub cid: Cid,
55    pub alt_text: String,
56    pub mime_type: String,
57}
58
59impl ImageEmbed {
60    fn from_atrium(value: Object<atrium_api::app::bsky::embed::images::ImageData>) -> Option<Self> {
61        let BlobRef::Typed(TypedBlobRef::Blob(blob)) = &value.image else {
62            return None;
63        };
64        Some(ImageEmbed {
65            cid: Cid(blob.r#ref.0.to_string()),
66            alt_text: value.alt.clone(),
67            mime_type: blob.mime_type.clone(),
68        })
69    }
70}
71
72#[derive(Debug, Clone)]
73pub struct VideoEmbed {
74    pub cid: Cid,
75    pub alt_text: String,
76}
77
78impl VideoEmbed {
79    fn from_atrium(video: Object<atrium_api::app::bsky::embed::video::MainData>) -> Option<Self> {
80        let BlobRef::Typed(TypedBlobRef::Blob(blob)) = &video.video else {
81            return None;
82        };
83        Some(VideoEmbed {
84            cid: Cid(blob.r#ref.0.to_string()),
85            alt_text: video.alt.clone().unwrap_or_default(),
86        })
87    }
88}
89
90#[derive(Debug, Clone)]
91pub struct ExternalEmbed {
92    pub title: String,
93    pub description: String,
94    pub uri: String,
95    pub thumbnail: Option<Cid>,
96}
97
98impl ExternalEmbed {
99    fn from_atrium(external: Object<atrium_api::app::bsky::embed::external::MainData>) -> Self {
100        ExternalEmbed {
101            title: external.external.title.clone(),
102            description: external.external.description.clone(),
103            uri: external.external.uri.clone(),
104            thumbnail: external.external.thumb.clone().and_then(|thumb| {
105                let BlobRef::Typed(TypedBlobRef::Blob(blob)) = &thumb else {
106                    return None;
107                };
108                Some(Cid(blob.r#ref.0.to_string()))
109            }),
110        }
111    }
112}
113
114#[derive(Debug, Clone)]
115pub struct QuoteEmbed {
116    pub cid: Cid,
117    pub uri: String,
118}
119
120impl Label {
121    pub(crate) fn from_atrium(value: &Union<RecordLabelsRefs>) -> Option<Vec<Label>> {
122        match value {
123            Union::Refs(refs) => match refs {
124                RecordLabelsRefs::ComAtprotoLabelDefsSelfLabels(object) => Some(
125                    object
126                        .values
127                        .clone()
128                        .into_iter()
129                        .map(|label| Label::from(label.val.clone()))
130                        .collect::<Vec<Label>>(),
131                ),
132            },
133            Union::Unknown(_) => None,
134        }
135    }
136}
137
138impl Embed {
139    pub(crate) fn from_atrium(value: &Union<RecordEmbedRefs>) -> Option<Self> {
140        match value {
141            Union::Refs(e) => match e {
142                RecordEmbedRefs::AppBskyEmbedImagesMain(object) => Some(Embed::Images(
143                    object
144                        .images
145                        .clone()
146                        .into_iter()
147                        .filter_map(ImageEmbed::from_atrium)
148                        .collect(),
149                )),
150                RecordEmbedRefs::AppBskyEmbedVideoMain(video) => {
151                    VideoEmbed::from_atrium(*video.clone()).map(Embed::Video)
152                }
153                RecordEmbedRefs::AppBskyEmbedExternalMain(external) => Some(Embed::External(
154                    ExternalEmbed::from_atrium(*external.clone()),
155                )),
156                RecordEmbedRefs::AppBskyEmbedRecordMain(quote) => {
157                    let Ok(cid) = serde_json::to_string(&quote.data.record.cid) else {
158                        trace!("Cid serialization failed");
159                        return None;
160                    };
161                    Some(Embed::Quote(QuoteEmbed {
162                        cid: Cid(cid),
163                        uri: quote.data.record.uri.clone(),
164                    }))
165                }
166                RecordEmbedRefs::AppBskyEmbedRecordWithMediaMain(quote_with_media) => {
167                    let Union::Refs(media) = &quote_with_media.media else {
168                        return None;
169                    };
170                    let media = match media {
171                        MainMediaRefs::AppBskyEmbedImagesMain(object) => MediaEmbed::Images(
172                            object
173                                .images
174                                .clone()
175                                .into_iter()
176                                .filter_map(ImageEmbed::from_atrium)
177                                .collect(),
178                        ),
179                        MainMediaRefs::AppBskyEmbedVideoMain(object) => {
180                            MediaEmbed::Video(VideoEmbed::from_atrium(*object.clone())?)
181                        }
182                        MainMediaRefs::AppBskyEmbedExternalMain(object) => {
183                            MediaEmbed::External(ExternalEmbed::from_atrium(*object.clone()))
184                        }
185                    };
186                    let Ok(cid) = serde_json::to_string(&quote_with_media.record.record.cid) else {
187                        trace!("Cid serialization failed");
188                        return None;
189                    };
190                    Some(Embed::QuoteWithMedia(
191                        QuoteEmbed {
192                            cid: Cid(cid),
193                            uri: quote_with_media.record.record.uri.clone(),
194                        },
195                        media,
196                    ))
197                }
198            },
199            Union::Unknown(_) => None,
200        }
201    }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum Label {
206    Hide,
207    Warn,
208    NoUnauthenticated,
209    Porn,
210    Sexual,
211    GraphicMedia,
212    Nudity,
213    Other(String),
214}
215
216impl From<String> for Label {
217    fn from(value: String) -> Self {
218        match value.as_str() {
219            "!hide" => Label::Hide,
220            "!warn" => Label::Warn,
221            "!no-unauthenticated" => Label::NoUnauthenticated,
222            "porn" => Label::Porn,
223            "sexual" => Label::Sexual,
224            "graphic-media" => Label::GraphicMedia,
225            "nudity" => Label::Nudity,
226            other => Label::Other(other.to_string()),
227        }
228    }
229}
230
231#[derive(Debug, Clone, Hash, PartialEq, Eq)]
232pub struct Uri(pub String);
233
234#[derive(Debug, Clone)]
235pub struct FeedResult {
236    pub cursor: Option<String>,
237    pub feed: Vec<Uri>,
238}