Sending Webmentions #10

Merged
h4kor merged 18 commits from webmention into master 2022-09-06 19:49:00 +00:00
6 changed files with 157 additions and 17 deletions
Showing only changes of commit 576fa32b20 - Show all commits

View File

@ -3,6 +3,7 @@ package owl_test
import ( import (
"h4kor/owl-blogs" "h4kor/owl-blogs"
"math/rand" "math/rand"
"net/url"
"time" "time"
) )
@ -16,12 +17,20 @@ func (*MockHttpParser) ParseLinks(data []byte) ([]string, error) {
return []string{"http://example.com"}, nil return []string{"http://example.com"}, nil
} }
func (*MockHttpParser) GetWebmentionEndpoint(data []byte) (string, error) {
return "http://example.com/webmention", nil
}
type MockHttpRetriever struct{} type MockHttpRetriever struct{}
func (*MockHttpRetriever) Get(url string) ([]byte, error) { func (*MockHttpRetriever) Get(url string) ([]byte, error) {
return []byte(""), nil return []byte(""), nil
} }
func (m *MockHttpRetriever) Post(url string, data url.Values) ([]byte, error) {
return []byte(""), nil
}
func randomName() string { func randomName() string {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
var letters = []rune("abcdefghijklmnopqrstuvwxyz") var letters = []rune("abcdefghijklmnopqrstuvwxyz")

58
post.go
View File

@ -6,9 +6,11 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"path" "path"
"sort" "sort"
"time"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
@ -272,8 +274,28 @@ func (post *Post) AddOutgoingWebmention(target string) error {
return post.PersistStatus(status) return post.PersistStatus(status)
} }
func (post *Post) UpdateOutgoingWebmention(webmention *WebmentionOut) error {
status := post.Status()
// if target is not in status, add it
replaced := false
for i, t := range status.Webmentions {
if t.Target == webmention.Target {
status.Webmentions[i] = *webmention
replaced = true
break
}
}
if !replaced {
status.Webmentions = append(status.Webmentions, *webmention)
}
return post.PersistStatus(status)
}
func (post *Post) EnrichWebmention(source string) error { func (post *Post) EnrichWebmention(source string) error {
html, err := post.user.repo.Retriever.Get(source) html, err := post.user.repo.HttpClient.Get(source)
if err == nil { if err == nil {
webmention, err := post.Webmention(source) webmention, err := post.Webmention(source)
if err != nil { if err != nil {
@ -346,3 +368,37 @@ func (post *Post) ScanForLinks() error {
} }
return nil return nil
} }
func (post *Post) SendWebmention(webmention WebmentionOut) error {
defer post.UpdateOutgoingWebmention(&webmention)
webmention.ScannedAt = time.Now()
html, err := post.user.repo.HttpClient.Get(webmention.Target)
if err != nil {
// TODO handle error
webmention.Supported = false
return err
}
endpoint, err := post.user.repo.Parser.GetWebmentionEndpoint(html)
if err != nil {
// TODO handle error
webmention.Supported = false
return err
}
webmention.Supported = true
// send webmention
payload := url.Values{}
payload.Set("source", post.FullUrl())
payload.Set("target", webmention.Target)
_, err = post.user.repo.HttpClient.Post(endpoint, payload)
if err != nil {
// TODO handle error
return err
}
// update webmention status
webmention.LastSentAt = time.Now()
return nil
}

View File

@ -192,7 +192,7 @@ func TestPersistWebmention(t *testing.T) {
func TestAddWebmentionCreatesFile(t *testing.T) { func TestAddWebmentionCreatesFile(t *testing.T) {
repo := getTestRepo() repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{} repo.HttpClient = &MockHttpRetriever{}
repo.Parser = &MockHttpParser{} repo.Parser = &MockHttpParser{}
user, _ := repo.CreateUser("testuser") user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost") post, _ := user.CreateNewPost("testpost")
@ -210,7 +210,7 @@ func TestAddWebmentionCreatesFile(t *testing.T) {
func TestAddWebmentionNotOverwritingFile(t *testing.T) { func TestAddWebmentionNotOverwritingFile(t *testing.T) {
repo := getTestRepo() repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{} repo.HttpClient = &MockHttpRetriever{}
repo.Parser = &MockHttpParser{} repo.Parser = &MockHttpParser{}
user, _ := repo.CreateUser("testuser") user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost") post, _ := user.CreateNewPost("testpost")
@ -240,7 +240,7 @@ func TestAddWebmentionNotOverwritingFile(t *testing.T) {
func TestAddWebmentionAddsParsedTitle(t *testing.T) { func TestAddWebmentionAddsParsedTitle(t *testing.T) {
repo := getTestRepo() repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{} repo.HttpClient = &MockHttpRetriever{}
repo.Parser = &MockHttpParser{} repo.Parser = &MockHttpParser{}
user, _ := repo.CreateUser("testuser") user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost") post, _ := user.CreateNewPost("testpost")
@ -352,3 +352,34 @@ func TestScanningForLinksDoesNotAddDuplicates(t *testing.T) {
t.Errorf("Expected target: %s, got %s", "https://example.com/hello", webmentions[0].Target) t.Errorf("Expected target: %s, got %s", "https://example.com/hello", webmentions[0].Target)
} }
} }
func TestCanSendWebmention(t *testing.T) {
repo := getTestRepo()
repo.HttpClient = &MockHttpRetriever{}
repo.Parser = &MockHttpParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
webmention := owl.WebmentionOut{
Target: "http://example.com",
}
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].Target != "http://example.com" {
t.Errorf("Expected target: %s, got %s", "http://example.com", webmentions[0].Target)
}
if webmentions[0].LastSentAt.IsZero() {
t.Errorf("Expected LastSentAt to be set")
}
}

View File

@ -20,8 +20,8 @@ type Repository struct {
single_user_mode bool single_user_mode bool
active_user string active_user string
allow_raw_html bool allow_raw_html bool
Retriever HttpRetriever HttpClient HttpClient
Parser HttpParser Parser HtmlParser
} }
type RepoConfig struct { type RepoConfig struct {
@ -29,7 +29,7 @@ type RepoConfig struct {
} }
func CreateRepository(name string) (Repository, error) { func CreateRepository(name string) (Repository, error) {
newRepo := Repository{name: name, Parser: OwlMicroformatParser{}, Retriever: OwlHttpRetriever{}} newRepo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: OwlHttpClient{}}
// check if repository already exists // check if repository already exists
if dirExists(newRepo.Dir()) { if dirExists(newRepo.Dir()) {
return Repository{}, fmt.Errorf("Repository already exists") return Repository{}, fmt.Errorf("Repository already exists")
@ -63,7 +63,7 @@ func CreateRepository(name string) (Repository, error) {
func OpenRepository(name string) (Repository, error) { func OpenRepository(name string) (Repository, error) {
repo := Repository{name: name, Parser: OwlMicroformatParser{}, Retriever: OwlHttpRetriever{}} repo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: OwlHttpClient{}}
if !dirExists(repo.Dir()) { if !dirExists(repo.Dir()) {
return Repository{}, fmt.Errorf("Repository does not exist: " + repo.Dir()) return Repository{}, fmt.Errorf("Repository does not exist: " + repo.Dir())
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
@ -24,24 +25,26 @@ type WebmentionOut struct {
LastSentAt time.Time `yaml:"last_sent_at"` LastSentAt time.Time `yaml:"last_sent_at"`
} }
type HttpRetriever interface { type HttpClient interface {
Get(url string) ([]byte, error) Get(url string) ([]byte, error)
Post(url string, data url.Values) ([]byte, error)
} }
type HttpParser interface { type HtmlParser interface {
ParseHEntry(data []byte) (ParsedHEntry, error) ParseHEntry(data []byte) (ParsedHEntry, error)
ParseLinks(data []byte) ([]string, error) ParseLinks(data []byte) ([]string, error)
GetWebmentionEndpoint(data []byte) (string, error)
} }
type OwlHttpRetriever struct{} type OwlHttpClient struct{}
type OwlMicroformatParser struct{} type OwlHtmlParser struct{}
type ParsedHEntry struct { type ParsedHEntry struct {
Title string Title string
} }
func (OwlHttpRetriever) Get(url string) ([]byte, error) { func (OwlHttpClient) Get(url string) ([]byte, error) {
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return []byte{}, err return []byte{}, err
@ -52,6 +55,17 @@ func (OwlHttpRetriever) Get(url string) ([]byte, error) {
return data, err return data, err
} }
func (OwlHttpClient) Post(url string, data url.Values) ([]byte, error) {
resp, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return []byte{}, err
}
var respData []byte
_, err = resp.Body.Read(respData)
return respData, err
}
func collectText(n *html.Node, buf *bytes.Buffer) { func collectText(n *html.Node, buf *bytes.Buffer) {
if n.Type == html.TextNode { if n.Type == html.TextNode {
buf.WriteString(n.Data) buf.WriteString(n.Data)
@ -61,7 +75,7 @@ func collectText(n *html.Node, buf *bytes.Buffer) {
} }
} }
func (OwlMicroformatParser) ParseHEntry(data []byte) (ParsedHEntry, error) { func (OwlHtmlParser) ParseHEntry(data []byte) (ParsedHEntry, error) {
doc, err := html.Parse(strings.NewReader(string(data))) doc, err := html.Parse(strings.NewReader(string(data)))
if err != nil { if err != nil {
return ParsedHEntry{}, err return ParsedHEntry{}, err
@ -104,7 +118,7 @@ func (OwlMicroformatParser) ParseHEntry(data []byte) (ParsedHEntry, error) {
return findHFeed(doc) return findHFeed(doc)
} }
func (OwlMicroformatParser) ParseLinks(data []byte) ([]string, error) { func (OwlHtmlParser) ParseLinks(data []byte) ([]string, error) {
doc, err := html.Parse(strings.NewReader(string(data))) doc, err := html.Parse(strings.NewReader(string(data)))
if err != nil { if err != nil {
return make([]string, 0), err return make([]string, 0), err
@ -129,3 +143,33 @@ func (OwlMicroformatParser) ParseLinks(data []byte) ([]string, error) {
return findLinks(doc) return findLinks(doc)
} }
func (OwlHtmlParser) GetWebmentionEndpoint(data []byte) (string, error) {
doc, err := html.Parse(strings.NewReader(string(data)))
if err != nil {
return "", err
}
var findEndpoint func(*html.Node) (string, error)
findEndpoint = func(n *html.Node) (string, error) {
if n.Type == html.ElementNode && n.Data == "link" {
for _, attr := range n.Attr {
if attr.Key == "rel" && attr.Val == "webmention" {
for _, attr := range n.Attr {
if attr.Key == "href" {
return attr.Val, nil
}
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
endpoint, err := findEndpoint(c)
if err == nil {
return endpoint, nil
}
}
return "", errors.New("no webmention endpoint found")
}
return findEndpoint(doc)
}

View File

@ -11,7 +11,7 @@ import (
func TestParseValidHEntry(t *testing.T) { func TestParseValidHEntry(t *testing.T) {
html := []byte("<div class=\"h-entry\"><div class=\"p-name\">Foo</div></div>") html := []byte("<div class=\"h-entry\"><div class=\"p-name\">Foo</div></div>")
parser := &owl.OwlMicroformatParser{} parser := &owl.OwlHtmlParser{}
entry, err := parser.ParseHEntry(html) entry, err := parser.ParseHEntry(html)
if err != nil { if err != nil {
@ -24,7 +24,7 @@ func TestParseValidHEntry(t *testing.T) {
func TestParseValidHEntryWithoutTitle(t *testing.T) { func TestParseValidHEntryWithoutTitle(t *testing.T) {
html := []byte("<div class=\"h-entry\"></div><div class=\"p-name\">Foo</div>") html := []byte("<div class=\"h-entry\"></div><div class=\"p-name\">Foo</div>")
parser := &owl.OwlMicroformatParser{} parser := &owl.OwlHtmlParser{}
entry, err := parser.ParseHEntry(html) entry, err := parser.ParseHEntry(html)
if err != nil { if err != nil {