xt/http-html.go
2025-04-06 19:44:56 -06:00

267 lines
6.1 KiB
Go

package main
import (
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"go.sour.is/xt/internal/otel"
"go.yarn.social/lextwt"
"go.yarn.social/types"
)
type HTML struct {
app *appState
db db
hostname string
}
func (a *HTML) healthcheck(w http.ResponseWriter, r *http.Request) {
_, span := otel.Span(r.Context())
defer span.End()
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write([]byte("ok"))
}
func (a *HTML) home(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Span(r.Context())
defer span.End()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
uri := r.URL.Query().Get("uri")
limit := 100
if v, ok := strconv.Atoi(r.URL.Query().Get("limit")); ok == nil {
limit = v
}
limit = min(100, max(1, limit))
var offset int64 = 0
if v, ok := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64); ok == nil {
offset = v
}
twts, offset, end, err := fetchTwts(ctx, a.db, uri, limit, offset)
span.RecordError(err)
if err != nil {
http.Error(w, "ERR", 500)
return
}
preamble := mkPreamble(a.hostname, uri, "", limit, int64(len(twts)), offset, end)
reg := &HTWriter{
lextwt.NewTwtRegistry(preamble, reverse(twts)),
}
reg.WriteTo(w)
}
func (a *HTML) conv(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Span(r.Context())
defer span.End()
w.Header().Set("Content-Type", "text/html; charset=utf-8")
hash := r.PathValue("hash")
// if (len(hash) < 6 || len(hash) > 8) && !notAny(hash, "abcdefghijklmnopqrstuvwxyz234567") {
// w.WriteHeader(http.StatusBadRequest)
// return
// }
limit := 100
if v, ok := strconv.Atoi(r.URL.Query().Get("limit")); ok == nil {
limit = v
}
var offset int64 = 0
if v, ok := strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64); ok == nil {
offset = v
}
twts, offset, end, err := fetchConv(ctx, a.db, hash, limit, offset)
span.RecordError(err)
if err != nil {
http.Error(w, "ERR", 500)
return
}
preamble := mkPreamble(a.hostname, "", "/conv/"+hash, limit, int64(len(twts)), offset, end)
reg := &HTWriter{
lextwt.NewTwtRegistry(preamble, twts),
}
reg.WriteTo(w)
}
type reg interface {
Preamble() lextwt.Comments
Twts() types.Twts
}
type HTWriter struct {
reg
}
func (r *HTWriter) WriteTo(w io.Writer) (int64, error) {
var output int64
i, err := fmt.Fprintln(w, `<!DOCTYPE html>
<html>
<head>
<title>The Watcher</title>
<style>
@media screen and (max-width: 500px) {
body { width: 100%; margin: 0; }
}
@media screen and (min-width: 500px) and (max-width: 940px) {
body { width: 90%; margin: auto; }
.h-card { columns: 2; }
}
@media screen and (min-width: 940px) {
body { width: 70%; margin: auto; }
.h-card { columns: 2; }
}
body { font-family: sans-serif; background: black; color: white; }
a { color: cornflowerblue; text-decoration: none; }
main { }
pre { white-space: pre-wrap; }
pre.preamble { color: green; }
article { background-color: #333; border: 1px solid green; border-radius: 4px; padding: 4px; margin: 2px; }
article pre { color: orange; }
.h-card .author { display: flex; }
.h-card .icon { width: 36px; margin: 4px; }
.h-card .u-photo { width: 32px; }
.p-org a { color: darkgrey; }
.h-card .date { text-align: right;}
video { width: 100%; }
section { padding: 1em; border: 1px solid darkgreen; background-color: #111; }
section img { max-width: 100%; }
</style>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body onload="setTimestamps()">
<pre class='preamble'>
`)
output += int64(i)
if err != nil {
return output, err
}
for _, c := range r.Preamble() {
if key := c.Key(); key != "" {
value := mkValue(c.Value())
i, err = fmt.Fprintf(w, "# %s = %s\n", key, value)
} else {
i, err = fmt.Fprintln(w, c.Text())
}
output += int64(i)
if err != nil {
return output, err
}
}
i, err = fmt.Fprintln(w, "</pre><main>")
output += int64(i)
for _, twt := range r.Twts() {
twter := twt.Twter()
uri, err := url.Parse(twter.URI)
if err != nil {
uri = &url.URL{
Scheme: "HTTPS",
Host: "unknown.txt",
}
}
subject := ""
if s := twt.Subject(); s != nil {
if tag, ok := s.Tag().(*lextwt.Tag); ok {
subject = tag.Text()
}
}
i, err = fmt.Fprintf(w, `
<article>
<header class="u-author h-card">
<div class="author">
<div class="icon">
<a href="%s" class="u-url">
<img class="avatar u-photo" src="%s" alt="" loading="lazy">
</a>
</div>
<div class="author-name">
<div class="p-name">
<a href="%s">%s</a>
</div>
<div class="p-org">
<a target="_blank" href="%s">%s</a>
</div>
</div>
</div>
<div class="date">
<div><a class="u-url" href="%s">%s</a></div>
<div><small><a href="%s"> View Thread</a></small></div>
</div>
</header>
<section>
%-h
</section>
</div>
</article>
`, "/?uri="+twter.URI, twter.Avatar,
"/?uri="+twter.URI, twter.Nick,
twter.URI, uri.Host,
"/conv/"+subject, fmt.Sprintf("<time datetime='%s'>%s</time>", twt.Created().Format(time.RFC3339), twt.Created().Format(time.RFC822)),
"/conv/"+subject,
twt,
)
output += int64(i)
if err != nil {
return output, err
}
}
i, err = fmt.Fprintln(w, `</main>
<script>
function setTimestamps() {
document.querySelectorAll("time").forEach((e) => {
const OneDay = new Date(new Date().getTime() - (1 * 24 * 60 * 60 * 1000))
const d = new Date(e.hasAttributes() && e.attributes.getNamedItem('datetime'). value);
if (d > OneDay)
e.innerHTML = d.toLocaleTimeString(navigator.language, { hour: '2-digit', minute: '2-digit' });
else
e.innerHTML = d.toLocaleDateString(navigator.language, { hour: '2-digit', minute: '2-digit' });
});
}
</script>
</body>`)
output += int64(i)
return output, err
}
func mkValue(v string) string {
if strings.HasPrefix(v, "http") {
return fmt.Sprintf(`<a href="%s">%s</a>`, v, v)
}
return v
}
func reverse[T any](s []T) []T {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}