package main import ( "bytes" "encoding/json" "errors" "fmt" "net/http" "net/url" "os" "time" ) func ImportProfile(fromSession *AtprotoSession, toSession *AtprotoSession, dryrun bool, verbose bool) error { fmt.Printf("fetching profile record from @%s...\n", fromSession.Handle) reqUrl, _ := url.Parse(fromSession.PdsUrl + "/xrpc/com.atproto.repo.getRecord") reqUrl.RawQuery = url.Values{ "repo": { fromSession.Did }, "collection": { "app.bsky.actor.profile" }, "rkey": { "self" }, }.Encode() req, _ := http.NewRequest(http.MethodGet, reqUrl.String(), nil) res, err := http.DefaultClient.Do(req) if err != nil { return errors.New(fmt.Sprintf("failed to fetch profile: %v\n", err)) } getProfileResponse := AtprotoGetRecordResponse{} err = json.NewDecoder(res.Body).Decode(&getProfileResponse) if err != nil { return errors.New(fmt.Sprintf("failed to parse PDS response: %v\n", err)) } profile := BskyActorProfile{} err = json.Unmarshal(getProfileResponse.Value, &profile) newProfile := BskyActorProfile{ LexiconTypeID: "app.bsky.actor.profile", DisplayName: profile.DisplayName, Description: profile.Description, Labels: profile.Labels, } if verbose { fmt.Printf( "\nimporting profile details:\n" + "- display name: %s\n" + "- description:\n%s\n\n", *newProfile.DisplayName, *newProfile.Description, ) } if !dryrun { // import avatar avatarBytes, err := GetAtprotoBlob(fromSession.PdsUrl, fromSession.Did, profile.Avatar.Ref.Link) if err != nil { return errors.New(fmt.Sprintf("failed to download avatar: %v\n", err)) } avatarBlob, err := UploadAtprotoBlob(toSession, avatarBytes, profile.Avatar.MimeType) if err != nil { return errors.New(fmt.Sprintf("failed to upload avatar: %v\n", err)) } newProfile.Avatar = avatarBlob // import banner bannerBytes, err := GetAtprotoBlob(fromSession.PdsUrl, fromSession.Did, profile.Banner.Ref.Link) if err != nil { return errors.New(fmt.Sprintf("failed to download banner: %v\n", err)) } bannerBlob, err := UploadAtprotoBlob(toSession, bannerBytes, profile.Banner.MimeType) if err != nil { return errors.New(fmt.Sprintf("failed to upload banner: %v\n", err)) } newProfile.Banner = bannerBlob // import all details _, err = PutAtprotoRecord(toSession, "app.bsky.actor.profile", newProfile) if err != nil { return errors.New(fmt.Sprintf("failed to update profile record: %v\n", err)) } } fmt.Printf("profile imported from @%s to @%s successfully.\n", fromSession.Handle, toSession.Handle) return nil } func ImportFollows(fromSession *AtprotoSession, toSession *AtprotoSession, dryrun bool, verbose bool) error { followDids := []string{} cursor := "" fmt.Printf("fetching follow records from @%s...\n", fromSession.Handle) for { reqUrl, _ := url.Parse(fromSession.PdsUrl + "/xrpc/app.bsky.graph.getFollows") reqUrlValues := url.Values{ "actor": { fromSession.Did }, "limit": { "100" }, } if cursor != "" { reqUrlValues.Set("cursor", cursor) } reqUrl.RawQuery = reqUrlValues.Encode() req, _ := http.NewRequest(http.MethodGet, reqUrl.String(), nil) req.Header.Set("Authorization", "Bearer " + fromSession.AccessJwt) res, err := http.DefaultClient.Do(req) if err != nil { return errors.New(fmt.Sprintf("failed to fetch follows: %v\n", err)) } getFollowsResponse := BskyGetFollowsResponse{} err = json.NewDecoder(res.Body).Decode(&getFollowsResponse) if err != nil { return errors.New(fmt.Sprintf("failed to parse PDS response: %v\n", err)) } for _, profile := range getFollowsResponse.Follows { if verbose { fmt.Printf("following: %s - %s\n", profile.Handle, *profile.DisplayName) } followDids = append(followDids, profile.Did) } if getFollowsResponse.Cursor == nil { break } cursor = *getFollowsResponse.Cursor } fmt.Printf("fetched %d follow records.\n", len(followDids)) fmt.Printf("importing follow records to @%s...\n", toSession.Handle) successful := 0 for i, did := range followDids { if dryrun { if verbose { fmt.Printf("(%d/%d) followed %s (dry run)\n", i, len(followDids), did) } successful += 1 continue } record := BskyGraphFollow{ Subject: did, CreatedAt: time.Now().Format(time.RFC3339), } recordJson, _ := json.Marshal(record) newRecord, err := CreateAtprotoRecord(toSession, "app.bsky.graph.follow", recordJson) if err != nil { fmt.Fprintf(os.Stderr, "warn: failed to create follow record for %s: %v\n", did, err) continue } if verbose { fmt.Printf("(%d/%d) followed %s (%s)\n", i, len(followDids), did, newRecord.Uri) } successful += 1 } fmt.Printf( "%d/%d follow records imported from @%s to @%s successfully!\n", successful, len(followDids), fromSession.Handle, toSession.Handle, ) return nil } func ImportPreferences(fromSession *AtprotoSession, toSession *AtprotoSession, dryrun bool, verbose bool) error { fmt.Printf("fetching preferences from @%s...\n", fromSession.Handle) req, _ := http.NewRequest(http.MethodGet, fromSession.PdsUrl + "/xrpc/app.bsky.actor.getPreferences", nil) req.Header.Set("Authorization", "Bearer " + fromSession.AccessJwt) res, err := http.DefaultClient.Do(req) if err != nil { return errors.New(fmt.Sprintf("failed to fetch preferences: %v\n", err)) } // // TODO: BskyGetPreferencesResponse data := BskyActorPreferences{} err = json.NewDecoder(res.Body).Decode(&data) if err != nil { return errors.New(fmt.Sprintf("failed to parse PDS response: %v\n", err)) } fmt.Printf("fetched %d preferences.\n", len(data.Preferences)) fmt.Printf("importing preferences to @%s...\n", toSession.Handle) if !dryrun { reqBodyBytes, _ := json.Marshal(data) req, _ := http.NewRequest(http.MethodPost, toSession.PdsUrl + "/xrpc/app.bsky.actor.putPreferences", bytes.NewReader(reqBodyBytes)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer " + toSession.AccessJwt) res, err := http.DefaultClient.Do(req) if err != nil { return err } if res.StatusCode != http.StatusOK { errBody := ErrorResponse{} json.NewDecoder(res.Body).Decode(&errBody) return errors.New(fmt.Sprintf("%s: %s", errBody.Error, errBody.Message)) } } fmt.Printf("preferences imported successfully!\n") return nil }