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}