From da197c7e4d51ee28fe0d6d2139c57becce5f4819 Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Sun, 4 Sep 2022 15:03:16 +0200 Subject: [PATCH 01/17] WIP outgoing webmentions --- README.md | 14 ++++++++++++++ post.go | 28 ++++++++++++++++------------ post_test.go | 10 +++++----- renderer_test.go | 4 ++-- webmention.go | 9 ++++++++- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f9e3993..e4afd22 100644 --- a/README.md +++ b/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. -- Must be present for the blog post to be valid. -- 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/ -- Contains all media files used in the blog post. -- All files in this folder will be publicly available @@ -59,3 +62,14 @@ aliases: 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 +``` \ No newline at end of file diff --git a/post.go b/post.go index f2a6ff9..5722c00 100644 --- a/post.go +++ b/post.go @@ -32,6 +32,10 @@ type PostMeta struct { Draft bool `yaml:"draft"` } +type PostStatus struct { + Webmentions []WebmentionOut +} + func (post Post) Id() string { return post.id } @@ -156,7 +160,7 @@ func (post *Post) WebmentionFile(source string) string { return path.Join(post.WebmentionDir(), hashStr+".yml") } -func (post *Post) PersistWebmention(webmention Webmention) error { +func (post *Post) PersistWebmention(webmention WebmentionIn) error { // ensure dir exists os.MkdirAll(post.WebmentionDir(), 0755) @@ -169,7 +173,7 @@ func (post *Post) PersistWebmention(webmention Webmention) error { 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 os.MkdirAll(post.WebmentionDir(), 0755) @@ -177,18 +181,18 @@ func (post *Post) Webmention(source string) (Webmention, error) { fileName := post.WebmentionFile(source) if !fileExists(fileName) { // 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) if err != nil { - return Webmention{}, err + return WebmentionIn{}, err } - mention := Webmention{} + mention := WebmentionIn{} err = yaml.Unmarshal(data, &mention) if err != nil { - return Webmention{}, err + return WebmentionIn{}, err } return mention, nil @@ -198,7 +202,7 @@ func (post *Post) AddWebmention(source string) error { // Check if file already exists _, err := post.Webmention(source) if err != nil { - webmention := Webmention{ + webmention := WebmentionIn{ Source: source, } defer post.EnrichWebmention(source) @@ -223,17 +227,17 @@ func (post *Post) EnrichWebmention(source string) error { return err } -func (post *Post) Webmentions() []Webmention { +func (post *Post) Webmentions() []WebmentionIn { // ensure dir exists os.MkdirAll(post.WebmentionDir(), 0755) files := listDir(post.WebmentionDir()) - webmentions := []Webmention{} + webmentions := []WebmentionIn{} for _, file := range files { data, err := os.ReadFile(path.Join(post.WebmentionDir(), file)) if err != nil { continue } - mention := Webmention{} + mention := WebmentionIn{} err = yaml.Unmarshal(data, &mention) if err != nil { continue @@ -244,9 +248,9 @@ func (post *Post) Webmentions() []Webmention { return webmentions } -func (post *Post) ApprovedWebmentions() []Webmention { +func (post *Post) ApprovedWebmentions() []WebmentionIn { webmentions := post.Webmentions() - approved := []Webmention{} + approved := []WebmentionIn{} for _, webmention := range webmentions { if webmention.ApprovalStatus == "approved" { approved = append(approved, webmention) diff --git a/post_test.go b/post_test.go index 5eafb59..698c956 100644 --- a/post_test.go +++ b/post_test.go @@ -173,7 +173,7 @@ func TestPersistWebmention(t *testing.T) { repo := getTestRepo() user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") - webmention := owl.Webmention{ + webmention := owl.WebmentionIn{ Source: "http://example.com/source", } err := post.PersistWebmention(webmention) @@ -265,25 +265,25 @@ func TestApprovedWebmentions(t *testing.T) { repo := getTestRepo() user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") - webmention := owl.Webmention{ + webmention := owl.WebmentionIn{ Source: "http://example.com/source", ApprovalStatus: "approved", RetrievedAt: time.Now(), } post.PersistWebmention(webmention) - webmention = owl.Webmention{ + webmention = owl.WebmentionIn{ Source: "http://example.com/source2", ApprovalStatus: "", RetrievedAt: time.Now().Add(time.Hour * -1), } post.PersistWebmention(webmention) - webmention = owl.Webmention{ + webmention = owl.WebmentionIn{ Source: "http://example.com/source3", ApprovalStatus: "approved", RetrievedAt: time.Now().Add(time.Hour * -2), } post.PersistWebmention(webmention) - webmention = owl.Webmention{ + webmention = owl.WebmentionIn{ Source: "http://example.com/source4", ApprovalStatus: "rejected", RetrievedAt: time.Now().Add(time.Hour * -3), diff --git a/renderer_test.go b/renderer_test.go index c6925fa..92d9cd7 100644 --- a/renderer_test.go +++ b/renderer_test.go @@ -162,14 +162,14 @@ func TestRenderPostIncludesRelToWebMention(t *testing.T) { func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) { user := getTestUser() post, _ := user.CreateNewPost("testpost") - webmention := owl.Webmention{ + webmention := owl.WebmentionIn{ Source: "http://example.com/source3", Title: "Test Title", ApprovalStatus: "approved", RetrievedAt: time.Now().Add(time.Hour * -2), } post.PersistWebmention(webmention) - webmention = owl.Webmention{ + webmention = owl.WebmentionIn{ Source: "http://example.com/source4", ApprovalStatus: "rejected", RetrievedAt: time.Now().Add(time.Hour * -3), diff --git a/webmention.go b/webmention.go index fccd815..f8ca0b1 100644 --- a/webmention.go +++ b/webmention.go @@ -10,13 +10,20 @@ import ( "golang.org/x/net/html" ) -type Webmention struct { +type WebmentionIn struct { Source string `yaml:"source"` Title string `yaml:"title"` ApprovalStatus string `yaml:"approval_status"` RetrievedAt time.Time `yaml:"retrieved_at"` } +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 HttpRetriever interface { Get(url string) ([]byte, error) } From f899184e297e1877707d8fa8275f4866cc32dd01 Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Sun, 4 Sep 2022 15:32:37 +0200 Subject: [PATCH 02/17] Scanning for webmentions in posts --- owl_test.go | 8 +++-- post.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ post_test.go | 55 ++++++++++++++++++++++++++++++++-- renderer.go | 7 ++++- repository.go | 2 +- webmention.go | 29 +++++++++++++++++- 6 files changed, 176 insertions(+), 8 deletions(-) diff --git a/owl_test.go b/owl_test.go index 5d3b5f4..dcb1b7f 100644 --- a/owl_test.go +++ b/owl_test.go @@ -6,12 +6,16 @@ import ( "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 } +func (*MockHttpParser) ParseLinks(data []byte) ([]string, error) { + return []string{"http://example.com"}, nil +} + type MockHttpRetriever struct{} func (*MockHttpRetriever) Get(url string) ([]byte, error) { diff --git a/post.go b/post.go index 5722c00..279719c 100644 --- a/post.go +++ b/post.go @@ -44,6 +44,10 @@ func (post Post) Dir() string { 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 { return path.Join(post.Dir(), "media") } @@ -85,6 +89,42 @@ func (post Post) Content() []byte { 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 { data := post.Content() @@ -211,6 +251,27 @@ func (post *Post) AddWebmention(source string) error { 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) EnrichWebmention(source string) error { html, err := post.user.repo.Retriever.Get(source) if err == nil { @@ -263,3 +324,25 @@ func (post *Post) ApprovedWebmentions() []WebmentionIn { }) 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 +} diff --git a/post_test.go b/post_test.go index 698c956..29e90ea 100644 --- a/post_test.go +++ b/post_test.go @@ -193,7 +193,7 @@ func TestPersistWebmention(t *testing.T) { func TestAddWebmentionCreatesFile(t *testing.T) { repo := getTestRepo() repo.Retriever = &MockHttpRetriever{} - repo.Parser = &MockMicroformatParser{} + repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -211,7 +211,7 @@ func TestAddWebmentionCreatesFile(t *testing.T) { func TestAddWebmentionNotOverwritingFile(t *testing.T) { repo := getTestRepo() repo.Retriever = &MockHttpRetriever{} - repo.Parser = &MockMicroformatParser{} + repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -241,7 +241,7 @@ func TestAddWebmentionNotOverwritingFile(t *testing.T) { func TestAddWebmentionAddsParsedTitle(t *testing.T) { repo := getTestRepo() repo.Retriever = &MockHttpRetriever{} - repo.Parser = &MockMicroformatParser{} + repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -303,3 +303,52 @@ 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) + } +} diff --git a/renderer.go b/renderer.go index 74034a6..a1b7643 100644 --- a/renderer.go +++ b/renderer.go @@ -68,13 +68,18 @@ func renderIntoBaseTemplate(user User, data PageContent) (string, error) { return html.String(), nil } -func RenderPost(post *Post) (string, error) { +func renderPostContent(post *Post) (string, error) { buf := post.RenderedContent() postHtml, err := renderEmbedTemplate("embed/post.html", PostRenderData{ Title: post.Title(), Post: post, Content: template.HTML(buf.String()), }) + return postHtml, err +} + +func RenderPost(post *Post) (string, error) { + postHtml, err := renderPostContent(post) if err != nil { return "", err } diff --git a/repository.go b/repository.go index c3c8c66..c5f8a1c 100644 --- a/repository.go +++ b/repository.go @@ -21,7 +21,7 @@ type Repository struct { active_user string allow_raw_html bool Retriever HttpRetriever - Parser MicroformatParser + Parser HttpParser } type RepoConfig struct { diff --git a/webmention.go b/webmention.go index f8ca0b1..ab02624 100644 --- a/webmention.go +++ b/webmention.go @@ -28,8 +28,9 @@ type HttpRetriever interface { Get(url string) ([]byte, error) } -type MicroformatParser interface { +type HttpParser interface { ParseHEntry(data []byte) (ParsedHEntry, error) + ParseLinks(data []byte) ([]string, error) } type OwlHttpRetriever struct{} @@ -102,3 +103,29 @@ func (OwlMicroformatParser) ParseHEntry(data []byte) (ParsedHEntry, error) { } return findHFeed(doc) } + +func (OwlMicroformatParser) 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) + +} From 576fa32b20ce1f8f157b0822c4deab307e5b0f0c Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Sun, 4 Sep 2022 17:10:40 +0200 Subject: [PATCH 03/17] function to send webmention --- owl_test.go | 9 +++++++ post.go | 58 +++++++++++++++++++++++++++++++++++++++++++++- post_test.go | 37 ++++++++++++++++++++++++++--- repository.go | 8 +++---- webmention.go | 58 ++++++++++++++++++++++++++++++++++++++++------ webmention_test.go | 4 ++-- 6 files changed, 157 insertions(+), 17 deletions(-) diff --git a/owl_test.go b/owl_test.go index dcb1b7f..3523afc 100644 --- a/owl_test.go +++ b/owl_test.go @@ -3,6 +3,7 @@ package owl_test import ( "h4kor/owl-blogs" "math/rand" + "net/url" "time" ) @@ -16,12 +17,20 @@ 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{} func (*MockHttpRetriever) Get(url string) ([]byte, error) { return []byte(""), nil } +func (m *MockHttpRetriever) Post(url string, data url.Values) ([]byte, error) { + return []byte(""), nil +} + func randomName() string { rand.Seed(time.Now().UnixNano()) var letters = []rune("abcdefghijklmnopqrstuvwxyz") diff --git a/post.go b/post.go index 279719c..d9fabd4 100644 --- a/post.go +++ b/post.go @@ -6,9 +6,11 @@ import ( "encoding/base64" "fmt" "io/ioutil" + "net/url" "os" "path" "sort" + "time" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" @@ -272,8 +274,28 @@ func (post *Post) AddOutgoingWebmention(target string) error { 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 { - html, err := post.user.repo.Retriever.Get(source) + html, err := post.user.repo.HttpClient.Get(source) if err == nil { webmention, err := post.Webmention(source) if err != nil { @@ -346,3 +368,37 @@ func (post *Post) ScanForLinks() error { } 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 +} diff --git a/post_test.go b/post_test.go index 29e90ea..6c441e2 100644 --- a/post_test.go +++ b/post_test.go @@ -192,7 +192,7 @@ func TestPersistWebmention(t *testing.T) { func TestAddWebmentionCreatesFile(t *testing.T) { repo := getTestRepo() - repo.Retriever = &MockHttpRetriever{} + repo.HttpClient = &MockHttpRetriever{} repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -210,7 +210,7 @@ func TestAddWebmentionCreatesFile(t *testing.T) { func TestAddWebmentionNotOverwritingFile(t *testing.T) { repo := getTestRepo() - repo.Retriever = &MockHttpRetriever{} + repo.HttpClient = &MockHttpRetriever{} repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -240,7 +240,7 @@ func TestAddWebmentionNotOverwritingFile(t *testing.T) { func TestAddWebmentionAddsParsedTitle(t *testing.T) { repo := getTestRepo() - repo.Retriever = &MockHttpRetriever{} + repo.HttpClient = &MockHttpRetriever{} repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") 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) } } + +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") + } +} diff --git a/repository.go b/repository.go index c5f8a1c..c1d1759 100644 --- a/repository.go +++ b/repository.go @@ -20,8 +20,8 @@ type Repository struct { single_user_mode bool active_user string allow_raw_html bool - Retriever HttpRetriever - Parser HttpParser + HttpClient HttpClient + Parser HtmlParser } type RepoConfig struct { @@ -29,7 +29,7 @@ type RepoConfig struct { } 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 if dirExists(newRepo.Dir()) { return Repository{}, fmt.Errorf("Repository already exists") @@ -63,7 +63,7 @@ func CreateRepository(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()) { return Repository{}, fmt.Errorf("Repository does not exist: " + repo.Dir()) } diff --git a/webmention.go b/webmention.go index ab02624..df4ab33 100644 --- a/webmention.go +++ b/webmention.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "net/http" + "net/url" "strings" "time" @@ -24,24 +25,26 @@ type WebmentionOut struct { LastSentAt time.Time `yaml:"last_sent_at"` } -type HttpRetriever interface { +type HttpClient interface { Get(url string) ([]byte, error) + Post(url string, data url.Values) ([]byte, error) } -type HttpParser interface { +type HtmlParser interface { 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 { Title string } -func (OwlHttpRetriever) Get(url string) ([]byte, error) { +func (OwlHttpClient) Get(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { return []byte{}, err @@ -52,6 +55,17 @@ func (OwlHttpRetriever) Get(url string) ([]byte, error) { 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) { if n.Type == html.TextNode { 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))) if err != nil { return ParsedHEntry{}, err @@ -104,7 +118,7 @@ func (OwlMicroformatParser) ParseHEntry(data []byte) (ParsedHEntry, error) { 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))) if err != nil { return make([]string, 0), err @@ -129,3 +143,33 @@ func (OwlMicroformatParser) ParseLinks(data []byte) ([]string, error) { 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) +} diff --git a/webmention_test.go b/webmention_test.go index e02d9cf..c7e95c7 100644 --- a/webmention_test.go +++ b/webmention_test.go @@ -11,7 +11,7 @@ import ( func TestParseValidHEntry(t *testing.T) { html := []byte("
Foo
") - parser := &owl.OwlMicroformatParser{} + parser := &owl.OwlHtmlParser{} entry, err := parser.ParseHEntry(html) if err != nil { @@ -24,7 +24,7 @@ func TestParseValidHEntry(t *testing.T) { func TestParseValidHEntryWithoutTitle(t *testing.T) { html := []byte("
Foo
") - parser := &owl.OwlMicroformatParser{} + parser := &owl.OwlHtmlParser{} entry, err := parser.ParseHEntry(html) if err != nil { From 38c38357d71732567531e94969b8cc977fc9ca7e Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Mon, 5 Sep 2022 20:34:24 +0200 Subject: [PATCH 04/17] start to use a proper cli library --- cmd/owl/main.go | 33 ++++++++++ cmd/owl/web.go | 28 +++++++++ cmd/{owl-web => owl/web}/aliases_test.go | 4 +- cmd/{owl-web => owl/web}/handler.go | 2 +- cmd/{owl-web => owl/web}/multi_user_test.go | 4 +- cmd/{owl-web => owl/web}/post_test.go | 4 +- cmd/{owl-web => owl/web}/rss_test.go | 4 +- cmd/{owl-web/main.go => owl/web/server.go} | 63 ++++---------------- cmd/{owl-web => owl/web}/single_user_test.go | 4 +- cmd/{owl-web => owl/web}/webmention_test.go | 4 +- go.mod | 15 +++-- go.sum | 16 +++-- 12 files changed, 106 insertions(+), 75 deletions(-) create mode 100644 cmd/owl/main.go create mode 100644 cmd/owl/web.go rename cmd/{owl-web => owl/web}/aliases_test.go (99%) rename cmd/{owl-web => owl/web}/handler.go (99%) rename cmd/{owl-web => owl/web}/multi_user_test.go (98%) rename cmd/{owl-web => owl/web}/post_test.go (93%) rename cmd/{owl-web => owl/web}/rss_test.go (95%) rename cmd/{owl-web/main.go => owl/web/server.go} (55%) rename cmd/{owl-web => owl/web}/single_user_test.go (98%) rename cmd/{owl-web => owl/web}/webmention_test.go (98%) diff --git a/cmd/owl/main.go b/cmd/owl/main.go new file mode 100644 index 0000000..1ceeda0 --- /dev/null +++ b/cmd/owl/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var repoPath string +var rootCmd = &cobra.Command{ + Use: "owl", + Short: "Owl Blogs is a not so static blog generator", + // Run: func(cmd *cobra.Command, args []string) { + // }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func init() { + + rootCmd.PersistentFlags().StringVar(&repoPath, "repo", ".", "Path to the repository to use.") + +} + +func main() { + Execute() +} diff --git a/cmd/owl/web.go b/cmd/owl/web.go new file mode 100644 index 0000000..8dc71d6 --- /dev/null +++ b/cmd/owl/web.go @@ -0,0 +1,28 @@ +package main + +import ( + web "h4kor/owl-blogs/cmd/owl/web" + + "github.com/spf13/cobra" +) + +var port int +var unsafe bool +var user string + +func init() { + rootCmd.AddCommand(webCmd) + + rootCmd.PersistentFlags().IntVar(&port, "port", 8080, "Port to use") + rootCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "Allow unsafe html") + rootCmd.PersistentFlags().StringVar(&user, "user", "", "Start server in single user mode.") +} + +var webCmd = &cobra.Command{ + Use: "web", + Short: "Start the web server", + Long: `Start the web server`, + Run: func(cmd *cobra.Command, args []string) { + web.StartServer(repoPath, port, unsafe, user) + }, +} diff --git a/cmd/owl-web/aliases_test.go b/cmd/owl/web/aliases_test.go similarity index 99% rename from cmd/owl-web/aliases_test.go rename to cmd/owl/web/aliases_test.go index 98c5dc4..2c6c161 100644 --- a/cmd/owl-web/aliases_test.go +++ b/cmd/owl/web/aliases_test.go @@ -1,7 +1,7 @@ -package main_test +package web_test import ( - main "h4kor/owl-blogs/cmd/owl-web" + main "h4kor/owl-blogs/cmd/owl/web" "net/http" "net/http/httptest" "os" diff --git a/cmd/owl-web/handler.go b/cmd/owl/web/handler.go similarity index 99% rename from cmd/owl-web/handler.go rename to cmd/owl/web/handler.go index a71c29e..38c855c 100644 --- a/cmd/owl-web/handler.go +++ b/cmd/owl/web/handler.go @@ -1,4 +1,4 @@ -package main +package web import ( "h4kor/owl-blogs" diff --git a/cmd/owl-web/multi_user_test.go b/cmd/owl/web/multi_user_test.go similarity index 98% rename from cmd/owl-web/multi_user_test.go rename to cmd/owl/web/multi_user_test.go index d55ab36..70c26a3 100644 --- a/cmd/owl-web/multi_user_test.go +++ b/cmd/owl/web/multi_user_test.go @@ -1,8 +1,8 @@ -package main_test +package web_test import ( "h4kor/owl-blogs" - main "h4kor/owl-blogs/cmd/owl-web" + main "h4kor/owl-blogs/cmd/owl/web" "math/rand" "net/http" "net/http/httptest" diff --git a/cmd/owl-web/post_test.go b/cmd/owl/web/post_test.go similarity index 93% rename from cmd/owl-web/post_test.go rename to cmd/owl/web/post_test.go index 67dbc0d..b3efe63 100644 --- a/cmd/owl-web/post_test.go +++ b/cmd/owl/web/post_test.go @@ -1,7 +1,7 @@ -package main_test +package web_test import ( - main "h4kor/owl-blogs/cmd/owl-web" + main "h4kor/owl-blogs/cmd/owl/web" "net/http" "net/http/httptest" "os" diff --git a/cmd/owl-web/rss_test.go b/cmd/owl/web/rss_test.go similarity index 95% rename from cmd/owl-web/rss_test.go rename to cmd/owl/web/rss_test.go index c1c2ff6..0967d7d 100644 --- a/cmd/owl-web/rss_test.go +++ b/cmd/owl/web/rss_test.go @@ -1,7 +1,7 @@ -package main_test +package web_test import ( - main "h4kor/owl-blogs/cmd/owl-web" + main "h4kor/owl-blogs/cmd/owl/web" "net/http" "net/http/httptest" "strings" diff --git a/cmd/owl-web/main.go b/cmd/owl/web/server.go similarity index 55% rename from cmd/owl-web/main.go rename to cmd/owl/web/server.go index 57dbe10..dc90604 100644 --- a/cmd/owl-web/main.go +++ b/cmd/owl/web/server.go @@ -1,4 +1,4 @@ -package main +package web import ( "h4kor/owl-blogs" @@ -34,63 +34,20 @@ func SingleUserRouter(repo *owl.Repository) http.Handler { return router } -func main() { - println("owl web server") - println("Parameters") - println("-repo - Specify the repository to use. Defaults to '.'") - println("-port - Specify the port to use, Default is '8080'") - println("-user - Start server in single user mode.") - println("-unsafe - Allow unsafe html.") - var repoName string - var port int - var singleUserName string - var allowRawHTML bool = false - for i, arg := range os.Args[0:len(os.Args)] { - if arg == "-port" { - if i+1 >= len(os.Args) { - println("-port requires a port number") - os.Exit(1) - } - port, _ = strconv.Atoi(os.Args[i+1]) - } - if arg == "-repo" { - if i+1 >= len(os.Args) { - println("-repo requires a repopath") - os.Exit(1) - } - repoName = os.Args[i+1] - } - if arg == "-user" { - if i+1 >= len(os.Args) { - println("-user requires a username") - os.Exit(1) - } - singleUserName = os.Args[i+1] - } - if arg == "-unsafe" { - allowRawHTML = true - } - } - if repoName == "" { - repoName = "." - } - if port == 0 { - port = 8080 - } - +func StartServer(repoPath string, port int, unsafe bool, user string) { var repo owl.Repository var err error - if singleUserName != "" { + if user != "" { println("Single user mode") - println("Repository:", repoName) - println("User:", singleUserName) - repo, err = owl.OpenSingleUserRepo(repoName, singleUserName) + println("Repository:", repoPath) + println("User:", user) + repo, err = owl.OpenSingleUserRepo(repoPath, user) } else { println("Multi user mode") - println("Repository:", repoName) - repo, err = owl.OpenRepository(repoName) + println("Repository:", repoPath) + repo, err = owl.OpenRepository(repoPath) } - repo.SetAllowRawHtml(allowRawHTML) + repo.SetAllowRawHtml(unsafe) if err != nil { println("Error opening repository: ", err.Error()) @@ -98,7 +55,7 @@ func main() { } var router http.Handler - if singleUserName == "" { + if user == "" { println("Multi user mode Router used") router = Router(&repo) } else { diff --git a/cmd/owl-web/single_user_test.go b/cmd/owl/web/single_user_test.go similarity index 98% rename from cmd/owl-web/single_user_test.go rename to cmd/owl/web/single_user_test.go index e657a41..0dc61d8 100644 --- a/cmd/owl-web/single_user_test.go +++ b/cmd/owl/web/single_user_test.go @@ -1,8 +1,8 @@ -package main_test +package web_test import ( owl "h4kor/owl-blogs" - main "h4kor/owl-blogs/cmd/owl-web" + main "h4kor/owl-blogs/cmd/owl/web" "net/http" "net/http/httptest" "os" diff --git a/cmd/owl-web/webmention_test.go b/cmd/owl/web/webmention_test.go similarity index 98% rename from cmd/owl-web/webmention_test.go rename to cmd/owl/web/webmention_test.go index 38b363f..0c9caa4 100644 --- a/cmd/owl-web/webmention_test.go +++ b/cmd/owl/web/webmention_test.go @@ -1,8 +1,8 @@ -package main_test +package web_test import ( "h4kor/owl-blogs" - main "h4kor/owl-blogs/cmd/owl-web" + main "h4kor/owl-blogs/cmd/owl/web" "net/http" "net/http/httptest" "net/url" diff --git a/go.mod b/go.mod index 11cb084..910f421 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,14 @@ module h4kor/owl-blogs go 1.18 require ( - github.com/julienschmidt/httprouter v1.3.0 // indirect - github.com/yuin/goldmark v1.4.13 // indirect - github.com/yuin/goldmark-meta v1.1.0 // indirect - golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect - gopkg.in/yaml.v2 v2.3.0 // indirect + github.com/julienschmidt/httprouter v1.3.0 + github.com/spf13/cobra v1.5.0 + github.com/yuin/goldmark v1.4.13 + golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect ) diff --git a/go.sum b/go.sum index d929445..1783953 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,19 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= -github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= From ef53bfa358b5cd69b7cb67da7be898fc15e46512 Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Mon, 5 Sep 2022 20:50:46 +0200 Subject: [PATCH 05/17] refactoring to have web config (single user, unsafe) in config of repo --- cmd/owl/web/aliases_test.go | 16 ++++----- cmd/owl/web/handler.go | 4 +-- cmd/owl/web/multi_user_test.go | 12 +++---- cmd/owl/web/post_test.go | 3 +- cmd/owl/web/rss_test.go | 3 +- cmd/owl/web/server.go | 23 ++++--------- cmd/owl/web/single_user_test.go | 3 +- cmd/owl/web/webmention_test.go | 12 +++---- embed/initial/repo/config.yml | 1 - owl_test.go | 6 ++-- post.go | 2 +- post_test.go | 24 ++++++------- renderer_test.go | 2 +- repository.go | 60 +++++++++++---------------------- repository_test.go | 48 +++++++++++++------------- user_test.go | 16 ++++----- 16 files changed, 100 insertions(+), 135 deletions(-) delete mode 100644 embed/initial/repo/config.yml diff --git a/cmd/owl/web/aliases_test.go b/cmd/owl/web/aliases_test.go index 2c6c161..dc9f4b1 100644 --- a/cmd/owl/web/aliases_test.go +++ b/cmd/owl/web/aliases_test.go @@ -1,6 +1,7 @@ package web_test import ( + "h4kor/owl-blogs" main "h4kor/owl-blogs/cmd/owl/web" "net/http" "net/http/httptest" @@ -9,7 +10,7 @@ import ( ) func TestRedirectOnAliases(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -47,7 +48,7 @@ func TestRedirectOnAliases(t *testing.T) { } func TestNoRedirectOnNonExistingAliases(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -78,7 +79,7 @@ func TestNoRedirectOnNonExistingAliases(t *testing.T) { } func TestNoRedirectIfValidPostUrl(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") post2, _ := user.CreateNewPost("post-2") @@ -109,7 +110,7 @@ func TestNoRedirectIfValidPostUrl(t *testing.T) { } func TestRedirectIfInvalidPostUrl(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -139,7 +140,7 @@ func TestRedirectIfInvalidPostUrl(t *testing.T) { } func TestRedirectIfInvalidUserUrl(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -169,7 +170,7 @@ func TestRedirectIfInvalidUserUrl(t *testing.T) { } func TestRedirectIfInvalidMediaUrl(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -199,9 +200,8 @@ func TestRedirectIfInvalidMediaUrl(t *testing.T) { } func TestDeepAliasInSingleUserMode(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{SingleUser: "test-1"}) user, _ := repo.CreateUser("test-1") - repo.SetSingleUser(user) post, _ := user.CreateNewPost("post-1") content := "---\n" diff --git a/cmd/owl/web/handler.go b/cmd/owl/web/handler.go index 38c855c..4d20ac6 100644 --- a/cmd/owl/web/handler.go +++ b/cmd/owl/web/handler.go @@ -11,8 +11,8 @@ import ( ) func getUserFromRepo(repo *owl.Repository, ps httprouter.Params) (owl.User, error) { - if repo.SingleUserName() != "" { - return repo.GetUser(repo.SingleUserName()) + if config, _ := repo.Config(); config.SingleUser != "" { + return repo.GetUser(config.SingleUser) } userName := ps.ByName("user") user, err := repo.GetUser(userName) diff --git a/cmd/owl/web/multi_user_test.go b/cmd/owl/web/multi_user_test.go index 70c26a3..279798a 100644 --- a/cmd/owl/web/multi_user_test.go +++ b/cmd/owl/web/multi_user_test.go @@ -27,13 +27,13 @@ func testRepoName() string { return "/tmp/" + randomName() } -func getTestRepo() owl.Repository { - repo, _ := owl.CreateRepository(testRepoName()) +func getTestRepo(config owl.RepoConfig) owl.Repository { + repo, _ := owl.CreateRepository(testRepoName(), config) return repo } func TestMultiUserRepoIndexHandler(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) repo.CreateUser("user_1") repo.CreateUser("user_2") @@ -64,7 +64,7 @@ func TestMultiUserRepoIndexHandler(t *testing.T) { } func TestMultiUserUserIndexHandler(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") user.CreateNewPost("post-1") @@ -91,7 +91,7 @@ func TestMultiUserUserIndexHandler(t *testing.T) { } func TestMultiUserPostHandler(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -112,7 +112,7 @@ func TestMultiUserPostHandler(t *testing.T) { } func TestMultiUserPostMediaHandler(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") diff --git a/cmd/owl/web/post_test.go b/cmd/owl/web/post_test.go index b3efe63..9c6e42d 100644 --- a/cmd/owl/web/post_test.go +++ b/cmd/owl/web/post_test.go @@ -1,6 +1,7 @@ package web_test import ( + "h4kor/owl-blogs" main "h4kor/owl-blogs/cmd/owl/web" "net/http" "net/http/httptest" @@ -9,7 +10,7 @@ import ( ) func TestPostHandlerReturns404OnDrafts(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") diff --git a/cmd/owl/web/rss_test.go b/cmd/owl/web/rss_test.go index 0967d7d..904cdf7 100644 --- a/cmd/owl/web/rss_test.go +++ b/cmd/owl/web/rss_test.go @@ -1,6 +1,7 @@ package web_test import ( + "h4kor/owl-blogs" main "h4kor/owl-blogs/cmd/owl/web" "net/http" "net/http/httptest" @@ -9,7 +10,7 @@ import ( ) func TestMultiUserUserRssIndexHandler(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") user.CreateNewPost("post-1") diff --git a/cmd/owl/web/server.go b/cmd/owl/web/server.go index dc90604..d64c051 100644 --- a/cmd/owl/web/server.go +++ b/cmd/owl/web/server.go @@ -34,20 +34,10 @@ func SingleUserRouter(repo *owl.Repository) http.Handler { return router } -func StartServer(repoPath string, port int, unsafe bool, user string) { +func StartServer(repoPath string, port int) { var repo owl.Repository var err error - if user != "" { - println("Single user mode") - println("Repository:", repoPath) - println("User:", user) - repo, err = owl.OpenSingleUserRepo(repoPath, user) - } else { - println("Multi user mode") - println("Repository:", repoPath) - repo, err = owl.OpenRepository(repoPath) - } - repo.SetAllowRawHtml(unsafe) + repo, err = owl.OpenRepository(repoPath) if err != nil { println("Error opening repository: ", err.Error()) @@ -55,13 +45,12 @@ func StartServer(repoPath string, port int, unsafe bool, user string) { } var router http.Handler - if user == "" { - println("Multi user mode Router used") - router = Router(&repo) - } else { - println("Single user mode Router used") + if config, _ := repo.Config(); config.SingleUser != "" { router = SingleUserRouter(&repo) + } else { + router = Router(&repo) } + println("Listening on port", port) http.ListenAndServe(":"+strconv.Itoa(port), router) diff --git a/cmd/owl/web/single_user_test.go b/cmd/owl/web/single_user_test.go index 0dc61d8..7ee722c 100644 --- a/cmd/owl/web/single_user_test.go +++ b/cmd/owl/web/single_user_test.go @@ -12,9 +12,8 @@ import ( ) func getSingleUserTestRepo() (owl.Repository, owl.User) { - repo, _ := owl.CreateRepository(testRepoName()) + repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{SingleUser: "test-1"}) user, _ := repo.CreateUser("test-1") - repo.SetSingleUser(user) return repo, user } diff --git a/cmd/owl/web/webmention_test.go b/cmd/owl/web/webmention_test.go index 0c9caa4..5d69061 100644 --- a/cmd/owl/web/webmention_test.go +++ b/cmd/owl/web/webmention_test.go @@ -42,7 +42,7 @@ func assertStatus(t *testing.T, rr *httptest.ResponseRecorder, expStatus int) { } func TestWebmentionHandleAccepts(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -60,7 +60,7 @@ func TestWebmentionHandleAccepts(t *testing.T) { func TestWebmentionWrittenToPost(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -89,7 +89,7 @@ func TestWebmentionWrittenToPost(t *testing.T) { // (Most commonly this means checking that the source and target schemes are http or https). func TestWebmentionSourceValidation(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -106,7 +106,7 @@ func TestWebmentionSourceValidation(t *testing.T) { func TestWebmentionTargetValidation(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -125,7 +125,7 @@ func TestWebmentionTargetValidation(t *testing.T) { func TestWebmentionSameTargetAndSource(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") @@ -144,7 +144,7 @@ func TestWebmentionSameTargetAndSource(t *testing.T) { // This check SHOULD happen synchronously to reject invalid Webmentions before more in-depth verification begins. // What a "valid resource" means is up to the receiver. func TestValidationOfTarget(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("test-1") post, _ := user.CreateNewPost("post-1") diff --git a/embed/initial/repo/config.yml b/embed/initial/repo/config.yml deleted file mode 100644 index 78122e0..0000000 --- a/embed/initial/repo/config.yml +++ /dev/null @@ -1 +0,0 @@ -domain: "http://localhost:8080" \ No newline at end of file diff --git a/owl_test.go b/owl_test.go index 3523afc..532d21f 100644 --- a/owl_test.go +++ b/owl_test.go @@ -50,13 +50,13 @@ func randomUserName() string { } func getTestUser() owl.User { - repo, _ := owl.CreateRepository(testRepoName()) + repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) return user } -func getTestRepo() owl.Repository { - repo, _ := owl.CreateRepository(testRepoName()) +func getTestRepo(config owl.RepoConfig) owl.Repository { + repo, _ := owl.CreateRepository(testRepoName(), config) return repo } diff --git a/post.go b/post.go index d9fabd4..bc6479f 100644 --- a/post.go +++ b/post.go @@ -144,7 +144,7 @@ func (post Post) RenderedContent() bytes.Buffer { } options := goldmark.WithRendererOptions() - if post.user.repo.AllowRawHtml() { + if config, _ := post.user.repo.Config(); config.AllowRawHtml { options = goldmark.WithRendererOptions( html.WithUnsafe(), ) diff --git a/post_test.go b/post_test.go index 6c441e2..2825d3c 100644 --- a/post_test.go +++ b/post_test.go @@ -89,7 +89,7 @@ func TestDraftInMetaData(t *testing.T) { } func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") content := "---\n" @@ -107,8 +107,7 @@ func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) { } func TestRawHTMLIfAllowedByRepo(t *testing.T) { - repo := getTestRepo() - repo.SetAllowRawHtml(true) + repo := getTestRepo(owl.RepoConfig{AllowRawHtml: true}) user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") content := "---\n" @@ -126,8 +125,7 @@ func TestRawHTMLIfAllowedByRepo(t *testing.T) { } func TestLoadMeta(t *testing.T) { - repo := getTestRepo() - repo.SetAllowRawHtml(true) + repo := getTestRepo(owl.RepoConfig{AllowRawHtml: true}) user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -170,7 +168,7 @@ func TestLoadMeta(t *testing.T) { /// func TestPersistWebmention(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") webmention := owl.WebmentionIn{ @@ -191,7 +189,7 @@ func TestPersistWebmention(t *testing.T) { } func TestAddWebmentionCreatesFile(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) repo.HttpClient = &MockHttpRetriever{} repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") @@ -209,7 +207,7 @@ func TestAddWebmentionCreatesFile(t *testing.T) { } func TestAddWebmentionNotOverwritingFile(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) repo.HttpClient = &MockHttpRetriever{} repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") @@ -239,7 +237,7 @@ func TestAddWebmentionNotOverwritingFile(t *testing.T) { } func TestAddWebmentionAddsParsedTitle(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) repo.HttpClient = &MockHttpRetriever{} repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") @@ -262,7 +260,7 @@ func TestAddWebmentionAddsParsedTitle(t *testing.T) { } func TestApprovedWebmentions(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") webmention := owl.WebmentionIn{ @@ -305,7 +303,7 @@ func TestApprovedWebmentions(t *testing.T) { } func TestScanningForLinks(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -328,7 +326,7 @@ func TestScanningForLinks(t *testing.T) { } func TestScanningForLinksDoesNotAddDuplicates(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -354,7 +352,7 @@ func TestScanningForLinksDoesNotAddDuplicates(t *testing.T) { } func TestCanSendWebmention(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) repo.HttpClient = &MockHttpRetriever{} repo.Parser = &MockHttpParser{} user, _ := repo.CreateUser("testuser") diff --git a/renderer_test.go b/renderer_test.go index 92d9cd7..534f4a8 100644 --- a/renderer_test.go +++ b/renderer_test.go @@ -107,7 +107,7 @@ func TestRenderIndexPageWithBrokenBaseTemplate(t *testing.T) { } func TestRenderUserList(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) repo.CreateUser("user1") repo.CreateUser("user2") diff --git a/repository.go b/repository.go index c1d1759..cf97038 100644 --- a/repository.go +++ b/repository.go @@ -16,19 +16,18 @@ var base_template string var VERSION = "0.0.1" type Repository struct { - name string - single_user_mode bool - active_user string - allow_raw_html bool - HttpClient HttpClient - Parser HtmlParser + name string + HttpClient HttpClient + Parser HtmlParser } type RepoConfig struct { - Domain string `yaml:"domain"` + Domain string `yaml:"domain"` + SingleUser string `yaml:"single_user"` + AllowRawHtml bool `yaml:"allow_raw_html"` } -func CreateRepository(name string) (Repository, error) { +func CreateRepository(name string, config RepoConfig) (Repository, error) { newRepo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: OwlHttpClient{}} // check if repository already exists if dirExists(newRepo.Dir()) { @@ -39,6 +38,13 @@ func CreateRepository(name string) (Repository, error) { os.Mkdir(newRepo.UsersDir(), 0755) os.Mkdir(newRepo.StaticDir(), 0755) + // create config file + if config.Domain == "" { + config.Domain = "http://localhost:8080" + } + config_data, _ := yaml.Marshal(config) + os.WriteFile(path.Join(newRepo.Dir(), "config.yml"), config_data, 0644) + // copy all files from static_files embed.FS to StaticDir staticFiles, _ := embed_files.ReadDir("embed/initial/static") for _, file := range staticFiles { @@ -71,36 +77,6 @@ func OpenRepository(name string) (Repository, error) { return repo, nil } -func OpenSingleUserRepo(name string, user_name string) (Repository, error) { - repo, err := OpenRepository(name) - if err != nil { - return Repository{}, err - } - user, err := repo.GetUser(user_name) - if err != nil { - return Repository{}, err - } - repo.SetSingleUser(user) - return repo, nil -} - -func (repo Repository) AllowRawHtml() bool { - return repo.allow_raw_html -} - -func (repo *Repository) SetAllowRawHtml(allow bool) { - repo.allow_raw_html = allow -} - -func (repo *Repository) SetSingleUser(user User) { - repo.single_user_mode = true - repo.active_user = user.name -} - -func (repo Repository) SingleUserName() string { - return repo.active_user -} - func (repo Repository) Dir() string { return repo.name } @@ -114,7 +90,8 @@ func (repo Repository) UsersDir() string { } func (repo Repository) UserUrlPath(user User) string { - if repo.single_user_mode { + config, _ := repo.Config() + if config.SingleUser != "" { return "/" } return "/user/" + user.name + "/" @@ -136,8 +113,9 @@ func (repo Repository) Template() (string, error) { } func (repo Repository) Users() ([]User, error) { - if repo.single_user_mode { - return []User{{repo: &repo, name: repo.active_user}}, nil + config, _ := repo.Config() + if config.SingleUser != "" { + return []User{{repo: &repo, name: config.SingleUser}}, nil } userNames := listDir(repo.UsersDir()) diff --git a/repository_test.go b/repository_test.go index 590a082..01ca4e9 100644 --- a/repository_test.go +++ b/repository_test.go @@ -9,7 +9,7 @@ import ( func TestCanCreateRepository(t *testing.T) { repoName := testRepoName() - _, err := owl.CreateRepository(repoName) + _, err := owl.CreateRepository(repoName, owl.RepoConfig{}) if err != nil { t.Error("Error creating repository: ", err.Error()) } @@ -18,8 +18,8 @@ func TestCanCreateRepository(t *testing.T) { func TestCannotCreateExistingRepository(t *testing.T) { repoName := testRepoName() - owl.CreateRepository(repoName) - _, err := owl.CreateRepository(repoName) + owl.CreateRepository(repoName, owl.RepoConfig{}) + _, err := owl.CreateRepository(repoName, owl.RepoConfig{}) if err == nil { t.Error("No error returned when creating existing repository") } @@ -27,7 +27,7 @@ func TestCannotCreateExistingRepository(t *testing.T) { func TestCanCreateANewUser(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) if _, err := os.Stat(path.Join(user.Dir(), "")); err != nil { t.Error("User directory not created") @@ -36,7 +36,7 @@ func TestCanCreateANewUser(t *testing.T) { func TestCannotRecreateExisitingUser(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) userName := randomUserName() repo.CreateUser(userName) _, err := repo.CreateUser(userName) @@ -47,7 +47,7 @@ func TestCannotRecreateExisitingUser(t *testing.T) { func TestCreateUserAddsVersionFile(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) if _, err := os.Stat(path.Join(user.Dir(), "/meta/VERSION")); err != nil { t.Error("Version file not created") @@ -56,7 +56,7 @@ func TestCreateUserAddsVersionFile(t *testing.T) { func TestCreateUserAddsBaseHtmlFile(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) if _, err := os.Stat(path.Join(user.Dir(), "/meta/base.html")); err != nil { t.Error("Base html file not created") @@ -65,7 +65,7 @@ func TestCreateUserAddsBaseHtmlFile(t *testing.T) { func TestCreateUserAddConfigYml(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) if _, err := os.Stat(path.Join(user.Dir(), "/meta/config.yml")); err != nil { t.Error("Config file not created") @@ -74,7 +74,7 @@ func TestCreateUserAddConfigYml(t *testing.T) { func TestCreateUserAddsPublicFolder(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) if _, err := os.Stat(path.Join(user.Dir(), "/public")); err != nil { t.Error("Public folder not created") @@ -83,7 +83,7 @@ func TestCreateUserAddsPublicFolder(t *testing.T) { func TestCanListRepoUsers(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user1, _ := repo.CreateUser(randomUserName()) user2, _ := repo.CreateUser(randomUserName()) // Create a new post @@ -101,7 +101,7 @@ func TestCanListRepoUsers(t *testing.T) { func TestCanOpenRepository(t *testing.T) { // Create a new user repoName := testRepoName() - repo, _ := owl.CreateRepository(repoName) + repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{}) // Open the repository repo2, err := owl.OpenRepository(repoName) if err != nil { @@ -121,7 +121,7 @@ func TestCannotOpenNonExisitingRepo(t *testing.T) { func TestGetUser(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Get the user user2, err := repo.GetUser(user.Name()) @@ -135,7 +135,7 @@ func TestGetUser(t *testing.T) { func TestCannotGetNonexistingUser(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) _, err := repo.GetUser(randomUserName()) if err == nil { t.Error("No error returned when getting non-existing user") @@ -144,7 +144,7 @@ func TestCannotGetNonexistingUser(t *testing.T) { func TestGetStaticDirOfRepo(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) // Get the user staticDir := repo.StaticDir() if staticDir == "" { @@ -154,7 +154,7 @@ func TestGetStaticDirOfRepo(t *testing.T) { func TestNewRepoGetsStaticFiles(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) if _, err := os.Stat(repo.StaticDir()); err != nil { t.Error("Static directory not found") } @@ -169,7 +169,7 @@ func TestNewRepoGetsStaticFiles(t *testing.T) { func TestNewRepoGetsStaticFilesPicoCSSWithContent(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) file, err := os.Open(path.Join(repo.StaticDir(), "pico.min.css")) if err != nil { t.Error("Error opening pico.min.css") @@ -183,7 +183,7 @@ func TestNewRepoGetsStaticFilesPicoCSSWithContent(t *testing.T) { func TestNewRepoGetsBaseHtml(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) if _, err := os.Stat(path.Join(repo.Dir(), "/base.html")); err != nil { t.Error("Base html file not found") } @@ -191,7 +191,7 @@ func TestNewRepoGetsBaseHtml(t *testing.T) { func TestCanGetRepoTemplate(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) // Get the user template, err := repo.Template() if err != nil { @@ -206,13 +206,13 @@ func TestCanOpenRepositoryInSingleUserMode(t *testing.T) { // Create a new user repoName := testRepoName() userName := randomUserName() - created_repo, _ := owl.CreateRepository(repoName) + created_repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{SingleUser: userName}) created_repo.CreateUser(userName) created_repo.CreateUser(randomUserName()) created_repo.CreateUser(randomUserName()) // Open the repository - repo, _ := owl.OpenSingleUserRepo(repoName, userName) + repo, _ := owl.OpenRepository(repoName) users, _ := repo.Users() if len(users) != 1 { @@ -227,11 +227,11 @@ func TestSingleUserRepoUserUrlPathIsSimple(t *testing.T) { // Create a new user repoName := testRepoName() userName := randomUserName() - created_repo, _ := owl.CreateRepository(repoName) + created_repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{SingleUser: userName}) created_repo.CreateUser(userName) // Open the repository - repo, _ := owl.OpenSingleUserRepo(repoName, userName) + repo, _ := owl.OpenRepository(repoName) user, _ := repo.GetUser(userName) if user.UrlPath() != "/" { t.Error("User url is not '/'. Got: ", user.UrlPath()) @@ -239,7 +239,7 @@ func TestSingleUserRepoUserUrlPathIsSimple(t *testing.T) { } func TestCanGetMapWithAllPostAliases(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) post, _ := user.CreateNewPost("test-1") @@ -276,7 +276,7 @@ func TestCanGetMapWithAllPostAliases(t *testing.T) { } func TestAliasesHaveCorrectPost(t *testing.T) { - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) post1, _ := user.CreateNewPost("test-1") post2, _ := user.CreateNewPost("test-2") diff --git a/user_test.go b/user_test.go index 0f8b5d4..7a4e800 100644 --- a/user_test.go +++ b/user_test.go @@ -11,7 +11,7 @@ import ( func TestCreateNewPostCreatesEntryInPublic(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Create a new post user.CreateNewPost("testpost") @@ -26,7 +26,7 @@ func TestCreateNewPostCreatesEntryInPublic(t *testing.T) { func TestCreateNewPostCreatesMediaDir(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Create a new post post, _ := user.CreateNewPost("testpost") @@ -49,7 +49,7 @@ func TestCreateNewPostAddsDateToMetaBlock(t *testing.T) { func TestCreateNewPostMultipleCalls(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Create a new post user.CreateNewPost("testpost") @@ -66,7 +66,7 @@ func TestCreateNewPostMultipleCalls(t *testing.T) { func TestCanListUserPosts(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Create a new post user.CreateNewPost("testpost") @@ -83,7 +83,7 @@ func TestCanListUserPosts(t *testing.T) { func TestCannotListUserPostsInSubdirectories(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Create a new post user.CreateNewPost("testpost") @@ -120,7 +120,7 @@ func TestCannotListUserPostsInSubdirectories(t *testing.T) { func TestCannotListUserPostsWithoutIndexMd(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Create a new post user.CreateNewPost("testpost") @@ -149,7 +149,7 @@ func TestCannotListUserPostsWithoutIndexMd(t *testing.T) { func TestListUserPostsDoesNotIncludeDrafts(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Create a new post post, _ := user.CreateNewPost("testpost") @@ -170,7 +170,7 @@ func TestListUserPostsDoesNotIncludeDrafts(t *testing.T) { func TestListUsersDraftsExcludedRealWorld(t *testing.T) { // Create a new user - repo := getTestRepo() + repo := getTestRepo(owl.RepoConfig{}) user, _ := repo.CreateUser(randomUserName()) // Create a new post post, _ := user.CreateNewPost("testpost") From e9ee59d2ecf1ff268558977728509a8a6ecb4b04 Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Mon, 5 Sep 2022 20:59:32 +0200 Subject: [PATCH 06/17] init command --- cmd/owl/init.go | 38 ++++++++++++++++++++++++++++++++++++++ cmd/owl/web.go | 8 ++------ 2 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 cmd/owl/init.go diff --git a/cmd/owl/init.go b/cmd/owl/init.go new file mode 100644 index 0000000..1eb7ca3 --- /dev/null +++ b/cmd/owl/init.go @@ -0,0 +1,38 @@ +package main + +import ( + "h4kor/owl-blogs" + + "github.com/spf13/cobra" +) + +var domain string +var singleUser string +var unsafe bool + +func init() { + rootCmd.AddCommand(initCmd) + + initCmd.PersistentFlags().StringVar(&domain, "domain", "http://localhost:8080", "Domain to use") + initCmd.PersistentFlags().StringVar(&singleUser, "single-user", "", "Use single user mode with given username") + initCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "Allow raw html") +} + +var initCmd = &cobra.Command{ + Use: "init", + Short: "Creates a new repository", + Long: `Creates a new repository`, + Run: func(cmd *cobra.Command, args []string) { + _, err := owl.CreateRepository(repoPath, owl.RepoConfig{ + Domain: domain, + SingleUser: singleUser, + AllowRawHtml: unsafe, + }) + if err != nil { + println("Error creating repository: ", err.Error()) + } else { + println("Repository created: ", repoPath) + } + + }, +} diff --git a/cmd/owl/web.go b/cmd/owl/web.go index 8dc71d6..6a939e4 100644 --- a/cmd/owl/web.go +++ b/cmd/owl/web.go @@ -7,15 +7,11 @@ import ( ) var port int -var unsafe bool -var user string func init() { rootCmd.AddCommand(webCmd) - rootCmd.PersistentFlags().IntVar(&port, "port", 8080, "Port to use") - rootCmd.PersistentFlags().BoolVar(&unsafe, "unsafe", false, "Allow unsafe html") - rootCmd.PersistentFlags().StringVar(&user, "user", "", "Start server in single user mode.") + webCmd.PersistentFlags().IntVar(&port, "port", 8080, "Port to use") } var webCmd = &cobra.Command{ @@ -23,6 +19,6 @@ var webCmd = &cobra.Command{ Short: "Start the web server", Long: `Start the web server`, Run: func(cmd *cobra.Command, args []string) { - web.StartServer(repoPath, port, unsafe, user) + web.StartServer(repoPath, port) }, } From 0766101ff69fb7c79bd609527e5147c7c839f4ac Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Mon, 5 Sep 2022 21:12:54 +0200 Subject: [PATCH 07/17] create post and user --- cmd/owl/main.go | 1 + cmd/owl/new_post.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ cmd/owl/new_user.go | 38 ++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 cmd/owl/new_post.go create mode 100644 cmd/owl/new_user.go diff --git a/cmd/owl/main.go b/cmd/owl/main.go index 1ceeda0..e46408a 100644 --- a/cmd/owl/main.go +++ b/cmd/owl/main.go @@ -25,6 +25,7 @@ func Execute() { func init() { rootCmd.PersistentFlags().StringVar(&repoPath, "repo", ".", "Path to the repository to use.") + rootCmd.PersistentFlags().StringVar(&user, "user", "", "Username") } diff --git a/cmd/owl/new_post.go b/cmd/owl/new_post.go new file mode 100644 index 0000000..583bbfc --- /dev/null +++ b/cmd/owl/new_post.go @@ -0,0 +1,50 @@ +package main + +import ( + "h4kor/owl-blogs" + + "github.com/spf13/cobra" +) + +var postTitle string + +func init() { + rootCmd.AddCommand(newPostCmd) + newPostCmd.PersistentFlags().StringVar(&postTitle, "title", "", "Post title") +} + +var newPostCmd = &cobra.Command{ + Use: "new-post", + Short: "Creates a new post", + Long: `Creates a new post`, + Run: func(cmd *cobra.Command, args []string) { + if user == "" { + println("Username is required") + return + } + + if postTitle == "" { + println("Post title is required") + return + } + + repo, err := owl.OpenRepository(repoPath) + if err != nil { + println("Error opening repository: ", err.Error()) + return + } + + user, err := repo.GetUser(user) + if err != nil { + println("Error getting user: ", err.Error()) + return + } + + _, err = user.CreateNewPost(postTitle) + if err != nil { + println("Error creating post: ", err.Error()) + } else { + println("Post created: ", postTitle) + } + }, +} diff --git a/cmd/owl/new_user.go b/cmd/owl/new_user.go new file mode 100644 index 0000000..d98d8a5 --- /dev/null +++ b/cmd/owl/new_user.go @@ -0,0 +1,38 @@ +package main + +import ( + "h4kor/owl-blogs" + + "github.com/spf13/cobra" +) + +var user string + +func init() { + rootCmd.AddCommand(newUserCmd) +} + +var newUserCmd = &cobra.Command{ + Use: "new-user", + Short: "Creates a new user", + Long: `Creates a new user`, + Run: func(cmd *cobra.Command, args []string) { + if user == "" { + println("Username is required") + return + } + + repo, err := owl.OpenRepository(repoPath) + if err != nil { + println("Error opening repository: ", err.Error()) + return + } + + _, err = repo.CreateUser(user) + if err != nil { + println("Error creating user: ", err.Error()) + } else { + println("User created: ", user) + } + }, +} From 57d6a93014a874f70f26faf0f719fb38a85ced0e Mon Sep 17 00:00:00 2001 From: Niko Abeler Date: Mon, 5 Sep 2022 21:14:15 +0200 Subject: [PATCH 08/17] remove old cli --- cmd/owl-cli/main.go | 71 --------------------------------------------- cmd/owl/main.go | 4 +-- 2 files changed, 1 insertion(+), 74 deletions(-) delete mode 100644 cmd/owl-cli/main.go diff --git a/cmd/owl-cli/main.go b/cmd/owl-cli/main.go deleted file mode 100644 index eb6081c..0000000 --- a/cmd/owl-cli/main.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "h4kor/owl-blogs" - "os" -) - -func main() { - println("owl blogs") - println("Commands") - println("init - Creates a new repository") - println(" new-user - Creates a new user") - println(" new-post - Creates a new post") - - if len(os.Args) < 3 { - println("Please specify a repository and command") - os.Exit(1) - } - - if os.Args[1] == "init" { - repoName := os.Args[2] - _, err := owl.CreateRepository(repoName) - if err != nil { - println("Error creating repository: ", err.Error()) - } - println("Repository created: ", repoName) - os.Exit(0) - } - - repoName := os.Args[1] - repo, err := owl.OpenRepository(repoName) - if err != nil { - println("Error opening repository: ", err.Error()) - os.Exit(1) - } - switch os.Args[2] { - case "new-user": - if len(os.Args) < 4 { - println("Please specify a user name") - os.Exit(1) - } - userName := os.Args[3] - user, err := repo.CreateUser(userName) - if err != nil { - println("Error creating user: ", err.Error()) - os.Exit(1) - } - println("User created: ", user.Name()) - case "new-post": - if len(os.Args) < 5 { - println("Please specify a user name and a title") - os.Exit(1) - } - userName := os.Args[3] - user, err := repo.GetUser(userName) - if err != nil { - println("Error finding user: ", err.Error()) - os.Exit(1) - } - title := os.Args[4] - post, err := user.CreateNewPost(title) - if err != nil { - println("Error creating post: ", err.Error()) - os.Exit(1) - } - println("Post created: ", post.Title()) - default: - println("Unknown command: ", os.Args[2]) - os.Exit(1) - } -} diff --git a/cmd/owl/main.go b/cmd/owl/main.go index e46408a..f9048c1 100644 --- a/cmd/owl/main.go +++ b/cmd/owl/main.go @@ -11,8 +11,6 @@ var repoPath string var rootCmd = &cobra.Command{ Use: "owl", Short: "Owl Blogs is a not so static blog generator", - // Run: func(cmd *cobra.Command, args []string) { - // }, } func Execute() { @@ -25,7 +23,7 @@ func Execute() { func init() { rootCmd.PersistentFlags().StringVar(&repoPath, "repo", ".", "Path to the repository to use.") - rootCmd.PersistentFlags().StringVar(&user, "user", "", "Username") + rootCmd.PersistentFlags().StringVar(&user, "user", "", "Username. Required for some commands.") } From a796a59a6f5ef4ed977e0135869ea30e41e212e1 Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Mon, 5 Sep 2022 21:23:43 +0200 Subject: [PATCH 09/17] adjust Dockerfile --- Dockerfile | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 71ee87b..e75fe32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,6 @@ +## +## Build Container +## FROM golang:1.19-alpine as build @@ -12,9 +15,12 @@ RUN go mod download COPY . . -RUN go build -o ./out/owl-web ./cmd/owl-web -RUN go build -o ./out/owl-cli ./cmd/owl-cli +RUN go build -o ./out/owl ./cmd/owl + +## +## Run Container +## FROM alpine:3.9 RUN apk add ca-certificates @@ -24,4 +30,4 @@ COPY --from=build /tmp/owl/out/ /bin/ EXPOSE 8080 # Run the binary program produced by `go install` -CMD ["/bin/owl-web"] \ No newline at end of file +ENTRYPOINT ["/bin/owl"] \ No newline at end of file From 6e6d8005da19396a0bb551392642b2b85ec61f22 Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Tue, 6 Sep 2022 19:47:15 +0200 Subject: [PATCH 10/17] command to send webmentions --- cmd/owl/webmention.go | 70 +++++++++++++++++++++++++++++++++++++++++++ post.go | 4 +-- webmention.go | 21 +++++++------ webmention_test.go | 26 ++++++++++++++++ 4 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 cmd/owl/webmention.go diff --git a/cmd/owl/webmention.go b/cmd/owl/webmention.go new file mode 100644 index 0000000..0de0233 --- /dev/null +++ b/cmd/owl/webmention.go @@ -0,0 +1,70 @@ +package main + +import ( + "h4kor/owl-blogs" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(webmentionCmd) +} + +var webmentionCmd = &cobra.Command{ + Use: "webmention", + Short: "Send webmentions for posts, optionally for a specific user", + Long: `Send webmentions for posts, optionally for a specific user`, + Run: func(cmd *cobra.Command, args []string) { + repo, err := owl.OpenRepository(repoPath) + if err != nil { + println("Error opening repository: ", err.Error()) + return + } + + var users []owl.User + if user == "" { + // send webmentions for all users + users, err = repo.Users() + if err != nil { + println("Error getting users: ", err.Error()) + return + } + } else { + // send webmentions for a specific user + user, err := repo.GetUser(user) + users = append(users, user) + if err != nil { + println("Error getting user: ", err.Error()) + return + } + } + + for _, user := range users { + posts, err := user.Posts() + if err != nil { + println("Error getting posts: ", err.Error()) + } + + for _, post := range posts { + println("Webmentions for post: ", post.Title()) + + err := post.ScanForLinks() + if err != nil { + println("Error scanning post for links: ", err.Error()) + continue + } + + webmentions := post.OutgoingWebmentions() + println("Found ", len(webmentions), " links") + for _, webmention := range webmentions { + err = post.SendWebmention(webmention) + if err != nil { + println("Error sending webmentions: ", err.Error()) + } else { + println("Webmention sent to ", webmention.Target) + } + } + } + } + }, +} diff --git a/post.go b/post.go index bc6479f..7cbb729 100644 --- a/post.go +++ b/post.go @@ -375,13 +375,12 @@ func (post *Post) SendWebmention(webmention WebmentionOut) error { 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 } @@ -394,7 +393,6 @@ func (post *Post) SendWebmention(webmention WebmentionOut) error { _, err = post.user.repo.HttpClient.Post(endpoint, payload) if err != nil { - // TODO handle error return err } diff --git a/webmention.go b/webmention.go index df4ab33..8b4eb70 100644 --- a/webmention.go +++ b/webmention.go @@ -3,6 +3,8 @@ package owl import ( "bytes" "errors" + "fmt" + "io" "net/http" "net/url" "strings" @@ -46,13 +48,16 @@ type ParsedHEntry struct { func (OwlHttpClient) Get(url string) ([]byte, error) { resp, err := http.Get(url) + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return make([]byte, 0), errors.New("Failed to get url. Status code: " + fmt.Sprint(resp.StatusCode)) + } + if err != nil { return []byte{}, err } - var data []byte - _, err = resp.Body.Read(data) - // TODO: encoding - return data, err + defer resp.Body.Close() + return io.ReadAll(resp.Body) } func (OwlHttpClient) Post(url string, data url.Values) ([]byte, error) { @@ -60,10 +65,8 @@ func (OwlHttpClient) Post(url string, data url.Values) ([]byte, error) { if err != nil { return []byte{}, err } - var respData []byte - _, err = resp.Body.Read(respData) - - return respData, err + defer resp.Body.Close() + return io.ReadAll(resp.Body) } func collectText(n *html.Node, buf *bytes.Buffer) { @@ -152,7 +155,7 @@ func (OwlHtmlParser) GetWebmentionEndpoint(data []byte) (string, error) { var findEndpoint func(*html.Node) (string, error) findEndpoint = func(n *html.Node) (string, error) { - if n.Type == html.ElementNode && n.Data == "link" { + if n.Type == html.ElementNode && (n.Data == "link" || n.Data == "a") { for _, attr := range n.Attr { if attr.Key == "rel" && attr.Val == "webmention" { for _, attr := range n.Attr { diff --git a/webmention_test.go b/webmention_test.go index c7e95c7..c49d158 100644 --- a/webmention_test.go +++ b/webmention_test.go @@ -34,3 +34,29 @@ func TestParseValidHEntryWithoutTitle(t *testing.T) { t.Errorf("Wrong Title. Expected %v, got %v", "Foo", entry.Title) } } + +func TestGetWebmentionEndpointLink(t *testing.T) { + html := []byte("<link rel=\"webmention\" href=\"http://example.com/webmention\" />") + parser := &owl.OwlHtmlParser{} + endpoint, err := parser.GetWebmentionEndpoint(html) + + if err != nil { + t.Errorf("Unable to parse feed: %v", err) + } + if endpoint != "http://example.com/webmention" { + t.Errorf("Wrong endpoint. Expected %v, got %v", "http://example.com/webmention", endpoint) + } +} + +func TestGetWebmentionEndpointLinkA(t *testing.T) { + html := []byte("<a rel=\"webmention\" href=\"http://example.com/webmention\" />") + parser := &owl.OwlHtmlParser{} + endpoint, err := parser.GetWebmentionEndpoint(html) + + if err != nil { + t.Errorf("Unable to parse feed: %v", err) + } + if endpoint != "http://example.com/webmention" { + t.Errorf("Wrong endpoint. Expected %v, got %v", "http://example.com/webmention", endpoint) + } +} From 945a13ed2f8c1a70e16c64a44b21680ec4af563e Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Tue, 6 Sep 2022 20:00:12 +0200 Subject: [PATCH 11/17] add u-url to post page --- embed/post.html | 1 + post.go | 7 ++----- renderer_test.go | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/embed/post.html b/embed/post.html index 28b8b6e..25d44c9 100644 --- a/embed/post.html +++ b/embed/post.html @@ -6,6 +6,7 @@ <time class="dt-published" datetime="{{.Post.Meta.Date}}"> {{.Post.Meta.Date}} </time> + <a class="u-url" href="{{.Post.FullUrl}}">#</a> </small> </hgroup> <hr> diff --git a/post.go b/post.go index 7cbb729..1259f5a 100644 --- a/post.go +++ b/post.go @@ -358,11 +358,8 @@ func (post *Post) OutgoingWebmentions() []WebmentionOut { 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)) + postHtml := post.RenderedContent() + links, _ := post.user.repo.Parser.ParseLinks(postHtml.Bytes()) for _, link := range links { post.AddOutgoingWebmention(link) } diff --git a/renderer_test.go b/renderer_test.go index 534f4a8..f36aacb 100644 --- a/renderer_test.go +++ b/renderer_test.go @@ -199,3 +199,17 @@ func TestRenderPostNotMentioningWebmentionsIfNoAvail(t *testing.T) { } } + +func TestRenderIncludesFullUrl(t *testing.T) { + user := getTestUser() + post, _ := user.CreateNewPost("testpost") + result, _ := owl.RenderPost(&post) + + if !strings.Contains(result, "class=\"u-url\"") { + t.Error("u-url not rendered. Got: " + result) + } + if !strings.Contains(result, post.FullUrl()) { + t.Error("Full url not rendered. Got: " + result) + t.Error("Expected: " + post.FullUrl()) + } +} From 0b9da1860f763064a3f1cc3505655976dbf8e706 Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Tue, 6 Sep 2022 20:32:21 +0200 Subject: [PATCH 12/17] refactoring to have full http response in parser --- owl_test.go | 33 ++++++++++++++------- post.go | 12 ++++---- post_test.go | 16 +++++------ repository.go | 4 +-- webmention.go | 71 ++++++++++++++++++++++------------------------ webmention_test.go | 51 ++++++++++++++++++++++++++++++--- 6 files changed, 119 insertions(+), 68 deletions(-) diff --git a/owl_test.go b/owl_test.go index 532d21f..8fb1159 100644 --- a/owl_test.go +++ b/owl_test.go @@ -2,33 +2,44 @@ package owl_test import ( "h4kor/owl-blogs" + "io" "math/rand" + "net/http" "net/url" "time" ) -type MockHttpParser struct{} +type MockHtmlParser struct{} -func (*MockHttpParser) ParseHEntry(data []byte) (owl.ParsedHEntry, error) { +func (*MockHtmlParser) ParseHEntry(resp *http.Response) (owl.ParsedHEntry, error) { return owl.ParsedHEntry{Title: "Mock Title"}, nil -} -func (*MockHttpParser) ParseLinks(data []byte) ([]string, error) { +} +func (*MockHtmlParser) ParseLinks(resp *http.Response) ([]string, error) { return []string{"http://example.com"}, nil -} -func (*MockHttpParser) GetWebmentionEndpoint(data []byte) (string, error) { +} +func (*MockHtmlParser) ParseLinksFromString(string) ([]string, error) { + return []string{"http://example.com"}, nil + +} +func (*MockHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) { return "http://example.com/webmention", nil + } -type MockHttpRetriever struct{} +type MockHttpClient struct{} -func (*MockHttpRetriever) Get(url string) ([]byte, error) { - return []byte(""), nil +func (*MockHttpClient) Get(url string) (resp *http.Response, err error) { + return &http.Response{}, nil } +func (*MockHttpClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { -func (m *MockHttpRetriever) Post(url string, data url.Values) ([]byte, error) { - return []byte(""), nil + return &http.Response{}, nil +} +func (*MockHttpClient) PostForm(url string, data url.Values) (resp *http.Response, err error) { + + return &http.Response{}, nil } func randomName() string { diff --git a/post.go b/post.go index 1259f5a..2136697 100644 --- a/post.go +++ b/post.go @@ -295,13 +295,13 @@ func (post *Post) UpdateOutgoingWebmention(webmention *WebmentionOut) error { } func (post *Post) EnrichWebmention(source string) error { - html, err := post.user.repo.HttpClient.Get(source) + resp, err := post.user.repo.HttpClient.Get(source) if err == nil { webmention, err := post.Webmention(source) if err != nil { return err } - entry, err := post.user.repo.Parser.ParseHEntry(html) + entry, err := post.user.repo.Parser.ParseHEntry(resp) if err == nil { webmention.Title = entry.Title return post.PersistWebmention(webmention) @@ -359,7 +359,7 @@ 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 := post.RenderedContent() - links, _ := post.user.repo.Parser.ParseLinks(postHtml.Bytes()) + links, _ := post.user.repo.Parser.ParseLinksFromString(string(postHtml.Bytes())) for _, link := range links { post.AddOutgoingWebmention(link) } @@ -370,13 +370,13 @@ func (post *Post) SendWebmention(webmention WebmentionOut) error { defer post.UpdateOutgoingWebmention(&webmention) webmention.ScannedAt = time.Now() - html, err := post.user.repo.HttpClient.Get(webmention.Target) + resp, err := post.user.repo.HttpClient.Get(webmention.Target) if err != nil { webmention.Supported = false return err } - endpoint, err := post.user.repo.Parser.GetWebmentionEndpoint(html) + endpoint, err := post.user.repo.Parser.GetWebmentionEndpoint(resp) if err != nil { webmention.Supported = false return err @@ -387,7 +387,7 @@ func (post *Post) SendWebmention(webmention WebmentionOut) error { payload := url.Values{} payload.Set("source", post.FullUrl()) payload.Set("target", webmention.Target) - _, err = post.user.repo.HttpClient.Post(endpoint, payload) + _, err = post.user.repo.HttpClient.PostForm(endpoint, payload) if err != nil { return err diff --git a/post_test.go b/post_test.go index 2825d3c..d26272c 100644 --- a/post_test.go +++ b/post_test.go @@ -190,8 +190,8 @@ func TestPersistWebmention(t *testing.T) { func TestAddWebmentionCreatesFile(t *testing.T) { repo := getTestRepo(owl.RepoConfig{}) - repo.HttpClient = &MockHttpRetriever{} - repo.Parser = &MockHttpParser{} + repo.HttpClient = &MockHttpClient{} + repo.Parser = &MockHtmlParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -208,8 +208,8 @@ func TestAddWebmentionCreatesFile(t *testing.T) { func TestAddWebmentionNotOverwritingFile(t *testing.T) { repo := getTestRepo(owl.RepoConfig{}) - repo.HttpClient = &MockHttpRetriever{} - repo.Parser = &MockHttpParser{} + repo.HttpClient = &MockHttpClient{} + repo.Parser = &MockHtmlParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -238,8 +238,8 @@ func TestAddWebmentionNotOverwritingFile(t *testing.T) { func TestAddWebmentionAddsParsedTitle(t *testing.T) { repo := getTestRepo(owl.RepoConfig{}) - repo.HttpClient = &MockHttpRetriever{} - repo.Parser = &MockHttpParser{} + repo.HttpClient = &MockHttpClient{} + repo.Parser = &MockHtmlParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") @@ -353,8 +353,8 @@ func TestScanningForLinksDoesNotAddDuplicates(t *testing.T) { func TestCanSendWebmention(t *testing.T) { repo := getTestRepo(owl.RepoConfig{}) - repo.HttpClient = &MockHttpRetriever{} - repo.Parser = &MockHttpParser{} + repo.HttpClient = &MockHttpClient{} + repo.Parser = &MockHtmlParser{} user, _ := repo.CreateUser("testuser") post, _ := user.CreateNewPost("testpost") diff --git a/repository.go b/repository.go index cf97038..4c35124 100644 --- a/repository.go +++ b/repository.go @@ -28,7 +28,7 @@ type RepoConfig struct { } func CreateRepository(name string, config RepoConfig) (Repository, error) { - newRepo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: OwlHttpClient{}} + newRepo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: &OwlHttpClient{}} // check if repository already exists if dirExists(newRepo.Dir()) { return Repository{}, fmt.Errorf("Repository already exists") @@ -69,7 +69,7 @@ func CreateRepository(name string, config RepoConfig) (Repository, error) { func OpenRepository(name string) (Repository, error) { - repo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: OwlHttpClient{}} + repo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: &OwlHttpClient{}} if !dirExists(repo.Dir()) { return Repository{}, fmt.Errorf("Repository does not exist: " + repo.Dir()) } diff --git a/webmention.go b/webmention.go index 8b4eb70..0e47c43 100644 --- a/webmention.go +++ b/webmention.go @@ -3,7 +3,6 @@ package owl import ( "bytes" "errors" - "fmt" "io" "net/http" "net/url" @@ -28,17 +27,19 @@ type WebmentionOut struct { } type HttpClient interface { - Get(url string) ([]byte, error) - Post(url string, data url.Values) ([]byte, error) + Get(url string) (resp *http.Response, err error) + Post(url, contentType string, body io.Reader) (resp *http.Response, err error) + PostForm(url string, data url.Values) (resp *http.Response, err error) } type HtmlParser interface { - ParseHEntry(data []byte) (ParsedHEntry, error) - ParseLinks(data []byte) ([]string, error) - GetWebmentionEndpoint(data []byte) (string, error) + ParseHEntry(resp *http.Response) (ParsedHEntry, error) + ParseLinks(resp *http.Response) ([]string, error) + ParseLinksFromString(string) ([]string, error) + GetWebmentionEndpoint(resp *http.Response) (string, error) } -type OwlHttpClient struct{} +type OwlHttpClient = http.Client type OwlHtmlParser struct{} @@ -46,30 +47,8 @@ type ParsedHEntry struct { Title string } -func (OwlHttpClient) Get(url string) ([]byte, error) { - resp, err := http.Get(url) - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return make([]byte, 0), errors.New("Failed to get url. Status code: " + fmt.Sprint(resp.StatusCode)) - } - - if err != nil { - return []byte{}, err - } - defer resp.Body.Close() - return io.ReadAll(resp.Body) -} - -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 - } - defer resp.Body.Close() - return io.ReadAll(resp.Body) -} - func collectText(n *html.Node, buf *bytes.Buffer) { + if n.Type == html.TextNode { buf.WriteString(n.Data) } @@ -78,8 +57,18 @@ func collectText(n *html.Node, buf *bytes.Buffer) { } } -func (OwlHtmlParser) ParseHEntry(data []byte) (ParsedHEntry, error) { - doc, err := html.Parse(strings.NewReader(string(data))) +func readResponseBody(resp *http.Response) (string, error) { + defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(bodyBytes), nil +} + +func (OwlHtmlParser) ParseHEntry(resp *http.Response) (ParsedHEntry, error) { + htmlStr, err := readResponseBody(resp) + doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { return ParsedHEntry{}, err } @@ -121,8 +110,16 @@ func (OwlHtmlParser) ParseHEntry(data []byte) (ParsedHEntry, error) { return findHFeed(doc) } -func (OwlHtmlParser) ParseLinks(data []byte) ([]string, error) { - doc, err := html.Parse(strings.NewReader(string(data))) +func (OwlHtmlParser) ParseLinks(resp *http.Response) ([]string, error) { + htmlStr, err := readResponseBody(resp) + if err != nil { + return []string{}, err + } + return OwlHtmlParser{}.ParseLinksFromString(htmlStr) +} + +func (OwlHtmlParser) ParseLinksFromString(htmlStr string) ([]string, error) { + doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { return make([]string, 0), err } @@ -144,11 +141,11 @@ func (OwlHtmlParser) ParseLinks(data []byte) ([]string, error) { return links, nil } return findLinks(doc) - } -func (OwlHtmlParser) GetWebmentionEndpoint(data []byte) (string, error) { - doc, err := html.Parse(strings.NewReader(string(data))) +func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) { + htmlStr, err := readResponseBody(resp) + doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { return "", err } diff --git a/webmention_test.go b/webmention_test.go index c49d158..abb7bea 100644 --- a/webmention_test.go +++ b/webmention_test.go @@ -1,7 +1,10 @@ package owl_test import ( + "bytes" "h4kor/owl-blogs" + "io" + "net/http" "testing" ) @@ -12,7 +15,7 @@ import ( func TestParseValidHEntry(t *testing.T) { html := []byte("<div class=\"h-entry\"><div class=\"p-name\">Foo</div></div>") parser := &owl.OwlHtmlParser{} - entry, err := parser.ParseHEntry(html) + entry, err := parser.ParseHEntry(&http.Response{Body: io.NopCloser(bytes.NewReader(html))}) if err != nil { t.Errorf("Unable to parse feed: %v", err) @@ -25,7 +28,7 @@ func TestParseValidHEntry(t *testing.T) { func TestParseValidHEntryWithoutTitle(t *testing.T) { html := []byte("<div class=\"h-entry\"></div><div class=\"p-name\">Foo</div>") parser := &owl.OwlHtmlParser{} - entry, err := parser.ParseHEntry(html) + entry, err := parser.ParseHEntry(&http.Response{Body: io.NopCloser(bytes.NewReader(html))}) if err != nil { t.Errorf("Unable to parse feed: %v", err) @@ -38,7 +41,7 @@ func TestParseValidHEntryWithoutTitle(t *testing.T) { func TestGetWebmentionEndpointLink(t *testing.T) { html := []byte("<link rel=\"webmention\" href=\"http://example.com/webmention\" />") parser := &owl.OwlHtmlParser{} - endpoint, err := parser.GetWebmentionEndpoint(html) + endpoint, err := parser.GetWebmentionEndpoint(&http.Response{Body: io.NopCloser(bytes.NewReader(html))}) if err != nil { t.Errorf("Unable to parse feed: %v", err) @@ -51,7 +54,7 @@ func TestGetWebmentionEndpointLink(t *testing.T) { func TestGetWebmentionEndpointLinkA(t *testing.T) { html := []byte("<a rel=\"webmention\" href=\"http://example.com/webmention\" />") parser := &owl.OwlHtmlParser{} - endpoint, err := parser.GetWebmentionEndpoint(html) + endpoint, err := parser.GetWebmentionEndpoint(&http.Response{Body: io.NopCloser(bytes.NewReader(html))}) if err != nil { t.Errorf("Unable to parse feed: %v", err) @@ -60,3 +63,43 @@ func TestGetWebmentionEndpointLinkA(t *testing.T) { t.Errorf("Wrong endpoint. Expected %v, got %v", "http://example.com/webmention", endpoint) } } + +// func TestRealWorldWebmention(t *testing.T) { +// links := []string{ +// "https://webmention.rocks/test/1", +// "https://webmention.rocks/test/2", +// "https://webmention.rocks/test/3", +// "https://webmention.rocks/test/4", +// "https://webmention.rocks/test/5", +// "https://webmention.rocks/test/6", +// "https://webmention.rocks/test/7", +// "https://webmention.rocks/test/8", +// "https://webmention.rocks/test/9", +// "https://webmention.rocks/test/10", +// "https://webmention.rocks/test/11", +// "https://webmention.rocks/test/12", +// "https://webmention.rocks/test/13", +// "https://webmention.rocks/test/14", +// "https://webmention.rocks/test/15", +// "https://webmention.rocks/test/16", +// "https://webmention.rocks/test/17", +// "https://webmention.rocks/test/18", +// "https://webmention.rocks/test/19", +// "https://webmention.rocks/test/20", +// "https://webmention.rocks/test/21", +// "https://webmention.rocks/test/22", +// "https://webmention.rocks/test/23/page", +// } + +// for _, link := range links { +// parser := &owl.OwlHtmlParser{} +// client := &owl.OwlHttpClient{} +// html, _ := client.Get(link) +// _, err := parser.GetWebmentionEndpoint(html) + +// if err != nil { +// t.Errorf("Unable to find webmention: %v for link %v", err, link) +// } +// } + +// } From edca1589858211c94e1c8665b47568b1bf251eb3 Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Tue, 6 Sep 2022 21:01:08 +0200 Subject: [PATCH 13/17] most webmention endpoints working --- webmention.go | 35 +++++++++++- webmention_test.go | 130 ++++++++++++++++++++++++++++++++------------- 2 files changed, 125 insertions(+), 40 deletions(-) diff --git a/webmention.go b/webmention.go index 0e47c43..4a82994 100644 --- a/webmention.go +++ b/webmention.go @@ -68,6 +68,9 @@ func readResponseBody(resp *http.Response) (string, error) { func (OwlHtmlParser) ParseHEntry(resp *http.Response) (ParsedHEntry, error) { htmlStr, err := readResponseBody(resp) + if err != nil { + return ParsedHEntry{}, err + } doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { return ParsedHEntry{}, err @@ -144,7 +147,27 @@ func (OwlHtmlParser) ParseLinksFromString(htmlStr string) ([]string, error) { } func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) { + //request url + requestUrl := resp.Request.URL + + // Check link headers + for _, link := range resp.Header["Link"] { + if strings.Contains(link, "rel=\"webmention\"") || strings.Contains(link, "rel=webmention") { + link := strings.Split(link, ";")[0] + link = strings.Trim(link, "<>") + linkUrl, err := url.Parse(link) + if err != nil { + return "", err + } + return requestUrl.ResolveReference(linkUrl).String(), nil + + } + } + htmlStr, err := readResponseBody(resp) + if err != nil { + return "", err + } doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { return "", err @@ -154,7 +177,7 @@ func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) findEndpoint = func(n *html.Node) (string, error) { if n.Type == html.ElementNode && (n.Data == "link" || n.Data == "a") { for _, attr := range n.Attr { - if attr.Key == "rel" && attr.Val == "webmention" { + if attr.Key == "rel" && strings.Contains(attr.Val, "webmention") { for _, attr := range n.Attr { if attr.Key == "href" { return attr.Val, nil @@ -171,5 +194,13 @@ func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) } return "", errors.New("no webmention endpoint found") } - return findEndpoint(doc) + linkUrlStr, err := findEndpoint(doc) + if err != nil { + return "", err + } + linkUrl, err := url.Parse(linkUrlStr) + if err != nil { + return "", err + } + return requestUrl.ResolveReference(linkUrl).String(), nil } diff --git a/webmention_test.go b/webmention_test.go index abb7bea..682587d 100644 --- a/webmention_test.go +++ b/webmention_test.go @@ -5,9 +5,20 @@ import ( "h4kor/owl-blogs" "io" "net/http" + "net/url" "testing" ) +func constructResponse(html []byte) *http.Response { + url, _ := url.Parse("http://example.com/foo/bar") + return &http.Response{ + Request: &http.Request{ + URL: url, + }, + Body: io.NopCloser(bytes.NewReader([]byte(html))), + } +} + // // https://www.w3.org/TR/webmention/#h-webmention-verification // @@ -41,7 +52,7 @@ func TestParseValidHEntryWithoutTitle(t *testing.T) { func TestGetWebmentionEndpointLink(t *testing.T) { html := []byte("<link rel=\"webmention\" href=\"http://example.com/webmention\" />") parser := &owl.OwlHtmlParser{} - endpoint, err := parser.GetWebmentionEndpoint(&http.Response{Body: io.NopCloser(bytes.NewReader(html))}) + endpoint, err := parser.GetWebmentionEndpoint(constructResponse(html)) if err != nil { t.Errorf("Unable to parse feed: %v", err) @@ -54,7 +65,7 @@ func TestGetWebmentionEndpointLink(t *testing.T) { func TestGetWebmentionEndpointLinkA(t *testing.T) { html := []byte("<a rel=\"webmention\" href=\"http://example.com/webmention\" />") parser := &owl.OwlHtmlParser{} - endpoint, err := parser.GetWebmentionEndpoint(&http.Response{Body: io.NopCloser(bytes.NewReader(html))}) + endpoint, err := parser.GetWebmentionEndpoint(constructResponse(html)) if err != nil { t.Errorf("Unable to parse feed: %v", err) @@ -64,42 +75,85 @@ func TestGetWebmentionEndpointLinkA(t *testing.T) { } } -// func TestRealWorldWebmention(t *testing.T) { -// links := []string{ -// "https://webmention.rocks/test/1", -// "https://webmention.rocks/test/2", -// "https://webmention.rocks/test/3", -// "https://webmention.rocks/test/4", -// "https://webmention.rocks/test/5", -// "https://webmention.rocks/test/6", -// "https://webmention.rocks/test/7", -// "https://webmention.rocks/test/8", -// "https://webmention.rocks/test/9", -// "https://webmention.rocks/test/10", -// "https://webmention.rocks/test/11", -// "https://webmention.rocks/test/12", -// "https://webmention.rocks/test/13", -// "https://webmention.rocks/test/14", -// "https://webmention.rocks/test/15", -// "https://webmention.rocks/test/16", -// "https://webmention.rocks/test/17", -// "https://webmention.rocks/test/18", -// "https://webmention.rocks/test/19", -// "https://webmention.rocks/test/20", -// "https://webmention.rocks/test/21", -// "https://webmention.rocks/test/22", -// "https://webmention.rocks/test/23/page", -// } +func TestGetWebmentionEndpointLinkHeader(t *testing.T) { + html := []byte("") + parser := &owl.OwlHtmlParser{} + resp := constructResponse(html) + resp.Header = http.Header{"Link": []string{"<http://example.com/webmention>; rel=\"webmention\""}} + endpoint, err := parser.GetWebmentionEndpoint(resp) -// for _, link := range links { -// parser := &owl.OwlHtmlParser{} -// client := &owl.OwlHttpClient{} -// html, _ := client.Get(link) -// _, err := parser.GetWebmentionEndpoint(html) + if err != nil { + t.Errorf("Unable to parse feed: %v", err) + } + if endpoint != "http://example.com/webmention" { + t.Errorf("Wrong endpoint. Expected %v, got %v", "http://example.com/webmention", endpoint) + } +} -// if err != nil { -// t.Errorf("Unable to find webmention: %v for link %v", err, link) -// } -// } +func TestGetWebmentionEndpointRelativeLink(t *testing.T) { + html := []byte("<link rel=\"webmention\" href=\"/webmention\" />") + parser := &owl.OwlHtmlParser{} + endpoint, err := parser.GetWebmentionEndpoint(constructResponse(html)) -// } + if err != nil { + t.Errorf("Unable to parse feed: %v", err) + } + if endpoint != "http://example.com/webmention" { + t.Errorf("Wrong endpoint. Expected %v, got %v", "http://example.com/webmention", endpoint) + } +} + +func TestGetWebmentionEndpointRelativeLinkInHeader(t *testing.T) { + html := []byte("<link rel=\"webmention\" href=\"/webmention\" />") + parser := &owl.OwlHtmlParser{} + resp := constructResponse(html) + resp.Header = http.Header{"Link": []string{"</webmention>; rel=\"webmention\""}} + endpoint, err := parser.GetWebmentionEndpoint(resp) + + if err != nil { + t.Errorf("Unable to parse feed: %v", err) + } + if endpoint != "http://example.com/webmention" { + t.Errorf("Wrong endpoint. Expected %v, got %v", "http://example.com/webmention", endpoint) + } +} + +func TestRealWorldWebmention(t *testing.T) { + links := []string{ + "https://webmention.rocks/test/1", + "https://webmention.rocks/test/2", + "https://webmention.rocks/test/3", + "https://webmention.rocks/test/4", + "https://webmention.rocks/test/5", + "https://webmention.rocks/test/6", + "https://webmention.rocks/test/7", + "https://webmention.rocks/test/8", + "https://webmention.rocks/test/9", + // "https://webmention.rocks/test/10", // not supported + "https://webmention.rocks/test/11", + "https://webmention.rocks/test/12", + "https://webmention.rocks/test/13", + "https://webmention.rocks/test/14", + "https://webmention.rocks/test/15", + "https://webmention.rocks/test/16", + "https://webmention.rocks/test/17", + "https://webmention.rocks/test/18", + "https://webmention.rocks/test/19", + "https://webmention.rocks/test/20", + "https://webmention.rocks/test/21", + "https://webmention.rocks/test/22", + "https://webmention.rocks/test/23/page", + } + + for _, link := range links { + parser := &owl.OwlHtmlParser{} + client := &owl.OwlHttpClient{} + html, _ := client.Get(link) + _, err := parser.GetWebmentionEndpoint(html) + + if err != nil { + t.Errorf("Unable to find webmention: %v for link %v", err, link) + } + } + +} From de017b86f9edfedfba195052975fb8e8deea03e7 Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Tue, 6 Sep 2022 21:05:51 +0200 Subject: [PATCH 14/17] fixed webmention discovery --- webmention.go | 13 +++++++++---- webmention_test.go | 13 +++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/webmention.go b/webmention.go index 4a82994..c6992db 100644 --- a/webmention.go +++ b/webmention.go @@ -177,10 +177,15 @@ func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) findEndpoint = func(n *html.Node) (string, error) { if n.Type == html.ElementNode && (n.Data == "link" || n.Data == "a") { for _, attr := range n.Attr { - if attr.Key == "rel" && strings.Contains(attr.Val, "webmention") { - for _, attr := range n.Attr { - if attr.Key == "href" { - return attr.Val, nil + if attr.Key == "rel" { + vals := strings.Split(attr.Val, " ") + for _, val := range vals { + if val == "webmention" { + for _, attr := range n.Attr { + if attr.Key == "href" { + return attr.Val, nil + } + } } } } diff --git a/webmention_test.go b/webmention_test.go index 682587d..a96588e 100644 --- a/webmention_test.go +++ b/webmention_test.go @@ -75,6 +75,19 @@ func TestGetWebmentionEndpointLinkA(t *testing.T) { } } +func TestGetWebmentionEndpointLinkAFakeWebmention(t *testing.T) { + html := []byte("<a rel=\"not-webmention\" href=\"http://example.com/foo\" /><a rel=\"webmention\" href=\"http://example.com/webmention\" />") + parser := &owl.OwlHtmlParser{} + endpoint, err := parser.GetWebmentionEndpoint(constructResponse(html)) + + if err != nil { + t.Errorf("Unable to parse feed: %v", err) + } + if endpoint != "http://example.com/webmention" { + t.Errorf("Wrong endpoint. Expected %v, got %v", "http://example.com/webmention", endpoint) + } +} + func TestGetWebmentionEndpointLinkHeader(t *testing.T) { html := []byte("") parser := &owl.OwlHtmlParser{} From 5596b578f46f0c48bdb2c143b807c79cae31bc6e Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Tue, 6 Sep 2022 21:17:25 +0200 Subject: [PATCH 15/17] improved header parsing --- webmention.go | 28 +++++++++++++++++++--------- webmention_test.go | 17 +++++++++++++++++ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/webmention.go b/webmention.go index c6992db..3602ffb 100644 --- a/webmention.go +++ b/webmention.go @@ -151,16 +151,26 @@ func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) requestUrl := resp.Request.URL // Check link headers - for _, link := range resp.Header["Link"] { - if strings.Contains(link, "rel=\"webmention\"") || strings.Contains(link, "rel=webmention") { - link := strings.Split(link, ";")[0] - link = strings.Trim(link, "<>") - linkUrl, err := url.Parse(link) - if err != nil { - return "", err + for _, linkHeader := range resp.Header["Link"] { + linkHeaderParts := strings.Split(linkHeader, ",") + for _, linkHeaderPart := range linkHeaderParts { + linkHeaderPart = strings.TrimSpace(linkHeaderPart) + params := strings.Split(linkHeaderPart, ";") + if len(params) != 2 { + continue + } + for _, param := range params[1:] { + param = strings.TrimSpace(param) + if strings.Contains(param, "webmention") { + link := strings.Split(params[0], ";")[0] + link = strings.Trim(link, "<>") + linkUrl, err := url.Parse(link) + if err != nil { + return "", err + } + return requestUrl.ResolveReference(linkUrl).String(), nil + } } - return requestUrl.ResolveReference(linkUrl).String(), nil - } } diff --git a/webmention_test.go b/webmention_test.go index a96588e..4086ace 100644 --- a/webmention_test.go +++ b/webmention_test.go @@ -103,6 +103,23 @@ func TestGetWebmentionEndpointLinkHeader(t *testing.T) { } } +func TestGetWebmentionEndpointLinkHeaderCommas(t *testing.T) { + html := []byte("") + parser := &owl.OwlHtmlParser{} + resp := constructResponse(html) + resp.Header = http.Header{ + "Link": []string{"<https://webmention.rocks/test/19/webmention/error>; rel=\"other\", <https://webmention.rocks/test/19/webmention>; rel=\"webmention\""}, + } + endpoint, err := parser.GetWebmentionEndpoint(resp) + + if err != nil { + t.Errorf("Unable to parse feed: %v", err) + } + if endpoint != "https://webmention.rocks/test/19/webmention" { + t.Errorf("Wrong endpoint. Expected %v, got %v", "https://webmention.rocks/test/19/webmention", endpoint) + } +} + func TestGetWebmentionEndpointRelativeLink(t *testing.T) { html := []byte("<link rel=\"webmention\" href=\"/webmention\" />") parser := &owl.OwlHtmlParser{} From 63f89491f36b719c864f975ba355e209fbbcbf5a Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Tue, 6 Sep 2022 21:32:31 +0200 Subject: [PATCH 16/17] twitter handle + refactor user config in renderer --- embed/initial/base.html | 19 ++++++++++++++++--- renderer.go | 12 +++++------- renderer_test.go | 19 +++++++++++++++++++ user.go | 7 ++++--- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/embed/initial/base.html b/embed/initial/base.html index 2fb6cb4..5e701b1 100644 --- a/embed/initial/base.html +++ b/embed/initial/base.html @@ -10,9 +10,15 @@ <link rel="webmention" href="{{ .User.WebmentionUrl }}"> <style> header { - background-color: {{.HeaderColor}}; + background-color: {{.UserConfig.HeaderColor}}; } + footer { + border-top: dashed 2px; + border-color: #ccc; + } + + hgroup h2 a { color: inherit; } </style> </head> @@ -23,8 +29,8 @@ <ul> <li> <hgroup> - <h2><a href="{{ .User.UrlPath }}">{{ .UserTitle }}</a></h2> - <h3>{{ .UserSubtitle }}</h3> + <h2><a href="{{ .User.UrlPath }}">{{ .UserConfig.Title }}</a></h2> + <h3>{{ .UserConfig.SubTitle }}</h3> </hgroup> </li> </ul> @@ -35,6 +41,13 @@ {{ .Content }} </main> <footer class="container"> + <nav> + <ul> + {{ if .UserConfig.TwitterHandle}} + <li><a href="https://twitter.com/{{.UserConfig.TwitterHandle}}" rel="me">@{{.UserConfig.TwitterHandle}} on Twitter</a></li> + {{ end }} + </ul> + </nav> </footer> </body> diff --git a/renderer.go b/renderer.go index a1b7643..7781ce3 100644 --- a/renderer.go +++ b/renderer.go @@ -50,16 +50,14 @@ func renderIntoBaseTemplate(user User, data PageContent) (string, error) { Title string Content template.HTML User User - UserTitle string + UserConfig UserConfig UserSubtitle string HeaderColor string }{ - Title: data.Title, - Content: data.Content, - User: user, - UserTitle: user_config.Title, - UserSubtitle: user_config.SubTitle, - HeaderColor: user_config.HeaderColor, + Title: data.Title, + Content: data.Content, + User: user, + UserConfig: user_config, } var html bytes.Buffer diff --git a/renderer_test.go b/renderer_test.go index f36aacb..5f80e6e 100644 --- a/renderer_test.go +++ b/renderer_test.go @@ -25,6 +25,25 @@ func TestCanRenderPost(t *testing.T) { } +func TestRenderTwitterHandle(t *testing.T) { + user := getTestUser() + config, _ := user.Config() + config.TwitterHandle = "testhandle" + user.SetConfig(config) + post, _ := user.CreateNewPost("testpost") + result, err := owl.RenderPost(&post) + + if err != nil { + t.Error("Error rendering post: " + err.Error()) + return + } + + if !strings.Contains(result, "href=\"https://twitter.com/testhandle\" rel=\"me\"") { + t.Error("Twitter handle not rendered. Got: " + result) + } + +} + func TestRenderPostHEntry(t *testing.T) { user := getTestUser() post, _ := user.CreateNewPost("testpost") diff --git a/user.go b/user.go index 82a9859..b9eeaa4 100644 --- a/user.go +++ b/user.go @@ -18,9 +18,10 @@ type User struct { } type UserConfig struct { - Title string `yaml:"title"` - SubTitle string `yaml:"subtitle"` - HeaderColor string `yaml:"header_color"` + Title string `yaml:"title"` + SubTitle string `yaml:"subtitle"` + HeaderColor string `yaml:"header_color"` + TwitterHandle string `yaml:"twitter_handle"` } func (user User) Dir() string { From c6cf34b51d33f0d340a622f3d99d8103d0d503ff Mon Sep 17 00:00:00 2001 From: Niko Abeler <niko@rerere.org> Date: Tue, 6 Sep 2022 21:48:38 +0200 Subject: [PATCH 17/17] title include blog title --- embed/initial/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embed/initial/base.html b/embed/initial/base.html index 5e701b1..c4062b6 100644 --- a/embed/initial/base.html +++ b/embed/initial/base.html @@ -5,7 +5,7 @@ <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>{{ .Title }} + {{ .Title }} - {{ .UserConfig.Title }}