Compare commits

...

14 Commits

19 changed files with 759 additions and 28 deletions

View File

@ -26,6 +26,9 @@ Each directory in the `/users/` directory of a repository is considered a user.
\- media/
-- Contains all media files used in the blog post.
-- All files in this folder will be publicly available
\- webmention/
\- <hash>.yml
-- Contains data for a received webmention
\- meta/
\- base.html
-- The template used to render all sites

View File

@ -5,6 +5,7 @@ import (
"net/http"
"os"
"path"
"strings"
"github.com/julienschmidt/httprouter"
)
@ -56,6 +57,77 @@ func userIndexHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Requ
}
}
func userWebmentionHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
user, err := getUserFromRepo(repo, ps)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("User not found"))
return
}
err = r.ParseForm()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Unable to parse form data"))
return
}
params := r.PostForm
target := params["target"]
source := params["source"]
if len(target) == 0 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("No target provided"))
return
}
if len(source) == 0 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("No source provided"))
return
}
if len(target[0]) < 7 || (target[0][:7] != "http://" && target[0][:8] != "https://") {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Not a valid target"))
return
}
if len(source[0]) < 7 || (source[0][:7] != "http://" && source[0][:8] != "https://") {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Not a valid source"))
return
}
if source[0] == target[0] {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("target and source are equal"))
return
}
parts := strings.Split(target[0], "/")
if len(parts) < 2 {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Not found"))
return
}
postId := parts[len(parts)-2]
post, err := user.GetPost(postId)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Post not found"))
return
}
err = post.AddWebmention(source[0])
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Unable to process webmention"))
return
}
w.WriteHeader(http.StatusAccepted)
w.Write([]byte(""))
}
}
func userRSSHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
user, err := getUserFromRepo(repo, ps)

View File

@ -14,6 +14,7 @@ func Router(repo *owl.Repository) http.Handler {
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
router.GET("/", repoIndexHandler(repo))
router.GET("/user/:user/", userIndexHandler(repo))
router.POST("/user/:user/webmention/", userWebmentionHandler(repo))
router.GET("/user/:user/index.xml", userRSSHandler(repo))
router.GET("/user/:user/posts/:post/", postHandler(repo))
router.GET("/user/:user/posts/:post/media/*filepath", postMediaHandler(repo))
@ -25,6 +26,7 @@ func SingleUserRouter(repo *owl.Repository) http.Handler {
router := httprouter.New()
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
router.GET("/", userIndexHandler(repo))
router.POST("/webmention/", userWebmentionHandler(repo))
router.GET("/index.xml", userRSSHandler(repo))
router.GET("/posts/:post/", postHandler(repo))
router.GET("/posts/:post/media/*filepath", postMediaHandler(repo))

View File

@ -0,0 +1,161 @@
package main_test
import (
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl-web"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
)
func setupWebmentionTest(repo owl.Repository, user owl.User, target string, source string) (*httptest.ResponseRecorder, error) {
data := url.Values{}
data.Set("target", target)
data.Set("source", source)
// Create Request and Response
req, err := http.NewRequest("POST", user.UrlPath()+"webmention/", strings.NewReader(data.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
if err != nil {
return nil, err
}
rr := httptest.NewRecorder()
router := main.Router(&repo)
router.ServeHTTP(rr, req)
return rr, nil
}
func assertStatus(t *testing.T, rr *httptest.ResponseRecorder, expStatus int) {
if status := rr.Code; status != expStatus {
t.Errorf("handler returned wrong status code: got %v want %v",
status, expStatus)
return
}
}
func TestWebmentionHandleAccepts(t *testing.T) {
repo := getTestRepo()
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
target := post.FullUrl()
source := "https://example.com"
rr, err := setupWebmentionTest(repo, user, target, source)
if err != nil {
t.Fatal(err)
}
assertStatus(t, rr, http.StatusAccepted)
}
func TestWebmentionWrittenToPost(t *testing.T) {
repo := getTestRepo()
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
target := post.FullUrl()
source := "https://example.com"
rr, err := setupWebmentionTest(repo, user, target, source)
if err != nil {
t.Fatal(err)
}
// Check the status code is what we expect.
assertStatus(t, rr, http.StatusAccepted)
if len(post.Webmentions()) != 1 {
t.Errorf("no webmention written to post")
}
}
//
// https://www.w3.org/TR/webmention/#h-request-verification
//
// The receiver MUST check that source and target are valid URLs [URL]
// and are of schemes that are supported by the receiver.
// (Most commonly this means checking that the source and target schemes are http or https).
func TestWebmentionSourceValidation(t *testing.T) {
repo := getTestRepo()
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
target := post.FullUrl()
source := "ftp://example.com"
rr, err := setupWebmentionTest(repo, user, target, source)
if err != nil {
t.Fatal(err)
}
assertStatus(t, rr, http.StatusBadRequest)
}
func TestWebmentionTargetValidation(t *testing.T) {
repo := getTestRepo()
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
target := "ftp://example.com"
source := post.FullUrl()
rr, err := setupWebmentionTest(repo, user, target, source)
if err != nil {
t.Fatal(err)
}
assertStatus(t, rr, http.StatusBadRequest)
}
// The receiver MUST reject the request if the source URL is the same as the target URL.
func TestWebmentionSameTargetAndSource(t *testing.T) {
repo := getTestRepo()
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
target := post.FullUrl()
source := post.FullUrl()
rr, err := setupWebmentionTest(repo, user, target, source)
if err != nil {
t.Fatal(err)
}
assertStatus(t, rr, http.StatusBadRequest)
}
// The receiver SHOULD check that target is a valid resource for which it can accept Webmentions.
// 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()
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
target := post.FullUrl()
target = target[:len(target)-1] + "invalid"
source := post.FullUrl()
rr, err := setupWebmentionTest(repo, user, target, source)
if err != nil {
t.Fatal(err)
}
assertStatus(t, rr, http.StatusBadRequest)
}

View File

@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="webmention" href="{{ .User.WebmentionUrl }}">
<style>
header {
background-color: {{.HeaderColor}};

View File

@ -13,4 +13,24 @@
<div class="e-content">
{{.Content}}
</div>
<hr>
{{if .Post.ApprovedWebmentions}}
<h3>
Webmentions
</h3>
<ul>
{{range .Post.ApprovedWebmentions}}
<li>
<a href="{{.Source}}">
{{if .Title}}
{{.Title}}
{{else}}
{{.Source}}
{{end}}
</a>
</li>
{{end}}
</ul>
{{end}}
</div>

1
go.mod
View File

@ -6,5 +6,6 @@ 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
)

2
go.sum
View File

@ -4,6 +4,8 @@ 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/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=

View File

@ -6,6 +6,18 @@ import (
"time"
)
type MockMicroformatParser struct{}
func (*MockMicroformatParser) ParseHEntry(data []byte) (owl.ParsedHEntry, error) {
return owl.ParsedHEntry{Title: "Mock Title"}, nil
}
type MockHttpRetriever struct{}
func (*MockHttpRetriever) Get(url string) ([]byte, error) {
return []byte(""), nil
}
func randomName() string {
rand.Seed(time.Now().UnixNano())
var letters = []rune("abcdefghijklmnopqrstuvwxyz")

120
post.go
View File

@ -2,8 +2,13 @@ package owl
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"path"
"sort"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
@ -39,6 +44,10 @@ func (post Post) MediaDir() string {
return path.Join(post.Dir(), "media")
}
func (post Post) WebmentionDir() string {
return path.Join(post.Dir(), "webmention")
}
func (post Post) UrlPath() string {
return post.user.UrlPath() + "posts/" + post.id + "/"
}
@ -139,3 +148,114 @@ func (post *Post) LoadMeta() error {
post.meta = meta
return nil
}
func (post *Post) WebmentionFile(source string) string {
hash := sha256.Sum256([]byte(source))
hashStr := base64.URLEncoding.EncodeToString(hash[:])
return path.Join(post.WebmentionDir(), hashStr+".yml")
}
func (post *Post) PersistWebmention(webmention Webmention) error {
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
// write to file
fileName := post.WebmentionFile(webmention.Source)
data, err := yaml.Marshal(webmention)
if err != nil {
return err
}
return os.WriteFile(fileName, data, 0644)
}
func (post *Post) Webmention(source string) (Webmention, error) {
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
// Check if file exists
fileName := post.WebmentionFile(source)
if !fileExists(fileName) {
// return error if file doesn't exist
return Webmention{}, fmt.Errorf("Webmention file not found: %s", source)
}
data, err := os.ReadFile(fileName)
if err != nil {
return Webmention{}, err
}
mention := Webmention{}
err = yaml.Unmarshal(data, &mention)
if err != nil {
return Webmention{}, err
}
return mention, nil
}
func (post *Post) AddWebmention(source string) error {
// Check if file already exists
_, err := post.Webmention(source)
if err != nil {
webmention := Webmention{
Source: source,
}
defer post.EnrichWebmention(source)
return post.PersistWebmention(webmention)
}
return nil
}
func (post *Post) EnrichWebmention(source string) error {
html, err := post.user.repo.Retriever.Get(source)
if err == nil {
webmention, err := post.Webmention(source)
if err != nil {
return err
}
entry, err := post.user.repo.Parser.ParseHEntry(html)
if err == nil {
webmention.Title = entry.Title
return post.PersistWebmention(webmention)
}
}
return err
}
func (post *Post) Webmentions() []Webmention {
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
files := listDir(post.WebmentionDir())
webmentions := []Webmention{}
for _, file := range files {
data, err := os.ReadFile(path.Join(post.WebmentionDir(), file))
if err != nil {
continue
}
mention := Webmention{}
err = yaml.Unmarshal(data, &mention)
if err != nil {
continue
}
webmentions = append(webmentions, mention)
}
return webmentions
}
func (post *Post) ApprovedWebmentions() []Webmention {
webmentions := post.Webmentions()
approved := []Webmention{}
for _, webmention := range webmentions {
if webmention.ApprovalStatus == "approved" {
approved = append(approved, webmention)
}
}
// sort by retrieved date
sort.Slice(approved, func(i, j int) bool {
return approved[i].RetrievedAt.After(approved[j].RetrievedAt)
})
return approved
}

View File

@ -1,10 +1,12 @@
package owl_test
import (
"h4kor/owl-blogs"
"os"
"path"
"strings"
"testing"
"time"
)
func TestCanGetPostTitle(t *testing.T) {
@ -161,5 +163,143 @@ func TestLoadMeta(t *testing.T) {
if post.Meta().Draft != true {
t.Errorf("Expected title: %v, got %v", true, post.Meta().Draft)
}
}
///
/// Webmention
///
func TestPersistWebmention(t *testing.T) {
repo := getTestRepo()
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
webmention := owl.Webmention{
Source: "http://example.com/source",
}
err := post.PersistWebmention(webmention)
if err != nil {
t.Errorf("Got error: %v", err)
}
mentions := post.Webmentions()
if len(mentions) != 1 {
t.Errorf("Expected 1 webmention, got %d", len(mentions))
}
if mentions[0].Source != webmention.Source {
t.Errorf("Expected source: %s, got %s", webmention.Source, mentions[0].Source)
}
}
func TestAddWebmentionCreatesFile(t *testing.T) {
repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{}
repo.Parser = &MockMicroformatParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
err := post.AddWebmention("https://example.com")
if err != nil {
t.Errorf("Got Error: %v", err)
}
mentions := post.Webmentions()
if len(mentions) != 1 {
t.Errorf("Expected 1 webmention, got %d", len(mentions))
}
}
func TestAddWebmentionNotOverwritingFile(t *testing.T) {
repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{}
repo.Parser = &MockMicroformatParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
post.AddWebmention("https://example.com")
dir, _ := os.Open(post.WebmentionDir())
defer dir.Close()
files, _ := dir.Readdirnames(-1)
if len(files) != 1 {
t.Error("No file created for webmention")
}
content := "url: https://example.com\n"
content += "verified: true"
os.WriteFile(path.Join(post.WebmentionDir(), files[0]), []byte(content), 0644)
post.AddWebmention("https://example.com")
fileContent, _ := os.ReadFile(path.Join(post.WebmentionDir(), files[0]))
if string(fileContent) != content {
t.Error("File content was modified.")
t.Errorf("Got: %v", fileContent)
t.Errorf("Expected: %v", content)
}
}
func TestAddWebmentionAddsParsedTitle(t *testing.T) {
repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{}
repo.Parser = &MockMicroformatParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
post.AddWebmention("https://example.com")
dir, _ := os.Open(post.WebmentionDir())
defer dir.Close()
files, _ := dir.Readdirnames(-1)
if len(files) != 1 {
t.Error("No file created for webmention")
}
fileContent, _ := os.ReadFile(path.Join(post.WebmentionDir(), files[0]))
if !strings.Contains(string(fileContent), "Mock Title") {
t.Error("File not containing the title.")
t.Errorf("Got: %v", string(fileContent))
}
}
func TestApprovedWebmentions(t *testing.T) {
repo := getTestRepo()
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
webmention := owl.Webmention{
Source: "http://example.com/source",
ApprovalStatus: "approved",
RetrievedAt: time.Now(),
}
post.PersistWebmention(webmention)
webmention = owl.Webmention{
Source: "http://example.com/source2",
ApprovalStatus: "",
RetrievedAt: time.Now().Add(time.Hour * -1),
}
post.PersistWebmention(webmention)
webmention = owl.Webmention{
Source: "http://example.com/source3",
ApprovalStatus: "approved",
RetrievedAt: time.Now().Add(time.Hour * -2),
}
post.PersistWebmention(webmention)
webmention = owl.Webmention{
Source: "http://example.com/source4",
ApprovalStatus: "rejected",
RetrievedAt: time.Now().Add(time.Hour * -3),
}
post.PersistWebmention(webmention)
webmentions := post.ApprovedWebmentions()
if len(webmentions) != 2 {
t.Errorf("Expected 2 webmentions, got %d", len(webmentions))
}
if webmentions[0].Source != "http://example.com/source" {
t.Errorf("Expected source: %s, got %s", "http://example.com/source", webmentions[0].Source)
}
if webmentions[1].Source != "http://example.com/source3" {
t.Errorf("Expected source: %s, got %s", "http://example.com/source3", webmentions[1].Source)
}
}

View File

@ -1,2 +1,2 @@
docker build . -t git.libove.org/h4kor/owl-blogs
docker push git.libove.org/h4kor/owl-blogs
docker build . -t git.libove.org/h4kor/owl-blogs:$1
docker push git.libove.org/h4kor/owl-blogs:$1

View File

@ -6,6 +6,7 @@ import (
"path"
"strings"
"testing"
"time"
)
func TestCanRenderPost(t *testing.T) {
@ -143,3 +144,58 @@ func TestRendersHeaderTitle(t *testing.T) {
t.Error("Header color not rendered. Got: " + result)
}
}
func TestRenderPostIncludesRelToWebMention(t *testing.T) {
user := getTestUser()
post, _ := user.CreateNewPost("testpost")
result, _ := owl.RenderPost(&post)
if !strings.Contains(result, "rel=\"webmention\"") {
t.Error("webmention rel not rendered. Got: " + result)
}
if !strings.Contains(result, "href=\""+user.WebmentionUrl()+"\"") {
t.Error("webmention href not rendered. Got: " + result)
}
}
func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) {
user := getTestUser()
post, _ := user.CreateNewPost("testpost")
webmention := owl.Webmention{
Source: "http://example.com/source3",
Title: "Test Title",
ApprovalStatus: "approved",
RetrievedAt: time.Now().Add(time.Hour * -2),
}
post.PersistWebmention(webmention)
webmention = owl.Webmention{
Source: "http://example.com/source4",
ApprovalStatus: "rejected",
RetrievedAt: time.Now().Add(time.Hour * -3),
}
post.PersistWebmention(webmention)
result, _ := owl.RenderPost(&post)
if !strings.Contains(result, "http://example.com/source3") {
t.Error("webmention not rendered. Got: " + result)
}
if !strings.Contains(result, "Test Title") {
t.Error("webmention title not rendered. Got: " + result)
}
if strings.Contains(result, "http://example.com/source4") {
t.Error("unapproved webmention rendered. Got: " + result)
}
}
func TestRenderPostNotMentioningWebmentionsIfNoAvail(t *testing.T) {
user := getTestUser()
post, _ := user.CreateNewPost("testpost")
result, _ := owl.RenderPost(&post)
if strings.Contains(result, "Webmention") {
t.Error("Webmention mentioned. Got: " + result)
}
}

View File

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

View File

@ -27,7 +27,7 @@ func TestCannotCreateExistingRepository(t *testing.T) {
func TestCanCreateANewUser(t *testing.T) {
// Create a new user
repo, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
user1, _ := repo.CreateUser(randomUserName())
user2, _ := repo.CreateUser(randomUserName())
// Create a new post
@ -121,7 +121,7 @@ func TestCannotOpenNonExisitingRepo(t *testing.T) {
func TestGetUser(t *testing.T) {
// Create a new user
repo, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
_, 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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
// 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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
// Get the user
template, err := repo.Template()
if err != nil {
@ -239,7 +239,7 @@ func TestSingleUserRepoUserUrlPathIsSimple(t *testing.T) {
}
func TestCanGetMapWithAllPostAliases(t *testing.T) {
repo, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
user, _ := repo.CreateUser(randomUserName())
post, _ := user.CreateNewPost("test-1")
@ -276,7 +276,7 @@ func TestCanGetMapWithAllPostAliases(t *testing.T) {
}
func TestAliasesHaveCorrectPost(t *testing.T) {
repo, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
user, _ := repo.CreateUser(randomUserName())
post1, _ := user.CreateNewPost("test-1")
post2, _ := user.CreateNewPost("test-2")

View File

@ -36,6 +36,11 @@ func (user User) FullUrl() string {
return url
}
func (user User) WebmentionUrl() string {
url, _ := url.JoinPath(user.FullUrl(), "webmention/")
return url
}
func (user User) PostDir() string {
return path.Join(user.Dir(), "public")
}
@ -150,6 +155,7 @@ func (user User) CreateNewPost(title string) (Post, error) {
os.WriteFile(post.ContentFile(), []byte(initial_content), 0644)
// create media dir
os.Mkdir(post.MediaDir(), 0755)
os.Mkdir(post.WebmentionDir(), 0755)
return post, nil
}

View File

@ -11,7 +11,7 @@ import (
func TestCreateNewPostCreatesEntryInPublic(t *testing.T) {
// Create a new user
repo, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
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, _ := owl.CreateRepository(testRepoName())
repo := getTestRepo()
user, _ := repo.CreateUser(randomUserName())
// Create a new post
post, _ := user.CreateNewPost("testpost")

97
webmention.go Normal file
View File

@ -0,0 +1,97 @@
package owl
import (
"bytes"
"errors"
"net/http"
"strings"
"time"
"golang.org/x/net/html"
)
type Webmention struct {
Source string `yaml:"source"`
Title string `yaml:"title"`
ApprovalStatus string `yaml:"approval_status"`
RetrievedAt time.Time `yaml:"retrieved_at"`
}
type HttpRetriever interface {
Get(url string) ([]byte, error)
}
type MicroformatParser interface {
ParseHEntry(data []byte) (ParsedHEntry, error)
}
type OwlHttpRetriever struct{}
type OwlMicroformatParser struct{}
type ParsedHEntry struct {
Title string
}
func (OwlHttpRetriever) Get(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return []byte{}, err
}
var data []byte
_, err = resp.Body.Read(data)
// TODO: encoding
return data, err
}
func collectText(n *html.Node, buf *bytes.Buffer) {
if n.Type == html.TextNode {
buf.WriteString(n.Data)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
collectText(c, buf)
}
}
func (OwlMicroformatParser) ParseHEntry(data []byte) (ParsedHEntry, error) {
doc, err := html.Parse(strings.NewReader(string(data)))
if err != nil {
return ParsedHEntry{}, err
}
var interpretHFeed func(*html.Node, *ParsedHEntry, bool) (ParsedHEntry, error)
interpretHFeed = func(n *html.Node, curr *ParsedHEntry, parent bool) (ParsedHEntry, error) {
attrs := n.Attr
for _, attr := range attrs {
if attr.Key == "class" && strings.Contains(attr.Val, "p-name") {
buf := &bytes.Buffer{}
collectText(n, buf)
curr.Title = buf.String()
return *curr, nil
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
interpretHFeed(c, curr, false)
}
return *curr, nil
}
var findHFeed func(*html.Node) (ParsedHEntry, error)
findHFeed = func(n *html.Node) (ParsedHEntry, error) {
attrs := n.Attr
for _, attr := range attrs {
if attr.Key == "class" && strings.Contains(attr.Val, "h-entry") {
return interpretHFeed(n, &ParsedHEntry{}, true)
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
entry, err := findHFeed(c)
if err == nil {
return entry, nil
}
}
return ParsedHEntry{}, errors.New("no h-entry found")
}
return findHFeed(doc)
}

36
webmention_test.go Normal file
View File

@ -0,0 +1,36 @@
package owl_test
import (
"h4kor/owl-blogs"
"testing"
)
//
// https://www.w3.org/TR/webmention/#h-webmention-verification
//
func TestParseValidHEntry(t *testing.T) {
html := []byte("<div class=\"h-entry\"><div class=\"p-name\">Foo</div></div>")
parser := &owl.OwlMicroformatParser{}
entry, err := parser.ParseHEntry(html)
if err != nil {
t.Errorf("Unable to parse feed: %v", err)
}
if entry.Title != "Foo" {
t.Errorf("Wrong Title. Expected %v, got %v", "Foo", entry.Title)
}
}
func TestParseValidHEntryWithoutTitle(t *testing.T) {
html := []byte("<div class=\"h-entry\"></div><div class=\"p-name\">Foo</div>")
parser := &owl.OwlMicroformatParser{}
entry, err := parser.ParseHEntry(html)
if err != nil {
t.Errorf("Unable to parse feed: %v", err)
}
if entry.Title != "" {
t.Errorf("Wrong Title. Expected %v, got %v", "Foo", entry.Title)
}
}