Live video on the AT Protocol
79
fork

Configure Feed

Select the types of activity you want to include in your feed.

at v0.9.7 209 lines 5.5 kB view raw
1package linking 2 3import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "net/url" 9 10 "golang.org/x/net/html" 11 "stream.place/streamplace/pkg/streamplace" 12) 13 14type Linker struct { 15 BaseHTML []byte 16} 17 18func NewLinker(ctx context.Context, baseHTML []byte) (*Linker, error) { 19 _, err := html.Parse(bytes.NewReader(baseHTML)) 20 if err != nil { 21 return nil, err 22 } 23 24 return &Linker{BaseHTML: baseHTML}, nil 25} 26 27type PageConfig struct { 28 Title string 29 Metas []MetaTag 30 SentryDSN string 31} 32 33// Define all meta tags in a structured way 34type MetaTag struct { 35 Type string // "name" or "property" 36 Key string 37 Content string 38} 39 40func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *streamplace.Livestream_LivestreamView, sentryDSN string) ([]byte, error) { 41 if u == nil { 42 return nil, errors.New("url is nil") 43 } 44 if lsv == nil { 45 return nil, errors.New("livestream view is nil") 46 } 47 ls, ok := lsv.Record.Val.(*streamplace.Livestream) 48 if !ok { 49 return nil, errors.New("livestream view is not a livestream") 50 } 51 52 titleStr := fmt.Sprintf("@%s's livestream on %s", lsv.Author.Handle, u.Host) 53 outURL := u.String() 54 55 pageTitle := fmt.Sprintf("@%s | %s", lsv.Author.Handle, u.Host) 56 57 thumbURL, _ := url.Parse(u.String()) 58 thumbURL.Path = "/xrpc/place.stream.live.getProfileCard" 59 thumbURL.RawQuery = fmt.Sprintf("id=%s", lsv.Author.Did) 60 61 // Define all meta tags 62 metaTags := []MetaTag{ 63 // Basic meta 64 {Type: "name", Key: "description", Content: ls.Title}, 65 66 // Facebook Meta Tags 67 {Type: "property", Key: "og:url", Content: u.String()}, 68 {Type: "property", Key: "og:type", Content: "website"}, 69 {Type: "property", Key: "og:title", Content: titleStr}, 70 {Type: "property", Key: "og:description", Content: ls.Title}, 71 {Type: "property", Key: "og:image", Content: thumbURL.String()}, 72 73 // Twitter Meta Tags 74 {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, 75 {Type: "property", Key: "twitter:domain", Content: u.Host}, 76 {Type: "property", Key: "twitter:url", Content: outURL}, 77 {Type: "name", Key: "twitter:title", Content: titleStr}, 78 {Type: "name", Key: "twitter:description", Content: ls.Title}, 79 {Type: "name", Key: "twitter:image", Content: thumbURL.String()}, 80 } 81 82 return l.GenerateHTML(ctx, &PageConfig{ 83 Title: pageTitle, 84 Metas: metaTags, 85 SentryDSN: sentryDSN, 86 }) 87} 88 89func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL, sentryDSN string) ([]byte, error) { 90 if u == nil { 91 return nil, errors.New("url is nil") 92 } 93 94 thumbURL, _ := url.Parse(u.String()) 95 thumbURL.Path = "/linkbanner.png" 96 97 // Define all meta tags 98 metaTags := []MetaTag{ 99 // Basic meta 100 {Type: "name", Key: "description", Content: "Stream.place is open-source livestreaming on the AT Protocol."}, 101 102 // Facebook Meta Tags 103 {Type: "property", Key: "og:url", Content: u.String()}, 104 {Type: "property", Key: "og:type", Content: "website"}, 105 {Type: "property", Key: "og:title", Content: "Stream.place"}, 106 {Type: "property", Key: "og:description", Content: "Stream.place is open-source livestreaming on the AT Protocol."}, 107 {Type: "property", Key: "og:image", Content: thumbURL.String()}, 108 109 // Twitter Meta Tags 110 {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, 111 {Type: "property", Key: "twitter:domain", Content: u.Host}, 112 {Type: "property", Key: "twitter:url", Content: u.String()}, 113 {Type: "name", Key: "twitter:title", Content: "Stream.place"}, 114 {Type: "name", Key: "twitter:description", Content: "Stream.place is open-source livestreaming on the AT Protocol."}, 115 {Type: "name", Key: "twitter:image", Content: thumbURL.String()}, 116 } 117 118 return l.GenerateHTML(ctx, &PageConfig{ 119 Title: "Stream.place", 120 Metas: metaTags, 121 SentryDSN: sentryDSN, 122 }) 123} 124 125func (l *Linker) GenerateHTML(ctx context.Context, pc *PageConfig) ([]byte, error) { 126 127 root, err := html.Parse(bytes.NewReader(l.BaseHTML)) 128 if err != nil { 129 return nil, err 130 } 131 132 var htmlNode *html.Node 133 for node := range root.ChildNodes() { 134 if node.Type == html.ElementNode && node.Data == "html" { 135 htmlNode = node 136 break 137 } 138 } 139 if htmlNode == nil { 140 return nil, errors.New("html not found") 141 } 142 143 var head *html.Node 144 for node := range htmlNode.ChildNodes() { 145 if node.Data == "head" { 146 head = node 147 break 148 } 149 } 150 if head == nil { 151 return nil, errors.New("head not found") 152 } 153 154 // Title tag (handled separately as it's not a meta tag) 155 156 var oldTitle *html.Node 157 for node := range head.ChildNodes() { 158 if node.Type == html.ElementNode && node.Data == "title" { 159 oldTitle = node 160 break 161 } 162 } 163 if oldTitle != nil { 164 head.RemoveChild(oldTitle) 165 } 166 167 title := &html.Node{ 168 Type: html.ElementNode, 169 Data: "title", 170 } 171 head.AppendChild(title) 172 title.AppendChild(&html.Node{ 173 Type: html.TextNode, 174 Data: pc.Title, 175 }) 176 177 // Add all meta tags in a loop 178 for _, tag := range pc.Metas { 179 head.AppendChild(&html.Node{ 180 Type: html.ElementNode, 181 Data: "meta", 182 Attr: []html.Attribute{ 183 {Key: tag.Type, Val: tag.Key}, 184 {Key: "content", Val: tag.Content}, 185 }, 186 }) 187 } 188 189 // Add Sentry DSN script if configured 190 if pc.SentryDSN != "" { 191 script := &html.Node{ 192 Type: html.ElementNode, 193 Data: "script", 194 } 195 head.AppendChild(script) 196 script.AppendChild(&html.Node{ 197 Type: html.TextNode, 198 Data: `window.SENTRY_DSN = "` + pc.SentryDSN + `";`, 199 }) 200 } 201 202 // Render the HTML to a string 203 var buf bytes.Buffer 204 if err := html.Render(&buf, root); err != nil { 205 return nil, err 206 } 207 208 return buf.Bytes(), nil 209}