CLI tool for migrating PDS
1use atrium_api::{ 2 agent::atp_agent::{store::MemorySessionStore, AtpAgent}, 3 app::bsky::actor::{get_preferences, put_preferences}, 4 com::atproto::{ 5 repo::list_missing_blobs, 6 server::{create_account, get_service_auth}, 7 sync::{get_blob, get_repo, list_blobs}, 8 }, 9 types::string::{Handle, Nsid}, 10}; 11use atrium_xrpc_client::reqwest::ReqwestClient; 12use std::{ 13 io::{self, Write}, sync::Arc 14}; 15 16mod jwt; 17 18fn readln(message: Option<impl Into<String>>) -> std::io::Result<Arc<str>> { 19 if let Some(message) = message { 20 print!("{}", message.into()); 21 io::stdout().flush()?; 22 } 23 let mut buffer = String::new(); 24 io::stdin().read_line(&mut buffer)?; 25 Ok(buffer.trim().into()) 26} 27 28#[tokio::main] 29async fn main() { 30 println!("Please log in to your current PDS. Authenticated access is needed throughout the migration process"); 31 let old_pds_url = match readln(Some("The URL of your current PDS: ")) { 32 Ok(string) => string, 33 Err(err) => { 34 println!("Could not read the URL of your current PDS due to error: {err}"); 35 return; 36 } 37 }; 38 let identity = match readln(Some("Identifier (handle, did or email): ")) { 39 Ok(string) => string.trim().to_string(), 40 Err(err) => { 41 println!("Could not read username due to error: {err}"); 42 return; 43 } 44 }; 45 let password = match readln(Some("Password: ")) { 46 Ok(string) => string.trim().to_string(), 47 Err(err) => { 48 println!("Could not read password due to error: {err}"); 49 return; 50 } 51 }; 52 println!("Authenticating with your PDS"); 53 let old_agent = AtpAgent::new( 54 ReqwestClient::new(&old_pds_url), 55 MemorySessionStore::default(), 56 ); 57 if let Err(err) = old_agent.login(identity, password).await { 58 println!("Failed to log in to your account on your current PDS due to error: {err}"); 59 return; 60 }; 61 println!("Log in successful!"); 62 println!(); 63 64 // Create new account 65 let new_pds_url = match readln(Some( 66 "Please type in the URL of the PDS you want to migrate to: ", 67 )) { 68 Ok(string) => string, 69 Err(err) => { 70 println!("Could not read the URL of your new PDS due to error: {err}"); 71 return; 72 } 73 }; 74 println!("Creating an account on your new PDS ..."); 75 let new_agent = AtpAgent::new( 76 ReqwestClient::new(&new_pds_url), 77 MemorySessionStore::default(), 78 ); 79 println!("Now the details you want for your new account"); 80 let email = match readln(Some("Email address: ")) { 81 Ok(string) => string, 82 Err(err) => { 83 println!("Could not read your email due to error: {err}"); 84 return; 85 } 86 }; 87 let handle = match Handle::new( 88 match readln(Some("Handle: ")) { 89 Ok(string) => string, 90 Err(err) => { 91 println!("Could not read your handle due to error: {err}"); 92 return; 93 } 94 } 95 .to_string(), 96 ) { 97 Ok(handle) => handle, 98 Err(err) => { 99 println!("Handle wasn't accepted because: {err}"); 100 return; 101 } 102 }; 103 let password = match readln(Some( 104 "Please type in the password you want to use on your new PDS", 105 )) { 106 Ok(string) => string, 107 Err(err) => { 108 println!("Could not read your password due to error: {err}"); 109 return; 110 } 111 }; 112 let invite_code = match readln(Some( 113 "Invite code (leave empty if your new PDS doesn't require one): ", 114 )) { 115 Ok(string) => { 116 if string.is_empty() { 117 None 118 } else { 119 Some(string.to_string()) 120 } 121 } 122 Err(err) => { 123 println!("Could not read your invite code due to error: {err}"); 124 return; 125 } 126 }; 127 128 let password = password.clone(); 129 let describe_res = match new_agent.api.com.atproto.server.describe_server().await { 130 Ok(response) => response, 131 Err(err) => { 132 println!("com.atproto.server.describeServer at new PDS failed due to error: {err}"); 133 return; 134 } 135 }; 136 let new_pds_did = &describe_res.did; 137 let service_jwt_res = match old_agent 138 .api 139 .com 140 .atproto 141 .server 142 .get_service_auth( 143 get_service_auth::ParametersData { 144 aud: new_pds_did.clone(), 145 lxm: Some(Nsid::new(create_account::NSID.to_string()).unwrap()), 146 exp: None, 147 } 148 .into(), 149 ) 150 .await 151 { 152 Ok(response) => response, 153 Err(err) => { 154 println!("com.atproto.server.getServiceAuth at current PDS failed due to error: {err}"); 155 return; 156 } 157 }; 158 159 let new_agent = AtpAgent::new( 160 jwt::JwtAuthedClient::new(&new_pds_url, service_jwt_res.token.clone()), 161 MemorySessionStore::default(), 162 ); 163 match new_agent 164 .api 165 .com 166 .atproto 167 .server 168 .create_account( 169 create_account::InputData { 170 did: old_agent.did().await, 171 email: Some(email.to_string()), 172 handle, 173 invite_code, 174 password: Some(password.to_string()), 175 plc_op: None, 176 recovery_key: None, 177 verification_code: None, 178 verification_phone: None, 179 } 180 .into(), 181 ) 182 .await 183 { 184 Ok(_) => (), 185 Err(err) => { 186 println!("com.atproto.server.createAccount at new PDS failed due to error: {err}"); 187 return; 188 } 189 } 190 println!("Successfully created account on your new PDS!"); 191 println!(); 192 193 // Migrate data 194 println!("Migrating your data"); 195 196 let car = match old_agent 197 .api 198 .com 199 .atproto 200 .sync 201 .get_repo( 202 get_repo::ParametersData { 203 did: old_agent.did().await.unwrap(), 204 since: None, 205 } 206 .into(), 207 ) 208 .await 209 { 210 Ok(response) => response, 211 Err(err) => { 212 println!("com.atproto.sync.getRepo at current PDS failed due to error: {err}"); 213 return; 214 } 215 }; 216 217 match new_agent.api.com.atproto.repo.import_repo(car).await { 218 Ok(_) => (), 219 Err(err) => { 220 println!("com.atproto.repo.importRepo at new PDS failed due to error: {err}"); 221 return; 222 } 223 } 224 println!("Repository successfully migrated"); 225 226 let mut listed_blobs = match old_agent 227 .api 228 .com 229 .atproto 230 .sync 231 .list_blobs( 232 list_blobs::ParametersData { 233 cursor: None, 234 did: old_agent.did().await.unwrap(), 235 limit: None, 236 since: None, 237 } 238 .into(), 239 ) 240 .await 241 { 242 Ok(response) => response, 243 Err(err) => { 244 println!("com.atproto.sync.listBlobs at old PDS failed due to error: {err}"); 245 return; 246 } 247 }; 248 249 for cid in listed_blobs.cids.iter() { 250 let blob = match old_agent 251 .api 252 .com 253 .atproto 254 .sync 255 .get_blob( 256 get_blob::ParametersData { 257 cid: cid.to_owned(), 258 did: old_agent.did().await.unwrap(), 259 } 260 .into(), 261 ) 262 .await 263 { 264 Ok(response) => response, 265 Err(err) => { 266 println!("com.atproto.sync.getBlob at current PDS failed due to error: {err}"); 267 return; 268 } 269 }; 270 271 match new_agent.api.com.atproto.repo.upload_blob(blob).await { 272 Ok(_) => (), 273 Err(err) => { 274 println!("com.atproto.repo.uploadBlob at new PDS failed due to error: {err}"); 275 return; 276 } 277 }; 278 } 279 280 let mut cursor = listed_blobs.cursor.clone(); 281 while cursor.is_some() { 282 listed_blobs = match old_agent 283 .api 284 .com 285 .atproto 286 .sync 287 .list_blobs( 288 list_blobs::ParametersData { 289 cursor: cursor.clone(), 290 did: old_agent.did().await.unwrap(), 291 limit: None, 292 since: None, 293 } 294 .into(), 295 ) 296 .await 297 { 298 Ok(response) => response, 299 Err(err) => { 300 println!("com.atproto.sync.listBlobs at old PDS failed due to error: {err}"); 301 return; 302 } 303 }; 304 305 for cid in listed_blobs.cids.iter() { 306 let blob = match old_agent 307 .api 308 .com 309 .atproto 310 .sync 311 .get_blob( 312 get_blob::ParametersData { 313 cid: cid.to_owned(), 314 did: old_agent.did().await.unwrap(), 315 } 316 .into(), 317 ) 318 .await 319 { 320 Ok(response) => response, 321 Err(err) => { 322 println!("com.atproto.sync.getBlob at current PDS failed due to error: {err}"); 323 return; 324 } 325 }; 326 327 match new_agent.api.com.atproto.repo.upload_blob(blob).await { 328 Ok(_) => (), 329 Err(err) => { 330 println!("com.atproto.repo.uploadBlob at new PDS failed due to error: {err}"); 331 return; 332 } 333 }; 334 } 335 cursor = listed_blobs.cursor.clone(); 336 } 337 println!("Blobs successfully migrated!"); 338 339 let prefs = match old_agent 340 .api 341 .app 342 .bsky 343 .actor 344 .get_preferences(get_preferences::ParametersData {}.into()) 345 .await 346 { 347 Ok(response) => response, 348 Err(err) => { 349 println!("app.bsky.actor.getPreferences at current PDS failed due to error: {err}"); 350 return; 351 } 352 }; 353 354 match new_agent 355 .api 356 .app 357 .bsky 358 .actor 359 .put_preferences( 360 put_preferences::InputData { 361 preferences: prefs.preferences.clone(), 362 } 363 .into(), 364 ) 365 .await 366 { 367 Ok(_) => (), 368 Err(err) => { 369 println!("app.bsky.actor.putPreferences at new PDS failed due to error: {err}"); 370 return; 371 } 372 } 373 println!("Preferences successfully migrated!"); 374}