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 = "/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}