arns-lt-gemini/main.go

423 lines
12 KiB
Go
Raw Normal View History

package main
import (
"context"
2022-08-17 21:06:47 +00:00
"flag"
"fmt"
2022-08-19 22:03:57 +00:00
"google.golang.org/grpc/credentials/insecure"
"log"
2022-08-19 08:17:27 +00:00
"regexp"
"strings"
"time"
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
2022-08-20 20:46:31 +00:00
"github.com/flosch/pongo2/v6"
2022-08-24 03:14:07 +00:00
"github.com/gorilla/feeds"
2022-08-19 22:03:57 +00:00
"google.golang.org/grpc"
)
2022-08-17 21:06:47 +00:00
var (
2022-08-17 21:40:05 +00:00
hostname string
2022-08-17 21:41:47 +00:00
port string
2022-08-17 21:40:05 +00:00
fileSrvHost string
fileSrvPort string
certificatePath string
2022-08-22 20:28:09 +00:00
key = "WgiPGpt5wM6SVEWo5iqI"
2022-08-17 21:06:47 +00:00
)
func init() {
2022-08-17 21:40:05 +00:00
flag.StringVar(&hostname, "hostname", "", "capsule hostname")
2022-08-17 21:41:47 +00:00
flag.StringVar(&port, "port", "1965", "capsule port")
2022-08-17 21:40:05 +00:00
flag.StringVar(&certificatePath, "certificatePath", "", "capsule certificate path")
2022-08-17 21:06:47 +00:00
flag.StringVar(&fileSrvHost, "fileSrvHost", "", "file push server host")
flag.StringVar(&fileSrvPort, "fileSrvPort", "", "file push server port")
2022-08-22 20:28:09 +00:00
flag.StringVar(&key, "key", "", "secret key to communicate admin")
2022-08-17 22:12:11 +00:00
flag.Parse()
2022-08-17 21:06:47 +00:00
}
func main() {
certificates := &certificate.Store{}
2022-08-17 21:40:05 +00:00
certificates.Register(hostname)
if err := certificates.Load(certificatePath); err != nil {
2022-08-17 22:12:11 +00:00
panic(err)
}
mux := &gemini.Mux{}
2022-08-17 23:49:38 +00:00
mux.HandleFunc("/favicon.txt", processFavicon)
2022-08-22 20:28:09 +00:00
mux.HandleFunc("/rebuild", processRebuild)
2022-08-19 08:17:27 +00:00
// security
// feed
mux.HandleFunc("/", process)
server := &gemini.Server{
2022-08-19 08:17:27 +00:00
Addr: ":" + port,
Handler: mux,
ReadTimeout: 30 * time.Second,
WriteTimeout: 1 * time.Minute,
GetCertificate: certificates.Get,
}
ctx := context.Background()
if err := server.ListenAndServe(ctx); err != nil {
log.Fatal(err)
}
}
2022-08-22 20:28:09 +00:00
func processRebuild(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
2022-08-24 03:14:07 +00:00
log.Println("REBUILD")
2022-08-22 20:28:09 +00:00
q, err := gemini.QueryUnescape(r.URL.RawQuery)
if err != nil || q != key {
w.WriteHeader(gemini.StatusServerUnavailable, "Don't wake up the dragons :-*")
}
conn, err := grpc.Dial(fileSrvHost+":"+fileSrvPort, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
log.Fatal(err)
return
}
defer conn.Close()
client := NewTreeManagerClient(conn)
ts := TreeSecret{Key: key}
rsp, err := client.RebuildTree(context.Background(), &ts)
if rsp.Success {
_, err = w.Write([]byte(":-)"))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
}
2022-08-24 03:14:07 +00:00
log.Println("REBUILD: success")
2022-08-22 20:28:09 +00:00
} else {
w.WriteHeader(gemini.StatusNotFound, "Out of space")
}
}
2022-08-19 22:03:57 +00:00
func processFavicon(_ context.Context, w gemini.ResponseWriter, _ *gemini.Request) {
2022-08-19 08:17:27 +00:00
w.SetMediaType("text/plain")
2022-08-20 20:46:31 +00:00
_, err := w.Write([]byte("\U0001F31B"))
if err != nil {
w.WriteHeader(gemini.StatusNotFound, "Out of space")
}
2022-08-17 23:49:38 +00:00
}
2022-08-19 22:03:57 +00:00
func process(_ context.Context, w gemini.ResponseWriter, r *gemini.Request) {
2022-08-19 08:17:27 +00:00
log.Println("-> " + r.URL.Path)
2022-08-17 23:49:38 +00:00
2022-08-19 08:17:27 +00:00
lang := regexp.MustCompile(`^/(sgs|en)`).FindString(r.URL.Path)
if lang != "" {
lang = lang[1:]
}
2022-08-19 22:03:57 +00:00
conn, err := grpc.Dial(fileSrvHost+":"+fileSrvPort, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
log.Fatal(err)
return
}
defer conn.Close()
client := NewTreeManagerClient(conn)
2022-08-19 08:17:27 +00:00
switch {
case "/" == r.URL.Path:
w.WriteHeader(gemini.StatusPermanentRedirect, "/sgs")
2022-08-19 22:03:57 +00:00
case regexp.MustCompile(`^/(sgs|en)/?$`).MatchString(r.URL.Path):
renderIndex(lang, w, client)
2022-08-24 03:14:07 +00:00
case regexp.MustCompile(`^/(sgs|en)/atom.xml$`).MatchString(r.URL.Path):
renderFeed(lang, w, r, client)
2022-08-19 22:03:57 +00:00
case regexp.MustCompile(`^/(sgs|en)/a/?$`).MatchString(r.URL.Path):
2022-08-19 08:17:27 +00:00
renderAbout(lang, w)
2022-08-19 22:03:57 +00:00
case regexp.MustCompile(`^/(sgs|en)/s/?$`).MatchString(r.URL.Path):
renderSearch(lang, w, r, client)
case regexp.MustCompile(`^/(sgs|en)/f/?$`).MatchString(r.URL.Path):
renderAllFiles(lang, w, client)
2022-08-21 16:35:05 +00:00
case regexp.MustCompile(`^/(sgs|en)/f/([\p{L}\d_+.]+/)+[\d\w]+/[\p{L}\d_+.]+.gmi$`).MatchString(r.URL.Path):
2022-08-20 20:46:31 +00:00
renderFile(lang, w, r, client)
case regexp.MustCompile(`^/(sgs|en)/f/[\p{L}\d_+.]+(/[\p{L}\d_+.]+)*/?$`).MatchString(r.URL.Path):
renderCategory(lang, w, r, client)
2022-08-20 22:33:27 +00:00
case regexp.MustCompile(`^/(sgs|en)/t/[\p{L}\d_+.]+/?$`).MatchString(r.URL.Path):
renderTag(lang, w, r, client)
// case abruozdelee
2022-08-19 08:17:27 +00:00
default:
w.WriteHeader(gemini.StatusNotFound, "Out of space")
}
2022-08-19 22:03:57 +00:00
}
2022-08-20 20:46:31 +00:00
func renderIndex(lang string, w gemini.ResponseWriter, client TreeManagerClient) {
2022-08-30 16:54:55 +00:00
filters := []*TreeRequest_Filter{{Key: "lang", Value: lang}}
if lang == "sgs" {
for _, l := range []string{"lt", "prg", "eo"} {
filters = append(filters, &TreeRequest_Filter{Key: "lang", Value: l})
}
}
2022-08-19 22:03:57 +00:00
path := ""
tr := TreeRequest{Path: &path, Filter: filters}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
2022-08-20 20:46:31 +00:00
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile(fmt.Sprintf("templates/%s/index.gmi", lang)))
page, err := tpl.Execute(pongo2.Context{"tree": tree, "lang": lang, "lastFiles": GetLastFiles(tree.Files)})
if err != nil {
log.Fatalf("template failed: %v", err)
return
2022-08-19 22:03:57 +00:00
}
2022-08-20 20:46:31 +00:00
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
2022-08-19 22:03:57 +00:00
}
2022-08-20 20:46:31 +00:00
}
2022-08-19 22:03:57 +00:00
2022-08-24 03:14:07 +00:00
func renderFeed(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
langFilter := TreeRequest_Filter{Key: "lang", Value: lang}
filters := []*TreeRequest_Filter{&langFilter}
path := ""
tr := TreeRequest{Path: &path, Filter: filters}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
w.SetMediaType("application/atom+xml")
now := time.Now()
language := "Samogitian"
if lang == "en" {
language = "English"
}
feed := &feeds.Feed{
Title: "\U0001F31B Arna alkierios [" + lang + "]",
Link: &feeds.Link{Href: "gemini://arns.lt/" + lang},
Description: "personal gemini capsule by Arns Udovič [" + language + " version]",
Author: &feeds.Author{Name: "Arns Udovič", Email: "zordsdavini@arns.lt"},
Created: now,
}
for _, file := range GetLastFiles(tree.Files) {
created, err := time.Parse("2006-01-02", file.Created)
if err != nil {
created = time.Now()
}
feed.Add(
&feeds.Item{
Title: file.Description,
Link: &feeds.Link{Href: fmt.Sprintf("gemini://%s/%s/f%s/%s/%s",
r.URL.Host,
lang,
file.CategoryPath(),
file.Id,
file.GmiName(),
)},
Description: fmt.Sprintf("%s %s [%s]",
file.Description,
file.CategoryPath(),
strings.Join(file.Tags, ","),
),
Author: &feeds.Author{Name: "Arns Udovič", Email: "zordsdavini@arns.lt"},
Created: created,
},
)
}
atom, err := feed.ToAtom()
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
_, err = w.Write([]byte(atom))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
2022-08-20 20:46:31 +00:00
func renderAbout(lang string, w gemini.ResponseWriter) {
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile(fmt.Sprintf("templates/%s/about.gmi", lang)))
page, err := tpl.Execute(pongo2.Context{})
if err != nil {
log.Fatalf("template failed: %v", err)
return
2022-08-19 22:03:57 +00:00
}
2022-08-20 20:46:31 +00:00
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
}
2022-08-19 22:03:57 +00:00
}
func renderSearch(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
q, err := gemini.QueryUnescape(r.URL.RawQuery)
if err != nil || q == "" {
searchStr := "Input searching tag, created date or word in description"
if lang == "sgs" {
searchStr = "Ivesk ėiškuoma žīma, sokūrėma data arba žuodi ėš aprašīma"
}
w.WriteHeader(gemini.StatusInput, searchStr)
return
}
2022-08-20 20:46:31 +00:00
descFilter := TreeRequest_Filter{Key: "description", Value: q}
createdFilter := TreeRequest_Filter{Key: "created", Value: q}
tagFilter := TreeRequest_Filter{Key: "tag", Value: q}
filters := []*TreeRequest_Filter{&descFilter, &createdFilter, &tagFilter}
2022-08-19 22:03:57 +00:00
path := ""
tr := TreeRequest{Path: &path, Filter: filters}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
2022-08-19 08:17:27 +00:00
w.SetMediaType("text/gemini")
2022-08-20 20:46:31 +00:00
tpl := pongo2.Must(pongo2.FromFile("templates/search.gmi"))
page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree, "q": q})
2022-08-19 08:17:27 +00:00
if err != nil {
2022-08-20 20:46:31 +00:00
log.Fatalf("template failed: %v", err)
2022-08-19 08:17:27 +00:00
return
}
2022-08-20 20:46:31 +00:00
_, err = w.Write([]byte(page))
2022-08-19 08:17:27 +00:00
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
2022-08-19 08:17:27 +00:00
}
2022-08-20 20:46:31 +00:00
func renderCategory(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
urlParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
path := "/" + strings.Join(urlParts[2:], "/")
tr := TreeRequest{Path: &path, Filter: []*TreeRequest_Filter{}}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
indexFile, err := tree.GetIndexFile()
var file *FileContent
if err == nil {
fr := FileRequest{Id: indexFile.Id}
file, err = client.GetFile(context.Background(), &fr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
w.SetMediaType("text/gemini")
2022-08-20 20:46:31 +00:00
tpl := pongo2.Must(pongo2.FromFile("templates/category.gmi"))
page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree, "path": path, "indexFile": file})
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
2022-08-20 20:46:31 +00:00
}
func renderFile(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
urlParts := strings.Split(r.URL.Path, "/")
id := urlParts[len(urlParts)-2]
fr := FileRequest{Id: id}
file, err := client.GetFile(context.Background(), &fr)
if err != nil {
log.Fatalf("client.GetSummery failed: %v", err)
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
2022-08-20 22:33:27 +00:00
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile("templates/page.gmi"))
page, err := tpl.Execute(pongo2.Context{"lang": lang, "file": file})
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
2022-08-20 20:46:31 +00:00
}
}
func renderAllFiles(lang string, w gemini.ResponseWriter, client TreeManagerClient) {
2022-08-19 22:03:57 +00:00
langFilter := TreeRequest_Filter{Key: "lang", Value: lang}
filters := []*TreeRequest_Filter{&langFilter}
path := ""
tr := TreeRequest{Path: &path, Filter: filters}
tree, err := client.GetSummery(context.Background(), &tr)
2022-08-17 22:12:11 +00:00
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
2022-08-20 22:33:27 +00:00
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile("templates/all_texts.gmi"))
page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree})
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
2022-08-20 22:33:27 +00:00
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
2022-08-20 22:33:27 +00:00
}
2022-08-20 22:33:27 +00:00
func renderTag(lang string, w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
urlParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
tag := urlParts[len(urlParts)-1]
tagFilter := TreeRequest_Filter{Key: "tag", Value: tag}
filters := []*TreeRequest_Filter{&tagFilter}
path := ""
tr := TreeRequest{Path: &path, Filter: filters}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
2022-08-20 22:33:27 +00:00
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile("templates/tag.gmi"))
page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree, "tag": tag})
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
2022-08-20 20:46:31 +00:00
2022-08-20 22:33:27 +00:00
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}