package main // Trying to implement gemini mentions from https://codeberg.org/bacardi55/gemini-mentions-rfc // Thanks to @bacardi55 and his example https://git.sr.ht/~bacardi55/ggm (this is forked from there) import ( "context" "fmt" "io" "log" "net/url" "regexp" "strings" "time" "g.arns.lt/zordsdavini/zordfsdb" "git.sr.ht/~adnano/go-gemini" ) const ( STATUS_MENTION_NOT_REVIEWED = "0" STATUS_MENTION_REVIEWED = "1" ) // Nota: Gemini servers might have max execution time for cgi scripts. // Eg: Gemserv has a 5s maximum policy before killing the request. const maxRequestTime = 4 func ProcessMention(remoteUrl string, pageId string) (bool, error) { u, err := validateUrl(remoteUrl) if err != nil { return false, fmt.Errorf("[gemini-mentions] Url is not valid.") } response, err := fetchGeminiPage(u) if err != nil || response.Status.Class() != gemini.StatusSuccess { return false, fmt.Errorf("[gemini-mentions] Error retrieving content from %s: %s", u, err) } if respCert := response.TLS().PeerCertificates; len(respCert) > 0 && time.Now().After(respCert[0].NotAfter) { return false, fmt.Errorf( "[gemini-mentions] Ignored url (invalid certificate for capsule): %s", u, ) } // If all good, let's find gemini mentions link inside. var content []byte content, err = io.ReadAll(response.Body) defer response.Body.Close() if err != nil { return false, fmt.Errorf("[gemini-mentions] Couldn't retrieve the provided URL content") } links := findMentionLinks(string(content), hostname) if len(links) < 1 { return false, fmt.Errorf("[gemini-mentions] Recieved a link with no mention: %s", u) } // log to db db, err := zordfsdb.InitDB("./db") if err != nil { return false, fmt.Errorf("[gemini-mentions] no db: %s", err) } _, found := db.GetNode("mentions") if !found { db.CreateNode("mentions") } _, found = db.GetNode("mentions." + pageId) if !found { db.CreateNode("mentions." + pageId) db.CreateNode("mentions." + pageId + ".mentions") } message := "# Naus gemini patiemėjėms " + hostname + "\n=> gemini://" + hostname + "/r/" + pageId + " Spausk nūruoda i puslapi parveizetė\n\nPamėnavuojėma:\n" for _, link := range links { id, err := db.AddObject("mentions." + pageId + ".mentions") if err != nil { return false, fmt.Errorf("[gemini-mentions] couldn't add object: %s", err) } db.Save("mentions."+pageId+".mentions."+id+".url", extractUrl(link)) db.Save("mentions."+pageId+".mentions."+id+".reviewed", STATUS_MENTION_NOT_REVIEWED) message += fmt.Sprintf("=> %s\n", extractUrl(link)) } message += "\nau/\n" err = SendMisfinMessage(message, ZORDSDAVINI_MISFIN) if err != nil { return false, err } return true, nil } func validateUrl(remoteUrl string) (string, error) { remote, e := url.QueryUnescape(remoteUrl) if e != nil { return "", fmt.Errorf("Provided URL is not a good URL: %s", e) } remote = strings.Replace(remote, "..", "", -1) u, err := url.Parse(remote) if err != nil { return "", fmt.Errorf("Provided URL is not a good URL: %s", err) } else if u.Scheme != "gemini" && u.Scheme != "" { return "", fmt.Errorf("Only gemini url are supported for now.") } else { return "gemini://" + u.Host + u.Path, nil } } func fetchGeminiPage(remoteUrl string) (*gemini.Response, error) { gemclient := &gemini.Client{} ctx, _ := context.WithTimeout(context.Background(), time.Duration(maxRequestTime)*time.Second) response, err := gemclient.Get(ctx, remoteUrl) if err != nil { return response, err } if respCert := response.TLS().PeerCertificates; len(respCert) > 0 && time.Now().After(respCert[0].NotAfter) { return response, fmt.Errorf("Certificate is unvalid") } return response, nil } func findMentionLinks(content string, capsuleRootAddress string) []string { exp := "(?im)^(=>)[ ]?gemini://" + capsuleRootAddress + "[^ ]+([ ](.*))?$" re := regexp.MustCompile(exp) return re.FindAllString(content, -1) } func extractUrl(url string) string { exp := "(?im)gemini://[^ ]+" re := regexp.MustCompile(exp) return re.FindString(url) } func GetMentionsForPage(pageId string, reviewStatus string) map[string]string { mentions := make(map[string]string) db, err := zordfsdb.InitDB("./db") if err != nil { log.Fatal(err) return mentions } _, found := db.Get("mentions." + pageId + ".mentions") if !found { return mentions } for _, id := range db.Keys("mentions." + pageId + ".mentions") { reviewed, _ := db.Get("mentions." + pageId + ".mentions." + id + ".reviewed") if reviewed == reviewStatus { url, _ := db.Get("mentions." + pageId + ".mentions." + id + ".url") mentions[id] = url } } return mentions } func MarkReviewed(pageId string, id string) { db, err := zordfsdb.InitDB("./db") if err != nil { log.Fatal(err) } db.Save("mentions."+pageId+".mentions."+id+".reviewed", STATUS_MENTION_REVIEWED) } func RemoveMention(pageId string, id string) { db, err := zordfsdb.InitDB("./db") if err != nil { log.Fatal(err) } db.Del("mentions." + pageId + ".mentions." + id) }