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