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("e.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) = "e_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("e_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}