fix(ui/og): robust OG/Twitter parsing via html tokenizer; keep navbar on summarizer; make tail pane scrollable; stub loadInfo; channel dropdown to be added; SSE broadcast already wired
This commit is contained in:
parent
62456fad97
commit
8c67ce27b5
1 changed files with 23 additions and 30 deletions
|
|
@ -16,6 +16,7 @@ import (
|
||||||
|
|
||||||
"sojuboy/internal/store"
|
"sojuboy/internal/store"
|
||||||
"sojuboy/internal/summarizer"
|
"sojuboy/internal/summarizer"
|
||||||
|
xhtml "golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
|
|
@ -217,45 +218,37 @@ func (s *Server) handleLinkCard(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = json.NewEncoder(w).Encode(s.cardCache[raw])
|
_ = json.NewEncoder(w).Encode(s.cardCache[raw])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// fetch minimal HTML and extract tags (very lightweight, no full readability here)
|
// fetch minimal HTML and extract tags using a tolerant HTML parser
|
||||||
// For brevity, we only parse a few tags by string search to keep dependencies minimal in this step
|
|
||||||
client := &http.Client{ Timeout: 10 * time.Second }
|
client := &http.Client{ Timeout: 10 * time.Second }
|
||||||
req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, raw, nil)
|
req, _ := http.NewRequestWithContext(r.Context(), http.MethodGet, raw, nil)
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil { w.WriteHeader(http.StatusBadGateway); _, _ = w.Write([]byte("fetch error")); return }
|
if err != nil { w.WriteHeader(http.StatusBadGateway); _, _ = w.Write([]byte("fetch error")); return }
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 { w.WriteHeader(http.StatusBadGateway); _, _ = w.Write([]byte("bad status")); return }
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 { w.WriteHeader(http.StatusBadGateway); _, _ = w.Write([]byte("bad status")); return }
|
||||||
// limit to 256KB
|
// limit to 256KB and parse tokens
|
||||||
limited := http.MaxBytesReader(w, resp.Body, 262144)
|
limited := http.MaxBytesReader(w, resp.Body, 262144)
|
||||||
b, _ := io.ReadAll(limited)
|
doc, err := xhtml.Parse(limited)
|
||||||
html := string(b)
|
if err != nil { w.WriteHeader(http.StatusBadGateway); _, _ = w.Write([]byte("parse error")); return }
|
||||||
// naive meta parsing
|
var title, desc, img string
|
||||||
get := func(names ...string) string {
|
var walker func(*xhtml.Node)
|
||||||
for _, n := range names {
|
getAttr := func(n *xhtml.Node, key string) string { for a := n.Attr; a != nil && len(n.Attr) > 0; a = nil { for _, at := range n.Attr { if strings.EqualFold(at.Key, key) { return at.Val } } ; return "" } }
|
||||||
// look for content="..."
|
walker = func(n *xhtml.Node) {
|
||||||
idx := strings.Index(strings.ToLower(html), strings.ToLower(n))
|
if n.Type == xhtml.ElementNode && strings.EqualFold(n.Data, "meta") {
|
||||||
if idx >= 0 {
|
// property or name + content
|
||||||
// slice forward
|
var pn = ""; var nm = ""; var content = ""
|
||||||
sfx := html[idx:]
|
for _, a := range n.Attr { if strings.EqualFold(a.Key, "property") { pn = a.Val } else if strings.EqualFold(a.Key, "name") { nm = a.Val } else if strings.EqualFold(a.Key, "content") { content = a.Val } }
|
||||||
ic := strings.Index(strings.ToLower(sfx), "content=")
|
key := strings.ToLower(pn)
|
||||||
if ic >= 0 {
|
if key == "" { key = strings.ToLower(nm) }
|
||||||
sfx = sfx[ic+8:]
|
switch key {
|
||||||
// trim quotes
|
case "og:title", "twitter:title": if title == "" { title = content }
|
||||||
if len(sfx) > 0 && (sfx[0] == '"' || sfx[0] == '\'') {
|
case "og:description", "twitter:description": if desc == "" { desc = content }
|
||||||
q := sfx[0]
|
case "og:image", "twitter:image": if img == "" { img = content }
|
||||||
sfx = sfx[1:]
|
|
||||||
iq := strings.IndexByte(sfx, q)
|
|
||||||
if iq >= 0 { return strings.TrimSpace(sfx[:iq]) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
for c := n.FirstChild; c != nil; c = c.NextSibling { walker(c) }
|
||||||
}
|
}
|
||||||
card := linkCard{ URL: raw }
|
walker(doc)
|
||||||
card.Title = get("property=\"og:title\"","name=\"og:title\"","name=\"twitter:title\"")
|
card := linkCard{ URL: raw, Title: strings.TrimSpace(title), Description: strings.TrimSpace(desc), Image: strings.TrimSpace(img) }
|
||||||
card.Description = get("property=\"og:description\"","name=\"og:description\"","name=\"twitter:description\"")
|
|
||||||
card.Image = get("property=\"og:image\"","name=\"og:image\"","name=\"twitter:image\"")
|
|
||||||
// cache for 24h
|
// cache for 24h
|
||||||
s.cardCache[raw] = card
|
s.cardCache[raw] = card
|
||||||
s.cardCacheExp[raw] = time.Now().Add(24 * time.Hour)
|
s.cardCacheExp[raw] = time.Now().Add(24 * time.Hour)
|
||||||
|
|
@ -393,7 +386,7 @@ func (s *Server) handleUI(w http.ResponseWriter, r *http.Request) {
|
||||||
<nav id="chanlist"></nav>
|
<nav id="chanlist"></nav>
|
||||||
</aside>
|
</aside>
|
||||||
<section class="chat">
|
<section class="chat">
|
||||||
<div id="tail"></div>
|
<div id="tail" style="height: calc(100vh - 4.5rem); overflow:auto"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue