266 lines
6.1 KiB
Go
266 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;}
|
|
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 := s.Tag(); tag != nil {
|
|
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
|
|
}
|