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 }