diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index 35e0797..8b5eaef 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -17,6 +17,8 @@ jobs: - name: Set up Docker Build Push Action uses: https://github.com/docker/build-push-action@v2 with: - tags: git.zio.sh/astra/bsky2tg:latest + tags: | + git.zio.sh/astra/bsky2tg:latest + git.zio.sh/astra/bsky2tg:${{ github.sha }} push: true load: false \ No newline at end of file diff --git a/README.md b/README.md index b61cf15..8ee216b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ BSKY_HANDLE= BSKY_PASSWORD= ``` +If you use a different Telegram bot endpoint, you can set it with + +```properties +TG_API_ENDPOINT=https://api.domain.com/bot%s/%s +``` + To run: ```bash diff --git a/bsky/bluesky.go b/bsky/bluesky.go index 38422f7..1ca7bd5 100644 --- a/bsky/bluesky.go +++ b/bsky/bluesky.go @@ -127,7 +127,7 @@ func (bluesky *Bluesky) CheckSessionValid() { type TelegramRecord struct { ChannelID int64 `json:"channel_id"` - MessageID int `json:"message_id"` + MessageID []int `json:"message_id"` Link *Link `json:"link"` Error string `json:"error"` Message string `json:"message"` diff --git a/bsky/parse.go b/bsky/parse.go index c600af9..a06f131 100644 --- a/bsky/parse.go +++ b/bsky/parse.go @@ -18,42 +18,50 @@ type Post struct { Facets *[]Facets `json:"facets,omitempty"` CreatedAt time.Time `json:"createdAt"` } + type Ref struct { Link string `json:"$link,omitempty"` } + type Thumb struct { Type string `json:"$type,omitempty"` Ref *Ref `json:"ref,omitempty"` MimeType string `json:"mimeType,omitempty"` Size int `json:"size,omitempty"` } + type External struct { URI string `json:"uri,omitempty"` Thumb *Thumb `json:"thumb,omitempty"` Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` } + type Video struct { Type string `json:"$type,omitempty"` Ref *Ref `json:"ref,omitempty"` MimeType string `json:"mimeType,omitempty"` Size int `json:"size,omitempty"` } + type Image struct { Type string `json:"$type,omitempty"` Ref *Ref `json:"ref,omitempty"` MimeType string `json:"mimeType,omitempty"` Size int `json:"size,omitempty"` } + type AspectRatio struct { Width int `json:"width,omitempty"` Height int `json:"height,omitempty"` } + type Images struct { Alt string `json:"alt,omitempty"` Image *Image `json:"image,omitempty"` AspectRatio *AspectRatio `json:"aspectRatio,omitempty"` } + type Media struct { Type string `json:"$type,omitempty"` External *External `json:"external,omitempty"` @@ -61,16 +69,19 @@ type Media struct { Images *[]Images `json:"images,omitempty"` AspectRatio *AspectRatio `json:"aspectRatio,omitempty"` } + type Record struct { Cid string `json:"cid,omitempty"` URI string `json:"uri,omitempty"` } + type PostRecord struct { Type string `json:"$type,omitempty"` Cid string `json:"cid,omitempty"` URI string `json:"uri,omitempty"` Record *Record `json:"record,omitempty"` } + type Embed struct { Type string `json:"$type,omitempty"` Media *Media `json:"media,omitempty"` @@ -79,35 +90,59 @@ type Embed struct { Record *PostRecord `json:"record,omitempty"` External *External `json:"external,omitempty"` } + type Values struct { Val string `json:"val,omitempty"` } + type Labels struct { Type string `json:"$type,omitempty"` Values *[]Values `json:"values,omitempty"` } + type Root struct { Cid string `json:"cid,omitempty"` URI string `json:"uri,omitempty"` } + +func (r *Root) GetDID() string { + return strings.Split(r.URI, "/")[2] +} + +func (r *Root) GetRKey() string { + return strings.Split(r.URI, "/")[4] +} + type Parent struct { Cid string `json:"cid,omitempty"` URI string `json:"uri,omitempty"` } + +func (p *Parent) GetDID() string { + return strings.Split(p.URI, "/")[2] +} + +func (p *Parent) GetRKey() string { + return strings.Split(p.URI, "/")[4] +} + type Reply struct { Root *Root `json:"root,omitempty"` Parent *Parent `json:"parent,omitempty"` } + type Index struct { ByteEnd int `json:"byteEnd,omitempty"` ByteStart int `json:"byteStart,omitempty"` } + type Features struct { Did string `json:"did,omitempty"` URI string `json:"uri,omitempty"` Tag string `json:"tag,omitempty"` Type string `json:"$type,omitempty"` } + type Facets struct { Type string `json:"$type"` Index *Index `json:"index,omitempty"` @@ -118,6 +153,7 @@ type ParsedEmbeds struct { Type string MimeType string Ref string + Cid string URI string Width int64 Height int64 diff --git a/main.go b/main.go index f59e825..2f4ac7e 100644 --- a/main.go +++ b/main.go @@ -21,8 +21,6 @@ import ( "git.zio.sh/astra/bsky2tg/bsky" 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" @@ -86,11 +84,15 @@ func main() { handle, _ = bskyClient.ResolveHandle(s[1]) } + if handle != bskyClient.Bluesky.Cfg.DID { + log.Fatal("Unable to send posts from other accounts") + } + + tgpost, tgposterr := h.bsky.Bluesky.GetTelegramData(s[2]) if *delete { - r, e := h.bsky.Bluesky.GetTelegramData(s[2]) - if e == "" { - log.Printf("Found post %s in channel %d, deleting", s[2], r.ChannelID) - m := tgbotapi.NewDeleteMessage(r.ChannelID, r.MessageID) + if tgposterr == "" { + log.Printf("Found post %s in channel %d, deleting", s[2], tgpost.ChannelID) + m := tgbotapi.NewDeleteMessages(tgpost.ChannelID, tgpost.MessageID) h.tg.Send(m) h.bsky.Bluesky.DeleteRecord([]string{s[2], s[1], "blue.zio.bsky2tg.post"}) } else { @@ -99,6 +101,11 @@ func main() { return } + if tgpost.ChannelID != 0 { + log.Printf("Post %s already sent to channel %d, exiting", s[2], tgpost.ChannelID) + return + } + postJSON := bskyClient.Bluesky.FetchPost(handle, s[2]) p, _ := json.Marshal(postJSON.Record) h.ProcessPost(&models.Event{ @@ -135,47 +142,6 @@ func main() { log.Fatalf("failed to create client: %v", err) } - // ------------------------------------------------------------------------------ - // file, err := os.Open("posts.json") - // if err != nil { - // fmt.Printf("Error opening file: %v\n", err) - // return - // } - // defer file.Close() - // byteValue, err := io.ReadAll(file) - // if err != nil { - // fmt.Printf("Error reading file: %v\n", err) - // return - // } - - // var posts = struct { - // Records []struct { - // URI string `json:"uri"` - // CID string `json:"cid"` - // Value *bsky.Post `json:"value"` - // } `json:"records"` - // }{} - - // // 4. Unmarshal (decode) the JSON data into the struct - // err = json.Unmarshal(byteValue, &posts) - // if err != nil { - // fmt.Printf("Error unmarshaling JSON: %v\n", err) - // return - // } - // for _, post := range posts.Records { - // log.Printf("post: %s\n", post.Value.ProcessFacets(h.bsky.Bluesky.FetchAliases())) - // s, _ := json.Marshal(post.Value) - // h.ProcessPost(&models.Event{Did: bskyClient.Bluesky.Cfg.DID, Commit: &models.Commit{ - // Record: s, - // RKey: strings.Split(post.URI, "/")[4], - // CID: post.CID, - // Collection: "app.bsky.feed.post", - // }}) - // time.Sleep(time.Second * 2) - // } - // return - // ------------------------------------------------------------------------------ - cursor := time.Now().UnixMicro() restartCount := 0 loop: @@ -208,7 +174,7 @@ func (h *handler) HandleEvent(ctx context.Context, event *models.Event) error { bsky.PersistAuthSession(h.bsky.Bluesky.Cfg) r, e := h.bsky.Bluesky.GetTelegramData(event.Commit.RKey) if e == "" { - m := tgbotapi.NewDeleteMessage(r.ChannelID, r.MessageID) + m := tgbotapi.NewDeleteMessages(r.ChannelID, r.MessageID) h.tg.Send(m) h.bsky.Bluesky.DeleteRecord([]string{event.Commit.RKey, event.Did, "blue.zio.bsky2tg.post"}) } @@ -317,9 +283,13 @@ func (h *handler) ProcessPost(event *models.Event) error { } else { resp, _ := h.tg.SendMediaGroup(tgbotapi.NewMediaGroup(cid, mediaGroup)) uri, cid := getLink(event) + var messageIDs []int + for _, msgID := range resp { + messageIDs = append(messageIDs, msgID.MessageID) + } h.bsky.Bluesky.CommitTelegramResponse(&bsky.TelegramRecord{ ChannelID: resp[0].Chat.ID, - MessageID: resp[0].MessageID, + MessageID: messageIDs, Link: &bsky.Link{ Cid: cid, URI: uri, @@ -337,10 +307,11 @@ func (h *handler) ProcessPost(event *models.Event) error { if ps.IsQuotePost() { m.LinkPreviewOptions = tgbotapi.LinkPreviewOptions{ IsDisabled: false, - URL: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", + URL: fmt.Sprintf("https://fxbsky.app/profile/%s/post/%s", strings.Split(ps.Embed.Record.URI, "/")[2], strings.Split(ps.Embed.Record.URI, "/")[4]), - PreferSmallMedia: true, + PreferSmallMedia: false, + PreferLargeMedia: true, ShowAboveText: true, } } else { @@ -350,7 +321,7 @@ func (h *handler) ProcessPost(event *models.Event) error { uri, cid := getLink(event) h.bsky.Bluesky.CommitTelegramResponse(&bsky.TelegramRecord{ ChannelID: resp.Chat.ID, - MessageID: resp.MessageID, + MessageID: []int{resp.MessageID}, Link: &bsky.Link{ Cid: cid, URI: uri, @@ -361,7 +332,7 @@ func (h *handler) ProcessPost(event *models.Event) error { } func buildBlobURL(server string, did string, cid string) string { - return server + "/xrpc/com.atproto.sync.getBlob?did=" + url.QueryEscape(did) + "&cid=" + url.QueryEscape(cid) + return server + "/xrpc/com.atproto.sync.getBlob?did=" + url.QueryEscape(did) + "&cid=" + cid } func getLink(event *models.Event) (string, string) {