-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
cmd/bsky-webhook: add support for facets #11
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"slices" | ||
"strings" | ||
) | ||
|
||
func bskyMessageToSlackMarkup(bskyMessage BskyMessage) (string, error) { | ||
var slackStringBuilder strings.Builder | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this needs to say so much, there's only one string builder here: var sb 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) | ||
} | ||
} | ||
Comment on lines
+18
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this loop could be simplified quite a bit with a helper method: func (b BskyFacet) featureURI() string {
for _, feat := range b.Features {
switch feat.URI {
case "app.bsky.richtext.facet#link":
return feat.URI
case "app.bsky.richtext.facet#mention":
return fmt.Sprintf("https://bsky.app/profile/%s", feat.DID)
case "app.bsky.richtext.facet#tag":
return fmt.Sprintf("https://bsky.app/hashtag/%s", feat.Tag)
}
}
return ""
} Then the whole body of the (outer) loop can be something like: for _, frag := range fragments {
if uri := frag.featureURI(); uri != "" {
fmt.Fprintf(&sb, "<%s|%s>", uri, frag.Text)
} else {
sb.WriteString(frag.Text)
}
} (Note as illustrated here, you can use the builder as an |
||
} | ||
|
||
return slackStringBuilder.String(), nil | ||
} | ||
|
||
func facetsToFragments(bskyMessage BskyMessage) ([]BskyTextFragment, error) { | ||
facets := bskyMessage.Commit.Record.Facets | ||
textString := bskyMessage.Commit.Record.Text | ||
|
||
fragments := []BskyTextFragment{} | ||
|
||
slices.SortStableFunc(facets, func(a, b BskyFacet) int { | ||
return a.Index.ByteStart - b.Index.ByteStart | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return cmp.Compare(a.Index.ByteStart, b.Index.ByteStart) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though if you really care about stability, maybe what we want here is a tie breaker, e.g., if c := cmp.Compare(a.Index.ByteStart, b.Index.ByteStart); c != 0 {
return c
}
return cmp.Compare(b.Index.ByteEnd, a.Index.ByteEnd) or something like that (so they have proper nesting order within the ties) YMMV. |
||
}) | ||
|
||
textCursor := 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand correctly, the delicate dance with indices here is something to do with the fact that facets are specified by offsets into the base text? Given that you went to the trouble of ordering them above, maybe this could be turned into a helper that uses I am sort of convinced this does what I described above, but index dancing is hard to follow, so I would suggest we try to be more obvious here. (Then too, I think maybe we don't need to do any additional appending, since the facet decorations could be glued directly into place on the |
||
facetCursor := 0 | ||
|
||
for facetCursor < len(facets) { | ||
currentFacet := facets[facetCursor] | ||
|
||
if textCursor < currentFacet.Index.ByteStart { | ||
fragments = append(fragments, BskyTextFragment{Text: textString[textCursor:currentFacet.Index.ByteStart]}) | ||
} else if textCursor > currentFacet.Index.ByteStart { | ||
facetCursor++ | ||
continue | ||
} | ||
|
||
if currentFacet.Index.ByteStart < currentFacet.Index.ByteEnd { | ||
fragmentText := textString[currentFacet.Index.ByteStart:currentFacet.Index.ByteEnd] | ||
|
||
// dont add the features if the text is blank | ||
if strings.TrimSpace(fragmentText) == "" { | ||
fragments = append(fragments, BskyTextFragment{ | ||
Text: textString[currentFacet.Index.ByteStart:currentFacet.Index.ByteEnd], | ||
}) | ||
} else { | ||
fragments = append(fragments, BskyTextFragment{ | ||
Text: textString[currentFacet.Index.ByteStart:currentFacet.Index.ByteEnd], | ||
Features: currentFacet.Features, | ||
}) | ||
} | ||
} | ||
textCursor = currentFacet.Index.ByteEnd | ||
facetCursor++ | ||
} | ||
if textCursor < len(textString) { | ||
fragments = append(fragments, BskyTextFragment{Text: textString[textCursor:]}) | ||
} | ||
|
||
return fragments, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could maybe have |
||
} | ||
|
||
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), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe a comment noting the expected format? (Is this RFC3339 or something?) |
||
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"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By convention these should be |
||
Did string `json:"did"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need the names explicitly here? It appears we only use these for decoding, and the JSON decoder does a case-insensitive match anyway. (We do need |
||
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"` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An optional general suggestion:
For parameters in small scopes where their meaning is close to its use, it's fine (and indeed conventionally preferable) to choose less verbose names. Since the type is right here, this could be
message
or even simplymsg
, there is only one message involved here.In particular, I recommend against re-stating the type of a declared variable in the name, unless for some reason it makes a distinction (e.g., "fooString" vs. "fooBytes" if we had both formats to deal with)