refactored webmentions to use a single file for in and out going mentions

This commit is contained in:
Niko Abeler 2022-09-07 22:06:59 +02:00
parent 1fc6b9e9d2
commit c0151dbc15
6 changed files with 123 additions and 176 deletions

View File

@ -116,7 +116,7 @@ func userWebmentionHandler(repo *owl.Repository) func(http.ResponseWriter, *http
w.Write([]byte("Post not found")) w.Write([]byte("Post not found"))
return return
} }
err = post.AddWebmention(source[0]) err = post.AddIncomingWebmention(source[0])
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Unable to process webmention")) w.Write([]byte("Unable to process webmention"))

View File

@ -75,7 +75,7 @@ func TestWebmentionWrittenToPost(t *testing.T) {
// Check the status code is what we expect. // Check the status code is what we expect.
assertStatus(t, rr, http.StatusAccepted) assertStatus(t, rr, http.StatusAccepted)
if len(post.Webmentions()) != 1 { if len(post.IncomingWebmentions()) != 1 {
t.Errorf("no webmention written to post") t.Errorf("no webmention written to post")
} }
} }

222
post.go
View File

@ -2,9 +2,7 @@ package owl
import ( import (
"bytes" "bytes"
"crypto/sha256" "errors"
"encoding/base64"
"fmt"
"io/ioutil" "io/ioutil"
"net/url" "net/url"
"os" "os"
@ -34,8 +32,9 @@ type PostMeta struct {
Draft bool `yaml:"draft"` Draft bool `yaml:"draft"`
} }
type PostStatus struct { type PostWebmetions struct {
Webmentions []WebmentionOut Incoming []WebmentionIn `ymal:"incoming"`
Outgoing []WebmentionOut `ymal:"outgoing"`
} }
func (post Post) Id() string { func (post Post) Id() string {
@ -46,18 +45,14 @@ 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 { func (post Post) WebmentionsFile() string {
return path.Join(post.Dir(), "status.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) WebmentionDir() string {
return path.Join(post.Dir(), "webmention")
}
func (post Post) UrlPath() string { func (post Post) UrlPath() string {
return post.user.UrlPath() + "posts/" + post.id + "/" return post.user.UrlPath() + "posts/" + post.id + "/"
} }
@ -91,35 +86,35 @@ func (post Post) Content() []byte {
return data return data
} }
func (post Post) Status() PostStatus { func (post Post) Webmentions() PostWebmetions {
// read status file // read status file
// return parsed webmentions // return parsed webmentions
fileName := post.StatusFile() fileName := post.WebmentionsFile()
if !fileExists(fileName) { if !fileExists(fileName) {
return PostStatus{} return PostWebmetions{}
} }
data, err := os.ReadFile(fileName) data, err := os.ReadFile(fileName)
if err != nil { if err != nil {
return PostStatus{} return PostWebmetions{}
} }
status := PostStatus{} webmentions := PostWebmetions{}
err = yaml.Unmarshal(data, &status) err = yaml.Unmarshal(data, &webmentions)
if err != nil { if err != nil {
return PostStatus{} return PostWebmetions{}
} }
return status return webmentions
} }
func (post Post) PersistStatus(status PostStatus) error { func (post Post) PersistWebmentions(webmentions PostWebmetions) error {
data, err := yaml.Marshal(status) data, err := yaml.Marshal(webmentions)
if err != nil { if err != nil {
return err return err
} }
err = os.WriteFile(post.StatusFile(), data, 0644) err = os.WriteFile(post.WebmentionsFile(), data, 0644)
if err != nil { if err != nil {
return err return err
} }
@ -195,103 +190,85 @@ func (post *Post) LoadMeta() error {
return nil return nil
} }
func (post *Post) WebmentionFile(source string) string { func (post *Post) PersistIncomingWebmention(webmention WebmentionIn) error {
wms := post.Webmentions()
hash := sha256.Sum256([]byte(source))
hashStr := base64.URLEncoding.EncodeToString(hash[:])
return path.Join(post.WebmentionDir(), hashStr+".yml")
}
func (post *Post) PersistWebmention(webmention WebmentionIn) error {
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
// write to file
fileName := post.WebmentionFile(webmention.Source)
data, err := yaml.Marshal(webmention)
if err != nil {
return err
}
return os.WriteFile(fileName, data, 0644)
}
func (post *Post) Webmention(source string) (WebmentionIn, error) {
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
// Check if file exists
fileName := post.WebmentionFile(source)
if !fileExists(fileName) {
// return error if file doesn't exist
return WebmentionIn{}, fmt.Errorf("Webmention file not found: %s", source)
}
data, err := os.ReadFile(fileName)
if err != nil {
return WebmentionIn{}, err
}
mention := WebmentionIn{}
err = yaml.Unmarshal(data, &mention)
if err != nil {
return WebmentionIn{}, err
}
return mention, nil
}
func (post *Post) AddWebmention(source string) error {
// Check if file already exists
_, err := post.Webmention(source)
if err != nil {
webmention := WebmentionIn{
Source: source,
}
defer post.EnrichWebmention(source)
return post.PersistWebmention(webmention)
}
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 // if target is not in status, add it
replaced := false replaced := false
for i, t := range status.Webmentions { for i, t := range wms.Incoming {
if t.Target == webmention.Target { if t.Source == webmention.Source {
status.Webmentions[i] = *webmention wms.Incoming[i] = webmention
replaced = true replaced = true
break break
} }
} }
if !replaced { if !replaced {
status.Webmentions = append(status.Webmentions, *webmention) wms.Incoming = append(wms.Incoming, webmention)
} }
return post.PersistStatus(status) return post.PersistWebmentions(wms)
}
func (post *Post) Webmention(source string) (WebmentionIn, error) {
wms := post.Webmentions()
for _, wm := range wms.Incoming {
if wm.Source == source {
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()
// Check if file already exists
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
for i, t := range wms.Outgoing {
if t.Target == webmention.Target {
wms.Outgoing[i] = *webmention
replaced = true
break
}
}
if !replaced {
wms.Outgoing = append(wms.Outgoing, *webmention)
}
return post.PersistWebmentions(wms)
} }
func (post *Post) EnrichWebmention(source string) error { func (post *Post) EnrichWebmention(source string) error {
@ -304,35 +281,18 @@ func (post *Post) EnrichWebmention(source string) error {
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
return post.PersistWebmention(webmention) return post.PersistIncomingWebmention(webmention)
} }
} }
return err return err
} }
func (post *Post) Webmentions() []WebmentionIn { func (post *Post) IncomingWebmentions() []WebmentionIn {
// ensure dir exists return post.Webmentions().Incoming
os.MkdirAll(post.WebmentionDir(), 0755)
files := listDir(post.WebmentionDir())
webmentions := []WebmentionIn{}
for _, file := range files {
data, err := os.ReadFile(path.Join(post.WebmentionDir(), file))
if err != nil {
continue
}
mention := WebmentionIn{}
err = yaml.Unmarshal(data, &mention)
if err != nil {
continue
}
webmentions = append(webmentions, mention)
}
return webmentions
} }
func (post *Post) ApprovedWebmentions() []WebmentionIn { func (post *Post) ApprovedWebmentions() []WebmentionIn {
webmentions := post.Webmentions() webmentions := post.IncomingWebmentions()
approved := []WebmentionIn{} approved := []WebmentionIn{}
for _, webmention := range webmentions { for _, webmention := range webmentions {
if webmention.ApprovalStatus == "approved" { if webmention.ApprovalStatus == "approved" {
@ -348,9 +308,7 @@ func (post *Post) ApprovedWebmentions() []WebmentionIn {
} }
func (post *Post) OutgoingWebmentions() []WebmentionOut { func (post *Post) OutgoingWebmentions() []WebmentionOut {
status := post.Status() return post.Webmentions().Outgoing
return status.Webmentions
} }
// ScanForLinks scans the post content for links and adds them to the // ScanForLinks scans the post content for links and adds them to the

View File

@ -167,18 +167,18 @@ func TestLoadMeta(t *testing.T) {
/// Webmention /// Webmention
/// ///
func TestPersistWebmention(t *testing.T) { func TestPersistIncomingWebmention(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")
webmention := owl.WebmentionIn{ webmention := owl.WebmentionIn{
Source: "http://example.com/source", Source: "http://example.com/source",
} }
err := post.PersistWebmention(webmention) err := post.PersistIncomingWebmention(webmention)
if err != nil { if err != nil {
t.Errorf("Got error: %v", err) t.Errorf("Got error: %v", err)
} }
mentions := post.Webmentions() mentions := post.IncomingWebmentions()
if len(mentions) != 1 { if len(mentions) != 1 {
t.Errorf("Expected 1 webmention, got %d", len(mentions)) t.Errorf("Expected 1 webmention, got %d", len(mentions))
} }
@ -188,74 +188,64 @@ func TestPersistWebmention(t *testing.T) {
} }
} }
func TestAddWebmentionCreatesFile(t *testing.T) { func TestAddIncomingWebmentionCreatesFile(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{}) repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{} repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{} repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser") user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost") post, _ := user.CreateNewPost("testpost")
err := post.AddWebmention("https://example.com") err := post.AddIncomingWebmention("https://example.com")
if err != nil { if err != nil {
t.Errorf("Got Error: %v", err) t.Errorf("Got Error: %v", err)
} }
mentions := post.Webmentions() mentions := post.IncomingWebmentions()
if len(mentions) != 1 { if len(mentions) != 1 {
t.Errorf("Expected 1 webmention, got %d", len(mentions)) t.Errorf("Expected 1 webmention, got %d", len(mentions))
} }
} }
func TestAddWebmentionNotOverwritingFile(t *testing.T) { func TestAddIncomingWebmentionNotOverwritingWebmention(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{}) repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{} repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{} repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser") user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost") post, _ := user.CreateNewPost("testpost")
post.AddWebmention("https://example.com") post.PersistIncomingWebmention(owl.WebmentionIn{
dir, _ := os.Open(post.WebmentionDir()) Source: "https://example.com",
defer dir.Close() ApprovalStatus: "approved",
files, _ := dir.Readdirnames(-1) })
if len(files) != 1 { post.AddIncomingWebmention("https://example.com")
t.Error("No file created for webmention")
mentions := post.IncomingWebmentions()
if len(mentions) != 1 {
t.Errorf("Expected 1 webmention, got %d", len(mentions))
} }
content := "url: https://example.com\n" if mentions[0].ApprovalStatus != "approved" {
content += "verified: true" t.Errorf("Expected approval status: %s, got %s", "approved", mentions[0].ApprovalStatus)
os.WriteFile(path.Join(post.WebmentionDir(), files[0]), []byte(content), 0644)
post.AddWebmention("https://example.com")
fileContent, _ := os.ReadFile(path.Join(post.WebmentionDir(), files[0]))
if string(fileContent) != content {
t.Error("File content was modified.")
t.Errorf("Got: %v", fileContent)
t.Errorf("Expected: %v", content)
} }
} }
func TestAddWebmentionAddsParsedTitle(t *testing.T) { func TestAddIncomingWebmentionAddsParsedTitle(t *testing.T) {
repo := getTestRepo(owl.RepoConfig{}) repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{} repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{} repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser") user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost") post, _ := user.CreateNewPost("testpost")
post.AddWebmention("https://example.com") post.AddIncomingWebmention("https://example.com")
dir, _ := os.Open(post.WebmentionDir())
defer dir.Close()
files, _ := dir.Readdirnames(-1)
if len(files) != 1 { mentions := post.IncomingWebmentions()
t.Error("No file created for webmention") if len(mentions) != 1 {
t.Errorf("Expected 1 webmention, got %d", len(mentions))
} }
fileContent, _ := os.ReadFile(path.Join(post.WebmentionDir(), files[0])) if mentions[0].Title != "Mock Title" {
if !strings.Contains(string(fileContent), "Mock Title") { t.Errorf("Expected title: %s, got %s", "Mock Title", mentions[0].Title)
t.Error("File not containing the title.")
t.Errorf("Got: %v", string(fileContent))
} }
} }
@ -268,25 +258,25 @@ func TestApprovedWebmentions(t *testing.T) {
ApprovalStatus: "approved", ApprovalStatus: "approved",
RetrievedAt: time.Now(), RetrievedAt: time.Now(),
} }
post.PersistWebmention(webmention) post.PersistIncomingWebmention(webmention)
webmention = owl.WebmentionIn{ 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.PersistIncomingWebmention(webmention)
webmention = owl.WebmentionIn{ 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.PersistIncomingWebmention(webmention)
webmention = owl.WebmentionIn{ 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),
} }
post.PersistWebmention(webmention) post.PersistIncomingWebmention(webmention)
webmentions := post.ApprovedWebmentions() webmentions := post.ApprovedWebmentions()
if len(webmentions) != 2 { if len(webmentions) != 2 {

View File

@ -187,13 +187,13 @@ func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) {
ApprovalStatus: "approved", ApprovalStatus: "approved",
RetrievedAt: time.Now().Add(time.Hour * -2), RetrievedAt: time.Now().Add(time.Hour * -2),
} }
post.PersistWebmention(webmention) post.PersistIncomingWebmention(webmention)
webmention = owl.WebmentionIn{ 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),
} }
post.PersistWebmention(webmention) post.PersistIncomingWebmention(webmention)
result, _ := owl.RenderPost(&post) result, _ := owl.RenderPost(&post)
if !strings.Contains(result, "http://example.com/source3") { if !strings.Contains(result, "http://example.com/source3") {

View File

@ -156,7 +156,6 @@ func (user User) CreateNewPost(title string) (Post, error) {
os.WriteFile(post.ContentFile(), []byte(initial_content), 0644) os.WriteFile(post.ContentFile(), []byte(initial_content), 0644)
// create media dir // create media dir
os.Mkdir(post.MediaDir(), 0755) os.Mkdir(post.MediaDir(), 0755)
os.Mkdir(post.WebmentionDir(), 0755)
return post, nil return post, nil
} }