Live video on the AT Protocol
at eli/bump-xcode 192 lines 5.0 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} 31 32// Define all meta tags in a structured way 33type MetaTag struct { 34 Type string // "name" or "property" 35 Key string 36 Content string 37} 38 39func (l *Linker) GenerateStreamerCard(ctx context.Context, u *url.URL, lsv *streamplace.Livestream_LivestreamView) ([]byte, error) { 40 if u == nil { 41 return nil, errors.New("url is nil") 42 } 43 if lsv == nil { 44 return nil, errors.New("livestream view is nil") 45 } 46 ls, ok := lsv.Record.Val.(*streamplace.Livestream) 47 if !ok { 48 return nil, errors.New("livestream view is not a livestream") 49 } 50 51 titleStr := fmt.Sprintf("@%s's livestream on %s", lsv.Author.Handle, u.Host) 52 outURL := u.String() 53 54 pageTitle := fmt.Sprintf("@%s | %s", lsv.Author.Handle, u.Host) 55 56 thumbURL, _ := url.Parse(u.String()) 57 thumbURL.Path = fmt.Sprintf("/api/playback/%s/stream.jpg", lsv.Author.Did) 58 59 // Define all meta tags 60 metaTags := []MetaTag{ 61 // Basic meta 62 {Type: "name", Key: "description", Content: ls.Title}, 63 64 // Facebook Meta Tags 65 {Type: "property", Key: "og:url", Content: u.String()}, 66 {Type: "property", Key: "og:type", Content: "website"}, 67 {Type: "property", Key: "og:title", Content: titleStr}, 68 {Type: "property", Key: "og:description", Content: ls.Title}, 69 {Type: "property", Key: "og:image", Content: thumbURL.String()}, 70 71 // Twitter Meta Tags 72 {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, 73 {Type: "property", Key: "twitter:domain", Content: u.Host}, 74 {Type: "property", Key: "twitter:url", Content: outURL}, 75 {Type: "name", Key: "twitter:title", Content: titleStr}, 76 {Type: "name", Key: "twitter:description", Content: ls.Title}, 77 {Type: "name", Key: "twitter:image", Content: thumbURL.String()}, 78 } 79 80 return l.GenerateHTML(ctx, &PageConfig{ 81 Title: pageTitle, 82 Metas: metaTags, 83 }) 84} 85 86func (l *Linker) GenerateDefaultCard(ctx context.Context, u *url.URL) ([]byte, error) { 87 if u == nil { 88 return nil, errors.New("url is nil") 89 } 90 91 thumbURL, _ := url.Parse(u.String()) 92 thumbURL.Path = "/linkbanner.png" 93 94 // Define all meta tags 95 metaTags := []MetaTag{ 96 // Basic meta 97 {Type: "name", Key: "description", Content: "Stream.place is open-source livestreaming on the AT Protocol."}, 98 99 // Facebook Meta Tags 100 {Type: "property", Key: "og:url", Content: u.String()}, 101 {Type: "property", Key: "og:type", Content: "website"}, 102 {Type: "property", Key: "og:title", Content: "Stream.place"}, 103 {Type: "property", Key: "og:description", Content: "Stream.place is open-source livestreaming on the AT Protocol."}, 104 {Type: "property", Key: "og:image", Content: thumbURL.String()}, 105 106 // Twitter Meta Tags 107 {Type: "name", Key: "twitter:card", Content: "summary_large_image"}, 108 {Type: "property", Key: "twitter:domain", Content: u.Host}, 109 {Type: "property", Key: "twitter:url", Content: u.String()}, 110 {Type: "name", Key: "twitter:title", Content: "Stream.place"}, 111 {Type: "name", Key: "twitter:description", Content: "Stream.place is open-source livestreaming on the AT Protocol."}, 112 {Type: "name", Key: "twitter:image", Content: thumbURL.String()}, 113 } 114 115 return l.GenerateHTML(ctx, &PageConfig{ 116 Title: "Stream.place", 117 Metas: metaTags, 118 }) 119} 120 121func (l *Linker) GenerateHTML(ctx context.Context, pc *PageConfig) ([]byte, error) { 122 123 root, err := html.Parse(bytes.NewReader(l.BaseHTML)) 124 if err != nil { 125 return nil, err 126 } 127 128 var htmlNode *html.Node 129 for node := range root.ChildNodes() { 130 if node.Type == html.ElementNode && node.Data == "html" { 131 htmlNode = node 132 break 133 } 134 } 135 if htmlNode == nil { 136 return nil, errors.New("html not found") 137 } 138 139 var head *html.Node 140 for node := range htmlNode.ChildNodes() { 141 if node.Data == "head" { 142 head = node 143 break 144 } 145 } 146 if head == nil { 147 return nil, errors.New("head not found") 148 } 149 150 // Title tag (handled separately as it's not a meta tag) 151 152 var oldTitle *html.Node 153 for node := range head.ChildNodes() { 154 if node.Type == html.ElementNode && node.Data == "title" { 155 oldTitle = node 156 break 157 } 158 } 159 if oldTitle != nil { 160 head.RemoveChild(oldTitle) 161 } 162 163 title := &html.Node{ 164 Type: html.ElementNode, 165 Data: "title", 166 } 167 head.AppendChild(title) 168 title.AppendChild(&html.Node{ 169 Type: html.TextNode, 170 Data: pc.Title, 171 }) 172 173 // Add all meta tags in a loop 174 for _, tag := range pc.Metas { 175 head.AppendChild(&html.Node{ 176 Type: html.ElementNode, 177 Data: "meta", 178 Attr: []html.Attribute{ 179 {Key: tag.Type, Val: tag.Key}, 180 {Key: "content", Val: tag.Content}, 181 }, 182 }) 183 } 184 185 // Render the HTML to a string 186 var buf bytes.Buffer 187 if err := html.Render(&buf, root); err != nil { 188 return nil, err 189 } 190 191 return buf.Bytes(), nil 192}