arns-lt-gemini/mention.go
Arnas Udovic e5f39eb0e7
All checks were successful
continuous-integration/drone/push Build is passing
gemini mention misfin body
2025-03-28 21:17:32 +02:00

185 lines
4.9 KiB
Go

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)
}