1use crate::agent::{
2 account_export, account_import, activate_account, create_account, deactivate_account,
3 export_preferences, get_blob, get_service_auth, import_preferences, login_helper,
4 missing_blobs, recommended_plc, request_token, sign_plc, submit_plc, upload_blob,
5};
6use crate::errors::PdsError;
7use bsky_sdk::api::agent::Configure;
8use bsky_sdk::api::types::string::Did;
9use bsky_sdk::BskyAgent;
10use ipld_core::cid::Cid;
11use serde::{Deserialize, Serialize};
12use std::io::ErrorKind;
13
14pub mod agent;
15pub mod errors;
16
17#[derive(Debug, Deserialize, Serialize)]
18pub struct ServiceAuthRequest {
19 pub pds_host: String,
20 pub aud: String,
21 pub did: String,
22 pub token: String,
23}
24
25#[derive(Debug, Deserialize, Serialize)]
26pub struct ServiceAuthResponse {
27 pub token: String,
28}
29
30pub async fn get_service_auth_api(req: ServiceAuthRequest) -> Result<String, PdsError> {
31 let agent = BskyAgent::builder().build().await.unwrap();
32 login_helper(
33 &agent,
34 req.pds_host.as_str(),
35 req.did.as_str(),
36 req.token.as_str(),
37 )
38 .await?;
39 let token = get_service_auth(&agent, req.aud.as_str()).await?;
40 Ok(token)
41}
42
43#[derive(Debug, Deserialize, Serialize)]
44pub struct CreateAccountApiRequest {
45 pub email: String,
46 pub handle: String,
47 pub invite_code: String,
48 pub password: String,
49 pub token: String,
50 pub pds_host: String,
51 pub did: String,
52 #[serde(skip_serializing_if = "core::option::Option::is_none")]
53 pub recovery_key: Option<String>,
54}
55
56#[derive(Debug, Deserialize, Serialize)]
57pub struct CreateAccountRequest {
58 pub did: Did,
59 pub email: Option<String>,
60 pub handle: String,
61 pub invite_code: Option<String>,
62 pub password: Option<String>,
63 pub recovery_key: Option<String>,
64 pub verification_code: Option<String>,
65 pub verification_phone: Option<String>,
66 pub plc_op: Option<String>,
67 pub token: String,
68}
69
70#[tracing::instrument(skip(req))]
71pub async fn create_account_api(req: CreateAccountApiRequest) -> Result<(), PdsError> {
72 create_account(
73 req.pds_host.as_str(),
74 &CreateAccountRequest {
75 did: req.did.parse().unwrap(),
76 email: Some(req.email.clone()),
77 handle: req.handle.parse().unwrap(),
78 invite_code: Some(req.invite_code.clone()),
79 password: Some(req.password.clone()),
80 recovery_key: req.recovery_key.clone(),
81 verification_code: Some(String::from("")),
82 verification_phone: None,
83 plc_op: None,
84 token: req.token.clone(),
85 },
86 )
87 .await?;
88 Ok(())
89}
90
91#[derive(Debug, Deserialize, Serialize)]
92pub struct ExportPDSRequest {
93 pub pds_host: String,
94 pub did: String,
95 pub token: String,
96}
97
98#[tracing::instrument]
99pub async fn export_pds_api(req: ExportPDSRequest) -> Result<(), PdsError> {
100 let agent = BskyAgent::builder().build().await.unwrap();
101 let session = login_helper(
102 &agent,
103 req.pds_host.as_str(),
104 req.did.as_str(),
105 req.token.as_str(),
106 )
107 .await?;
108 account_export(&agent, &session.did).await?;
109 Ok(())
110}
111
112#[derive(Debug, Deserialize, Serialize)]
113pub struct ImportPDSRequest {
114 pub pds_host: String,
115 pub did: String,
116 pub token: String,
117}
118
119#[tracing::instrument]
120pub async fn import_pds_api(req: ImportPDSRequest) -> Result<(), PdsError> {
121 let agent = BskyAgent::builder().build().await.unwrap();
122 let session = login_helper(
123 &agent,
124 req.pds_host.as_str(),
125 req.did.as_str(),
126 req.token.as_str(),
127 )
128 .await?;
129 account_import(&agent, (session.did.as_str().to_string() + ".car").as_str()).await?;
130 Ok(())
131}
132
133#[derive(Debug, Deserialize, Serialize)]
134pub struct MissingBlobsRequest {
135 pub pds_host: String,
136 pub did: String,
137 pub token: String,
138}
139
140#[tracing::instrument]
141pub async fn missing_blobs_api(req: MissingBlobsRequest) -> Result<String, PdsError> {
142 let agent = BskyAgent::builder().build().await.unwrap();
143 login_helper(
144 &agent,
145 req.pds_host.as_str(),
146 req.did.as_str(),
147 req.token.as_str(),
148 )
149 .await?;
150 let initial_missing_blobs = missing_blobs(&agent).await?;
151 let mut missing_blob_cids = Vec::new();
152 for blob in &initial_missing_blobs {
153 missing_blob_cids.push(Cid::to_string(blob.cid.as_ref()));
154 }
155
156 let response = serde_json::to_string(&missing_blob_cids).unwrap();
157 Ok(response)
158}
159
160#[derive(Debug, Deserialize, Serialize)]
161pub struct ExportBlobsRequest {
162 pub destination: String,
163 pub origin: String,
164 pub did: String,
165 pub origin_token: String,
166 pub destination_token: String,
167}
168
169#[tracing::instrument]
170pub async fn export_blobs_api(req: ExportBlobsRequest) -> Result<(), PdsError> {
171 let agent = BskyAgent::builder().build().await.map_err(|error| {
172 tracing::error!("{}", error.to_string());
173 PdsError::Runtime
174 })?;
175 login_helper(
176 &agent,
177 req.destination.as_str(),
178 req.did.as_str(),
179 req.destination_token.as_str(),
180 )
181 .await?;
182 let missing_blobs = missing_blobs(&agent).await?;
183 let session = login_helper(
184 &agent,
185 req.origin.as_str(),
186 req.did.as_str(),
187 req.origin_token.as_str(),
188 )
189 .await?;
190 for missing_blob in &missing_blobs {
191 match tokio::fs::create_dir(session.did.as_str()).await {
192 Ok(_) => {}
193 Err(e) => {
194 if e.kind() != ErrorKind::AlreadyExists {
195 tracing::error!("Error creating directory: {:?}", e);
196 return Err(PdsError::Validation);
197 }
198 }
199 }
200 match get_blob(&agent, missing_blob.cid.clone(), session.did.clone()).await {
201 Ok(output) => {
202 tracing::info!("Successfully fetched missing blob");
203 tokio::fs::write(
204 String::from(session.did.as_str())
205 + "/"
206 + missing_blob.record_uri.as_str().split("/").last().unwrap(),
207 output,
208 )
209 .await
210 .map_err(|error| {
211 tracing::error!("{}", error.to_string());
212 PdsError::Runtime
213 })?;
214 }
215 Err(_) => {
216 tracing::error!("Failed to determine missing blobs");
217 return Err(PdsError::Validation);
218 }
219 }
220 }
221 Ok(())
222}
223
224#[derive(Debug, Deserialize, Serialize)]
225pub struct UploadBlobsRequest {
226 pub pds_host: String,
227 pub did: String,
228 pub token: String,
229}
230
231#[tracing::instrument]
232pub async fn upload_blobs_api(req: UploadBlobsRequest) -> Result<(), PdsError> {
233 let agent = BskyAgent::builder().build().await.unwrap();
234 agent.configure_endpoint(req.pds_host.clone());
235 let session = login_helper(
236 &agent,
237 req.pds_host.as_str(),
238 req.did.as_str(),
239 req.token.as_str(),
240 )
241 .await?;
242
243 let mut blob_dir;
244 match tokio::fs::read_dir(session.did.as_str()).await {
245 Ok(output) => blob_dir = output,
246 Err(_) => return Err(PdsError::Validation),
247 }
248 while let Some(blob) = blob_dir.next_entry().await.unwrap() {
249 let file = tokio::fs::read(blob.path()).await.unwrap();
250 upload_blob(&agent, file).await?;
251 }
252
253 Ok(())
254}
255
256#[derive(Debug, Deserialize, Serialize)]
257pub struct ActivateAccountRequest {
258 pub pds_host: String,
259 pub did: String,
260 pub token: String,
261}
262
263#[tracing::instrument]
264pub async fn activate_account_api(req: ActivateAccountRequest) -> Result<(), PdsError> {
265 let agent = BskyAgent::builder().build().await.unwrap();
266 login_helper(
267 &agent,
268 req.pds_host.as_str(),
269 req.did.as_str(),
270 req.token.as_str(),
271 )
272 .await?;
273 activate_account(&agent).await?;
274 Ok(())
275}
276
277#[derive(Debug, Deserialize, Serialize)]
278pub struct DeactivateAccountRequest {
279 pub pds_host: String,
280 pub did: String,
281 pub token: String,
282}
283
284#[derive(Debug, Deserialize, Serialize)]
285pub struct DeactivateAccountResponse {}
286
287#[tracing::instrument]
288pub async fn deactivate_account_api(req: DeactivateAccountRequest) -> Result<(), PdsError> {
289 let agent = BskyAgent::builder().build().await.unwrap();
290 login_helper(
291 &agent,
292 req.pds_host.as_str(),
293 req.did.as_str(),
294 req.token.as_str(),
295 )
296 .await?;
297 deactivate_account(&agent).await?;
298 Ok(())
299}
300
301#[derive(Debug, Deserialize, Serialize)]
302pub struct MigratePreferencesRequest {
303 pub destination: String,
304 pub destination_token: String,
305 pub origin: String,
306 pub did: String,
307 pub origin_token: String,
308}
309
310#[tracing::instrument]
311pub async fn migrate_preferences_api(req: MigratePreferencesRequest) -> Result<(), PdsError> {
312 let agent = BskyAgent::builder().build().await.unwrap();
313 login_helper(
314 &agent,
315 req.origin.as_str(),
316 req.did.as_str(),
317 req.origin_token.as_str(),
318 )
319 .await?;
320 let preferences = export_preferences(&agent).await?;
321 login_helper(
322 &agent,
323 req.destination.as_str(),
324 req.did.as_str(),
325 req.destination_token.as_str(),
326 )
327 .await?;
328 import_preferences(&agent, preferences).await?;
329 Ok(())
330}
331
332#[derive(Debug, Deserialize, Serialize)]
333pub struct RequestTokenRequest {
334 pub pds_host: String,
335 pub did: String,
336 pub token: String,
337}
338
339#[tracing::instrument]
340pub async fn request_token_api(req: RequestTokenRequest) -> Result<(), PdsError> {
341 let agent = BskyAgent::builder().build().await.unwrap();
342 login_helper(
343 &agent,
344 req.pds_host.as_str(),
345 req.did.as_str(),
346 req.token.as_str(),
347 )
348 .await?;
349 request_token(&agent).await?;
350 Ok(())
351}
352
353#[derive(Debug, Deserialize, Serialize)]
354pub struct MigratePlcRequest {
355 pub destination: String,
356 pub destination_token: String,
357 pub origin: String,
358 pub did: String,
359 pub origin_token: String,
360 pub plc_signing_token: String,
361 #[serde(skip_serializing_if = "core::option::Option::is_none")]
362 pub user_recovery_key: Option<String>,
363}
364
365#[tracing::instrument(skip(req))]
366pub async fn migrate_plc_api(req: MigratePlcRequest) -> Result<(), PdsError> {
367 let agent = BskyAgent::builder().build().await.unwrap();
368 login_helper(
369 &agent,
370 req.destination.as_str(),
371 req.did.as_str(),
372 req.destination_token.as_str(),
373 )
374 .await?;
375 let recommended_did = recommended_plc(&agent).await?;
376 use bsky_sdk::api::com::atproto::identity::sign_plc_operation::InputData;
377
378 let mut rotation_keys = recommended_did.rotation_keys.unwrap();
379
380 if let Some(recovery_key) = &req.user_recovery_key {
381 rotation_keys.insert(0, recovery_key.clone());
382 }
383
384 let new_plc = InputData {
385 also_known_as: recommended_did.also_known_as,
386 rotation_keys: Some(rotation_keys),
387 services: recommended_did.services,
388 token: Some(req.plc_signing_token.clone()),
389 verification_methods: recommended_did.verification_methods,
390 };
391 login_helper(
392 &agent,
393 req.origin.as_str(),
394 req.did.as_str(),
395 req.origin_token.as_str(),
396 )
397 .await?;
398 let output = sign_plc(&agent, new_plc.clone()).await?;
399 login_helper(
400 &agent,
401 req.destination.as_str(),
402 req.did.as_str(),
403 req.destination_token.as_str(),
404 )
405 .await?;
406 submit_plc(&agent, output).await?;
407 Ok(())
408}