// Eko: A terminal-native social media platform
// Copyright (C) 2025 Kyren223
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
package webserver
import (
"fmt"
"io"
"log/slog"
"net/http"
"strings"
"time"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/kyren223/eko/embeds"
"github.com/kyren223/eko/pkg/assert"
)
func ServePrometheusMetrics() {
slog.Info("starting metrics webserver...")
metricsMux := http.NewServeMux()
metricsMux.Handle("/metrics", promhttp.Handler())
srv := &http.Server{
Addr: ":2112",
Handler: metricsMux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
err := srv.ListenAndServe()
if err != nil {
slog.Error("metrics webserver error", "error", err)
} else {
slog.Info("metrics webserver terminated")
}
}
func ServeEkoWebsite() {
slog.Info("starting public webserver...")
publicMux := http.NewServeMux()
publicMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://github.com/kyren223/eko", http.StatusFound)
})
publicMux.HandleFunc("/install.sh", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-sh")
_, err := io.WriteString(w, embeds.Installer)
assert.NoError(err, "installer should be valid")
})
publicMux.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
_, err := w.Write([]byte(css))
assert.NoError(err, "css *should* be valid")
})
publicMux.HandleFunc("/terms-of-service", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
tos := embeds.TermsOfService.Load().(string)
tos = strings.ReplaceAll(tos, "Privacy Policy", "[Privacy Policy](../privacy-policy)")
html := mdToHTML(tos)
writeLegalLayoutHtml(w, html)
})
publicMux.HandleFunc("/privacy-policy", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
privacy := embeds.PrivacyPolicy.Load().(string)
privacy = strings.ReplaceAll(privacy, "Terms of Service", "[Terms of Service](../terms-of-service)")
html := mdToHTML(privacy)
writeLegalLayoutHtml(w, html)
})
srv := &http.Server{
Addr: ":7443",
Handler: publicMux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
err := srv.ListenAndServe()
if err != nil {
slog.Error("public webserver error", "error", err)
} else {
slog.Info("public webserver terminated")
}
}
func writeLegalLayoutHtml(w io.Writer, html string) {
_, err := fmt.Fprintf(w, `
Terms of Service
%s
`, html)
assert.NoError(err, "html *should* be valid")
}
func mdToHTML(md string) string {
// create markdown parser with extensions
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
doc := p.Parse([]byte(md))
// create HTML renderer with extensions
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := html.NewRenderer(opts)
return string(markdown.Render(doc, renderer))
}
// Yes this is horrible, but it works and I don't like web dev :)
// Feel free to improve it and open a PR
// Probably most of these are unncessary, I just copy pasted it from my website css
var css = `
.sl-markdown-content
:not(.expressive-code *)
+ :not(a, strong, em, del, span, input, code, br)
+ :not(a, strong, em, del, span, input, code, br, :where(.not-content *)) {
margin-top: 1rem;
}
/* Headings after non-headings have more spacing. */
.sl-markdown-content
:not(h1, h2, h3, h4, h5, h6)
+ :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *)) {
margin-top: 1.5em;
}
.sl-markdown-content li + li:not(:where(.not-content *)),
.sl-markdown-content dt + dt:not(:where(.not-content *)),
.sl-markdown-content dt + dd:not(:where(.not-content *)),
.sl-markdown-content dd + dd:not(:where(.not-content *)) {
margin-top: 0.25rem;
}
.sl-markdown-content li:not(:where(.not-content *)) {
overflow-wrap: anywhere;
}
.sl-markdown-content
li
> :last-child:not(li, ul, ol):not(
a,
strong,
em,
del,
span,
input,
:where(.not-content *)
) {
margin-bottom: 1.25rem;
}
.sl-markdown-content dt:not(:where(.not-content *)) {
font-weight: 700;
}
.sl-markdown-content dd:not(:where(.not-content *)) {
padding-inline-start: 1rem;
}
.sl-markdown-content :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *)) {
color: var(--sl-color-white);
line-height: var(--sl-line-height-headings);
font-weight: 600;
font-size: 10000%;
}
.sl-markdown-content
:is(img, picture, video, canvas, svg, iframe):not(:where(.not-content *)) {
display: block;
max-width: 100%;
height: auto;
}
.sl-markdown-content h1:not(:where(.not-content *)) {
font-size: var(--sl-text-h1);
}
.sl-markdown-content h2:not(:where(.not-content *)) {
font-size: var(--sl-text-h2);
}
.sl-markdown-content h3:not(:where(.not-content *)) {
font-size: var(--sl-text-h3);
}
.sl-markdown-content h4:not(:where(.not-content *)) {
font-size: var(--sl-text-h4);
}
.sl-markdown-content h5:not(:where(.not-content *)) {
font-size: var(--sl-text-h5);
}
.sl-markdown-content h6:not(:where(.not-content *)) {
font-size: var(--sl-text-h6);
}
.sl-markdown-content a:not(:where(.not-content *)) {
color: var(--sl-color-text-accent);
}
.sl-markdown-content a:hover:not(:where(.not-content *)) {
color: var(--sl-color-white);
}
.sl-markdown-content code:not(:where(.not-content *)) {
background-color: var(--sl-color-bg-inline-code);
margin-block: -0.125rem;
padding: 0.125rem 0.375rem;
font-size: var(--sl-text-code-sm);
}
.sl-markdown-content :is(h1, h2, h3, h4, h5, h6) code {
font-size: inherit;
}
.sl-markdown-content pre:not(:where(.not-content *)) {
border: 1px solid var(--sl-color-gray-5);
padding: 0.75rem 1rem;
font-size: var(--sl-text-code);
tab-size: 2;
}
.sl-markdown-content pre code:not(:where(.not-content *)) {
all: unset;
font-family: var(--__sl-font-mono);
}
.sl-markdown-content blockquote:not(:where(.not-content *)) {
border-inline-start: 1px solid var(--sl-color-gray-5);
padding-inline-start: 1rem;
}
/* Table styling */
.sl-markdown-content table:not(:where(.not-content *)) {
display: block;
overflow: auto;
border-spacing: 0;
}
.sl-markdown-content :is(th, td):not(:where(.not-content *)) {
border-bottom: 1px solid var(--sl-color-gray-5);
padding: 0.5rem 1rem;
/* Align text to the top of the row in multiline tables. */
vertical-align: baseline;
}
.sl-markdown-content
:is(th:first-child, td:first-child):not(:where(.not-content *)) {
padding-inline-start: 0;
}
.sl-markdown-content
:is(th:last-child, td:last-child):not(:where(.not-content *)) {
padding-inline-end: 0;
}
.sl-markdown-content th:not(:where(.not-content *)) {
color: var(--sl-color-white);
font-weight: 600;
}
/* Align headings to the start of the line unless set by the align attribute. */
.sl-markdown-content th:not([align]):not(:where(.not-content *)) {
text-align: start;
}
/*