Sending Webmentions #10
14
README.md
14
README.md
|
@ -23,6 +23,9 @@ Each directory in the `/users/` directory of a repository is considered a user.
|
||||||
-- This will be rendered as the blog post.
|
-- This will be rendered as the blog post.
|
||||||
-- Must be present for the blog post to be valid.
|
-- Must be present for the blog post to be valid.
|
||||||
-- All other folders will be ignored
|
-- All other folders will be ignored
|
||||||
|
\- status.yml
|
||||||
|
-- Used to track various process status related to the post,
|
||||||
|
-- such as if a webmention was sent.
|
||||||
\- media/
|
\- media/
|
||||||
-- Contains all media files used in the blog post.
|
-- Contains all media files used in the blog post.
|
||||||
-- All files in this folder will be publicly available
|
-- All files in this folder will be publicly available
|
||||||
|
@ -59,3 +62,14 @@ aliases:
|
||||||
Actual post
|
Actual post
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### status.yml
|
||||||
|
|
||||||
|
```
|
||||||
|
webmentions:
|
||||||
|
- target: https://example.com/post
|
||||||
|
supported: true
|
||||||
|
scanned_at: 2021-08-13T17:07:00Z
|
||||||
|
last_sent_at: 2021-08-13T17:07:00Z
|
||||||
|
```
|
17
owl_test.go
17
owl_test.go
|
@ -3,21 +3,34 @@ package owl_test
|
||||||
import (
|
import (
|
||||||
"h4kor/owl-blogs"
|
"h4kor/owl-blogs"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockMicroformatParser struct{}
|
type MockHttpParser struct{}
|
||||||
|
|
||||||
func (*MockMicroformatParser) ParseHEntry(data []byte) (owl.ParsedHEntry, error) {
|
func (*MockHttpParser) ParseHEntry(data []byte) (owl.ParsedHEntry, error) {
|
||||||
return owl.ParsedHEntry{Title: "Mock Title"}, nil
|
return owl.ParsedHEntry{Title: "Mock Title"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*MockHttpParser) ParseLinks(data []byte) ([]string, error) {
|
||||||
|
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")
|
||||||
|
|
169
post.go
169
post.go
|
@ -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"
|
||||||
|
@ -32,6 +34,10 @@ type PostMeta struct {
|
||||||
Draft bool `yaml:"draft"`
|
Draft bool `yaml:"draft"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostStatus struct {
|
||||||
|
Webmentions []WebmentionOut
|
||||||
|
}
|
||||||
|
|
||||||
func (post Post) Id() string {
|
func (post Post) Id() string {
|
||||||
return post.id
|
return post.id
|
||||||
}
|
}
|
||||||
|
@ -40,6 +46,10 @@ 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) StatusFile() string {
|
||||||
|
return path.Join(post.Dir(), "status.yml")
|
||||||
|
}
|
||||||
|
|
||||||
func (post Post) MediaDir() string {
|
func (post Post) MediaDir() string {
|
||||||
return path.Join(post.Dir(), "media")
|
return path.Join(post.Dir(), "media")
|
||||||
}
|
}
|
||||||
|
@ -81,6 +91,42 @@ func (post Post) Content() []byte {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (post Post) Status() PostStatus {
|
||||||
|
// read status file
|
||||||
|
// return parsed webmentions
|
||||||
|
fileName := post.StatusFile()
|
||||||
|
if !fileExists(fileName) {
|
||||||
|
return PostStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return PostStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
status := PostStatus{}
|
||||||
|
err = yaml.Unmarshal(data, &status)
|
||||||
|
if err != nil {
|
||||||
|
return PostStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (post Post) PersistStatus(status PostStatus) error {
|
||||||
|
data, err := yaml.Marshal(status)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(post.StatusFile(), data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (post Post) RenderedContent() bytes.Buffer {
|
func (post Post) RenderedContent() bytes.Buffer {
|
||||||
data := post.Content()
|
data := post.Content()
|
||||||
|
|
||||||
|
@ -156,7 +202,7 @@ func (post *Post) WebmentionFile(source string) string {
|
||||||
return path.Join(post.WebmentionDir(), hashStr+".yml")
|
return path.Join(post.WebmentionDir(), hashStr+".yml")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (post *Post) PersistWebmention(webmention Webmention) error {
|
func (post *Post) PersistWebmention(webmention WebmentionIn) error {
|
||||||
// ensure dir exists
|
// ensure dir exists
|
||||||
os.MkdirAll(post.WebmentionDir(), 0755)
|
os.MkdirAll(post.WebmentionDir(), 0755)
|
||||||
|
|
||||||
|
@ -169,7 +215,7 @@ func (post *Post) PersistWebmention(webmention Webmention) error {
|
||||||
return os.WriteFile(fileName, data, 0644)
|
return os.WriteFile(fileName, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (post *Post) Webmention(source string) (Webmention, error) {
|
func (post *Post) Webmention(source string) (WebmentionIn, error) {
|
||||||
// ensure dir exists
|
// ensure dir exists
|
||||||
os.MkdirAll(post.WebmentionDir(), 0755)
|
os.MkdirAll(post.WebmentionDir(), 0755)
|
||||||
|
|
||||||
|
@ -177,18 +223,18 @@ func (post *Post) Webmention(source string) (Webmention, error) {
|
||||||
fileName := post.WebmentionFile(source)
|
fileName := post.WebmentionFile(source)
|
||||||
if !fileExists(fileName) {
|
if !fileExists(fileName) {
|
||||||
// return error if file doesn't exist
|
// return error if file doesn't exist
|
||||||
return Webmention{}, fmt.Errorf("Webmention file not found: %s", source)
|
return WebmentionIn{}, fmt.Errorf("Webmention file not found: %s", source)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(fileName)
|
data, err := os.ReadFile(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Webmention{}, err
|
return WebmentionIn{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mention := Webmention{}
|
mention := WebmentionIn{}
|
||||||
err = yaml.Unmarshal(data, &mention)
|
err = yaml.Unmarshal(data, &mention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Webmention{}, err
|
return WebmentionIn{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return mention, nil
|
return mention, nil
|
||||||
|
@ -198,7 +244,7 @@ func (post *Post) AddWebmention(source string) error {
|
||||||
// Check if file already exists
|
// Check if file already exists
|
||||||
_, err := post.Webmention(source)
|
_, err := post.Webmention(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
webmention := Webmention{
|
webmention := WebmentionIn{
|
||||||
Source: source,
|
Source: source,
|
||||||
}
|
}
|
||||||
defer post.EnrichWebmention(source)
|
defer post.EnrichWebmention(source)
|
||||||
|
@ -207,8 +253,49 @@ func (post *Post) AddWebmention(source string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (post *Post) AddOutgoingWebmention(target string) error {
|
||||||
|
status := post.Status()
|
||||||
|
|
||||||
|
// Check if file already exists
|
||||||
|
_, err := post.Webmention(target)
|
||||||
|
if err != nil {
|
||||||
|
webmention := WebmentionOut{
|
||||||
|
Target: target,
|
||||||
|
}
|
||||||
|
// if target is not in status, add it
|
||||||
|
for _, t := range status.Webmentions {
|
||||||
|
if t.Target == webmention.Target {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.Webmentions = append(status.Webmentions, webmention)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
@ -223,17 +310,17 @@ func (post *Post) EnrichWebmention(source string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (post *Post) Webmentions() []Webmention {
|
func (post *Post) Webmentions() []WebmentionIn {
|
||||||
// ensure dir exists
|
// ensure dir exists
|
||||||
os.MkdirAll(post.WebmentionDir(), 0755)
|
os.MkdirAll(post.WebmentionDir(), 0755)
|
||||||
files := listDir(post.WebmentionDir())
|
files := listDir(post.WebmentionDir())
|
||||||
webmentions := []Webmention{}
|
webmentions := []WebmentionIn{}
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
data, err := os.ReadFile(path.Join(post.WebmentionDir(), file))
|
data, err := os.ReadFile(path.Join(post.WebmentionDir(), file))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mention := Webmention{}
|
mention := WebmentionIn{}
|
||||||
err = yaml.Unmarshal(data, &mention)
|
err = yaml.Unmarshal(data, &mention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
@ -244,9 +331,9 @@ func (post *Post) Webmentions() []Webmention {
|
||||||
return webmentions
|
return webmentions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (post *Post) ApprovedWebmentions() []Webmention {
|
func (post *Post) ApprovedWebmentions() []WebmentionIn {
|
||||||
webmentions := post.Webmentions()
|
webmentions := post.Webmentions()
|
||||||
approved := []Webmention{}
|
approved := []WebmentionIn{}
|
||||||
for _, webmention := range webmentions {
|
for _, webmention := range webmentions {
|
||||||
if webmention.ApprovalStatus == "approved" {
|
if webmention.ApprovalStatus == "approved" {
|
||||||
approved = append(approved, webmention)
|
approved = append(approved, webmention)
|
||||||
|
@ -259,3 +346,59 @@ func (post *Post) ApprovedWebmentions() []Webmention {
|
||||||
})
|
})
|
||||||
return approved
|
return approved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (post *Post) OutgoingWebmentions() []WebmentionOut {
|
||||||
|
status := post.Status()
|
||||||
|
return status.Webmentions
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (post *Post) ScanForLinks() error {
|
||||||
|
// this could be done in markdown parsing, but I don't want to
|
||||||
|
// rely on goldmark for this (yet)
|
||||||
|
postHtml, err := renderPostContent(post)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
links, _ := post.user.repo.Parser.ParseLinks([]byte(postHtml))
|
||||||
|
for _, link := range links {
|
||||||
|
post.AddOutgoingWebmention(link)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
102
post_test.go
102
post_test.go
|
@ -173,7 +173,7 @@ func TestPersistWebmention(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo()
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
webmention := owl.Webmention{
|
webmention := owl.WebmentionIn{
|
||||||
Source: "http://example.com/source",
|
Source: "http://example.com/source",
|
||||||
}
|
}
|
||||||
err := post.PersistWebmention(webmention)
|
err := post.PersistWebmention(webmention)
|
||||||
|
@ -192,8 +192,8 @@ 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 = &MockMicroformatParser{}
|
repo.Parser = &MockHttpParser{}
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
@ -210,8 +210,8 @@ 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 = &MockMicroformatParser{}
|
repo.Parser = &MockHttpParser{}
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
@ -240,8 +240,8 @@ 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 = &MockMicroformatParser{}
|
repo.Parser = &MockHttpParser{}
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
@ -265,25 +265,25 @@ func TestApprovedWebmentions(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo()
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
webmention := owl.Webmention{
|
webmention := owl.WebmentionIn{
|
||||||
Source: "http://example.com/source",
|
Source: "http://example.com/source",
|
||||||
ApprovalStatus: "approved",
|
ApprovalStatus: "approved",
|
||||||
RetrievedAt: time.Now(),
|
RetrievedAt: time.Now(),
|
||||||
}
|
}
|
||||||
post.PersistWebmention(webmention)
|
post.PersistWebmention(webmention)
|
||||||
webmention = owl.Webmention{
|
webmention = owl.WebmentionIn{
|
||||||
Source: "http://example.com/source2",
|
Source: "http://example.com/source2",
|
||||||
ApprovalStatus: "",
|
ApprovalStatus: "",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -1),
|
RetrievedAt: time.Now().Add(time.Hour * -1),
|
||||||
}
|
}
|
||||||
post.PersistWebmention(webmention)
|
post.PersistWebmention(webmention)
|
||||||
webmention = owl.Webmention{
|
webmention = owl.WebmentionIn{
|
||||||
Source: "http://example.com/source3",
|
Source: "http://example.com/source3",
|
||||||
ApprovalStatus: "approved",
|
ApprovalStatus: "approved",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -2),
|
RetrievedAt: time.Now().Add(time.Hour * -2),
|
||||||
}
|
}
|
||||||
post.PersistWebmention(webmention)
|
post.PersistWebmention(webmention)
|
||||||
webmention = owl.Webmention{
|
webmention = owl.WebmentionIn{
|
||||||
Source: "http://example.com/source4",
|
Source: "http://example.com/source4",
|
||||||
ApprovalStatus: "rejected",
|
ApprovalStatus: "rejected",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -3),
|
RetrievedAt: time.Now().Add(time.Hour * -3),
|
||||||
|
@ -303,3 +303,83 @@ func TestApprovedWebmentions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScanningForLinks(t *testing.T) {
|
||||||
|
repo := getTestRepo()
|
||||||
|
user, _ := repo.CreateUser("testuser")
|
||||||
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
content := "---\n"
|
||||||
|
content += "title: test\n"
|
||||||
|
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||||
|
content += "---\n"
|
||||||
|
content += "\n"
|
||||||
|
content += "[Hello](https://example.com/hello)\n"
|
||||||
|
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||||
|
|
||||||
|
post.ScanForLinks()
|
||||||
|
webmentions := post.OutgoingWebmentions()
|
||||||
|
if len(webmentions) != 1 {
|
||||||
|
t.Errorf("Expected 1 webmention, got %d", len(webmentions))
|
||||||
|
}
|
||||||
|
if webmentions[0].Target != "https://example.com/hello" {
|
||||||
|
t.Errorf("Expected target: %s, got %s", "https://example.com/hello", webmentions[0].Target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanningForLinksDoesNotAddDuplicates(t *testing.T) {
|
||||||
|
repo := getTestRepo()
|
||||||
|
user, _ := repo.CreateUser("testuser")
|
||||||
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
content := "---\n"
|
||||||
|
content += "title: test\n"
|
||||||
|
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||||
|
content += "---\n"
|
||||||
|
content += "\n"
|
||||||
|
content += "[Hello](https://example.com/hello)\n"
|
||||||
|
content += "[Hello](https://example.com/hello)\n"
|
||||||
|
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||||
|
|
||||||
|
post.ScanForLinks()
|
||||||
|
post.ScanForLinks()
|
||||||
|
post.ScanForLinks()
|
||||||
|
webmentions := post.OutgoingWebmentions()
|
||||||
|
if len(webmentions) != 1 {
|
||||||
|
t.Errorf("Expected 1 webmention, got %d", len(webmentions))
|
||||||
|
}
|
||||||
|
if webmentions[0].Target != "https://example.com/hello" {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -68,13 +68,18 @@ func renderIntoBaseTemplate(user User, data PageContent) (string, error) {
|
||||||
return html.String(), nil
|
return html.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderPost(post *Post) (string, error) {
|
func renderPostContent(post *Post) (string, error) {
|
||||||
buf := post.RenderedContent()
|
buf := post.RenderedContent()
|
||||||
postHtml, err := renderEmbedTemplate("embed/post.html", PostRenderData{
|
postHtml, err := renderEmbedTemplate("embed/post.html", PostRenderData{
|
||||||
Title: post.Title(),
|
Title: post.Title(),
|
||||||
Post: post,
|
Post: post,
|
||||||
Content: template.HTML(buf.String()),
|
Content: template.HTML(buf.String()),
|
||||||
})
|
})
|
||||||
|
return postHtml, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderPost(post *Post) (string, error) {
|
||||||
|
postHtml, err := renderPostContent(post)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,14 +162,14 @@ func TestRenderPostIncludesRelToWebMention(t *testing.T) {
|
||||||
func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) {
|
func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) {
|
||||||
user := getTestUser()
|
user := getTestUser()
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
webmention := owl.Webmention{
|
webmention := owl.WebmentionIn{
|
||||||
Source: "http://example.com/source3",
|
Source: "http://example.com/source3",
|
||||||
Title: "Test Title",
|
Title: "Test Title",
|
||||||
ApprovalStatus: "approved",
|
ApprovalStatus: "approved",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -2),
|
RetrievedAt: time.Now().Add(time.Hour * -2),
|
||||||
}
|
}
|
||||||
post.PersistWebmention(webmention)
|
post.PersistWebmention(webmention)
|
||||||
webmention = owl.Webmention{
|
webmention = owl.WebmentionIn{
|
||||||
Source: "http://example.com/source4",
|
Source: "http://example.com/source4",
|
||||||
ApprovalStatus: "rejected",
|
ApprovalStatus: "rejected",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -3),
|
RetrievedAt: time.Now().Add(time.Hour * -3),
|
||||||
|
|
|
@ -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 MicroformatParser
|
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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,36 +4,47 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Webmention struct {
|
type WebmentionIn struct {
|
||||||
Source string `yaml:"source"`
|
Source string `yaml:"source"`
|
||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
ApprovalStatus string `yaml:"approval_status"`
|
ApprovalStatus string `yaml:"approval_status"`
|
||||||
RetrievedAt time.Time `yaml:"retrieved_at"`
|
RetrievedAt time.Time `yaml:"retrieved_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpRetriever interface {
|
type WebmentionOut struct {
|
||||||
|
Target string `yaml:"target"`
|
||||||
|
Supported bool `yaml:"supported"`
|
||||||
|
ScannedAt time.Time `yaml:"scanned_at"`
|
||||||
|
LastSentAt time.Time `yaml:"last_sent_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpClient interface {
|
||||||
Get(url string) ([]byte, error)
|
Get(url string) ([]byte, error)
|
||||||
|
Post(url string, data url.Values) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MicroformatParser interface {
|
type HtmlParser interface {
|
||||||
ParseHEntry(data []byte) (ParsedHEntry, error)
|
ParseHEntry(data []byte) (ParsedHEntry, 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
|
||||||
|
@ -44,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)
|
||||||
|
@ -53,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
|
||||||
|
@ -95,3 +117,59 @@ func (OwlMicroformatParser) ParseHEntry(data []byte) (ParsedHEntry, error) {
|
||||||
}
|
}
|
||||||
return findHFeed(doc)
|
return findHFeed(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (OwlHtmlParser) ParseLinks(data []byte) ([]string, error) {
|
||||||
|
doc, err := html.Parse(strings.NewReader(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
return make([]string, 0), err
|
||||||
|
}
|
||||||
|
|
||||||
|
var findLinks func(*html.Node) ([]string, error)
|
||||||
|
findLinks = func(n *html.Node) ([]string, error) {
|
||||||
|
links := make([]string, 0)
|
||||||
|
if n.Type == html.ElementNode && n.Data == "a" {
|
||||||
|
for _, attr := range n.Attr {
|
||||||
|
if attr.Key == "href" {
|
||||||
|
links = append(links, attr.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
childLinks, _ := findLinks(c)
|
||||||
|
links = append(links, childLinks...)
|
||||||
|
}
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue