Live video on the AT Protocol
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}