Compare commits
20 commits
Author | SHA1 | Date | |
---|---|---|---|
c4d4e91548 | |||
acbdd41680 | |||
5ff08f5acc | |||
dc7382f162 | |||
aff13c04dd | |||
1690279d5c | |||
bd8a437f43 | |||
dbc89e5b95 | |||
be8b787c52 | |||
ce0709f72d | |||
4f94ea647c | |||
21722264d1 | |||
f572ee3958 | |||
bf2b621171 | |||
bfa829d8c7 | |||
2bb3946237 | |||
e0a63bd7d5 | |||
798f8134f4 | |||
f67964b5fd | |||
0298c21668 |
7 changed files with 359 additions and 82 deletions
24
.forgejo/workflows/build.yml
Normal file
24
.forgejo/workflows/build.yml
Normal file
|
@ -0,0 +1,24 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
uses: https://github.com/docker/login-action@v3
|
||||
with:
|
||||
registry: git.zio.sh
|
||||
username: ${{ secrets.REPO_USER }}
|
||||
password: ${{ secrets.REPO_PASS }}
|
||||
- name: Set up Docker Build Push Action
|
||||
uses: https://github.com/docker/build-push-action@v2
|
||||
with:
|
||||
tags: |
|
||||
git.zio.sh/astra/bsky2tg:latest
|
||||
git.zio.sh/astra/bsky2tg:${{ github.sha }}
|
||||
push: true
|
||||
load: false
|
15
Dockerfile
Normal file
15
Dockerfile
Normal file
|
@ -0,0 +1,15 @@
|
|||
FROM golang:alpine AS builder
|
||||
|
||||
WORKDIR /go/src/git.zio.sh/bsky2tg
|
||||
COPY . .
|
||||
|
||||
RUN apk update && \
|
||||
apk add --no-cache git bash && \
|
||||
go get -d -v ./... && \
|
||||
go install
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
COPY --from=builder /go/bin/bsky2tg /usr/local/bin/bsky2tg
|
||||
|
||||
CMD ["bsky2tg"]
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
200
bsky/parse.go
200
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,11 +153,166 @@ type ParsedEmbeds struct {
|
|||
Type string
|
||||
MimeType string
|
||||
Ref string
|
||||
Cid string
|
||||
URI string
|
||||
Width int64
|
||||
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 +350,10 @@ func (post *Post) ProcessFacets(aliases []Records) string {
|
|||
switch feature.Type {
|
||||
case "app.bsky.richtext.facet#mention":
|
||||
link := fmt.Sprintf(`<a href="https://bsky.app/profile/%s">%s</a>`, feature.Did, post.Text[start:end])
|
||||
if aliases != nil {
|
||||
for _, alias := range aliases {
|
||||
if alias.Value.Subject == feature.Did {
|
||||
link = fmt.Sprintf(`<a href="%s">%s</a>`,
|
||||
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(`<a href="%s">%s</a>`,
|
||||
strings.SplitN(alias.Value.Target, "#", 2)[0], strings.SplitN(alias.Value.Target, "#", 2)[1])
|
||||
}
|
||||
}
|
||||
result.WriteString(link)
|
||||
|
|
158
main.go
158
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"
|
||||
|
@ -18,7 +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"
|
||||
|
@ -39,7 +41,14 @@ type handler struct {
|
|||
bsky *bsky.BSky
|
||||
}
|
||||
|
||||
var (
|
||||
post = flag.String("post", "", "URL to a BlueSky post")
|
||||
delete = flag.Bool("delete", false, "true/false to delete post")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var handle = os.Getenv("BSKY_HANDLE")
|
||||
var password = os.Getenv("BSKY_PASSWORD")
|
||||
bskyClient := bsky.NewBSky()
|
||||
|
@ -48,6 +57,72 @@ func main() {
|
|||
log.Fatal(err, ". please set BSKY_HANDLE and BSKY_PASSWORD env variables")
|
||||
}
|
||||
|
||||
h := &handler{
|
||||
seenSeqs: make(map[int64]struct{}),
|
||||
bsky: bskyClient,
|
||||
}
|
||||
|
||||
endpoint := "https://api.telegram.org/bot%s/%s"
|
||||
if os.Getenv("TG_API_ENDPOINT") != "" {
|
||||
endpoint = os.Getenv("TG_API_ENDPOINT")
|
||||
}
|
||||
bot, err := tgbotapi.NewBotAPIWithAPIEndpoint(os.Getenv("TG_TOKEN"), endpoint)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
h.tg = bot
|
||||
|
||||
if os.Getenv("TG_CHANNEL_ID") == "" {
|
||||
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])
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
log.Printf("Unable to find post %s on PDS", s[2])
|
||||
}
|
||||
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{
|
||||
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(),
|
||||
|
@ -60,11 +135,6 @@ func main() {
|
|||
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)
|
||||
|
@ -72,57 +142,6 @@ func main() {
|
|||
log.Fatalf("failed to create client: %v", err)
|
||||
}
|
||||
|
||||
bot, err := tgbotapi.NewBotAPIWithAPIEndpoint(os.Getenv("TG_TOKEN"), "https://bot.astra.blue/bot%s/%s")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
h.tg = bot
|
||||
|
||||
if os.Getenv("TG_CHANNEL_ID") == "" {
|
||||
log.Fatal("TG_CHANNEL_ID is not set")
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// 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:
|
||||
|
@ -145,19 +164,19 @@ func (h *handler) HandleEvent(ctx context.Context, event *models.Event) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if event.Commit.Operation == models.CommitOperationCreate ||
|
||||
event.Commit.Operation == models.CommitOperationUpdate {
|
||||
switch event.Commit.Operation {
|
||||
case models.CommitOperationCreate, models.CommitOperationUpdate:
|
||||
h.bsky.Bluesky.Cfg.Cursor = event.TimeUS + 1 // +1 to not show same post
|
||||
bsky.PersistAuthSession(h.bsky.Bluesky.Cfg)
|
||||
h.ProcessPost(event)
|
||||
} else if event.Commit.Operation == models.CommitOperationDelete {
|
||||
case models.CommitOperationDelete:
|
||||
h.bsky.Bluesky.Cfg.Cursor = event.TimeUS + 1 // +1 to not show same post
|
||||
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, event.Commit.Collection})
|
||||
h.bsky.Bluesky.DeleteRecord([]string{event.Commit.RKey, event.Did, "blue.zio.bsky2tg.post"})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -264,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,
|
||||
|
@ -284,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 {
|
||||
|
@ -297,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,
|
||||
|
@ -308,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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue