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 LINKFILTER: for _, link := range links { if strings.HasPrefix(link, BlogUrl) { continue } for _, ignoredURL := range IgnoredWebmentionUrls { if strings.HasPrefix(link, ignoredURL) { continue LINKFILTER } } 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(), 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 in 10 seconds go func() { if SelectedCdn != nil { time.Sleep(10 * time.Second) SelectedCdn.Purge(target) } }() return }