bsky2tg/bsky/parse.go
2025-09-13 17:45:28 +01:00

470 lines
12 KiB
Go

package bsky
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
type Post struct {
Text string `json:"text,omitempty"`
Type string `json:"$type,omitempty"`
Embed *Embed `json:"embed,omitempty"`
Langs []string `json:"langs,omitempty"`
Labels *Labels `json:"labels,omitempty"`
Reply *Reply `json:"reply,omitempty"`
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"`
Video *Video `json:"video,omitempty"`
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"`
Images *[]Images `json:"images,omitempty"`
Video *Video `json:"video,omitempty"`
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"`
Features *[]Features `json:"features,omitempty"`
}
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)
if err != nil {
return nil, err
}
return p, nil
}
func (post *Post) ProcessFacets(aliases []Records) string {
if post == nil {
return ""
}
if post.Facets == nil {
return post.Text
}
sort.Slice((*post.Facets), func(i, j int) bool {
return (*post.Facets)[i].Index.ByteStart < (*post.Facets)[j].Index.ByteStart
})
var result strings.Builder
lastIndex := 0
// post.Text = html.EscapeString(post.Text)
for _, facet := range *post.Facets {
start := facet.Index.ByteStart
end := facet.Index.ByteEnd
result.WriteString(post.Text[lastIndex:start])
for _, feature := range *facet.Features {
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])
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)
case "app.bsky.richtext.facet#link":
link := fmt.Sprintf(`<a href="%s">%s</a>`, feature.URI, post.Text[start:end])
result.WriteString(link)
case "app.bsky.richtext.facet#tag":
link := fmt.Sprintf(`<a href="https://bsky.app/search?q=%%23%s">%s</a>`, feature.Tag, post.Text[start:end])
result.WriteString(link)
default:
result.WriteString(post.Text[start:end])
}
}
lastIndex = end
}
result.WriteString(post.Text[lastIndex:])
return result.String()
}
func (p *Post) GetEmbeds() *[]ParsedEmbeds {
var parsedEmbeds = &[]ParsedEmbeds{}
if p.Embed != nil {
if p.Embed.Video != nil {
parsedEmbed := ParsedEmbeds{
URI: p.Embed.Video.Ref.Link,
Type: "video",
}
*parsedEmbeds = append(*parsedEmbeds, parsedEmbed)
}
if p.Embed.External != nil {
if strings.Contains(p.Embed.External.URI, "media.tenor.com") {
parsedEmbed := ParsedEmbeds{
URI: p.Embed.External.URI,
Type: "external",
}
*parsedEmbeds = append(*parsedEmbeds, parsedEmbed)
}
}
if p.Embed.Media != nil {
if p.Embed.Media.Images != nil {
for _, image := range *p.Embed.Media.Images {
parsedEmbed := ParsedEmbeds{
URI: image.Image.Ref.Link,
Type: "image",
}
*parsedEmbeds = append(*parsedEmbeds, parsedEmbed)
}
}
if p.Embed.Media.Video != nil {
parsedEmbed := ParsedEmbeds{
URI: p.Embed.Media.Video.Ref.Link,
Type: "video",
}
*parsedEmbeds = append(*parsedEmbeds, parsedEmbed)
}
if p.Embed.Media.External != nil {
parsedEmbed := ParsedEmbeds{
URI: p.Embed.Media.External.URI,
Type: "external",
}
*parsedEmbeds = append(*parsedEmbeds, parsedEmbed)
}
}
if p.Embed.Images != nil {
for _, image := range *p.Embed.Images {
parsedEmbed := ParsedEmbeds{
URI: image.Image.Ref.Link,
Type: "image",
}
*parsedEmbeds = append(*parsedEmbeds, parsedEmbed)
}
}
}
return parsedEmbeds
}
func (p *Post) GetMedia() *Media {
if p.GetEmbeds() != nil {
if p.Embed.Media != nil {
return p.Embed.Media
}
}
return nil
}
func (p *Post) GetMediaImages() *[]Images {
if p.GetMedia() != nil {
return p.GetMedia().Images
}
return nil
}
func (p *Post) GetExternal() *External {
if p.GetMedia() != nil {
if p.GetMedia().External != nil {
return p.GetMedia().External
}
}
return nil
}
func (p *Post) IsReply() bool {
return p.Reply != nil
}
func (p *Post) IsQuotePost() bool {
if p.Embed != nil {
if p.Embed.Record != nil {
return true
}
}
return false
}