diff --git a/bsky/bluesky.go b/bsky/bluesky.go
index ae8d5cf..38422f7 100644
--- a/bsky/bluesky.go
+++ b/bsky/bluesky.go
@@ -263,3 +263,17 @@ type Records struct {
Cid string `json:"cid"`
Value Value `json:"value"`
}
+
+func (bluesky *Bluesky) FetchPost(did string, rkey string) FetchedPost {
+ resp := &struct {
+ Posts []FetchedPost `json:"posts"`
+ }{}
+ params := struct {
+ URIs string `url:"uris"`
+ }{
+ URIs: fmt.Sprintf("at://%s/app.bsky.feed.post/%s", did, rkey),
+ }
+ bluesky.sling.New().Base("https://public.api.bsky.app").
+ Get("/xrpc/app.bsky.feed.getPosts").QueryStruct(¶ms).Receive(resp, resp)
+ return resp.Posts[0]
+}
diff --git a/bsky/client.go b/bsky/client.go
index 34047a4..c11661d 100644
--- a/bsky/client.go
+++ b/bsky/client.go
@@ -28,7 +28,7 @@ func NewBSky() *BSky {
}
}
-func (b *BSky) getPDS() error {
+func (b *BSky) ResolveHandle(handle string) (string, error) {
httpClient := &http.Client{Timeout: 3 * time.Second}
resp := new(BSkySessionResponse)
errResp := &struct {
@@ -38,23 +38,29 @@ func (b *BSky) getPDS() error {
params := struct {
Handle string `url:"handle"`
}{
- Handle: b.Bluesky.Cfg.Handle,
+ Handle: handle,
}
sling.New().Base("https://public.api.bsky.app/").Client(httpClient).
Get("/xrpc/com.atproto.identity.resolveHandle").QueryStruct(params).
Receive(resp, errResp)
if errResp.Error != "" {
- return errors.New(errResp.Message)
+ return "", errors.New(errResp.Message)
}
+ return resp.DID, nil
+}
+
+func (b *BSky) getPDS() error {
+ did, _ := b.ResolveHandle(b.Bluesky.Cfg.Handle)
+
var didURL url.URL
- if strings.HasPrefix(resp.DID, "did:web:") {
- didURL.Host = "https://" + resp.DID[8:]
+ if strings.HasPrefix(did, "did:web:") {
+ didURL.Host = "https://" + did[8:]
didURL.Path = "/.well-known/did.json"
- } else if strings.HasPrefix(resp.DID, "did:plc:") {
+ } else if strings.HasPrefix(did, "did:plc:") {
didURL.Host = "https://plc.directory"
- didURL.Path = "/" + resp.DID
+ didURL.Path = "/" + did
} else {
return errors.New("DID is not supported")
}
@@ -104,7 +110,7 @@ func (b *BSky) Auth(authData []string) error {
b.Bluesky.Cfg.AppPassword = authData[1]
err = b.Bluesky.CreateSession(b.Bluesky.Cfg)
if err != nil {
- return errors.New(fmt.Sprintf("unable to auth: %s", err))
+ return fmt.Errorf("unable to auth: %s", err)
}
b.Bluesky.Cfg.AppPassword = "" // we don't need to save this
PersistAuthSession(b.Bluesky.Cfg)
diff --git a/bsky/parse.go b/bsky/parse.go
index 1560be2..c600af9 100644
--- a/bsky/parse.go
+++ b/bsky/parse.go
@@ -123,6 +123,160 @@ type ParsedEmbeds struct {
Height int64
}
+type FetchedPost struct {
+ URI string `json:"uri"`
+ Cid string `json:"cid"`
+ Author struct {
+ Did string `json:"did"`
+ Handle string `json:"handle"`
+ DisplayName string `json:"displayName"`
+ Avatar string `json:"avatar"`
+ Associated struct {
+ Chat struct {
+ AllowIncoming string `json:"allowIncoming"`
+ } `json:"chat"`
+ } `json:"associated"`
+ Labels []interface{} `json:"labels"`
+ CreatedAt time.Time `json:"createdAt"`
+ } `json:"author"`
+ Record *Post `json:"record"`
+ // Record struct {
+ // Type string `json:"$type"`
+ // CreatedAt time.Time `json:"createdAt"`
+ // Embed struct {
+ // Type string `json:"$type"`
+ // Media struct {
+ // Type string `json:"$type"`
+ // Images []struct {
+ // Alt string `json:"alt"`
+ // AspectRatio struct {
+ // Height int `json:"height"`
+ // Width int `json:"width"`
+ // } `json:"aspectRatio"`
+ // Image struct {
+ // Type string `json:"$type"`
+ // Ref struct {
+ // Link string `json:"$link"`
+ // } `json:"ref"`
+ // MimeType string `json:"mimeType"`
+ // Size int `json:"size"`
+ // } `json:"image"`
+ // } `json:"images"`
+ // } `json:"media"`
+ // Record struct {
+ // Type string `json:"$type"`
+ // Record struct {
+ // Cid string `json:"cid"`
+ // URI string `json:"uri"`
+ // } `json:"record"`
+ // } `json:"record"`
+ // } `json:"embed"`
+ // Labels struct {
+ // Type string `json:"$type"`
+ // Values []struct {
+ // Val string `json:"val"`
+ // } `json:"values"`
+ // } `json:"labels"`
+ // Langs []string `json:"langs"`
+ // Text string `json:"text"`
+ // } `json:"record"`
+ Embed struct {
+ Type string `json:"$type"`
+ Media struct {
+ Type string `json:"$type"`
+ Images []struct {
+ Thumb string `json:"thumb"`
+ Fullsize string `json:"fullsize"`
+ Alt string `json:"alt"`
+ AspectRatio struct {
+ Height int `json:"height"`
+ Width int `json:"width"`
+ } `json:"aspectRatio"`
+ } `json:"images"`
+ } `json:"media"`
+ Record struct {
+ Record struct {
+ Type string `json:"$type"`
+ URI string `json:"uri"`
+ Cid string `json:"cid"`
+ Author struct {
+ Did string `json:"did"`
+ Handle string `json:"handle"`
+ DisplayName string `json:"displayName"`
+ Avatar string `json:"avatar"`
+ Associated struct {
+ Chat struct {
+ AllowIncoming string `json:"allowIncoming"`
+ } `json:"chat"`
+ } `json:"associated"`
+ Labels []interface{} `json:"labels"`
+ CreatedAt time.Time `json:"createdAt"`
+ } `json:"author"`
+ Value struct {
+ Type string `json:"$type"`
+ CreatedAt time.Time `json:"createdAt"`
+ Embed struct {
+ Type string `json:"$type"`
+ AspectRatio struct {
+ Height int `json:"height"`
+ Width int `json:"width"`
+ } `json:"aspectRatio"`
+ Video struct {
+ Type string `json:"$type"`
+ Ref struct {
+ Link string `json:"$link"`
+ } `json:"ref"`
+ MimeType string `json:"mimeType"`
+ Size int `json:"size"`
+ } `json:"video"`
+ } `json:"embed"`
+ Facets []struct {
+ Type string `json:"$type"`
+ Features []struct {
+ Type string `json:"$type"`
+ Did string `json:"did"`
+ } `json:"features"`
+ Index struct {
+ ByteEnd int `json:"byteEnd"`
+ ByteStart int `json:"byteStart"`
+ } `json:"index"`
+ } `json:"facets"`
+ Langs []string `json:"langs"`
+ Text string `json:"text"`
+ } `json:"value"`
+ Labels []interface{} `json:"labels"`
+ LikeCount int `json:"likeCount"`
+ ReplyCount int `json:"replyCount"`
+ RepostCount int `json:"repostCount"`
+ QuoteCount int `json:"quoteCount"`
+ IndexedAt time.Time `json:"indexedAt"`
+ Embeds []struct {
+ Type string `json:"$type"`
+ Cid string `json:"cid"`
+ Playlist string `json:"playlist"`
+ Thumbnail string `json:"thumbnail"`
+ AspectRatio struct {
+ Height int `json:"height"`
+ Width int `json:"width"`
+ } `json:"aspectRatio"`
+ } `json:"embeds"`
+ } `json:"record"`
+ } `json:"record"`
+ } `json:"embed,omitempty"`
+ ReplyCount int `json:"replyCount"`
+ RepostCount int `json:"repostCount"`
+ LikeCount int `json:"likeCount"`
+ QuoteCount int `json:"quoteCount"`
+ IndexedAt time.Time `json:"indexedAt"`
+ Labels []struct {
+ Src string `json:"src"`
+ URI string `json:"uri"`
+ Cid string `json:"cid"`
+ Val string `json:"val"`
+ Cts time.Time `json:"cts"`
+ } `json:"labels"`
+}
+
func (b *BSky) ParsePost(post []byte) (*Post, error) {
var p = &Post{}
err := json.Unmarshal(post, &p)
@@ -160,12 +314,10 @@ func (post *Post) ProcessFacets(aliases []Records) string {
switch feature.Type {
case "app.bsky.richtext.facet#mention":
link := fmt.Sprintf(`%s`, feature.Did, post.Text[start:end])
- if aliases != nil {
- for _, alias := range aliases {
- if alias.Value.Subject == feature.Did {
- link = fmt.Sprintf(`%s`,
- strings.SplitN(alias.Value.Target, "#", 2)[0], strings.SplitN(alias.Value.Target, "#", 2)[1])
- }
+ for _, alias := range aliases {
+ if alias.Value.Subject == feature.Did {
+ link = fmt.Sprintf(`%s`,
+ strings.SplitN(alias.Value.Target, "#", 2)[0], strings.SplitN(alias.Value.Target, "#", 2)[1])
}
}
result.WriteString(link)
diff --git a/main.go b/main.go
index 8f8e462..29c3282 100644
--- a/main.go
+++ b/main.go
@@ -3,6 +3,8 @@ package main
import (
"bytes"
"context"
+ "encoding/json"
+ "flag"
"fmt"
"image/jpeg"
"io"
@@ -11,6 +13,7 @@ import (
"net/http"
"net/url"
"os"
+ "regexp"
"strconv"
"strings"
"time"
@@ -19,6 +22,7 @@ import (
tgbotapi "github.com/OvyFlash/telegram-bot-api"
// apibsky "github.com/bluesky-social/indigo/api/bsky"
+
"github.com/bluesky-social/jetstream/pkg/client"
"github.com/bluesky-social/jetstream/pkg/client/schedulers/sequential"
"github.com/bluesky-social/jetstream/pkg/models"
@@ -39,7 +43,13 @@ type handler struct {
bsky *bsky.BSky
}
+var (
+ post = flag.String("post", "", "URL to a BlueSky post")
+)
+
func main() {
+ flag.Parse()
+
var handle = os.Getenv("BSKY_HANDLE")
var password = os.Getenv("BSKY_PASSWORD")
bskyClient := bsky.NewBSky()
@@ -48,30 +58,11 @@ func main() {
log.Fatal(err, ". please set BSKY_HANDLE and BSKY_PASSWORD env variables")
}
- ctx := context.Background()
- slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
- Level: slog.LevelDebug.Level(),
- })))
-
- logger := slog.Default()
- config := client.DefaultClientConfig()
- config.WebsocketURL = serverAddr
- config.WantedCollections = []string{"app.bsky.feed.post"}
- config.WantedDids = []string{bskyClient.Bluesky.Cfg.DID}
- config.Compress = true
-
h := &handler{
seenSeqs: make(map[int64]struct{}),
bsky: bskyClient,
}
- scheduler := sequential.NewScheduler("jetstream_localdev", logger, h.HandleEvent)
-
- c, err := client.NewClient(config, logger, scheduler)
- if err != nil {
- log.Fatalf("failed to create client: %v", err)
- }
-
endpoint := "https://api.telegram.org/bot%s/%s"
if os.Getenv("TG_API_ENDPOINT") != "" {
endpoint = os.Getenv("TG_API_ENDPOINT")
@@ -86,6 +77,50 @@ func main() {
log.Fatal("TG_CHANNEL_ID is not set")
}
+ if *post != "" {
+ r := regexp.MustCompile(`^https:\/\/.*?\/profile\/(.*?)\/post\/(.*?)$`)
+ s := r.FindStringSubmatch(*post)
+ handle := s[1]
+ if s[1][0:4] != "did:" {
+ handle, _ = bskyClient.ResolveHandle(s[1])
+ }
+
+ postJSON := bskyClient.Bluesky.FetchPost(handle, s[2])
+ p, _ := json.Marshal(postJSON.Record)
+ h.ProcessPost(&models.Event{
+ Did: postJSON.Author.Did,
+ TimeUS: postJSON.Record.CreatedAt.Unix(),
+ Kind: "",
+ Commit: &models.Commit{
+ CID: postJSON.Cid,
+ Operation: "create",
+ RKey: strings.Split(postJSON.URI, "/")[4],
+ Collection: "app.bsky.feed.post",
+ Record: p,
+ },
+ })
+ return
+ }
+
+ ctx := context.Background()
+ slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
+ Level: slog.LevelDebug.Level(),
+ })))
+
+ logger := slog.Default()
+ config := client.DefaultClientConfig()
+ config.WebsocketURL = serverAddr
+ config.WantedCollections = []string{"app.bsky.feed.post"}
+ config.WantedDids = []string{bskyClient.Bluesky.Cfg.DID}
+ config.Compress = true
+
+ scheduler := sequential.NewScheduler("jetstream_localdev", logger, h.HandleEvent)
+
+ c, err := client.NewClient(config, logger, scheduler)
+ if err != nil {
+ log.Fatalf("failed to create client: %v", err)
+ }
+
// ------------------------------------------------------------------------------
// file, err := os.Open("posts.json")
// if err != nil {