hugo-micropub/webmention.go

211 lines
5.7 KiB
Go

package main
import (
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
"willnorris.com/go/webmention"
)
type Mention struct {
Source string `json:"source"`
Target string `json:"target"`
Date string `json:"date"`
}
func SendWebmentions(url string) {
client := webmention.New(nil)
dl, err := client.DiscoverLinks(url, ".h-entry")
if err != nil {
return
}
// Send Webmentions
for _, link := range filterLinks(dl) {
endpoint, err := client.DiscoverEndpoint(link)
if err != nil || len(endpoint) < 1 {
continue
}
_, err = client.SendWebmention(endpoint, url, link)
if err != nil {
log.Println("Sent webmention to " + link + " failed")
continue
}
log.Println("Sent webmention to " + link)
}
}
func filterLinks(links []string) []string {
var filteredLinks []string
checkPrefix := func(link string, prefix string) bool {
return strings.HasPrefix(link, strings.TrimSuffix(prefix, "/"))
}
checkAny := func(link string) bool {
for _, ignoredUrl := range IgnoredWebmentionUrls {
if checkPrefix(link, ignoredUrl) {
return true
}
}
return false
}
for _, link := range links {
if !(checkPrefix(link, BlogUrl) || checkAny(link)) {
filteredLinks = append(filteredLinks, link)
}
}
return filteredLinks
}
func HandleWebmention(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
_, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request"))
return
}
sourceUrl, err := url.Parse(r.FormValue("source"))
if err != nil || !(sourceUrl.Scheme == "http" || sourceUrl.Scheme == "https") {
err = errors.New("failed to parse webmention source URL")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
targetUrl, err := url.Parse(r.FormValue("target"))
if err != nil || !(sourceUrl.Scheme == "http" || sourceUrl.Scheme == "https") {
err = errors.New("failed to parse webmention target URL")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
// Check if urls don't equal
if sourceUrl.String() == targetUrl.String() {
err = errors.New("source and target equal")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
// Check if target is blog
if !strings.HasPrefix(targetUrl.String(), strings.TrimSuffix(BlogUrl, "/")) {
err = errors.New("wrong webmention target")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
// Check response code for source
respCode, err := responseCodeForSource(sourceUrl.String())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}
if respCode < 200 || respCode >= 300 {
if respCode == 410 || respCode == 404 {
// Delete mention, because source is gone
go func() {
e := deleteWebmention(sourceUrl.String(), targetUrl.String())
if e != nil {
fmt.Print("Tried to delete webmention", sourceUrl.String(), "but failed:", e.Error())
}
}()
returnSuccess(targetUrl.String(), w, r)
return
} else {
err = errors.New("source returned error")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
}
// Check if source mentions target
if !sourceMentionsTarget(sourceUrl.String(), targetUrl.String()) {
err = errors.New("source doesn't mention target")
go func() {
// Try to delete webmention nevertheless
e := deleteWebmention(sourceUrl.String(), targetUrl.String())
if e != nil {
fmt.Print("Tried to delete webmention (source doesn't mention target) ", sourceUrl.String(), "but failed:", e.Error())
}
}()
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
go func() {
e := saveWebmention(&Mention{
Source: sourceUrl.String(),
Target: targetUrl.String(),
Date: time.Now().Format(time.RFC3339),
})
if e != nil {
fmt.Println("Failed to save webmention:", e.Error())
}
}()
returnSuccess(targetUrl.String(), w, r)
go func() {
if SelectedNotificationServices != nil {
SelectedNotificationServices.Post("New webmention: " + sourceUrl.String())
}
}()
return
}
func responseCodeForSource(source string) (int, error) {
client := &http.Client{}
resp, err := client.Get(source)
if err != nil || resp == nil {
return 0, err
}
return resp.StatusCode, nil
}
func sourceMentionsTarget(source string, target string) bool {
client := webmention.New(nil)
dl, err := client.DiscoverLinks(source, "")
if err != nil {
return false
}
for _, link := range dl {
if link == target {
return true
}
}
return false
}
func saveWebmention(mention *Mention) (err error) {
bytesRepresentation, err := json.Marshal(mention)
if err != nil {
return errors.New("failed to marshal json before committing")
}
filePath := fmt.Sprintf("data/mentions/%x/%x.json", md5.Sum([]byte(strings.ReplaceAll(mention.Target, "/", ""))), md5.Sum([]byte(mention.Source)))
err = SelectedStorage.UpdateFile(filePath, string(bytesRepresentation), "New webmention from "+mention.Source)
return
}
func deleteWebmention(source string, target string) (err error) {
filePath := fmt.Sprintf("data/mentions/%x/%x.json", md5.Sum([]byte(strings.ReplaceAll(target, "/", ""))), md5.Sum([]byte(source)))
err = SelectedStorage.DeleteFile(filePath, "Delete webmention from "+source)
return
}
func returnSuccess(target string, w http.ResponseWriter, r *http.Request) {
w.Header().Add("Location", target)
if strings.Contains(r.Header.Get("Accept"), "text/html") {
// Redirect browser
w.WriteHeader(http.StatusSeeOther)
} else {
w.WriteHeader(http.StatusCreated)
}
// Purge CDN after 30 seconds
go func() {
time.Sleep(30 * time.Second)
if SelectedCdn != nil {
SelectedCdn.Purge(target)
}
}()
return
}