pdsmigration_common/
lib.rs

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}