Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
14 kB · 476 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477package linking
import ( "bytes" "context" "encoding/json" "errors" "fmt" "net/url" "strings"
"stream.place/streamplace/pkg/log"
"golang.org/x/net/html" "stream.place/streamplace/pkg/config" "stream.place/streamplace/pkg/placestream" "stream.place/streamplace/pkg/statedb")
type Linker struct { BaseHTML []byte sdb *statedb.StatefulDB cli *config.CLI}
func NewLinker(ctx context.Context, baseHTML []byte, sdb *statedb.StatefulDB, cli *config.CLI) (*Linker, error) { _, err := html.Parse(bytes.NewReader(baseHTML)) if err != nil { return nil, err }
return &Linker{BaseHTML: baseHTML, sdb: sdb, cli: cli}, nil}
type PageConfig struct { Title string Metas []MetaTag SentryDSN string Branding []string}
// Define all meta tags in a structured waytype MetaTag struct { Type string // "name" or "property" Key string Content string}
var BrandingAssetList = [...]string{ "siteTitle", "siteDescription", "primaryColor", "accentColor", "defaultStreamer", "mainLogo", "favicon", "sidebarBg", "legalLinks",}
// atTags returns meta tags implementing the at-tags proposal// (https://tangled.org/chrisshank.com/at-tags), which maps web pages back to// atproto identities and records via <meta name="at:*"> tags in the page// <head>://// - at:canonical — the AT URI of the record this page canonically maps to// (pass "" to omit, e.g. for pages that don't represent a record)// - at:author — the atproto identity of the page's author, as an// at://<did> URI (pass "" to omit)// - at:me — the identity of the overall website, i.e. this node's// did:web, when a broadcaster host is configuredfunc (l *Linker) atTags(canonicalURI string, authorDID string) []MetaTag { tags := make([]MetaTag, 0, 3) if strings.HasPrefix(canonicalURI, "at://") { tags = append(tags, MetaTag{Type: "name", Key: "at:canonical", Content: canonicalURI}) } if authorDID != "" { tags = append(tags, MetaTag{Type: "name", Key: "at:author", Content: "at://" + authorDID}) } if l.cli != nil && l.cli.BroadcasterHost != "" { tags = append(tags, MetaTag{Type: "name", Key: "at:me", Content: "at://did:web:" + l.cli.BroadcasterHost}) } return tags}
// fetch branding assets for a given broadcaster DIDfunc (l *Linker) getBrandingAssets(broadcasterDid string) ([]placestream.BrandingGetBranding_BrandingAsset, error) { ret := make([]placestream.BrandingGetBranding_BrandingAsset, 0) for _, asset := range BrandingAssetList { blob, err := l.sdb.GetBrandingBlob(broadcasterDid, asset) if err != nil { // this can probably include a 'record not found' error, in which case we skip log.Debug(context.Background(), "error fetching branding asset for broadcaster", "asset", asset, "broadcaster", broadcasterDid, "error", err) continue } asset := placestream.BrandingGetBranding_BrandingAsset{ Key: blob.Key, MimeType: blob.MimeType, }
if blob.Width != nil { w := int64(*blob.Width) asset.Width = &w } if blob.Height != nil { h := int64(*blob.Height) asset.Height = &h }
// process based on mime type if blob.MimeType == "text/plain" { str := string(blob.Data) asset.Data = &str } else { url := fmt.Sprintf("/xrpc/place.stream.branding.getBlob?key=%s&broadcaster=%s", blob.Key, broadcasterDid) asset.Url = &url } ret = append(ret, asset) }
return ret, nil}
func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *placestream.Livestream_LivestreamView, sentryDSN string) ([]byte, error) { if u == nil { return nil, errors.New("url is nil") } if lsv == nil { return nil, errors.New("livestream view is nil") } ls, ok := lsv.Record.Val.(*placestream.Livestream) if !ok { return nil, errors.New("livestream view is not a livestream") }
titleStr := fmt.Sprintf("@%s's livestream on ", lsv.Author.Handle) outURL := u.String()
thumbURL, _ := url.Parse(u.String()) thumbURL.Path = "/xrpc/place.stream.live.getProfileCard" thumbURL.RawQuery = fmt.Sprintf("id=%s", lsv.Author.Did)
// Define all meta tags metaTags := []MetaTag{ // Basic meta {Type: "name", Key: "description", Content: ls.Title},
// Facebook Meta Tags {Type: "property", Key: "og:url", Content: u.String()}, {Type: "property", Key: "og:type", Content: "website"}, {Type: "property", Key: "og:description", Content: ls.Title}, {Type: "property", Key: "og:image", Content: thumbURL.String()},
// Twitter Meta Tags {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, {Type: "property", Key: "twitter:domain", Content: u.Host}, {Type: "property", Key: "twitter:url", Content: outURL}, {Type: "name", Key: "twitter:description", Content: ls.Title}, {Type: "name", Key: "twitter:image", Content: thumbURL.String()}, } brandingTitle := "streamplace node" if l.sdb != nil && l.cli != nil { branding, err := l.getBrandingAssets("did:web:" + l.cli.BroadcasterHost) if err == nil { for i := range branding { val := branding[i] if val.Key == "siteTitle" && val.Data != nil { brandingTitle = *val.Data } marshalledJson, err := json.Marshal(val) if err != nil { log.Error(ctx, "error marshalling branding asset", "key", val.Key, "error", err) continue } metaTags = append(metaTags, MetaTag{ Type: "name", Key: "internal-brand:" + val.Key, Content: string(marshalledJson), }) } } else { // log but we should not block rendering log.Error(ctx, "error fetching branding assets", "error", err) } }
// do twitter/og title after metaTags = append(metaTags, MetaTag{ Type: "property", Key: "og:title", Content: fmt.Sprintf("%s%s", titleStr, brandingTitle), }) metaTags = append(metaTags, MetaTag{ Type: "name", Key: "twitter:title", Content: fmt.Sprintf("%s%s", titleStr, brandingTitle), })
// at-tags: this page canonically maps to the livestream record, authored // by the streamer metaTags = append(metaTags, l.atTags(lsv.Uri, lsv.Author.Did)...)
return l.GenerateHTML(ctx, &PageConfig{ Title: fmt.Sprintf("%s%s", titleStr, brandingTitle), Metas: metaTags, SentryDSN: sentryDSN, })}
func (l *Linker) GenerateVideoCard(ctx context.Context, u *url.URL, vv *placestream.MediaGetVideo_VideoView, sentryDSN string) ([]byte, error) { if u == nil { return nil, errors.New("url is nil") } if vv == nil { return nil, errors.New("video view is nil") } video, ok := vv.Record.Val.(*placestream.Video) if !ok { return nil, errors.New("video view record is not a video") }
outURL := u.String()
authorDid := "" authorHandle := "" if !false { authorDid = vv.Author.Did authorHandle = vv.Author.Handle }
// og:image is the VOD's own thumbnail, served through the Bluesky image // CDN (which fetches the blob from the author's PDS — the same trick used // for game cover art). When the video has no thumbnail we fall back to the // author's generated profile card so the link still carries an image. var imageURL string if video.Thumb != nil && authorDid != "" { imageURL = fmt.Sprintf( "https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s@jpeg", authorDid, video.Thumb.Ref.String(), ) } else { cardURL, _ := url.Parse(u.String()) cardURL.Path = "/xrpc/place.stream.live.getProfileCard" cardURL.RawQuery = fmt.Sprintf("id=%s", authorDid) imageURL = cardURL.String() }
// Define all meta tags. The title/description pair is appended after we // resolve the node's branding title below: the card headline is the // video's own title, and the author + node go in the description. metaTags := []MetaTag{ // Facebook Meta Tags {Type: "property", Key: "og:url", Content: outURL}, {Type: "property", Key: "og:type", Content: "video.other"}, {Type: "property", Key: "og:image", Content: imageURL},
// Twitter Meta Tags {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, {Type: "property", Key: "twitter:domain", Content: u.Host}, {Type: "property", Key: "twitter:url", Content: outURL}, {Type: "name", Key: "twitter:image", Content: imageURL}, }
brandingTitle := "streamplace node" if l.sdb != nil && l.cli != nil { branding, err := l.getBrandingAssets("did:web:" + l.cli.BroadcasterHost) if err == nil { for i := range branding { val := branding[i] if val.Key == "siteTitle" && val.Data != nil { brandingTitle = *val.Data } marshalledJson, err := json.Marshal(val) if err != nil { log.Error(ctx, "error marshalling branding asset %s", "key", val.Key, "error", err) continue } metaTags = append(metaTags, MetaTag{ Type: "name", Key: "internal-brand:" + val.Key, Content: string(marshalledJson), }) } } else { // log but we should not block rendering log.Error(ctx, "error fetching branding assets", "error", err) } }
// The card headline is the video's own title; the author + node name go // in the description. Appended after the branding loop so the description // can reference the resolved branding title. title := video.Title description := fmt.Sprintf("@%s's video on %s", authorHandle, brandingTitle) metaTags = append(metaTags, MetaTag{Type: "name", Key: "description", Content: description}, MetaTag{Type: "property", Key: "og:description", Content: description}, MetaTag{Type: "property", Key: "og:title", Content: title}, MetaTag{Type: "name", Key: "twitter:description", Content: description}, MetaTag{Type: "name", Key: "twitter:title", Content: title}, )
// at-tags: this page canonically maps to the place.stream.video record, // authored by the streamer metaTags = append(metaTags, l.atTags(vv.Uri, authorDid)...)
return l.GenerateHTML(ctx, &PageConfig{ Title: title, Metas: metaTags, SentryDSN: sentryDSN, })}
func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL, sentryDSN string) ([]byte, error) { if u == nil { return nil, errors.New("url is nil") }
thumbURL, _ := url.Parse(u.String()) thumbURL.Path = "/linkbanner.png"
// Define all meta tags metaTags := []MetaTag{ // Basic meta {Type: "name", Key: "description", Content: "Stream.place is open-source livestreaming on the AT Protocol."},
// Facebook Meta Tags {Type: "property", Key: "og:url", Content: u.String()}, {Type: "property", Key: "og:type", Content: "website"}, {Type: "property", Key: "og:title", Content: "Stream.place"}, {Type: "property", Key: "og:description", Content: "Open-source livestreaming on the AT Protocol."}, {Type: "property", Key: "og:image", Content: thumbURL.String()},
// Twitter Meta Tags {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, {Type: "property", Key: "twitter:domain", Content: u.Host}, {Type: "property", Key: "twitter:url", Content: u.String()}, {Type: "name", Key: "twitter:title", Content: "Stream.place"}, {Type: "name", Key: "twitter:description", Content: "Open-source livestreaming on the AT Protocol."}, {Type: "name", Key: "twitter:image", Content: thumbURL.String()}, }
brandingTitle := "streamplace node" if l.sdb != nil && l.cli != nil { branding, err := l.getBrandingAssets("did:web:" + l.cli.BroadcasterHost) if err == nil { for i := range branding { val := branding[i] if val.Key == "siteTitle" && val.Data != nil { brandingTitle = *val.Data } marshalledJson, err := json.Marshal(val) if err != nil { log.Error(ctx, "error marshalling branding asset", "key", val.Key, "error", err) continue } metaTags = append(metaTags, MetaTag{ Type: "name", Key: "internal-brand:" + val.Key, Content: string(marshalledJson), }) } } else { // log but we should not block rendering log.Error(ctx, "error fetching branding assets", "error", err) } }
// do twitter/og title after metaTags = append(metaTags, MetaTag{ Type: "property", Key: "og:title", Content: brandingTitle, }) metaTags = append(metaTags, MetaTag{ Type: "name", Key: "twitter:title", Content: brandingTitle, })
// at-tags: the site itself is identified by this node's did:web; there's // no single author or canonical record for the front page metaTags = append(metaTags, l.atTags("", "")...)
return l.GenerateHTML(ctx, &PageConfig{ Title: brandingTitle, Metas: metaTags, SentryDSN: sentryDSN, })}
func (l *Linker) GenerateHTML(ctx context.Context, pc *PageConfig) ([]byte, error) {
root, err := html.Parse(bytes.NewReader(l.BaseHTML)) if err != nil { return nil, err }
var htmlNode *html.Node for node := range root.ChildNodes() { if node.Type == html.ElementNode && node.Data == "html" { htmlNode = node break } } if htmlNode == nil { return nil, errors.New("html not found") }
var head *html.Node for node := range htmlNode.ChildNodes() { if node.Data == "head" { head = node break } } if head == nil { return nil, errors.New("head not found") }
// Title tag (handled separately as it's not a meta tag)
var oldTitle *html.Node for node := range head.ChildNodes() { if node.Type == html.ElementNode && node.Data == "title" { oldTitle = node break } } if oldTitle != nil { head.RemoveChild(oldTitle) }
title := &html.Node{ Type: html.ElementNode, Data: "title", } head.AppendChild(title) title.AppendChild(&html.Node{ Type: html.TextNode, Data: pc.Title, })
// Add all meta tags in a loop for _, tag := range pc.Metas { head.AppendChild(&html.Node{ Type: html.ElementNode, Data: "meta", Attr: []html.Attribute{ {Key: tag.Type, Val: tag.Key}, {Key: "content", Val: tag.Content}, }, }) }
// Add Sentry DSN script if configured if pc.SentryDSN != "" { script := &html.Node{ Type: html.ElementNode, Data: "script", } head.AppendChild(script) script.AppendChild(&html.Node{ Type: html.TextNode, Data: `window.SENTRY_DSN = "` + pc.SentryDSN + `";`, }) }
// Render the HTML to a string var buf bytes.Buffer if err := html.Render(&buf, root); err != nil { return nil, err }
return buf.Bytes(), nil}