Live video on the AT Protocol
at eli/rtmp-rec 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}