this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Checkpoint2: Quotes working, opengraph link working

+171 -28
+8 -5
go.mod
··· 3 3 go 1.25.5 4 4 5 5 require ( 6 + github.com/go-sql-driver/mysql v1.9.3 7 + golang.org/x/net v0.48.0 8 + gopkg.in/yaml.v2 v2.4.0 9 + modernc.org/sqlite v1.43.0 10 + ) 11 + 12 + require ( 6 13 filippo.io/edwards25519 v1.1.0 // indirect 7 14 github.com/dustin/go-humanize v1.0.1 // indirect 8 - github.com/go-sql-driver/mysql v1.9.3 // indirect 9 15 github.com/google/uuid v1.6.0 // indirect 10 16 github.com/mattn/go-isatty v0.0.20 // indirect 11 - github.com/mattn/go-sqlite3 v1.14.33 // indirect 12 17 github.com/ncruces/go-strftime v0.1.9 // indirect 13 18 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 14 19 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect 15 - golang.org/x/sys v0.36.0 // indirect 16 - gopkg.in/yaml.v2 v2.4.0 // indirect 20 + golang.org/x/sys v0.39.0 // indirect 17 21 modernc.org/libc v1.66.10 // indirect 18 22 modernc.org/mathutil v1.7.1 // indirect 19 23 modernc.org/memory v1.11.0 // indirect 20 - modernc.org/sqlite v1.43.0 // indirect 21 24 )
+31 -4
go.sum
··· 4 4 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 5 github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= 6 6 github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 7 + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= 8 + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 7 9 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 8 10 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 11 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 10 12 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 11 - github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= 12 - github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 13 13 github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 14 14 github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 15 15 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 16 16 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 17 17 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= 18 18 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 19 + golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= 20 + golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= 21 + golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= 22 + golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 23 + golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 24 + golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 19 25 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 - golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 21 - golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 26 + golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 27 + golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 28 + golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= 29 + golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 30 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 31 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 32 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 24 33 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 34 + modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= 35 + modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 36 + modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= 37 + modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= 38 + modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= 39 + modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= 40 + modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= 41 + modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 42 + modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= 43 + modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= 25 44 modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= 26 45 modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= 27 46 modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 28 47 modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 29 48 modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= 30 49 modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 50 + modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= 51 + modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 52 + modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 53 + modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 31 54 modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA= 32 55 modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8= 56 + modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 57 + modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 58 + modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 59 + modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+7
internal/handler/handlers.go
··· 52 52 dtype := params.Get("dtype") 53 53 iParam := params.Get("i") 54 54 55 + // Infer dtype from path if not set 56 + if dtype == "" { 57 + if r.URL.Path == "/index.xml" || r.URL.Path == "/index.rss" { 58 + dtype = "xml" 59 + } 60 + } 61 + 55 62 i := 1 56 63 if iParam != "" { 57 64 val, err := strconv.Atoi(iParam)
+60 -12
internal/handler/preview.go
··· 3 3 import ( 4 4 "encoding/json" 5 5 "net/http" 6 + 7 + "golang.org/x/net/html" 6 8 ) 7 9 8 10 // OGPreviewHandler handles /ogpreview.cgi ··· 15 17 return 16 18 } 17 19 20 + // Fetch data 18 21 resp, err := http.Get(urlParam) 19 22 if err != nil { 20 23 json.NewEncoder(w).Encode(map[string]string{"error": "Failed to fetch URL"}) ··· 22 25 } 23 26 defer resp.Body.Close() 24 27 25 - // Simple parsing using net/html tokenizer or just simple logic 26 - // For "100% compatibility" we need a decent parser. 27 - // Since I cannot import external packages easily without go get, and I already did go get... 28 - // Wait, I didn't get `golang.org/x/net/html`. I should probably skip full parsing and do regex 29 - // matching like the fallback in Perl to avoid dependency hell in this environment? 30 - // The Perl code had a regex fallback! 31 - // I'll implement the regex fallback logic using `io/ioutil` and `regexp`. 28 + // Parse HTML 29 + doc, err := html.Parse(resp.Body) 30 + if err != nil { 31 + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to parse HTML"}) 32 + return 33 + } 32 34 33 - // ... (Parsing logic similar to Perl regex) 34 - // Placeholder for now: 35 - json.NewEncoder(w).Encode(map[string]string{ 36 - "title": "Preview not implemented fully in migration yet", 37 - }) 35 + metadata := make(map[string]string) 36 + var f func(*html.Node) 37 + f = func(n *html.Node) { 38 + if n.Type == html.ElementNode && n.Data == "meta" { 39 + var property, content, name string 40 + for _, a := range n.Attr { 41 + if a.Key == "property" { 42 + property = a.Val 43 + } 44 + if a.Key == "content" { 45 + content = a.Val 46 + } 47 + if a.Key == "name" { 48 + name = a.Val 49 + } 50 + } 51 + 52 + if property == "og:title" { 53 + metadata["title"] = content 54 + } else if property == "og:description" { 55 + metadata["description"] = content 56 + } else if property == "og:image" { 57 + metadata["image"] = content 58 + } else if name == "twitter:image" { 59 + metadata["twitter_image"] = content 60 + } else if name == "twitter:title" { 61 + metadata["twitter_title"] = content 62 + } else if name == "twitter:description" { 63 + metadata["twitter_description"] = content 64 + } else if name == "description" { 65 + if _, ok := metadata["description"]; !ok { 66 + metadata["description"] = content 67 + } 68 + } 69 + } 70 + // Also look for title tag 71 + if n.Type == html.ElementNode && n.Data == "title" { 72 + if n.FirstChild != nil { 73 + if _, ok := metadata["title"]; !ok { 74 + metadata["title"] = n.FirstChild.Data 75 + } 76 + } 77 + } 78 + 79 + for c := n.FirstChild; c != nil; c = c.NextSibling { 80 + f(c) 81 + } 82 + } 83 + f(doc) 84 + 85 + json.NewEncoder(w).Encode(metadata) 38 86 }
+8 -2
internal/service/content.go
··· 31 31 Content template.HTML `json:"content"` 32 32 Description string `json:"description,omitempty"` 33 33 ContentType string `json:"content_type"` 34 + Quote string `json:"quote,omitempty"` 35 + BaseURL string `json:"base_url"` 34 36 35 37 // Date components for grouping 36 38 DateDay string `json:"date_day"` // e.g. "Mon" ··· 52 54 URL: item.URL, 53 55 Clicks: item.Clicks, 54 56 ContentType: item.ContentType, 57 + BaseURL: s.Config.BaseURL, 55 58 } 56 59 s.formatDate(&d) 57 60 ··· 64 67 65 68 // Image check 66 69 if strings.Contains(item.ContentType, "image") && !strings.Contains(item.User, "nsfw") && !strings.Contains(item.User, "otd") { 67 - linkFiller = fmt.Sprintf(`<img src="%s">`, item.URL) 70 + linkFiller = fmt.Sprintf(`<img src="%s" />`, item.URL) 68 71 } 69 72 70 73 isYoutube := false ··· 101 104 } 102 105 103 106 if videoID != "" { 104 - embed := fmt.Sprintf(`<div class="youtube-embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/%s?rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`, videoID) 107 + embed := fmt.Sprintf(`<div class="youtube-embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/%s?rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="allowfullscreen"></iframe></div>`, videoID) 105 108 d.Content = template.HTML(embed) 106 109 isYoutube = true 107 110 } ··· 122 125 Timestamp: item.Timestamp, 123 126 Title: item.Title, 124 127 URL: item.URL, 128 + BaseURL: s.Config.BaseURL, 125 129 } 126 130 s.formatDate(&d) 127 131 d.Content = template.HTML(fmt.Sprintf(`<img src="%s" alt="image" />`, item.URL)) ··· 134 138 Type: "quote", 135 139 Timestamp: item.Timestamp, 136 140 Author: item.Author, // Quote author field 141 + Quote: item.Quote, 142 + BaseURL: s.Config.BaseURL, 137 143 } 138 144 s.formatDate(&d) 139 145 // For quotes, content is text + author
+27 -5
internal/templates/renderer.go
··· 5 5 "embed" 6 6 "html/template" 7 7 "io" 8 + "strings" 9 + texttemplate "text/template" 8 10 ) 9 11 10 12 //go:embed views/*.html views/*.xml 11 13 var viewsFS embed.FS 12 14 13 15 type Renderer struct { 14 - tmpls *template.Template 16 + htmlTmpls *template.Template 17 + xmlTmpls *texttemplate.Template 15 18 } 16 19 17 20 func NewRenderer() (*Renderer, error) { 18 - tmpls, err := template.ParseFS(viewsFS, "views/*.html", "views/*.xml") 21 + htmlTmpls, err := template.ParseFS(viewsFS, "views/*.html") 19 22 if err != nil { 20 23 return nil, err 21 24 } 22 - return &Renderer{tmpls: tmpls}, nil 25 + 26 + xmlTmpls, err := texttemplate.ParseFS(viewsFS, "views/*.xml") 27 + if err != nil { 28 + return nil, err 29 + } 30 + 31 + return &Renderer{ 32 + htmlTmpls: htmlTmpls, 33 + xmlTmpls: xmlTmpls, 34 + }, nil 23 35 } 24 36 25 37 func (r *Renderer) Render(w io.Writer, name string, data interface{}) error { 26 - return r.tmpls.ExecuteTemplate(w, name, data) 38 + if strings.HasSuffix(name, ".xml") { 39 + return r.xmlTmpls.ExecuteTemplate(w, name, data) 40 + } 41 + return r.htmlTmpls.ExecuteTemplate(w, name, data) 27 42 } 28 43 29 44 func (r *Renderer) RenderToString(name string, data interface{}) (string, error) { 30 45 var buf bytes.Buffer 31 - if err := r.tmpls.ExecuteTemplate(&buf, name, data); err != nil { 46 + var err error 47 + if strings.HasSuffix(name, ".xml") { 48 + err = r.xmlTmpls.ExecuteTemplate(&buf, name, data) 49 + } else { 50 + err = r.htmlTmpls.ExecuteTemplate(&buf, name, data) 51 + } 52 + 53 + if err != nil { 32 54 return "", err 33 55 } 34 56 return buf.String(), nil
+5
tests/fixtures_hot.sql
··· 1 + -- Insert items from 8 days ago (Hot Links window is 6-12 days) 2 + INSERT INTO ircLink (user, title, url, clicks, content_type, timestamp) VALUES 3 + ('history_buff', 'Ancient Link 1', 'http://old.example.com/1', 10, 'text', datetime('now', '-8 days')), 4 + ('history_buff', 'Ancient Link 2', 'http://old.example.com/2', 5, 'text', datetime('now', '-8 days')), 5 + ('history_buff', 'Ancient Link 3', 'http://old.example.com/3', 20, 'text', datetime('now', '-9 days'));
+10
tests/load_fixtures.sh
··· 19 19 # Standard Link 20 20 $ADD_LINK_SCRIPT "web_surfer" "http://costs.wtf" 21 21 22 + ADD_QUOTE_SCRIPT="./tests/add_quote.sh" 23 + 24 + # Quotes 25 + $ADD_QUOTE_SCRIPT "Linus Torvalds" "Talk is cheap. Show me the code." 26 + $ADD_QUOTE_SCRIPT "Brian Kernighan" "Debugging is twice as hard as writing the code in the first place." 27 + $ADD_QUOTE_SCRIPT "Simba" "Everything the light touches is our kingdom." 28 + 29 + echo "Loading backdated 'Hot Links' directly into DB..." 30 + sqlite3 tumble.sqlite < tests/fixtures_hot.sql 31 + 22 32 echo "Fixtures loaded."
+15
thoughts
··· 9 9 api docs 10 10 document code 11 11 12 + makefile needs help target 13 + add year to the date in the stylesheet. 14 + Page next/prev buttons at the top too, or have keyboard shortcuts 15 + 16 + Tweet we could link 17 + https://x.com/jcockhren/status/1229101594505097216?s=20 18 + 19 + Testing other media types 20 + 21 + Trailing whitespace 22 + 23 + Search-fu 404 page needs text updated 24 + Resposive sizing, mobile maybe 25 + 26 + If it's botd blur it out unless you mouse over?