package main import ( "context" "flag" "fmt" "google.golang.org/grpc/credentials/insecure" "log" "regexp" "strings" "time" "git.sr.ht/~adnano/go-gemini" "git.sr.ht/~adnano/go-gemini/certificate" "github.com/flosch/pongo2/v6" "google.golang.org/grpc" ) var ( hostname string port string fileSrvHost string fileSrvPort string certificatePath string ) func init() { flag.StringVar(&hostname, "hostname", "", "capsule hostname") flag.StringVar(&port, "port", "1965", "capsule port") flag.StringVar(&certificatePath, "certificatePath", "", "capsule certificate path") flag.StringVar(&fileSrvHost, "fileSrvHost", "", "file push server host") flag.StringVar(&fileSrvPort, "fileSrvPort", "", "file push server port") flag.Parse() } func main() { certificates := &certificate.Store{} certificates.Register(hostname) if err := certificates.Load(certificatePath); err != nil { panic(err) } mux := &gemini.Mux{} mux.HandleFunc("/favicon.txt", processFavicon) // security // feed mux.HandleFunc("/", process) server := &gemini.Server{ 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) } } func processFavicon(_ context.Context, w gemini.ResponseWriter, _ *gemini.Request) { w.SetMediaType("text/plain") _, err := w.Write([]byte("\U0001F31B")) if err != nil { w.WriteHeader(gemini.StatusNotFound, "Out of space") } } func process(_ context.Context, w gemini.ResponseWriter, r *gemini.Request) { log.Println("-> " + r.URL.Path) lang := regexp.MustCompile(`^/(sgs|en)`).FindString(r.URL.Path) if lang != "" { lang = lang[1:] } 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) switch { case "/" == r.URL.Path: w.WriteHeader(gemini.StatusPermanentRedirect, "/sgs") case regexp.MustCompile(`^/(sgs|en)/?$`).MatchString(r.URL.Path): renderIndex(lang, w, client) case regexp.MustCompile(`^/(sgs|en)/a/?$`).MatchString(r.URL.Path): renderAbout(lang, w) 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) case regexp.MustCompile(`^/(sgs|en)/f/([\p{L}\d_+.]+/)+[\d\w]+/[\w_]+.gmi$`).MatchString(r.URL.Path): 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) default: w.WriteHeader(gemini.StatusNotFound, "Out of space") } } func renderIndex(lang string, w gemini.ResponseWriter, 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("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 } _, err = w.Write([]byte(page)) if err != nil { w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error") return } } 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 } _, err = w.Write([]byte(page)) if err != nil { w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error") } } 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 } 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} 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("text/gemini") tpl := pongo2.Must(pongo2.FromFile("templates/search.gmi")) page, err := tpl.Execute(pongo2.Context{"lang": lang, "tree": tree, "q": q}) 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 } } 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") tpl := pongo2.Must(pongo2.FromFile("templates/category.gmi")) fmt.Println(file, &file) 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 } } 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 } category := "" for i, cat := range file.File.Category { category = category + fmt.Sprintf("=> /%s/f/%s .%s└─ %s\n", lang, strings.Join(file.File.Category[:i+1], "/"), strings.Repeat(" ", i), cat, ) } content := fmt.Sprintf( "# %s\n\n"+ "%s\n\n"+ "%s\n%s\n\n"+ "%s\n\n[ %s ]", file.File.Description, file.Content, file.File.Created, file.File.Copyright, category, strings.Join(file.File.Tags, " "), ) w.Write([]byte(content)) } func renderAllFiles(lang string, w gemini.ResponseWriter, 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 { log.Fatalf("client.GetSummery failed: %v", err) w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error") return } content := "# Arnas alkierios :: " if lang == "sgs" { content = content + "vėsė tekstā\n\n" } else { content = content + "all texts\n\n" } for _, f := range tree.Files { content = appendFileLink(lang, content, f) } if lang == "sgs" { content = content + "\n=> /sgs ← grīžtė" } else { content = content + "\n=> /en ← back" } w.Write([]byte(content)) } func appendFileLink(lang string, content string, f *TreeFile) string { content = content + fmt.Sprintf( "=> /%s/f/%s/%s/%s %s (%s)\n", lang, strings.Join(f.Category, "/"), f.Id, strings.Replace(f.Name, ".md", ".gmi", 1), f.Description, f.Created, ) return content }