first commit (and successful migration!) 🎉
This commit is contained in:
commit
24db172462
4 changed files with 541 additions and 0 deletions
250
atproto.go
Normal file
250
atproto.go
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
AtprotoCreateSession struct {
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
AtprotoSession struct {
|
||||||
|
AccessJwt string `json:"accessJwt"`
|
||||||
|
RefreshJwt string `json:"refreshJwt"`
|
||||||
|
Handle string `json:"handle"`
|
||||||
|
Did string `json:"did"`
|
||||||
|
}
|
||||||
|
|
||||||
|
AtprotoIdentity struct {
|
||||||
|
Did string `json:"did"`
|
||||||
|
Handle string `json:"handle"`
|
||||||
|
DidDoc string `json:"didDoc"`
|
||||||
|
}
|
||||||
|
AtprotoGetRecordResponse struct {
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
Cid string `json:"cid"`
|
||||||
|
Value json.RawMessage `json:"value"`
|
||||||
|
}
|
||||||
|
AtprotoCreateRecord struct {
|
||||||
|
Repo string `json:"repo"`
|
||||||
|
Collection string `json:"collection"`
|
||||||
|
Record json.RawMessage `json:"record"`
|
||||||
|
}
|
||||||
|
AtprotoPutRecord struct {
|
||||||
|
Repo string `json:"repo"`
|
||||||
|
Collection string `json:"collection"`
|
||||||
|
Rkey string `json:"rkey"`
|
||||||
|
Record any `json:"record"`
|
||||||
|
}
|
||||||
|
AtprotoUidCid struct {
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
Cid string `json:"cid"`
|
||||||
|
}
|
||||||
|
AtprotoRecord struct {
|
||||||
|
Type string `json:"$type"`
|
||||||
|
}
|
||||||
|
AtprotoRef struct {
|
||||||
|
Link string `json:"$link"`
|
||||||
|
}
|
||||||
|
AtprotoBlob struct {
|
||||||
|
LexiconTypeID string `json:"$type,const=blob"`
|
||||||
|
Ref AtprotoRef `json:"ref"`
|
||||||
|
MimeType string `json:"mimeType"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BskyActorProfileView struct {
|
||||||
|
Did string `json:"did"`
|
||||||
|
Handle string `json:"handle"`
|
||||||
|
DisplayName *string `json:"displayName"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Avatar *string `json:"avatar"`
|
||||||
|
Associated *json.RawMessage `json:"associated"`
|
||||||
|
IndexedAt *string `json:"indexedAt"`
|
||||||
|
CreatedAt *string `json:"createdAt"`
|
||||||
|
Viewer *json.RawMessage `json:"viewer"`
|
||||||
|
Labels *json.RawMessage `json:"labels"`
|
||||||
|
Verification *json.RawMessage `json:"verification"`
|
||||||
|
Status *json.RawMessage `json:"status"`
|
||||||
|
}
|
||||||
|
BskyGetFollowsResponse struct {
|
||||||
|
Subject BskyActorProfileView `json:"subject"`
|
||||||
|
Cursor *string `json:"cursor"`
|
||||||
|
Follows []BskyActorProfileView `json:"follows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
BskyActorProfile struct {
|
||||||
|
LexiconTypeID string `json:"$type,const=app.bsky.actor.profile"`
|
||||||
|
DisplayName *string `json:"displayName,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Avatar *AtprotoBlob `json:"avatar,omitempty"`
|
||||||
|
Banner *AtprotoBlob `json:"banner,omitempty"`
|
||||||
|
Labels []string `json:"labels,omitempty"`
|
||||||
|
PinnedPost *AtprotoUidCid `json:"pinnedPost,omitempty"`
|
||||||
|
}
|
||||||
|
BskyGraphFollow struct {
|
||||||
|
LexiconTypeID string `json:"$type,const=app.bsky.graph.follow"`
|
||||||
|
CreatedAt string `json:"createdAt"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateSession (pdsUrl string, identifier string, password string) (*AtprotoSession, error) {
|
||||||
|
reqBody, reqBodyBytes := AtprotoCreateSession{
|
||||||
|
Identifier: identifier,
|
||||||
|
Password: password,
|
||||||
|
}, new(bytes.Buffer)
|
||||||
|
json.NewEncoder(reqBodyBytes).Encode(reqBody)
|
||||||
|
res, err := http.Post(pdsUrl + "/xrpc/com.atproto.server.createSession", "application/json", reqBodyBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("failed to log into @%s: %v\n", identifier, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
session := AtprotoSession{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&session)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("failed to parse PDS response: %v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveAtprotoHandle(session *AtprotoSession, pdsUrl string, handle string) (string, error) {
|
||||||
|
reqUrl, _ := url.Parse(pdsUrl + "/xrpc/com.atproto.identity.resolveHandle")
|
||||||
|
reqUrl.RawQuery = url.Values{
|
||||||
|
"handle": { handle },
|
||||||
|
}.Encode()
|
||||||
|
req, _ := http.NewRequest(http.MethodGet, reqUrl.String(), nil)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer " + session.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
type DidResponse struct {
|
||||||
|
Did string `json:"did"`
|
||||||
|
}
|
||||||
|
identity := DidResponse{}
|
||||||
|
json.NewDecoder(res.Body).Decode(&identity)
|
||||||
|
|
||||||
|
return identity.Did, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAtprotoRecord(session *AtprotoSession, pdsUrl string, collection string, record json.RawMessage) (*AtprotoUidCid, error) {
|
||||||
|
reqBody, reqBodyBytes := AtprotoCreateRecord{
|
||||||
|
Repo: session.Did,
|
||||||
|
Collection: collection,
|
||||||
|
Record: record,
|
||||||
|
}, new(bytes.Buffer)
|
||||||
|
err := json.NewEncoder(reqBodyBytes).Encode(reqBody)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, pdsUrl + "/xrpc/com.atproto.repo.createRecord", reqBodyBytes)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer " + session.AccessJwt)
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
errBody := ErrorResponse{}
|
||||||
|
json.NewDecoder(res.Body).Decode(&errBody)
|
||||||
|
return nil, errors.New(fmt.Sprintf("%s: %s", errBody.Error, errBody.Message))
|
||||||
|
}
|
||||||
|
createdRecord := AtprotoUidCid{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&createdRecord)
|
||||||
|
return &createdRecord, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutAtprotoRecord(session *AtprotoSession, pdsUrl string, collection string, record any) (*AtprotoUidCid, error) {
|
||||||
|
reqBody, reqBodyBytes := AtprotoPutRecord{
|
||||||
|
Repo: session.Did,
|
||||||
|
Collection: collection,
|
||||||
|
Record: record,
|
||||||
|
Rkey: "self",
|
||||||
|
}, new(bytes.Buffer)
|
||||||
|
err := json.NewEncoder(reqBodyBytes).Encode(reqBody)
|
||||||
|
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, pdsUrl + "/xrpc/com.atproto.repo.putRecord", reqBodyBytes)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer " + session.AccessJwt)
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil { return nil, err }
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
errBody := ErrorResponse{}
|
||||||
|
json.NewDecoder(res.Body).Decode(&errBody)
|
||||||
|
return nil, errors.New(fmt.Sprintf("%s: %s", errBody.Error, errBody.Message))
|
||||||
|
}
|
||||||
|
updatedRecord := AtprotoUidCid{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&updatedRecord)
|
||||||
|
return &updatedRecord, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAtprotoBlob(pdsUrl string, did string, cid string) ([]byte, error) {
|
||||||
|
reqUrl, _ := url.Parse(pdsUrl + "/xrpc/com.atproto.sync.getBlob")
|
||||||
|
reqUrl.RawQuery = url.Values{
|
||||||
|
"did": { did },
|
||||||
|
"cid": { cid },
|
||||||
|
}.Encode()
|
||||||
|
res, err := http.Get(reqUrl.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
errBody := ErrorResponse{}
|
||||||
|
json.NewDecoder(res.Body).Decode(&errBody)
|
||||||
|
return nil, errors.New(fmt.Sprintf("%s: %s", errBody.Error, errBody.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("failed to read bytes: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadAtprotoBlob(session *AtprotoSession, pdsUrl string, data []byte, mimeType string) (*AtprotoBlob, error) {
|
||||||
|
req, _ := http.NewRequest(http.MethodPost, pdsUrl + "/xrpc/com.atproto.repo.uploadBlob", bytes.NewReader(data))
|
||||||
|
req.Header.Set("Content-Type", mimeType)
|
||||||
|
req.Header.Set("Authorization", "Bearer " + session.AccessJwt)
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
errBody := ErrorResponse{}
|
||||||
|
json.NewDecoder(res.Body).Decode(&errBody)
|
||||||
|
return nil, errors.New(fmt.Sprintf("%s: %s", errBody.Error, errBody.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Blob AtprotoBlob `json:"blob"`
|
||||||
|
}
|
||||||
|
blob := Response{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&blob)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("failed to read response body: %v", err))
|
||||||
|
}
|
||||||
|
blob.Blob.LexiconTypeID = "blob"
|
||||||
|
return &blob.Blob, nil
|
||||||
|
}
|
161
funcs.go
Normal file
161
funcs.go
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ImportProfile(session *AtprotoSession, pdsUrl string, fromAccount string, dryrun bool, verbose bool) error {
|
||||||
|
fmt.Printf("fetching profile record from @%s...\n", fromAccount)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if !strings.HasPrefix(fromAccount, "did:") {
|
||||||
|
fromAccount, err = ResolveAtprotoHandle(session, pdsUrl, fromAccount)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("failed to resolve identity: %v\n", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reqUrl, _ := url.Parse(pdsUrl + "/xrpc/com.atproto.repo.getRecord")
|
||||||
|
reqUrl.RawQuery = url.Values{
|
||||||
|
"repo": { fromAccount },
|
||||||
|
"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(pdsUrl, fromAccount, profile.Avatar.Ref.Link)
|
||||||
|
if err != nil { return errors.New(fmt.Sprintf("failed to download avatar: %v\n", err)) }
|
||||||
|
|
||||||
|
avatarBlob, err := UploadAtprotoBlob(session, pdsUrl, 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(pdsUrl, fromAccount, profile.Banner.Ref.Link)
|
||||||
|
if err != nil { return errors.New(fmt.Sprintf("failed to download banner: %v\n", err)) }
|
||||||
|
|
||||||
|
bannerBlob, err := UploadAtprotoBlob(session, pdsUrl, 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(session, pdsUrl, "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 successfully.\n", fromAccount)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImportFollows(session *AtprotoSession, pdsUrl string, fromAccount string, dryrun bool, verbose bool) error {
|
||||||
|
followDids := []string{}
|
||||||
|
cursor := ""
|
||||||
|
|
||||||
|
fmt.Printf("fetching follow records from @%s...\n", fromAccount)
|
||||||
|
for {
|
||||||
|
reqUrl, _ := url.Parse(pdsUrl + "/xrpc/app.bsky.graph.getFollows")
|
||||||
|
reqUrlValues := url.Values{
|
||||||
|
"actor": { fromAccount },
|
||||||
|
"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 " + session.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", session.Handle)
|
||||||
|
successful := 0
|
||||||
|
for i, did := range followDids[len(followDids)-3:] {
|
||||||
|
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(session, pdsUrl, "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 successfully!\n", successful, len(followDids))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
3
go.mod
Normal file
3
go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module arimelody.me/bluesky-follow-migrator
|
||||||
|
|
||||||
|
go 1.24.4
|
127
main.go
Normal file
127
main.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) == 1 || slices.Contains(os.Args, "-help") {
|
||||||
|
printHelp()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var srcUser string
|
||||||
|
var destUser string
|
||||||
|
var password string
|
||||||
|
var pdsUrl string
|
||||||
|
var dryrun bool = false
|
||||||
|
var verbose bool = false
|
||||||
|
|
||||||
|
var importProfile bool = false
|
||||||
|
var importFollows bool = false
|
||||||
|
|
||||||
|
i := 1
|
||||||
|
for {
|
||||||
|
if i >= len(os.Args) { break }
|
||||||
|
if os.Args[i][0] != '-' {
|
||||||
|
fmt.Fprintf(os.Stderr, "unrecognised argument `%s`.\n", os.Args[i])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue := func(i int) (int, string) {
|
||||||
|
if i + 1 >= len(os.Args) {
|
||||||
|
fmt.Fprintf(os.Stderr, "-%s requires a value.\n", os.Args[i])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return i + 1, os.Args[i + 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch os.Args[i][1:] {
|
||||||
|
case "from":
|
||||||
|
i, srcUser = getValue(i)
|
||||||
|
case "to":
|
||||||
|
i, destUser = getValue(i)
|
||||||
|
case "pass":
|
||||||
|
i, password = getValue(i)
|
||||||
|
case "pds":
|
||||||
|
i, pdsUrl = getValue(i)
|
||||||
|
case "dryrun":
|
||||||
|
dryrun = true
|
||||||
|
case "v":
|
||||||
|
verbose = true
|
||||||
|
case "profile":
|
||||||
|
importProfile = true
|
||||||
|
case "follows":
|
||||||
|
importFollows = true
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if srcUser == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "missing required argument -from.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if destUser == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "missing required argument -to.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if password == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "missing required argument -pass.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if pdsUrl == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "missing required argument -pds.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !importProfile && !importFollows {
|
||||||
|
fmt.Fprintf(os.Stderr, "no action was specified.\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := CreateSession(pdsUrl, destUser, password)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to create session: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if importProfile {
|
||||||
|
err := ImportProfile(session, pdsUrl, srcUser, dryrun, verbose)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to import profile: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if importFollows {
|
||||||
|
err := ImportFollows(session, pdsUrl, srcUser, dryrun, verbose)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to import follows: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
fmt.Printf(
|
||||||
|
`usage: %s <arguments...> <imports...>
|
||||||
|
|
||||||
|
required arguments:
|
||||||
|
-from <handle>: the account to import follow records from.
|
||||||
|
-to <handle>: the account to import follow records to.
|
||||||
|
-pass <password>: the password of the `+"`to`"+` account.
|
||||||
|
-pds <pds-url>: the full https:// url of the `+"`to`"+` account's PDS.
|
||||||
|
|
||||||
|
imports (at least one required):
|
||||||
|
-profile: imports the account profile, including avatar, banner, display name, and description.
|
||||||
|
-follows: imports the following list of the account.
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-dryrun: does not import follow records; good for sanity testing!
|
||||||
|
-v: verbose output
|
||||||
|
-help: shows this help message.
|
||||||
|
`,
|
||||||
|
os.Args[0],
|
||||||
|
)
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue