Compare commits

...

5 Commits

Author SHA1 Message Date
Niko Abeler 41c2286311 missing wait 2022-09-10 14:17:00 +02:00
Niko Abeler fe66d5842e only scan once per week 2022-09-10 14:16:22 +02:00
Niko Abeler 881940cd88 refactoring webmention interface 2022-09-10 14:04:19 +02:00
Niko Abeler 4b9a5adf5c more thread safety 2022-09-10 13:44:25 +02:00
Niko Abeler d66c1a6817 WIP adding guards to webmention 2022-09-09 21:14:49 +02:00
7 changed files with 376 additions and 160 deletions

View File

@ -2,6 +2,7 @@ package main
import ( import (
"h4kor/owl-blogs" "h4kor/owl-blogs"
"sync"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -56,14 +57,20 @@ var webmentionCmd = &cobra.Command{
webmentions := post.OutgoingWebmentions() webmentions := post.OutgoingWebmentions()
println("Found ", len(webmentions), " links") println("Found ", len(webmentions), " links")
wg := sync.WaitGroup{}
wg.Add(len(webmentions))
for _, webmention := range webmentions { for _, webmention := range webmentions {
go func(webmention owl.WebmentionOut) {
defer wg.Done()
err = post.SendWebmention(webmention) err = post.SendWebmention(webmention)
if err != nil { if err != nil {
println("Error sending webmentions: ", err.Error()) println("Error sending webmentions: ", err.Error())
} else { } else {
println("Webmention sent to ", webmention.Target) println("Webmention sent to ", webmention.Target)
} }
}(webmention)
} }
wg.Wait()
return nil return nil
} }

View File

@ -16,12 +16,12 @@
</div> </div>
<hr> <hr>
{{if .Post.ApprovedWebmentions}} {{if .Post.ApprovedIncomingWebmentions}}
<h3> <h3>
Webmentions Webmentions
</h3> </h3>
<ul> <ul>
{{range .Post.ApprovedWebmentions}} {{range .Post.ApprovedIncomingWebmentions}}
<li> <li>
<a href="{{.Source}}"> <a href="{{.Source}}">
{{if .Title}} {{if .Title}}

View File

@ -28,6 +28,27 @@ func (*MockHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error
} }
type MockParseLinksHtmlParser struct {
Links []string
}
func (*MockParseLinksHtmlParser) ParseHEntry(resp *http.Response) (owl.ParsedHEntry, error) {
return owl.ParsedHEntry{Title: "Mock Title"}, nil
}
func (parser *MockParseLinksHtmlParser) ParseLinks(resp *http.Response) ([]string, error) {
return parser.Links, nil
}
func (parser *MockParseLinksHtmlParser) ParseLinksFromString(string) ([]string, error) {
return parser.Links, nil
}
func (*MockParseLinksHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) {
return "http://example.com/webmention", nil
}
type MockHttpClient struct{} type MockHttpClient struct{}
func (*MockHttpClient) Get(url string) (resp *http.Response, err error) { func (*MockHttpClient) Get(url string) (resp *http.Response, err error) {

211
post.go
View File

@ -2,12 +2,12 @@ package owl
import ( import (
"bytes" "bytes"
"errors"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os" "os"
"path" "path"
"sort" "sort"
"sync"
"time" "time"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
@ -23,6 +23,7 @@ type Post struct {
title string title string
metaLoaded bool metaLoaded bool
meta PostMeta meta PostMeta
wmLock sync.Mutex
} }
type PostMeta struct { type PostMeta struct {
@ -37,39 +38,39 @@ type PostWebmetions struct {
Outgoing []WebmentionOut `ymal:"outgoing"` Outgoing []WebmentionOut `ymal:"outgoing"`
} }
func (post Post) Id() string { func (post *Post) Id() string {
return post.id return post.id
} }
func (post Post) Dir() string { func (post *Post) Dir() string {
return path.Join(post.user.Dir(), "public", post.id) return path.Join(post.user.Dir(), "public", post.id)
} }
func (post Post) WebmentionsFile() string { func (post *Post) WebmentionsFile() string {
return path.Join(post.Dir(), "webmentions.yml") return path.Join(post.Dir(), "webmentions.yml")
} }
func (post Post) MediaDir() string { func (post *Post) MediaDir() string {
return path.Join(post.Dir(), "media") return path.Join(post.Dir(), "media")
} }
func (post Post) UrlPath() string { func (post *Post) UrlPath() string {
return post.user.UrlPath() + "posts/" + post.id + "/" return post.user.UrlPath() + "posts/" + post.id + "/"
} }
func (post Post) FullUrl() string { func (post *Post) FullUrl() string {
return post.user.FullUrl() + "posts/" + post.id + "/" return post.user.FullUrl() + "posts/" + post.id + "/"
} }
func (post Post) UrlMediaPath(filename string) string { func (post *Post) UrlMediaPath(filename string) string {
return post.UrlPath() + "media/" + filename return post.UrlPath() + "media/" + filename
} }
func (post Post) Title() string { func (post *Post) Title() string {
return post.title return post.title
} }
func (post Post) ContentFile() string { func (post *Post) ContentFile() string {
return path.Join(post.Dir(), "index.md") return path.Join(post.Dir(), "index.md")
} }
@ -80,49 +81,13 @@ func (post *Post) Meta() PostMeta {
return post.meta return post.meta
} }
func (post Post) Content() []byte { func (post *Post) Content() []byte {
// read file // read file
data, _ := ioutil.ReadFile(post.ContentFile()) data, _ := ioutil.ReadFile(post.ContentFile())
return data return data
} }
func (post Post) Webmentions() PostWebmetions { func (post *Post) RenderedContent() bytes.Buffer {
// read status file
// return parsed webmentions
fileName := post.WebmentionsFile()
if !fileExists(fileName) {
return PostWebmetions{}
}
data, err := os.ReadFile(fileName)
if err != nil {
return PostWebmetions{}
}
webmentions := PostWebmetions{}
err = yaml.Unmarshal(data, &webmentions)
if err != nil {
return PostWebmetions{}
}
return webmentions
}
func (post Post) PersistWebmentions(webmentions PostWebmetions) error {
data, err := yaml.Marshal(webmentions)
if err != nil {
return err
}
err = os.WriteFile(post.WebmentionsFile(), data, 0644)
if err != nil {
return err
}
return nil
}
func (post Post) RenderedContent() bytes.Buffer {
data := post.Content() data := post.Content()
// trim yaml block // trim yaml block
@ -162,7 +127,7 @@ func (post Post) RenderedContent() bytes.Buffer {
} }
func (post Post) Aliases() []string { func (post *Post) Aliases() []string {
return post.Meta().Aliases return post.Meta().Aliases
} }
@ -190,14 +155,63 @@ func (post *Post) LoadMeta() error {
return nil return nil
} }
// Webmentions returns list of incoming and outgoing webmentions
func (post *Post) Webmentions() PostWebmetions {
// read status file
// return parsed webmentions
fileName := post.WebmentionsFile()
if !fileExists(fileName) {
return PostWebmetions{}
}
data, err := os.ReadFile(fileName)
if err != nil {
return PostWebmetions{}
}
webmentions := PostWebmetions{}
err = yaml.Unmarshal(data, &webmentions)
if err != nil {
return PostWebmetions{}
}
return webmentions
}
func (post *Post) IncomingWebmentions() []WebmentionIn {
return post.Webmentions().Incoming
}
func (post *Post) OutgoingWebmentions() []WebmentionOut {
return post.Webmentions().Outgoing
}
func (post *Post) persistWebmentions(webmentions PostWebmetions) error {
data, err := yaml.Marshal(webmentions)
if err != nil {
return err
}
err = os.WriteFile(post.WebmentionsFile(), data, 0644)
if err != nil {
return err
}
return nil
}
// PersistWebmentionOutgoing persists incoming webmention
func (post *Post) PersistIncomingWebmention(webmention WebmentionIn) error { func (post *Post) PersistIncomingWebmention(webmention WebmentionIn) error {
post.wmLock.Lock()
defer post.wmLock.Unlock()
wms := post.Webmentions() wms := post.Webmentions()
// if target is not in status, add it // if target is not in status, add it
replaced := false replaced := false
for i, t := range wms.Incoming { for i, t := range wms.Incoming {
if t.Source == webmention.Source { if t.Source == webmention.Source {
wms.Incoming[i] = webmention wms.Incoming[i].UpdateWith(webmention)
replaced = true replaced = true
break break
} }
@ -207,58 +221,21 @@ func (post *Post) PersistIncomingWebmention(webmention WebmentionIn) error {
wms.Incoming = append(wms.Incoming, webmention) wms.Incoming = append(wms.Incoming, webmention)
} }
return post.PersistWebmentions(wms) return post.persistWebmentions(wms)
} }
func (post *Post) Webmention(source string) (WebmentionIn, error) { // PersistOutgoingWebmention persists a webmention to the webmention file.
wms := post.Webmentions() func (post *Post) PersistOutgoingWebmention(webmention *WebmentionOut) error {
for _, wm := range wms.Incoming { post.wmLock.Lock()
if wm.Source == source { defer post.wmLock.Unlock()
return wm, nil
}
}
return WebmentionIn{}, errors.New("not found")
}
func (post *Post) AddIncomingWebmention(source string) error {
// Check if file already exists
_, err := post.Webmention(source)
if err != nil {
wms := post.Webmentions()
wms.Incoming = append(wms.Incoming, WebmentionIn{
Source: source,
})
defer post.EnrichWebmention(source)
return post.PersistWebmentions(wms)
}
return nil
}
func (post *Post) AddOutgoingWebmention(target string) error {
wms := post.Webmentions() wms := post.Webmentions()
// Check if file already exists // if target is not in webmention, add it
for _, wm := range wms.Outgoing {
if wm.Target == target {
return nil
}
}
webmention := WebmentionOut{
Target: target,
}
wms.Outgoing = append(wms.Outgoing, webmention)
return post.PersistWebmentions(wms)
}
func (post *Post) UpdateOutgoingWebmention(webmention *WebmentionOut) error {
wms := post.Webmentions()
// if target is not in status, add it
replaced := false replaced := false
for i, t := range wms.Outgoing { for i, t := range wms.Outgoing {
if t.Target == webmention.Target { if t.Target == webmention.Target {
wms.Outgoing[i] = *webmention wms.Outgoing[i].UpdateWith(*webmention)
replaced = true replaced = true
break break
} }
@ -268,16 +245,24 @@ func (post *Post) UpdateOutgoingWebmention(webmention *WebmentionOut) error {
wms.Outgoing = append(wms.Outgoing, *webmention) wms.Outgoing = append(wms.Outgoing, *webmention)
} }
return post.PersistWebmentions(wms) return post.persistWebmentions(wms)
} }
func (post *Post) EnrichWebmention(source string) error { func (post *Post) AddIncomingWebmention(source string) error {
resp, err := post.user.repo.HttpClient.Get(source) // Check if file already exists
if err == nil { wm := WebmentionIn{
webmention, err := post.Webmention(source) Source: source,
if err != nil {
return err
} }
defer func() {
go post.EnrichWebmention(wm)
}()
return post.PersistIncomingWebmention(wm)
}
func (post *Post) EnrichWebmention(webmention WebmentionIn) error {
resp, err := post.user.repo.HttpClient.Get(webmention.Source)
if err == nil {
entry, err := post.user.repo.Parser.ParseHEntry(resp) entry, err := post.user.repo.Parser.ParseHEntry(resp)
if err == nil { if err == nil {
webmention.Title = entry.Title webmention.Title = entry.Title
@ -287,11 +272,7 @@ func (post *Post) EnrichWebmention(source string) error {
return err return err
} }
func (post *Post) IncomingWebmentions() []WebmentionIn { func (post *Post) ApprovedIncomingWebmentions() []WebmentionIn {
return post.Webmentions().Incoming
}
func (post *Post) ApprovedWebmentions() []WebmentionIn {
webmentions := post.IncomingWebmentions() webmentions := post.IncomingWebmentions()
approved := []WebmentionIn{} approved := []WebmentionIn{}
for _, webmention := range webmentions { for _, webmention := range webmentions {
@ -307,25 +288,29 @@ func (post *Post) ApprovedWebmentions() []WebmentionIn {
return approved return approved
} }
func (post *Post) OutgoingWebmentions() []WebmentionOut {
return post.Webmentions().Outgoing
}
// ScanForLinks scans the post content for links and adds them to the // ScanForLinks scans the post content for links and adds them to the
// `status.yml` file for the post. The links are not scanned by this function. // `status.yml` file for the post. The links are not scanned by this function.
func (post *Post) ScanForLinks() error { func (post *Post) ScanForLinks() error {
// this could be done in markdown parsing, but I don't want to // this could be done in markdown parsing, but I don't want to
// rely on goldmark for this (yet) // rely on goldmark for this (yet)
postHtml := post.RenderedContent() postHtml := post.RenderedContent()
links, _ := post.user.repo.Parser.ParseLinksFromString(string(postHtml.Bytes())) links, _ := post.user.repo.Parser.ParseLinksFromString(postHtml.String())
for _, link := range links { for _, link := range links {
post.AddOutgoingWebmention(link) post.PersistOutgoingWebmention(&WebmentionOut{
Target: link,
})
} }
return nil return nil
} }
func (post *Post) SendWebmention(webmention WebmentionOut) error { func (post *Post) SendWebmention(webmention WebmentionOut) error {
defer post.UpdateOutgoingWebmention(&webmention) defer post.PersistOutgoingWebmention(&webmention)
// if last scan is less than 7 days ago, don't send webmention
if webmention.ScannedAt.After(time.Now().Add(-7*24*time.Hour)) && !webmention.Supported {
return nil
}
webmention.ScannedAt = time.Now() webmention.ScannedAt = time.Now()
resp, err := post.user.repo.HttpClient.Get(webmention.Target) resp, err := post.user.repo.HttpClient.Get(webmention.Target)

View File

@ -4,7 +4,9 @@ import (
"h4kor/owl-blogs" "h4kor/owl-blogs"
"os" "os"
"path" "path"
"strconv"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
) )
@ -230,7 +232,7 @@ func TestAddIncomingWebmentionNotOverwritingWebmention(t *testing.T) {
} }
} }
func TestAddIncomingWebmentionAddsParsedTitle(t *testing.T) { func TestEnrichAddsTitle(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{}) repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{} repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{} repo.Parser = &MockHtmlParser{}
@ -238,6 +240,7 @@ func TestAddIncomingWebmentionAddsParsedTitle(t *testing.T) {
post, _ := user.CreateNewPost("testpost") post, _ := user.CreateNewPost("testpost")
post.AddIncomingWebmention("https://example.com") post.AddIncomingWebmention("https://example.com")
post.EnrichWebmention(owl.WebmentionIn{Source: "https://example.com"})
mentions := post.IncomingWebmentions() mentions := post.IncomingWebmentions()
if len(mentions) != 1 { if len(mentions) != 1 {
@ -249,7 +252,7 @@ func TestAddIncomingWebmentionAddsParsedTitle(t *testing.T) {
} }
} }
func TestApprovedWebmentions(t *testing.T) { func TestApprovedIncomingWebmentions(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{}) repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("testuser") user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost") post, _ := user.CreateNewPost("testpost")
@ -278,7 +281,7 @@ func TestApprovedWebmentions(t *testing.T) {
} }
post.PersistIncomingWebmention(webmention) post.PersistIncomingWebmention(webmention)
webmentions := post.ApprovedWebmentions() webmentions := post.ApprovedIncomingWebmentions()
if len(webmentions) != 2 { if len(webmentions) != 2 {
t.Errorf("Expected 2 webmentions, got %d", len(webmentions)) t.Errorf("Expected 2 webmentions, got %d", len(webmentions))
} }
@ -371,3 +374,179 @@ func TestCanSendWebmention(t *testing.T) {
t.Errorf("Expected LastSentAt to be set") t.Errorf("Expected LastSentAt to be set")
} }
} }
func TestSendWebmentionOnlyScansOncePerWeek(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
webmention := owl.WebmentionOut{
Target: "http://example.com",
ScannedAt: time.Now().Add(time.Hour * -24 * 6),
}
post.PersistOutgoingWebmention(&webmention)
webmentions := post.OutgoingWebmentions()
webmention = webmentions[0]
err := post.SendWebmention(webmention)
if err != nil {
t.Errorf("Error sending webmention: %v", err)
}
webmentions = post.OutgoingWebmentions()
if len(webmentions) != 1 {
t.Errorf("Expected 1 webmention, got %d", len(webmentions))
}
if webmentions[0].ScannedAt != webmention.ScannedAt {
t.Errorf("Expected ScannedAt to be unchanged. Expected: %v, got %v", webmention.ScannedAt, webmentions[0].ScannedAt)
}
}
func TestSendingMultipleWebmentions(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 20; i++ {
go func(k int) {
webmention := owl.WebmentionOut{
Target: "http://example.com" + strconv.Itoa(k),
}
post.SendWebmention(webmention)
wg.Done()
}(i)
}
wg.Wait()
webmentions := post.OutgoingWebmentions()
if len(webmentions) != 20 {
t.Errorf("Expected 20 webmentions, got %d", len(webmentions))
}
}
func TestReceivingMultipleWebmentions(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 20; i++ {
go func(k int) {
post.AddIncomingWebmention("http://example.com" + strconv.Itoa(k))
wg.Done()
}(i)
}
wg.Wait()
webmentions := post.IncomingWebmentions()
if len(webmentions) != 20 {
t.Errorf("Expected 20 webmentions, got %d", len(webmentions))
}
}
func TestSendingAndReceivingMultipleWebmentions(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
wg := sync.WaitGroup{}
wg.Add(40)
for i := 0; i < 20; i++ {
go func(k int) {
post.AddIncomingWebmention("http://example.com" + strconv.Itoa(k))
wg.Done()
}(i)
go func(k int) {
webmention := owl.WebmentionOut{
Target: "http://example.com" + strconv.Itoa(k),
}
post.SendWebmention(webmention)
wg.Done()
}(i)
}
wg.Wait()
ins := post.IncomingWebmentions()
if len(ins) != 20 {
t.Errorf("Expected 20 webmentions, got %d", len(ins))
}
outs := post.OutgoingWebmentions()
if len(outs) != 20 {
t.Errorf("Expected 20 webmentions, got %d", len(outs))
}
}
func TestComplexParallelWebmentions(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockParseLinksHtmlParser{
Links: []string{
"http://example.com/1",
"http://example.com/2",
"http://example.com/3",
},
}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
wg := sync.WaitGroup{}
wg.Add(60)
for i := 0; i < 20; i++ {
go func(k int) {
post.AddIncomingWebmention("http://example.com/" + strconv.Itoa(k))
wg.Done()
}(i)
go func(k int) {
webmention := owl.WebmentionOut{
Target: "http://example.com/" + strconv.Itoa(k),
}
post.SendWebmention(webmention)
wg.Done()
}(i)
go func() {
post.ScanForLinks()
wg.Done()
}()
}
wg.Wait()
ins := post.IncomingWebmentions()
if len(ins) != 20 {
t.Errorf("Expected 20 webmentions, got %d", len(ins))
}
outs := post.OutgoingWebmentions()
if len(outs) != 20 {
t.Errorf("Expected 20 webmentions, got %d", len(outs))
}
}

View File

@ -19,6 +19,18 @@ type WebmentionIn struct {
RetrievedAt time.Time `yaml:"retrieved_at"` RetrievedAt time.Time `yaml:"retrieved_at"`
} }
func (webmention *WebmentionIn) UpdateWith(update WebmentionIn) {
if update.Title != "" {
webmention.Title = update.Title
}
if update.ApprovalStatus != "" {
webmention.ApprovalStatus = update.ApprovalStatus
}
if !update.RetrievedAt.IsZero() {
webmention.RetrievedAt = update.RetrievedAt
}
}
type WebmentionOut struct { type WebmentionOut struct {
Target string `yaml:"target"` Target string `yaml:"target"`
Supported bool `yaml:"supported"` Supported bool `yaml:"supported"`
@ -26,6 +38,18 @@ type WebmentionOut struct {
LastSentAt time.Time `yaml:"last_sent_at"` LastSentAt time.Time `yaml:"last_sent_at"`
} }
func (webmention *WebmentionOut) UpdateWith(update WebmentionOut) {
if update.Supported {
webmention.Supported = update.Supported
}
if !update.ScannedAt.IsZero() {
webmention.ScannedAt = update.ScannedAt
}
if !update.LastSentAt.IsZero() {
webmention.LastSentAt = update.LastSentAt
}
}
type HttpClient interface { type HttpClient interface {
Get(url string) (resp *http.Response, err error) Get(url string) (resp *http.Response, err error)
Post(url, contentType string, body io.Reader) (resp *http.Response, err error) Post(url, contentType string, body io.Reader) (resp *http.Response, err error)

View File

@ -148,42 +148,42 @@ func TestGetWebmentionEndpointRelativeLinkInHeader(t *testing.T) {
} }
} }
func TestRealWorldWebmention(t *testing.T) { // func TestRealWorldWebmention(t *testing.T) {
links := []string{ // links := []string{
"https://webmention.rocks/test/1", // "https://webmention.rocks/test/1",
"https://webmention.rocks/test/2", // "https://webmention.rocks/test/2",
"https://webmention.rocks/test/3", // "https://webmention.rocks/test/3",
"https://webmention.rocks/test/4", // "https://webmention.rocks/test/4",
"https://webmention.rocks/test/5", // "https://webmention.rocks/test/5",
"https://webmention.rocks/test/6", // "https://webmention.rocks/test/6",
"https://webmention.rocks/test/7", // "https://webmention.rocks/test/7",
"https://webmention.rocks/test/8", // "https://webmention.rocks/test/8",
"https://webmention.rocks/test/9", // "https://webmention.rocks/test/9",
// "https://webmention.rocks/test/10", // not supported // // "https://webmention.rocks/test/10", // not supported
"https://webmention.rocks/test/11", // "https://webmention.rocks/test/11",
"https://webmention.rocks/test/12", // "https://webmention.rocks/test/12",
"https://webmention.rocks/test/13", // "https://webmention.rocks/test/13",
"https://webmention.rocks/test/14", // "https://webmention.rocks/test/14",
"https://webmention.rocks/test/15", // "https://webmention.rocks/test/15",
"https://webmention.rocks/test/16", // "https://webmention.rocks/test/16",
"https://webmention.rocks/test/17", // "https://webmention.rocks/test/17",
"https://webmention.rocks/test/18", // "https://webmention.rocks/test/18",
"https://webmention.rocks/test/19", // "https://webmention.rocks/test/19",
"https://webmention.rocks/test/20", // "https://webmention.rocks/test/20",
"https://webmention.rocks/test/21", // "https://webmention.rocks/test/21",
"https://webmention.rocks/test/22", // "https://webmention.rocks/test/22",
"https://webmention.rocks/test/23/page", // "https://webmention.rocks/test/23/page",
} // }
for _, link := range links { // for _, link := range links {
parser := &owl.OwlHtmlParser{} // parser := &owl.OwlHtmlParser{}
client := &owl.OwlHttpClient{} // client := &owl.OwlHttpClient{}
html, _ := client.Get(link) // html, _ := client.Get(link)
_, err := parser.GetWebmentionEndpoint(html) // _, err := parser.GetWebmentionEndpoint(html)
if err != nil { // if err != nil {
t.Errorf("Unable to find webmention: %v for link %v", err, link) // t.Errorf("Unable to find webmention: %v for link %v", err, link)
} // }
} // }
} // }