migratesky/atproto.go

255 lines
7.6 KiB
Go

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"`
PdsUrl string `json:"-"`
}
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"`
}
AtprotoUriCid 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 *AtprotoUriCid `json:"pinnedPost,omitempty"`
}
BskyActorPreferences struct {
Preferences []json.RawMessage `json:"preferences"`
}
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))
}
session.PdsUrl = pdsUrl
return &session, nil
}
func ResolveAtprotoHandle(session *AtprotoSession, handle string) (string, error) {
reqUrl, _ := url.Parse(session.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, collection string, record json.RawMessage) (*AtprotoUriCid, 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, session.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 := AtprotoUriCid{}
err = json.NewDecoder(res.Body).Decode(&createdRecord)
return &createdRecord, err
}
func PutAtprotoRecord(session *AtprotoSession, collection string, record any) (*AtprotoUriCid, 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, session.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 := AtprotoUriCid{}
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, data []byte, mimeType string) (*AtprotoBlob, error) {
req, _ := http.NewRequest(http.MethodPost, session.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
}