194 lines
5.0 KiB
Go
194 lines
5.0 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 {
|
|
// Delete mention, because source is gone
|
|
// TODO: Implement deletion
|
|
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")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
mention := &Mention{
|
|
Source: sourceUrl.String(),
|
|
Target: targetUrl.String(),
|
|
Date: time.Now().Format(time.RFC3339),
|
|
}
|
|
err = saveWebmention(mention)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte(err.Error()))
|
|
return
|
|
}
|
|
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 returnSuccess(target string, w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.Header.Get("Accept"), "text/html") {
|
|
// Redirect browser
|
|
w.Header().Add("Location", target)
|
|
w.WriteHeader(http.StatusSeeOther)
|
|
} else {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
// Purge CDN after 30 seconds
|
|
go func() {
|
|
time.Sleep(30 * time.Second)
|
|
if SelectedCdn != nil {
|
|
SelectedCdn.Purge(target)
|
|
}
|
|
}()
|
|
return
|
|
} |