From 935da56d0b759519085755c9fa5c447e77f74825 Mon Sep 17 00:00:00 2001 From: Erisa A Date: Wed, 25 Dec 2024 04:43:22 +0000 Subject: [PATCH] cmd/bsky-webhook: add support for facets Reference: https://docs.bsky.app/docs/advanced-guides/post-richtext Fixes #9 Signed-off-by: Erisa A --- cmd/bsky-webhook/facets.go | 90 ++++++++++++++++++++++++++++++++++++++ cmd/bsky-webhook/main.go | 14 +++++- cmd/bsky-webhook/types.go | 29 ++++++++++-- 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 cmd/bsky-webhook/facets.go diff --git a/cmd/bsky-webhook/facets.go b/cmd/bsky-webhook/facets.go new file mode 100644 index 0000000..7087fdf --- /dev/null +++ b/cmd/bsky-webhook/facets.go @@ -0,0 +1,90 @@ +package main + +import ( + "fmt" + "slices" + "strings" +) + +func bskyMessageToSlackMarkup(bskyMessage BskyMessage) (string, error) { + var slackStringBuilder strings.Builder + + fragments, err := facetsToFragments(bskyMessage) + if err != nil { + return "", err + } + + for _, fragment := range fragments { + if fragment.Features == nil { + slackStringBuilder.WriteString(fragment.Text) + } else { + uri := "" + for _, feature := range fragment.Features { + if feature.Type == "app.bsky.richtext.facet#link" { + uri = feature.Uri + break + } else if feature.Type == "app.bsky.richtext.facet#mention" { + uri = fmt.Sprintf("https://bsky.app/profile/%s", feature.Did) + break + } else if feature.Type == "app.bsky.richtext.facet#tag" { + uri = fmt.Sprintf("https://bsky.app/hashtag/%s", feature.Tag) + } + } + if uri != "" { + slackStringBuilder.WriteString(fmt.Sprintf("<%s|%s>", uri, fragment.Text)) + } else { + slackStringBuilder.WriteString(fragment.Text) + } + } + } + + return slackStringBuilder.String(), nil +} + +func facetsToFragments(bskyMessage BskyMessage) ([]BskyTextFragment, error) { + facets := bskyMessage.Commit.Record.Facets + runes := []rune(bskyMessage.Commit.Record.Text) + + fragments := []BskyTextFragment{} + + slices.SortStableFunc(facets, func(a, b BskyFacet) int { + return a.Index.ByteStart - b.Index.ByteStart + }) + + textCursor := 0 + facetCursor := 0 + + for facetCursor < len(facets) { + currentFacet := facets[facetCursor] + + if textCursor < currentFacet.Index.ByteStart { + fragments = append(fragments, BskyTextFragment{Text: string(runes[textCursor:currentFacet.Index.ByteStart])}) + } else if textCursor > currentFacet.Index.ByteStart { + facetCursor++ + continue + } + + if currentFacet.Index.ByteStart < currentFacet.Index.ByteEnd { + fragmentText := string(runes[currentFacet.Index.ByteStart:currentFacet.Index.ByteEnd]) + + // dont add the features if the text is blank + if strings.TrimSpace(fragmentText) == "" { + fragments = append(fragments, BskyTextFragment{ + Text: string(runes[currentFacet.Index.ByteStart:currentFacet.Index.ByteEnd]), + }) + } else { + fragments = append(fragments, BskyTextFragment{ + Text: string(runes[currentFacet.Index.ByteStart:currentFacet.Index.ByteEnd]), + Features: currentFacet.Features, + }) + } + } + textCursor = currentFacet.Index.ByteEnd + facetCursor++ + } + if textCursor < len(runes) { + fragments = append(fragments, BskyTextFragment{Text: string(runes[textCursor:])}) + } + + return fragments, nil +} diff --git a/cmd/bsky-webhook/main.go b/cmd/bsky-webhook/main.go index 1755b79..bd1f333 100644 --- a/cmd/bsky-webhook/main.go +++ b/cmd/bsky-webhook/main.go @@ -288,12 +288,24 @@ func getBskyProfile(ctx context.Context, bskyMessage BskyMessage, bsky *bluesky. } func sendToSlack(ctx context.Context, jetstreamMessageStr string, bskyMessage BskyMessage, imageURL string, profile bluesky.Profile, postTime time.Time) error { + var messageText string + var err error + + if len(bskyMessage.Commit.Record.Facets) != 0 { + messageText, err = bskyMessageToSlackMarkup(bskyMessage) + if err != nil { + return err + } + } else { + messageText = bskyMessage.Commit.Record.Text + } + attachments := []SlackAttachment{ { AuthorName: fmt.Sprintf("%s (@%s)", profile.Name, profile.Handle), AuthorIcon: profile.AvatarURL, AuthorLink: fmt.Sprintf("https://bsky.app/profile/%s", profile.Handle), - Text: fmt.Sprintf("%s\n<%s|View post on Bluesky ↗>", bskyMessage.Commit.Record.Text, bskyMessage.toURL(&profile.Handle)), + Text: fmt.Sprintf("%s\n<%s|View post on Bluesky ↗>", messageText, bskyMessage.toURL(&profile.Handle)), ImageUrl: imageURL, Footer: "Posted", Ts: strconv.FormatInt(postTime.Unix(), 10), diff --git a/cmd/bsky-webhook/types.go b/cmd/bsky-webhook/types.go index e480c04..8cc3a99 100644 --- a/cmd/bsky-webhook/types.go +++ b/cmd/bsky-webhook/types.go @@ -29,9 +29,10 @@ type BskyCommit struct { } type BskyRecord struct { - Text string `json:"text"` - Embed BskyEmbed `json:"embed"` - CreatedAtString string `json:"createdAt"` + Text string `json:"text"` + Embed BskyEmbed `json:"embed"` + CreatedAtString string `json:"createdAt"` + Facets []BskyFacet `json:"facets"` } type BskyEmbed struct { @@ -50,6 +51,28 @@ type BskyImageRef struct { Link string `json:"$link"` } +type BskyFacet struct { + Features []BskyFacetFeatures `json:"features"` + Index BskyFacetIndex `json:"index"` +} + +type BskyFacetFeatures struct { + Type string `json:"$type"` + Uri string `json:"uri"` + Did string `json:"did"` + Tag string `json:"tag"` +} + +type BskyFacetIndex struct { + ByteEnd int `json:"byteEnd"` + ByteStart int `json:"byteStart"` +} + +type BskyTextFragment struct { + Text string + Features []BskyFacetFeatures +} + type SlackAttachment struct { AuthorName string `json:"author_name"` AuthorIcon string `json:"author_icon"`