this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

early work on account migration

+216
+1
cmd/goat/account.go
··· 53 53 ArgsUsage: `<at-identifier>`, 54 54 Action: runAccountStatus, 55 55 }, 56 + cmdAccountMigrate, 56 57 }, 57 58 } 58 59
+215
cmd/goat/account_migrate.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "fmt" 7 + "log/slog" 8 + "time" 9 + 10 + comatproto "github.com/bluesky-social/indigo/api/atproto" 11 + appbsky "github.com/bluesky-social/indigo/api/bsky" 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "github.com/bluesky-social/indigo/xrpc" 14 + 15 + "github.com/urfave/cli/v2" 16 + ) 17 + 18 + var cmdAccountMigrate = &cli.Command{ 19 + Name: "migrate", 20 + Usage: "move account to a new PDS. requires full auth.", 21 + Flags: []cli.Flag{ 22 + &cli.StringFlag{ 23 + Name: "new-handle", 24 + Required: true, 25 + Usage: "handle on new PDS", 26 + EnvVars: []string{"NEW_ACCOUNT_HANDLE"}, 27 + }, 28 + &cli.StringFlag{ 29 + Name: "new-password", 30 + Required: true, 31 + Usage: "password on new PDS", 32 + EnvVars: []string{"NEW_ACCOUNT_PASSWORD"}, 33 + }, 34 + }, 35 + Action: runAccountMigrate, 36 + } 37 + 38 + func runAccountMigrate(cctx *cli.Context) error { 39 + // TODO: this could check rev / commit before and after 40 + ctx := context.Background() 41 + 42 + oldClient, err := loadAuthClient(ctx) 43 + if err == ErrNoAuthSession { 44 + return fmt.Errorf("auth required, but not logged in") 45 + } else if err != nil { 46 + return err 47 + } 48 + did := oldClient.Auth.Did 49 + 50 + // connect to new host to discover service DID 51 + newHostURL := "XXX" 52 + newHandle := "XXX" 53 + newPassword := "XXX" 54 + newEmail := "XXX" 55 + inviteCode := "XXX" 56 + newClient := xrpc.Client{ 57 + Host: newHostURL, 58 + } 59 + newHostDesc, err := comatproto.ServerDescribeServer(ctx, &newClient) 60 + if err != nil { 61 + return fmt.Errorf("failed connecting to new host: %w", err) 62 + } 63 + newHostDID, err := syntax.ParseDID(newHostDesc.Did) 64 + if err != nil { 65 + return err 66 + } 67 + slog.Info("new host", "did", newHostDID, "url", newHostURL) 68 + 69 + // 1. Create New Account 70 + slog.Info("creating account on new host", "handle", newHandle) 71 + 72 + // get service auth token from old host 73 + // args: (ctx, client, aud string, exp int64, lxm string) 74 + expTimestamp := time.Now().Unix() + 60 75 + createAuthResp, err := comatproto.ServerGetServiceAuth(ctx, oldClient, newHostDID.String(), expTimestamp, "com.atproto.server.createAccount") 76 + if err != nil { 77 + return fmt.Errorf("failed getting service auth token from old host: %w", err) 78 + } 79 + 80 + // then create the new account 81 + // XXX: "Authorization Bearer <createAuthResp.Token>" 82 + _ = createAuthResp 83 + createAccountResp, err := comatproto.ServerCreateAccount(ctx, &newClient, &comatproto.ServerCreateAccount_Input{ 84 + Did: &did, 85 + Email: &newEmail, 86 + Handle: newHandle, 87 + InviteCode: &inviteCode, 88 + Password: &newPassword, 89 + }) 90 + if err != nil { 91 + return fmt.Errorf("failed getting service auth token from old host: %w", err) 92 + } 93 + // TODO: validate response? 94 + _ = createAccountResp 95 + 96 + // login client on the new host 97 + sess, err := comatproto.ServerCreateSession(ctx, &newClient, &comatproto.ServerCreateSession_Input{ 98 + Identifier: did, 99 + Password: newPassword, 100 + }) 101 + if err != nil { 102 + return fmt.Errorf("failed login to newly created account on new host: %w", err) 103 + } 104 + newClient.Auth = &xrpc.AuthInfo{ 105 + Did: did, 106 + AccessJwt: sess.AccessJwt, 107 + RefreshJwt: sess.RefreshJwt, 108 + } 109 + 110 + // 2. Migrate Data 111 + 112 + slog.Info("migrating repo") 113 + repoBytes, err := comatproto.SyncGetRepo(ctx, oldClient, did, "") 114 + if err != nil { 115 + return fmt.Errorf("failed exporting repo: %w", err) 116 + } 117 + err = comatproto.RepoImportRepo(ctx, &newClient, bytes.NewReader(repoBytes)) 118 + if err != nil { 119 + return fmt.Errorf("failed importing repo: %w", err) 120 + } 121 + 122 + slog.Info("migrating preferences") 123 + // TODO: service proxy header 124 + prefResp, err := appbsky.ActorGetPreferences(ctx, oldClient) 125 + if err != nil { 126 + return fmt.Errorf("failed fetching old preferences: %w", err) 127 + } 128 + err = appbsky.ActorPutPreferences(ctx, &newClient, &appbsky.ActorPutPreferences_Input{ 129 + Preferences: prefResp.Preferences, 130 + }) 131 + 132 + slog.Info("migrating blobs") 133 + blobCursor := "" 134 + for { 135 + listResp, err := comatproto.SyncListBlobs(ctx, oldClient, blobCursor, did, 100, "") 136 + if err != nil { 137 + return fmt.Errorf("failed listing blobs: %w", err) 138 + } 139 + for _, blobCID := range listResp.Cids { 140 + blobBytes, err := comatproto.SyncGetBlob(ctx, oldClient, blobCID, did) 141 + if err != nil { 142 + slog.Warn("failed downloading blob", "cid", blobCID, "err", err) 143 + continue 144 + } 145 + _, err = comatproto.RepoUploadBlob(ctx, &newClient, bytes.NewReader(blobBytes)) 146 + if err != nil { 147 + slog.Warn("failed uploading blob", "cid", blobCID, "err", err, "size", len(blobBytes)) 148 + } 149 + slog.Info("transfered blob", "cid", blobCID, "size", len(blobBytes)) 150 + } 151 + if listResp.Cursor == nil || *listResp.Cursor == "" { 152 + break 153 + } 154 + blobCursor = *listResp.Cursor 155 + } 156 + 157 + // display migration status 158 + // TODO: should this loop? with a delay? 159 + statusResp, err := comatproto.ServerCheckAccountStatus(ctx, &newClient) 160 + if err != nil { 161 + return fmt.Errorf("failed checking account status: %w", err) 162 + } 163 + slog.Info("account migration status", "status", statusResp) 164 + 165 + // 3. Migrate Identity 166 + // TODO: support did:web etc 167 + slog.Info("updating identity to new host") 168 + 169 + credsResp, err := comatproto.IdentityGetRecommendedDidCredentials(ctx, &newClient) 170 + if err != nil { 171 + return fmt.Errorf("failed fetching new credentials: %w", err) 172 + } 173 + 174 + // TODO: add some safety checks here around rotation key set intersection? 175 + 176 + err = comatproto.IdentityRequestPlcOperationSignature(ctx, oldClient) 177 + if err != nil { 178 + return fmt.Errorf("failed requesting PLC operation token: %w", err) 179 + } 180 + 181 + plcAuthToken := "XXX" 182 + signedPlcOpResp, err := comatproto.IdentitySignPlcOperation(ctx, oldClient, &comatproto.IdentitySignPlcOperation_Input{ 183 + // XXX: not just pass-through 184 + AlsoKnownAs: credsResp.AlsoKnownAs, 185 + RotationKeys: credsResp.RotationKeys, 186 + Services: credsResp.Services, 187 + Token: &plcAuthToken, 188 + VerificationMethods: credsResp.VerificationMethods, 189 + }) 190 + if err != nil { 191 + return fmt.Errorf("failed requesting PLC operation signature: %w", err) 192 + } 193 + 194 + err = comatproto.IdentitySubmitPlcOperation(ctx, &newClient, &comatproto.IdentitySubmitPlcOperation_Input{ 195 + Operation: signedPlcOpResp.Operation, 196 + }) 197 + if err != nil { 198 + return fmt.Errorf("failed submitting PLC operation: %w", err) 199 + } 200 + 201 + // 4. Finalize Migration 202 + slog.Info("activating new account") 203 + 204 + err = comatproto.ServerActivateAccount(ctx, &newClient) 205 + if err != nil { 206 + return fmt.Errorf("failed activating new host: %w", err) 207 + } 208 + err = comatproto.ServerDeactivateAccount(ctx, oldClient, nil) 209 + if err != nil { 210 + return fmt.Errorf("failed deactivating old host: %w", err) 211 + } 212 + 213 + slog.Info("account migration completed") 214 + return nil 215 + }