Sending Webmentions #10
12
Dockerfile
12
Dockerfile
|
@ -1,3 +1,6 @@
|
||||||
|
##
|
||||||
|
## Build Container
|
||||||
|
##
|
||||||
FROM golang:1.19-alpine as build
|
FROM golang:1.19-alpine as build
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,9 +15,12 @@ RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN go build -o ./out/owl-web ./cmd/owl-web
|
RUN go build -o ./out/owl ./cmd/owl
|
||||||
RUN go build -o ./out/owl-cli ./cmd/owl-cli
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Run Container
|
||||||
|
##
|
||||||
FROM alpine:3.9
|
FROM alpine:3.9
|
||||||
RUN apk add ca-certificates
|
RUN apk add ca-certificates
|
||||||
|
|
||||||
|
@ -24,4 +30,4 @@ COPY --from=build /tmp/owl/out/ /bin/
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Run the binary program produced by `go install`
|
# Run the binary program produced by `go install`
|
||||||
CMD ["/bin/owl-web"]
|
ENTRYPOINT ["/bin/owl"]
|
14
README.md
14
README.md
|
@ -23,6 +23,9 @@ Each directory in the `/users/` directory of a repository is considered a user.
|
||||||
-- This will be rendered as the blog post.
|
-- This will be rendered as the blog post.
|
||||||
-- Must be present for the blog post to be valid.
|
-- Must be present for the blog post to be valid.
|
||||||
-- All other folders will be ignored
|
-- All other folders will be ignored
|
||||||
|
\- status.yml
|
||||||
|
-- Used to track various process status related to the post,
|
||||||
|
-- such as if a webmention was sent.
|
||||||
\- media/
|
\- media/
|
||||||
-- Contains all media files used in the blog post.
|
-- Contains all media files used in the blog post.
|
||||||
-- All files in this folder will be publicly available
|
-- All files in this folder will be publicly available
|
||||||
|
@ -59,3 +62,14 @@ aliases:
|
||||||
Actual post
|
Actual post
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### status.yml
|
||||||
|
|
||||||
|
```
|
||||||
|
webmentions:
|
||||||
|
- target: https://example.com/post
|
||||||
|
supported: true
|
||||||
|
scanned_at: 2021-08-13T17:07:00Z
|
||||||
|
last_sent_at: 2021-08-13T17:07:00Z
|
||||||
|
```
|
|
@ -1,71 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"h4kor/owl-blogs"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
println("owl blogs")
|
|
||||||
println("Commands")
|
|
||||||
println("init <repo> - Creates a new repository")
|
|
||||||
println("<repo> new-user <name> - Creates a new user")
|
|
||||||
println("<repo> new-post <user> <title> - 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
|
||||||
|
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.")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&user, "user", "", "Username. Required for some commands.")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
Execute()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
web "h4kor/owl-blogs/cmd/owl/web"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var port int
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(webCmd)
|
||||||
|
|
||||||
|
webCmd.PersistentFlags().IntVar(&port, "port", 8080, "Port to use")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
package main_test
|
package web_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
main "h4kor/owl-blogs/cmd/owl-web"
|
"h4kor/owl-blogs"
|
||||||
|
main "h4kor/owl-blogs/cmd/owl/web"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
@ -9,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRedirectOnAliases(t *testing.T) {
|
func TestRedirectOnAliases(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ func TestRedirectOnAliases(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoRedirectOnNonExistingAliases(t *testing.T) {
|
func TestNoRedirectOnNonExistingAliases(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -78,7 +79,7 @@ func TestNoRedirectOnNonExistingAliases(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoRedirectIfValidPostUrl(t *testing.T) {
|
func TestNoRedirectIfValidPostUrl(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
post2, _ := user.CreateNewPost("post-2")
|
post2, _ := user.CreateNewPost("post-2")
|
||||||
|
@ -109,7 +110,7 @@ func TestNoRedirectIfValidPostUrl(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRedirectIfInvalidPostUrl(t *testing.T) {
|
func TestRedirectIfInvalidPostUrl(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -139,7 +140,7 @@ func TestRedirectIfInvalidPostUrl(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRedirectIfInvalidUserUrl(t *testing.T) {
|
func TestRedirectIfInvalidUserUrl(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -169,7 +170,7 @@ func TestRedirectIfInvalidUserUrl(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRedirectIfInvalidMediaUrl(t *testing.T) {
|
func TestRedirectIfInvalidMediaUrl(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -199,9 +200,8 @@ func TestRedirectIfInvalidMediaUrl(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeepAliasInSingleUserMode(t *testing.T) {
|
func TestDeepAliasInSingleUserMode(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{SingleUser: "test-1"})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
repo.SetSingleUser(user)
|
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
content := "---\n"
|
content := "---\n"
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"h4kor/owl-blogs"
|
"h4kor/owl-blogs"
|
||||||
|
@ -11,8 +11,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getUserFromRepo(repo *owl.Repository, ps httprouter.Params) (owl.User, error) {
|
func getUserFromRepo(repo *owl.Repository, ps httprouter.Params) (owl.User, error) {
|
||||||
if repo.SingleUserName() != "" {
|
if config, _ := repo.Config(); config.SingleUser != "" {
|
||||||
return repo.GetUser(repo.SingleUserName())
|
return repo.GetUser(config.SingleUser)
|
||||||
}
|
}
|
||||||
userName := ps.ByName("user")
|
userName := ps.ByName("user")
|
||||||
user, err := repo.GetUser(userName)
|
user, err := repo.GetUser(userName)
|
|
@ -1,8 +1,8 @@
|
||||||
package main_test
|
package web_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"h4kor/owl-blogs"
|
"h4kor/owl-blogs"
|
||||||
main "h4kor/owl-blogs/cmd/owl-web"
|
main "h4kor/owl-blogs/cmd/owl/web"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -27,13 +27,13 @@ func testRepoName() string {
|
||||||
return "/tmp/" + randomName()
|
return "/tmp/" + randomName()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTestRepo() owl.Repository {
|
func getTestRepo(config owl.RepoConfig) owl.Repository {
|
||||||
repo, _ := owl.CreateRepository(testRepoName())
|
repo, _ := owl.CreateRepository(testRepoName(), config)
|
||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiUserRepoIndexHandler(t *testing.T) {
|
func TestMultiUserRepoIndexHandler(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
repo.CreateUser("user_1")
|
repo.CreateUser("user_1")
|
||||||
repo.CreateUser("user_2")
|
repo.CreateUser("user_2")
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ func TestMultiUserRepoIndexHandler(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiUserUserIndexHandler(t *testing.T) {
|
func TestMultiUserUserIndexHandler(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
user.CreateNewPost("post-1")
|
user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ func TestMultiUserUserIndexHandler(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiUserPostHandler(t *testing.T) {
|
func TestMultiUserPostHandler(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ func TestMultiUserPostHandler(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiUserPostMediaHandler(t *testing.T) {
|
func TestMultiUserPostMediaHandler(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package main_test
|
package web_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
main "h4kor/owl-blogs/cmd/owl-web"
|
"h4kor/owl-blogs"
|
||||||
|
main "h4kor/owl-blogs/cmd/owl/web"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
@ -9,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPostHandlerReturns404OnDrafts(t *testing.T) {
|
func TestPostHandlerReturns404OnDrafts(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package main_test
|
package web_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
main "h4kor/owl-blogs/cmd/owl-web"
|
"h4kor/owl-blogs"
|
||||||
|
main "h4kor/owl-blogs/cmd/owl/web"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -9,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMultiUserUserRssIndexHandler(t *testing.T) {
|
func TestMultiUserUserRssIndexHandler(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
user.CreateNewPost("post-1")
|
user.CreateNewPost("post-1")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"h4kor/owl-blogs"
|
"h4kor/owl-blogs"
|
||||||
|
@ -34,63 +34,10 @@ func SingleUserRouter(repo *owl.Repository) http.Handler {
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func StartServer(repoPath string, port int) {
|
||||||
println("owl web server")
|
|
||||||
println("Parameters")
|
|
||||||
println("-repo <repo> - Specify the repository to use. Defaults to '.'")
|
|
||||||
println("-port <port> - Specify the port to use, Default is '8080'")
|
|
||||||
println("-user <name> - 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
|
|
||||||
}
|
|
||||||
|
|
||||||
var repo owl.Repository
|
var repo owl.Repository
|
||||||
var err error
|
var err error
|
||||||
if singleUserName != "" {
|
repo, err = owl.OpenRepository(repoPath)
|
||||||
println("Single user mode")
|
|
||||||
println("Repository:", repoName)
|
|
||||||
println("User:", singleUserName)
|
|
||||||
repo, err = owl.OpenSingleUserRepo(repoName, singleUserName)
|
|
||||||
} else {
|
|
||||||
println("Multi user mode")
|
|
||||||
println("Repository:", repoName)
|
|
||||||
repo, err = owl.OpenRepository(repoName)
|
|
||||||
}
|
|
||||||
repo.SetAllowRawHtml(allowRawHTML)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println("Error opening repository: ", err.Error())
|
println("Error opening repository: ", err.Error())
|
||||||
|
@ -98,13 +45,12 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var router http.Handler
|
var router http.Handler
|
||||||
if singleUserName == "" {
|
if config, _ := repo.Config(); config.SingleUser != "" {
|
||||||
println("Multi user mode Router used")
|
|
||||||
router = Router(&repo)
|
|
||||||
} else {
|
|
||||||
println("Single user mode Router used")
|
|
||||||
router = SingleUserRouter(&repo)
|
router = SingleUserRouter(&repo)
|
||||||
|
} else {
|
||||||
|
router = Router(&repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
println("Listening on port", port)
|
println("Listening on port", port)
|
||||||
http.ListenAndServe(":"+strconv.Itoa(port), router)
|
http.ListenAndServe(":"+strconv.Itoa(port), router)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package main_test
|
package web_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
owl "h4kor/owl-blogs"
|
owl "h4kor/owl-blogs"
|
||||||
main "h4kor/owl-blogs/cmd/owl-web"
|
main "h4kor/owl-blogs/cmd/owl/web"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
|
@ -12,9 +12,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getSingleUserTestRepo() (owl.Repository, owl.User) {
|
func getSingleUserTestRepo() (owl.Repository, owl.User) {
|
||||||
repo, _ := owl.CreateRepository(testRepoName())
|
repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{SingleUser: "test-1"})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
repo.SetSingleUser(user)
|
|
||||||
return repo, user
|
return repo, user
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package main_test
|
package web_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"h4kor/owl-blogs"
|
"h4kor/owl-blogs"
|
||||||
main "h4kor/owl-blogs/cmd/owl-web"
|
main "h4kor/owl-blogs/cmd/owl/web"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -42,7 +42,7 @@ func assertStatus(t *testing.T, rr *httptest.ResponseRecorder, expStatus int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebmentionHandleAccepts(t *testing.T) {
|
func TestWebmentionHandleAccepts(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ func TestWebmentionHandleAccepts(t *testing.T) {
|
||||||
|
|
||||||
func TestWebmentionWrittenToPost(t *testing.T) {
|
func TestWebmentionWrittenToPost(t *testing.T) {
|
||||||
|
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-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).
|
// (Most commonly this means checking that the source and target schemes are http or https).
|
||||||
func TestWebmentionSourceValidation(t *testing.T) {
|
func TestWebmentionSourceValidation(t *testing.T) {
|
||||||
|
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ func TestWebmentionSourceValidation(t *testing.T) {
|
||||||
|
|
||||||
func TestWebmentionTargetValidation(t *testing.T) {
|
func TestWebmentionTargetValidation(t *testing.T) {
|
||||||
|
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ func TestWebmentionTargetValidation(t *testing.T) {
|
||||||
|
|
||||||
func TestWebmentionSameTargetAndSource(t *testing.T) {
|
func TestWebmentionSameTargetAndSource(t *testing.T) {
|
||||||
|
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-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.
|
// 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.
|
// What a "valid resource" means is up to the receiver.
|
||||||
func TestValidationOfTarget(t *testing.T) {
|
func TestValidationOfTarget(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("test-1")
|
user, _ := repo.CreateUser("test-1")
|
||||||
post, _ := user.CreateNewPost("post-1")
|
post, _ := user.CreateNewPost("post-1")
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
|
@ -5,14 +5,20 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{ .Title }}</title>
|
<title>{{ .Title }} - {{ .UserConfig.Title }}</title>
|
||||||
<link rel="stylesheet" href="/static/pico.min.css">
|
<link rel="stylesheet" href="/static/pico.min.css">
|
||||||
<link rel="webmention" href="{{ .User.WebmentionUrl }}">
|
<link rel="webmention" href="{{ .User.WebmentionUrl }}">
|
||||||
<style>
|
<style>
|
||||||
header {
|
header {
|
||||||
background-color: {{.HeaderColor}};
|
background-color: {{.UserConfig.HeaderColor}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
border-top: dashed 2px;
|
||||||
|
border-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
hgroup h2 a { color: inherit; }
|
hgroup h2 a { color: inherit; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
@ -23,8 +29,8 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<hgroup>
|
<hgroup>
|
||||||
<h2><a href="{{ .User.UrlPath }}">{{ .UserTitle }}</a></h2>
|
<h2><a href="{{ .User.UrlPath }}">{{ .UserConfig.Title }}</a></h2>
|
||||||
<h3>{{ .UserSubtitle }}</h3>
|
<h3>{{ .UserConfig.SubTitle }}</h3>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -35,6 +41,13 @@
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
</main>
|
</main>
|
||||||
<footer class="container">
|
<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>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
domain: "http://localhost:8080"
|
|
|
@ -6,6 +6,7 @@
|
||||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||||
{{.Post.Meta.Date}}
|
{{.Post.Meta.Date}}
|
||||||
</time>
|
</time>
|
||||||
|
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||||
</small>
|
</small>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
15
go.mod
15
go.mod
|
@ -3,9 +3,14 @@ module h4kor/owl-blogs
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/julienschmidt/httprouter v1.3.0 // indirect
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
github.com/yuin/goldmark v1.4.13 // indirect
|
github.com/spf13/cobra v1.5.0
|
||||||
github.com/yuin/goldmark-meta v1.1.0 // indirect
|
github.com/yuin/goldmark v1.4.13
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
|
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
|
||||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
)
|
)
|
||||||
|
|
16
go.sum
16
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 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
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 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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 h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
|
||||||
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
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/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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
|
40
owl_test.go
40
owl_test.go
|
@ -2,20 +2,44 @@ package owl_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"h4kor/owl-blogs"
|
"h4kor/owl-blogs"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockMicroformatParser struct{}
|
type MockHtmlParser struct{}
|
||||||
|
|
||||||
func (*MockMicroformatParser) ParseHEntry(data []byte) (owl.ParsedHEntry, error) {
|
func (*MockHtmlParser) ParseHEntry(resp *http.Response) (owl.ParsedHEntry, error) {
|
||||||
return owl.ParsedHEntry{Title: "Mock Title"}, nil
|
return owl.ParsedHEntry{Title: "Mock Title"}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
func (*MockHtmlParser) ParseLinks(resp *http.Response) ([]string, error) {
|
||||||
|
return []string{"http://example.com"}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
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) {
|
func (*MockHttpClient) Get(url string) (resp *http.Response, err error) {
|
||||||
return []byte(""), nil
|
return &http.Response{}, nil
|
||||||
|
}
|
||||||
|
func (*MockHttpClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
|
||||||
|
|
||||||
|
return &http.Response{}, nil
|
||||||
|
}
|
||||||
|
func (*MockHttpClient) PostForm(url string, data url.Values) (resp *http.Response, err error) {
|
||||||
|
|
||||||
|
return &http.Response{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomName() string {
|
func randomName() string {
|
||||||
|
@ -37,13 +61,13 @@ func randomUserName() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTestUser() owl.User {
|
func getTestUser() owl.User {
|
||||||
repo, _ := owl.CreateRepository(testRepoName())
|
repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTestRepo() owl.Repository {
|
func getTestRepo(config owl.RepoConfig) owl.Repository {
|
||||||
repo, _ := owl.CreateRepository(testRepoName())
|
repo, _ := owl.CreateRepository(testRepoName(), config)
|
||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
168
post.go
168
post.go
|
@ -6,9 +6,11 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
|
@ -32,6 +34,10 @@ type PostMeta struct {
|
||||||
Draft bool `yaml:"draft"`
|
Draft bool `yaml:"draft"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PostStatus struct {
|
||||||
|
Webmentions []WebmentionOut
|
||||||
|
}
|
||||||
|
|
||||||
func (post Post) Id() string {
|
func (post Post) Id() string {
|
||||||
return post.id
|
return post.id
|
||||||
}
|
}
|
||||||
|
@ -40,6 +46,10 @@ func (post Post) Dir() string {
|
||||||
return path.Join(post.user.Dir(), "public", post.id)
|
return path.Join(post.user.Dir(), "public", post.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (post Post) StatusFile() string {
|
||||||
|
return path.Join(post.Dir(), "status.yml")
|
||||||
|
}
|
||||||
|
|
||||||
func (post Post) MediaDir() string {
|
func (post Post) MediaDir() string {
|
||||||
return path.Join(post.Dir(), "media")
|
return path.Join(post.Dir(), "media")
|
||||||
}
|
}
|
||||||
|
@ -81,6 +91,42 @@ func (post Post) Content() []byte {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (post Post) Status() PostStatus {
|
||||||
|
// read status file
|
||||||
|
// return parsed webmentions
|
||||||
|
fileName := post.StatusFile()
|
||||||
|
if !fileExists(fileName) {
|
||||||
|
return PostStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return PostStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
status := PostStatus{}
|
||||||
|
err = yaml.Unmarshal(data, &status)
|
||||||
|
if err != nil {
|
||||||
|
return PostStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (post Post) PersistStatus(status PostStatus) error {
|
||||||
|
data, err := yaml.Marshal(status)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(post.StatusFile(), data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (post Post) RenderedContent() bytes.Buffer {
|
func (post Post) RenderedContent() bytes.Buffer {
|
||||||
data := post.Content()
|
data := post.Content()
|
||||||
|
|
||||||
|
@ -98,7 +144,7 @@ func (post Post) RenderedContent() bytes.Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
options := goldmark.WithRendererOptions()
|
options := goldmark.WithRendererOptions()
|
||||||
if post.user.repo.AllowRawHtml() {
|
if config, _ := post.user.repo.Config(); config.AllowRawHtml {
|
||||||
options = goldmark.WithRendererOptions(
|
options = goldmark.WithRendererOptions(
|
||||||
html.WithUnsafe(),
|
html.WithUnsafe(),
|
||||||
)
|
)
|
||||||
|
@ -156,7 +202,7 @@ func (post *Post) WebmentionFile(source string) string {
|
||||||
return path.Join(post.WebmentionDir(), hashStr+".yml")
|
return path.Join(post.WebmentionDir(), hashStr+".yml")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (post *Post) PersistWebmention(webmention Webmention) error {
|
func (post *Post) PersistWebmention(webmention WebmentionIn) error {
|
||||||
// ensure dir exists
|
// ensure dir exists
|
||||||
os.MkdirAll(post.WebmentionDir(), 0755)
|
os.MkdirAll(post.WebmentionDir(), 0755)
|
||||||
|
|
||||||
|
@ -169,7 +215,7 @@ func (post *Post) PersistWebmention(webmention Webmention) error {
|
||||||
return os.WriteFile(fileName, data, 0644)
|
return os.WriteFile(fileName, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (post *Post) Webmention(source string) (Webmention, error) {
|
func (post *Post) Webmention(source string) (WebmentionIn, error) {
|
||||||
// ensure dir exists
|
// ensure dir exists
|
||||||
os.MkdirAll(post.WebmentionDir(), 0755)
|
os.MkdirAll(post.WebmentionDir(), 0755)
|
||||||
|
|
||||||
|
@ -177,18 +223,18 @@ func (post *Post) Webmention(source string) (Webmention, error) {
|
||||||
fileName := post.WebmentionFile(source)
|
fileName := post.WebmentionFile(source)
|
||||||
if !fileExists(fileName) {
|
if !fileExists(fileName) {
|
||||||
// return error if file doesn't exist
|
// return error if file doesn't exist
|
||||||
return Webmention{}, fmt.Errorf("Webmention file not found: %s", source)
|
return WebmentionIn{}, fmt.Errorf("Webmention file not found: %s", source)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(fileName)
|
data, err := os.ReadFile(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Webmention{}, err
|
return WebmentionIn{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mention := Webmention{}
|
mention := WebmentionIn{}
|
||||||
err = yaml.Unmarshal(data, &mention)
|
err = yaml.Unmarshal(data, &mention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Webmention{}, err
|
return WebmentionIn{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return mention, nil
|
return mention, nil
|
||||||
|
@ -198,7 +244,7 @@ func (post *Post) AddWebmention(source string) error {
|
||||||
// Check if file already exists
|
// Check if file already exists
|
||||||
_, err := post.Webmention(source)
|
_, err := post.Webmention(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
webmention := Webmention{
|
webmention := WebmentionIn{
|
||||||
Source: source,
|
Source: source,
|
||||||
}
|
}
|
||||||
defer post.EnrichWebmention(source)
|
defer post.EnrichWebmention(source)
|
||||||
|
@ -207,14 +253,55 @@ func (post *Post) AddWebmention(source string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (post *Post) AddOutgoingWebmention(target string) error {
|
||||||
|
status := post.Status()
|
||||||
|
|
||||||
|
// Check if file already exists
|
||||||
|
_, err := post.Webmention(target)
|
||||||
|
if err != nil {
|
||||||
|
webmention := WebmentionOut{
|
||||||
|
Target: target,
|
||||||
|
}
|
||||||
|
// if target is not in status, add it
|
||||||
|
for _, t := range status.Webmentions {
|
||||||
|
if t.Target == webmention.Target {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
status.Webmentions = append(status.Webmentions, webmention)
|
||||||
|
}
|
||||||
|
|
||||||
|
return post.PersistStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (post *Post) UpdateOutgoingWebmention(webmention *WebmentionOut) error {
|
||||||
|
status := post.Status()
|
||||||
|
|
||||||
|
// if target is not in status, add it
|
||||||
|
replaced := false
|
||||||
|
for i, t := range status.Webmentions {
|
||||||
|
if t.Target == webmention.Target {
|
||||||
|
status.Webmentions[i] = *webmention
|
||||||
|
replaced = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !replaced {
|
||||||
|
status.Webmentions = append(status.Webmentions, *webmention)
|
||||||
|
}
|
||||||
|
|
||||||
|
return post.PersistStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
func (post *Post) EnrichWebmention(source string) error {
|
func (post *Post) EnrichWebmention(source string) error {
|
||||||
html, err := post.user.repo.Retriever.Get(source)
|
resp, err := post.user.repo.HttpClient.Get(source)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
webmention, err := post.Webmention(source)
|
webmention, err := post.Webmention(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
entry, err := post.user.repo.Parser.ParseHEntry(html)
|
entry, err := post.user.repo.Parser.ParseHEntry(resp)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
webmention.Title = entry.Title
|
webmention.Title = entry.Title
|
||||||
return post.PersistWebmention(webmention)
|
return post.PersistWebmention(webmention)
|
||||||
|
@ -223,17 +310,17 @@ func (post *Post) EnrichWebmention(source string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (post *Post) Webmentions() []Webmention {
|
func (post *Post) Webmentions() []WebmentionIn {
|
||||||
// ensure dir exists
|
// ensure dir exists
|
||||||
os.MkdirAll(post.WebmentionDir(), 0755)
|
os.MkdirAll(post.WebmentionDir(), 0755)
|
||||||
files := listDir(post.WebmentionDir())
|
files := listDir(post.WebmentionDir())
|
||||||
webmentions := []Webmention{}
|
webmentions := []WebmentionIn{}
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
data, err := os.ReadFile(path.Join(post.WebmentionDir(), file))
|
data, err := os.ReadFile(path.Join(post.WebmentionDir(), file))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mention := Webmention{}
|
mention := WebmentionIn{}
|
||||||
err = yaml.Unmarshal(data, &mention)
|
err = yaml.Unmarshal(data, &mention)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
|
@ -244,9 +331,9 @@ func (post *Post) Webmentions() []Webmention {
|
||||||
return webmentions
|
return webmentions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (post *Post) ApprovedWebmentions() []Webmention {
|
func (post *Post) ApprovedWebmentions() []WebmentionIn {
|
||||||
webmentions := post.Webmentions()
|
webmentions := post.Webmentions()
|
||||||
approved := []Webmention{}
|
approved := []WebmentionIn{}
|
||||||
for _, webmention := range webmentions {
|
for _, webmention := range webmentions {
|
||||||
if webmention.ApprovalStatus == "approved" {
|
if webmention.ApprovalStatus == "approved" {
|
||||||
approved = append(approved, webmention)
|
approved = append(approved, webmention)
|
||||||
|
@ -259,3 +346,54 @@ func (post *Post) ApprovedWebmentions() []Webmention {
|
||||||
})
|
})
|
||||||
return approved
|
return approved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (post *Post) OutgoingWebmentions() []WebmentionOut {
|
||||||
|
status := post.Status()
|
||||||
|
return status.Webmentions
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanForLinks scans the post content for links and adds them to the
|
||||||
|
// `status.yml` file for the post. The links are not scanned by this function.
|
||||||
|
func (post *Post) ScanForLinks() error {
|
||||||
|
// this could be done in markdown parsing, but I don't want to
|
||||||
|
// rely on goldmark for this (yet)
|
||||||
|
postHtml := post.RenderedContent()
|
||||||
|
links, _ := post.user.repo.Parser.ParseLinksFromString(string(postHtml.Bytes()))
|
||||||
|
for _, link := range links {
|
||||||
|
post.AddOutgoingWebmention(link)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (post *Post) SendWebmention(webmention WebmentionOut) error {
|
||||||
|
defer post.UpdateOutgoingWebmention(&webmention)
|
||||||
|
webmention.ScannedAt = time.Now()
|
||||||
|
|
||||||
|
resp, err := post.user.repo.HttpClient.Get(webmention.Target)
|
||||||
|
if err != nil {
|
||||||
|
webmention.Supported = false
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := post.user.repo.Parser.GetWebmentionEndpoint(resp)
|
||||||
|
if err != nil {
|
||||||
|
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.PostForm(endpoint, payload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update webmention status
|
||||||
|
webmention.LastSentAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
120
post_test.go
120
post_test.go
|
@ -89,7 +89,7 @@ func TestDraftInMetaData(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) {
|
func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
content := "---\n"
|
content := "---\n"
|
||||||
|
@ -107,8 +107,7 @@ func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRawHTMLIfAllowedByRepo(t *testing.T) {
|
func TestRawHTMLIfAllowedByRepo(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{AllowRawHtml: true})
|
||||||
repo.SetAllowRawHtml(true)
|
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
content := "---\n"
|
content := "---\n"
|
||||||
|
@ -126,8 +125,7 @@ func TestRawHTMLIfAllowedByRepo(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadMeta(t *testing.T) {
|
func TestLoadMeta(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{AllowRawHtml: true})
|
||||||
repo.SetAllowRawHtml(true)
|
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
@ -170,10 +168,10 @@ func TestLoadMeta(t *testing.T) {
|
||||||
///
|
///
|
||||||
|
|
||||||
func TestPersistWebmention(t *testing.T) {
|
func TestPersistWebmention(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
webmention := owl.Webmention{
|
webmention := owl.WebmentionIn{
|
||||||
Source: "http://example.com/source",
|
Source: "http://example.com/source",
|
||||||
}
|
}
|
||||||
err := post.PersistWebmention(webmention)
|
err := post.PersistWebmention(webmention)
|
||||||
|
@ -191,9 +189,9 @@ func TestPersistWebmention(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddWebmentionCreatesFile(t *testing.T) {
|
func TestAddWebmentionCreatesFile(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
repo.Retriever = &MockHttpRetriever{}
|
repo.HttpClient = &MockHttpClient{}
|
||||||
repo.Parser = &MockMicroformatParser{}
|
repo.Parser = &MockHtmlParser{}
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
@ -209,9 +207,9 @@ func TestAddWebmentionCreatesFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddWebmentionNotOverwritingFile(t *testing.T) {
|
func TestAddWebmentionNotOverwritingFile(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
repo.Retriever = &MockHttpRetriever{}
|
repo.HttpClient = &MockHttpClient{}
|
||||||
repo.Parser = &MockMicroformatParser{}
|
repo.Parser = &MockHtmlParser{}
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
@ -239,9 +237,9 @@ func TestAddWebmentionNotOverwritingFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddWebmentionAddsParsedTitle(t *testing.T) {
|
func TestAddWebmentionAddsParsedTitle(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
repo.Retriever = &MockHttpRetriever{}
|
repo.HttpClient = &MockHttpClient{}
|
||||||
repo.Parser = &MockMicroformatParser{}
|
repo.Parser = &MockHtmlParser{}
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
@ -262,28 +260,28 @@ func TestAddWebmentionAddsParsedTitle(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApprovedWebmentions(t *testing.T) {
|
func TestApprovedWebmentions(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser("testuser")
|
user, _ := repo.CreateUser("testuser")
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
webmention := owl.Webmention{
|
webmention := owl.WebmentionIn{
|
||||||
Source: "http://example.com/source",
|
Source: "http://example.com/source",
|
||||||
ApprovalStatus: "approved",
|
ApprovalStatus: "approved",
|
||||||
RetrievedAt: time.Now(),
|
RetrievedAt: time.Now(),
|
||||||
}
|
}
|
||||||
post.PersistWebmention(webmention)
|
post.PersistWebmention(webmention)
|
||||||
webmention = owl.Webmention{
|
webmention = owl.WebmentionIn{
|
||||||
Source: "http://example.com/source2",
|
Source: "http://example.com/source2",
|
||||||
ApprovalStatus: "",
|
ApprovalStatus: "",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -1),
|
RetrievedAt: time.Now().Add(time.Hour * -1),
|
||||||
}
|
}
|
||||||
post.PersistWebmention(webmention)
|
post.PersistWebmention(webmention)
|
||||||
webmention = owl.Webmention{
|
webmention = owl.WebmentionIn{
|
||||||
Source: "http://example.com/source3",
|
Source: "http://example.com/source3",
|
||||||
ApprovalStatus: "approved",
|
ApprovalStatus: "approved",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -2),
|
RetrievedAt: time.Now().Add(time.Hour * -2),
|
||||||
}
|
}
|
||||||
post.PersistWebmention(webmention)
|
post.PersistWebmention(webmention)
|
||||||
webmention = owl.Webmention{
|
webmention = owl.WebmentionIn{
|
||||||
Source: "http://example.com/source4",
|
Source: "http://example.com/source4",
|
||||||
ApprovalStatus: "rejected",
|
ApprovalStatus: "rejected",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -3),
|
RetrievedAt: time.Now().Add(time.Hour * -3),
|
||||||
|
@ -303,3 +301,83 @@ func TestApprovedWebmentions(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestScanningForLinks(t *testing.T) {
|
||||||
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
|
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(owl.RepoConfig{})
|
||||||
|
user, _ := repo.CreateUser("testuser")
|
||||||
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
||||||
|
content := "---\n"
|
||||||
|
content += "title: test\n"
|
||||||
|
content += "date: Wed, 17 Aug 2022 10:50:02 +0000\n"
|
||||||
|
content += "---\n"
|
||||||
|
content += "\n"
|
||||||
|
content += "[Hello](https://example.com/hello)\n"
|
||||||
|
content += "[Hello](https://example.com/hello)\n"
|
||||||
|
os.WriteFile(post.ContentFile(), []byte(content), 0644)
|
||||||
|
|
||||||
|
post.ScanForLinks()
|
||||||
|
post.ScanForLinks()
|
||||||
|
post.ScanForLinks()
|
||||||
|
webmentions := post.OutgoingWebmentions()
|
||||||
|
if len(webmentions) != 1 {
|
||||||
|
t.Errorf("Expected 1 webmention, got %d", len(webmentions))
|
||||||
|
}
|
||||||
|
if webmentions[0].Target != "https://example.com/hello" {
|
||||||
|
t.Errorf("Expected target: %s, got %s", "https://example.com/hello", webmentions[0].Target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCanSendWebmention(t *testing.T) {
|
||||||
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
|
repo.HttpClient = &MockHttpClient{}
|
||||||
|
repo.Parser = &MockHtmlParser{}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
13
renderer.go
13
renderer.go
|
@ -50,16 +50,14 @@ func renderIntoBaseTemplate(user User, data PageContent) (string, error) {
|
||||||
Title string
|
Title string
|
||||||
Content template.HTML
|
Content template.HTML
|
||||||
User User
|
User User
|
||||||
UserTitle string
|
UserConfig UserConfig
|
||||||
UserSubtitle string
|
UserSubtitle string
|
||||||
HeaderColor string
|
HeaderColor string
|
||||||
}{
|
}{
|
||||||
Title: data.Title,
|
Title: data.Title,
|
||||||
Content: data.Content,
|
Content: data.Content,
|
||||||
User: user,
|
User: user,
|
||||||
UserTitle: user_config.Title,
|
UserConfig: user_config,
|
||||||
UserSubtitle: user_config.SubTitle,
|
|
||||||
HeaderColor: user_config.HeaderColor,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var html bytes.Buffer
|
var html bytes.Buffer
|
||||||
|
@ -68,13 +66,18 @@ func renderIntoBaseTemplate(user User, data PageContent) (string, error) {
|
||||||
return html.String(), nil
|
return html.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderPost(post *Post) (string, error) {
|
func renderPostContent(post *Post) (string, error) {
|
||||||
buf := post.RenderedContent()
|
buf := post.RenderedContent()
|
||||||
postHtml, err := renderEmbedTemplate("embed/post.html", PostRenderData{
|
postHtml, err := renderEmbedTemplate("embed/post.html", PostRenderData{
|
||||||
Title: post.Title(),
|
Title: post.Title(),
|
||||||
Post: post,
|
Post: post,
|
||||||
Content: template.HTML(buf.String()),
|
Content: template.HTML(buf.String()),
|
||||||
})
|
})
|
||||||
|
return postHtml, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderPost(post *Post) (string, error) {
|
||||||
|
postHtml, err := renderPostContent(post)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
func TestRenderPostHEntry(t *testing.T) {
|
||||||
user := getTestUser()
|
user := getTestUser()
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
@ -107,7 +126,7 @@ func TestRenderIndexPageWithBrokenBaseTemplate(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenderUserList(t *testing.T) {
|
func TestRenderUserList(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
repo.CreateUser("user1")
|
repo.CreateUser("user1")
|
||||||
repo.CreateUser("user2")
|
repo.CreateUser("user2")
|
||||||
|
|
||||||
|
@ -162,14 +181,14 @@ func TestRenderPostIncludesRelToWebMention(t *testing.T) {
|
||||||
func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) {
|
func TestRenderPostAddsLinksToApprovedWebmention(t *testing.T) {
|
||||||
user := getTestUser()
|
user := getTestUser()
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
webmention := owl.Webmention{
|
webmention := owl.WebmentionIn{
|
||||||
Source: "http://example.com/source3",
|
Source: "http://example.com/source3",
|
||||||
Title: "Test Title",
|
Title: "Test Title",
|
||||||
ApprovalStatus: "approved",
|
ApprovalStatus: "approved",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -2),
|
RetrievedAt: time.Now().Add(time.Hour * -2),
|
||||||
}
|
}
|
||||||
post.PersistWebmention(webmention)
|
post.PersistWebmention(webmention)
|
||||||
webmention = owl.Webmention{
|
webmention = owl.WebmentionIn{
|
||||||
Source: "http://example.com/source4",
|
Source: "http://example.com/source4",
|
||||||
ApprovalStatus: "rejected",
|
ApprovalStatus: "rejected",
|
||||||
RetrievedAt: time.Now().Add(time.Hour * -3),
|
RetrievedAt: time.Now().Add(time.Hour * -3),
|
||||||
|
@ -199,3 +218,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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,19 +17,18 @@ var VERSION = "0.0.1"
|
||||||
|
|
||||||
type Repository struct {
|
type Repository struct {
|
||||||
name string
|
name string
|
||||||
single_user_mode bool
|
HttpClient HttpClient
|
||||||
active_user string
|
Parser HtmlParser
|
||||||
allow_raw_html bool
|
|
||||||
Retriever HttpRetriever
|
|
||||||
Parser MicroformatParser
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepoConfig struct {
|
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: OwlMicroformatParser{}, Retriever: OwlHttpRetriever{}}
|
newRepo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: &OwlHttpClient{}}
|
||||||
// check if repository already exists
|
// check if repository already exists
|
||||||
if dirExists(newRepo.Dir()) {
|
if dirExists(newRepo.Dir()) {
|
||||||
return Repository{}, fmt.Errorf("Repository already exists")
|
return Repository{}, fmt.Errorf("Repository already exists")
|
||||||
|
@ -39,6 +38,13 @@ func CreateRepository(name string) (Repository, error) {
|
||||||
os.Mkdir(newRepo.UsersDir(), 0755)
|
os.Mkdir(newRepo.UsersDir(), 0755)
|
||||||
os.Mkdir(newRepo.StaticDir(), 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
|
// copy all files from static_files embed.FS to StaticDir
|
||||||
staticFiles, _ := embed_files.ReadDir("embed/initial/static")
|
staticFiles, _ := embed_files.ReadDir("embed/initial/static")
|
||||||
for _, file := range staticFiles {
|
for _, file := range staticFiles {
|
||||||
|
@ -63,7 +69,7 @@ func CreateRepository(name string) (Repository, error) {
|
||||||
|
|
||||||
func OpenRepository(name string) (Repository, error) {
|
func OpenRepository(name string) (Repository, error) {
|
||||||
|
|
||||||
repo := Repository{name: name, Parser: OwlMicroformatParser{}, Retriever: OwlHttpRetriever{}}
|
repo := Repository{name: name, Parser: OwlHtmlParser{}, HttpClient: &OwlHttpClient{}}
|
||||||
if !dirExists(repo.Dir()) {
|
if !dirExists(repo.Dir()) {
|
||||||
return Repository{}, fmt.Errorf("Repository does not exist: " + repo.Dir())
|
return Repository{}, fmt.Errorf("Repository does not exist: " + repo.Dir())
|
||||||
}
|
}
|
||||||
|
@ -71,36 +77,6 @@ func OpenRepository(name string) (Repository, error) {
|
||||||
return repo, nil
|
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 {
|
func (repo Repository) Dir() string {
|
||||||
return repo.name
|
return repo.name
|
||||||
}
|
}
|
||||||
|
@ -114,7 +90,8 @@ func (repo Repository) UsersDir() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Repository) UserUrlPath(user User) string {
|
func (repo Repository) UserUrlPath(user User) string {
|
||||||
if repo.single_user_mode {
|
config, _ := repo.Config()
|
||||||
|
if config.SingleUser != "" {
|
||||||
return "/"
|
return "/"
|
||||||
}
|
}
|
||||||
return "/user/" + user.name + "/"
|
return "/user/" + user.name + "/"
|
||||||
|
@ -136,8 +113,9 @@ func (repo Repository) Template() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo Repository) Users() ([]User, error) {
|
func (repo Repository) Users() ([]User, error) {
|
||||||
if repo.single_user_mode {
|
config, _ := repo.Config()
|
||||||
return []User{{repo: &repo, name: repo.active_user}}, nil
|
if config.SingleUser != "" {
|
||||||
|
return []User{{repo: &repo, name: config.SingleUser}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
userNames := listDir(repo.UsersDir())
|
userNames := listDir(repo.UsersDir())
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
|
|
||||||
func TestCanCreateRepository(t *testing.T) {
|
func TestCanCreateRepository(t *testing.T) {
|
||||||
repoName := testRepoName()
|
repoName := testRepoName()
|
||||||
_, err := owl.CreateRepository(repoName)
|
_, err := owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("Error creating repository: ", err.Error())
|
t.Error("Error creating repository: ", err.Error())
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,8 @@ func TestCanCreateRepository(t *testing.T) {
|
||||||
|
|
||||||
func TestCannotCreateExistingRepository(t *testing.T) {
|
func TestCannotCreateExistingRepository(t *testing.T) {
|
||||||
repoName := testRepoName()
|
repoName := testRepoName()
|
||||||
owl.CreateRepository(repoName)
|
owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||||
_, err := owl.CreateRepository(repoName)
|
_, err := owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("No error returned when creating existing repository")
|
t.Error("No error returned when creating existing repository")
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ func TestCannotCreateExistingRepository(t *testing.T) {
|
||||||
|
|
||||||
func TestCanCreateANewUser(t *testing.T) {
|
func TestCanCreateANewUser(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
if _, err := os.Stat(path.Join(user.Dir(), "")); err != nil {
|
if _, err := os.Stat(path.Join(user.Dir(), "")); err != nil {
|
||||||
t.Error("User directory not created")
|
t.Error("User directory not created")
|
||||||
|
@ -36,7 +36,7 @@ func TestCanCreateANewUser(t *testing.T) {
|
||||||
|
|
||||||
func TestCannotRecreateExisitingUser(t *testing.T) {
|
func TestCannotRecreateExisitingUser(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
userName := randomUserName()
|
userName := randomUserName()
|
||||||
repo.CreateUser(userName)
|
repo.CreateUser(userName)
|
||||||
_, err := repo.CreateUser(userName)
|
_, err := repo.CreateUser(userName)
|
||||||
|
@ -47,7 +47,7 @@ func TestCannotRecreateExisitingUser(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateUserAddsVersionFile(t *testing.T) {
|
func TestCreateUserAddsVersionFile(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
if _, err := os.Stat(path.Join(user.Dir(), "/meta/VERSION")); err != nil {
|
if _, err := os.Stat(path.Join(user.Dir(), "/meta/VERSION")); err != nil {
|
||||||
t.Error("Version file not created")
|
t.Error("Version file not created")
|
||||||
|
@ -56,7 +56,7 @@ func TestCreateUserAddsVersionFile(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateUserAddsBaseHtmlFile(t *testing.T) {
|
func TestCreateUserAddsBaseHtmlFile(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
if _, err := os.Stat(path.Join(user.Dir(), "/meta/base.html")); err != nil {
|
if _, err := os.Stat(path.Join(user.Dir(), "/meta/base.html")); err != nil {
|
||||||
t.Error("Base html file not created")
|
t.Error("Base html file not created")
|
||||||
|
@ -65,7 +65,7 @@ func TestCreateUserAddsBaseHtmlFile(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateUserAddConfigYml(t *testing.T) {
|
func TestCreateUserAddConfigYml(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
if _, err := os.Stat(path.Join(user.Dir(), "/meta/config.yml")); err != nil {
|
if _, err := os.Stat(path.Join(user.Dir(), "/meta/config.yml")); err != nil {
|
||||||
t.Error("Config file not created")
|
t.Error("Config file not created")
|
||||||
|
@ -74,7 +74,7 @@ func TestCreateUserAddConfigYml(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateUserAddsPublicFolder(t *testing.T) {
|
func TestCreateUserAddsPublicFolder(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
if _, err := os.Stat(path.Join(user.Dir(), "/public")); err != nil {
|
if _, err := os.Stat(path.Join(user.Dir(), "/public")); err != nil {
|
||||||
t.Error("Public folder not created")
|
t.Error("Public folder not created")
|
||||||
|
@ -83,7 +83,7 @@ func TestCreateUserAddsPublicFolder(t *testing.T) {
|
||||||
|
|
||||||
func TestCanListRepoUsers(t *testing.T) {
|
func TestCanListRepoUsers(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user1, _ := repo.CreateUser(randomUserName())
|
user1, _ := repo.CreateUser(randomUserName())
|
||||||
user2, _ := repo.CreateUser(randomUserName())
|
user2, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
|
@ -101,7 +101,7 @@ func TestCanListRepoUsers(t *testing.T) {
|
||||||
func TestCanOpenRepository(t *testing.T) {
|
func TestCanOpenRepository(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repoName := testRepoName()
|
repoName := testRepoName()
|
||||||
repo, _ := owl.CreateRepository(repoName)
|
repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||||
// Open the repository
|
// Open the repository
|
||||||
repo2, err := owl.OpenRepository(repoName)
|
repo2, err := owl.OpenRepository(repoName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -121,7 +121,7 @@ func TestCannotOpenNonExisitingRepo(t *testing.T) {
|
||||||
|
|
||||||
func TestGetUser(t *testing.T) {
|
func TestGetUser(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Get the user
|
// Get the user
|
||||||
user2, err := repo.GetUser(user.Name())
|
user2, err := repo.GetUser(user.Name())
|
||||||
|
@ -135,7 +135,7 @@ func TestGetUser(t *testing.T) {
|
||||||
|
|
||||||
func TestCannotGetNonexistingUser(t *testing.T) {
|
func TestCannotGetNonexistingUser(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
_, err := repo.GetUser(randomUserName())
|
_, err := repo.GetUser(randomUserName())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("No error returned when getting non-existing user")
|
t.Error("No error returned when getting non-existing user")
|
||||||
|
@ -144,7 +144,7 @@ func TestCannotGetNonexistingUser(t *testing.T) {
|
||||||
|
|
||||||
func TestGetStaticDirOfRepo(t *testing.T) {
|
func TestGetStaticDirOfRepo(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
// Get the user
|
// Get the user
|
||||||
staticDir := repo.StaticDir()
|
staticDir := repo.StaticDir()
|
||||||
if staticDir == "" {
|
if staticDir == "" {
|
||||||
|
@ -154,7 +154,7 @@ func TestGetStaticDirOfRepo(t *testing.T) {
|
||||||
|
|
||||||
func TestNewRepoGetsStaticFiles(t *testing.T) {
|
func TestNewRepoGetsStaticFiles(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
if _, err := os.Stat(repo.StaticDir()); err != nil {
|
if _, err := os.Stat(repo.StaticDir()); err != nil {
|
||||||
t.Error("Static directory not found")
|
t.Error("Static directory not found")
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ func TestNewRepoGetsStaticFiles(t *testing.T) {
|
||||||
|
|
||||||
func TestNewRepoGetsStaticFilesPicoCSSWithContent(t *testing.T) {
|
func TestNewRepoGetsStaticFilesPicoCSSWithContent(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
file, err := os.Open(path.Join(repo.StaticDir(), "pico.min.css"))
|
file, err := os.Open(path.Join(repo.StaticDir(), "pico.min.css"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error("Error opening pico.min.css")
|
t.Error("Error opening pico.min.css")
|
||||||
|
@ -183,7 +183,7 @@ func TestNewRepoGetsStaticFilesPicoCSSWithContent(t *testing.T) {
|
||||||
|
|
||||||
func TestNewRepoGetsBaseHtml(t *testing.T) {
|
func TestNewRepoGetsBaseHtml(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
if _, err := os.Stat(path.Join(repo.Dir(), "/base.html")); err != nil {
|
if _, err := os.Stat(path.Join(repo.Dir(), "/base.html")); err != nil {
|
||||||
t.Error("Base html file not found")
|
t.Error("Base html file not found")
|
||||||
}
|
}
|
||||||
|
@ -191,7 +191,7 @@ func TestNewRepoGetsBaseHtml(t *testing.T) {
|
||||||
|
|
||||||
func TestCanGetRepoTemplate(t *testing.T) {
|
func TestCanGetRepoTemplate(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
// Get the user
|
// Get the user
|
||||||
template, err := repo.Template()
|
template, err := repo.Template()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -206,13 +206,13 @@ func TestCanOpenRepositoryInSingleUserMode(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repoName := testRepoName()
|
repoName := testRepoName()
|
||||||
userName := randomUserName()
|
userName := randomUserName()
|
||||||
created_repo, _ := owl.CreateRepository(repoName)
|
created_repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{SingleUser: userName})
|
||||||
created_repo.CreateUser(userName)
|
created_repo.CreateUser(userName)
|
||||||
created_repo.CreateUser(randomUserName())
|
created_repo.CreateUser(randomUserName())
|
||||||
created_repo.CreateUser(randomUserName())
|
created_repo.CreateUser(randomUserName())
|
||||||
|
|
||||||
// Open the repository
|
// Open the repository
|
||||||
repo, _ := owl.OpenSingleUserRepo(repoName, userName)
|
repo, _ := owl.OpenRepository(repoName)
|
||||||
|
|
||||||
users, _ := repo.Users()
|
users, _ := repo.Users()
|
||||||
if len(users) != 1 {
|
if len(users) != 1 {
|
||||||
|
@ -227,11 +227,11 @@ func TestSingleUserRepoUserUrlPathIsSimple(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repoName := testRepoName()
|
repoName := testRepoName()
|
||||||
userName := randomUserName()
|
userName := randomUserName()
|
||||||
created_repo, _ := owl.CreateRepository(repoName)
|
created_repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{SingleUser: userName})
|
||||||
created_repo.CreateUser(userName)
|
created_repo.CreateUser(userName)
|
||||||
|
|
||||||
// Open the repository
|
// Open the repository
|
||||||
repo, _ := owl.OpenSingleUserRepo(repoName, userName)
|
repo, _ := owl.OpenRepository(repoName)
|
||||||
user, _ := repo.GetUser(userName)
|
user, _ := repo.GetUser(userName)
|
||||||
if user.UrlPath() != "/" {
|
if user.UrlPath() != "/" {
|
||||||
t.Error("User url is not '/'. Got: ", 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) {
|
func TestCanGetMapWithAllPostAliases(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
post, _ := user.CreateNewPost("test-1")
|
post, _ := user.CreateNewPost("test-1")
|
||||||
|
|
||||||
|
@ -276,7 +276,7 @@ func TestCanGetMapWithAllPostAliases(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAliasesHaveCorrectPost(t *testing.T) {
|
func TestAliasesHaveCorrectPost(t *testing.T) {
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
post1, _ := user.CreateNewPost("test-1")
|
post1, _ := user.CreateNewPost("test-1")
|
||||||
post2, _ := user.CreateNewPost("test-2")
|
post2, _ := user.CreateNewPost("test-2")
|
||||||
|
|
1
user.go
1
user.go
|
@ -21,6 +21,7 @@ type UserConfig struct {
|
||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
SubTitle string `yaml:"subtitle"`
|
SubTitle string `yaml:"subtitle"`
|
||||||
HeaderColor string `yaml:"header_color"`
|
HeaderColor string `yaml:"header_color"`
|
||||||
|
TwitterHandle string `yaml:"twitter_handle"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user User) Dir() string {
|
func (user User) Dir() string {
|
||||||
|
|
16
user_test.go
16
user_test.go
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
func TestCreateNewPostCreatesEntryInPublic(t *testing.T) {
|
func TestCreateNewPostCreatesEntryInPublic(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
user.CreateNewPost("testpost")
|
user.CreateNewPost("testpost")
|
||||||
|
@ -26,7 +26,7 @@ func TestCreateNewPostCreatesEntryInPublic(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateNewPostCreatesMediaDir(t *testing.T) {
|
func TestCreateNewPostCreatesMediaDir(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
@ -49,7 +49,7 @@ func TestCreateNewPostAddsDateToMetaBlock(t *testing.T) {
|
||||||
|
|
||||||
func TestCreateNewPostMultipleCalls(t *testing.T) {
|
func TestCreateNewPostMultipleCalls(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
user.CreateNewPost("testpost")
|
user.CreateNewPost("testpost")
|
||||||
|
@ -66,7 +66,7 @@ func TestCreateNewPostMultipleCalls(t *testing.T) {
|
||||||
|
|
||||||
func TestCanListUserPosts(t *testing.T) {
|
func TestCanListUserPosts(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
user.CreateNewPost("testpost")
|
user.CreateNewPost("testpost")
|
||||||
|
@ -83,7 +83,7 @@ func TestCanListUserPosts(t *testing.T) {
|
||||||
|
|
||||||
func TestCannotListUserPostsInSubdirectories(t *testing.T) {
|
func TestCannotListUserPostsInSubdirectories(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
user.CreateNewPost("testpost")
|
user.CreateNewPost("testpost")
|
||||||
|
@ -120,7 +120,7 @@ func TestCannotListUserPostsInSubdirectories(t *testing.T) {
|
||||||
|
|
||||||
func TestCannotListUserPostsWithoutIndexMd(t *testing.T) {
|
func TestCannotListUserPostsWithoutIndexMd(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
user.CreateNewPost("testpost")
|
user.CreateNewPost("testpost")
|
||||||
|
@ -149,7 +149,7 @@ func TestCannotListUserPostsWithoutIndexMd(t *testing.T) {
|
||||||
|
|
||||||
func TestListUserPostsDoesNotIncludeDrafts(t *testing.T) {
|
func TestListUserPostsDoesNotIncludeDrafts(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
@ -170,7 +170,7 @@ func TestListUserPostsDoesNotIncludeDrafts(t *testing.T) {
|
||||||
|
|
||||||
func TestListUsersDraftsExcludedRealWorld(t *testing.T) {
|
func TestListUsersDraftsExcludedRealWorld(t *testing.T) {
|
||||||
// Create a new user
|
// Create a new user
|
||||||
repo := getTestRepo()
|
repo := getTestRepo(owl.RepoConfig{})
|
||||||
user, _ := repo.CreateUser(randomUserName())
|
user, _ := repo.CreateUser(randomUserName())
|
||||||
// Create a new post
|
// Create a new post
|
||||||
post, _ := user.CreateNewPost("testpost")
|
post, _ := user.CreateNewPost("testpost")
|
||||||
|
|
164
webmention.go
164
webmention.go
|
@ -3,48 +3,52 @@ package owl
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Webmention struct {
|
type WebmentionIn struct {
|
||||||
Source string `yaml:"source"`
|
Source string `yaml:"source"`
|
||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
ApprovalStatus string `yaml:"approval_status"`
|
ApprovalStatus string `yaml:"approval_status"`
|
||||||
RetrievedAt time.Time `yaml:"retrieved_at"`
|
RetrievedAt time.Time `yaml:"retrieved_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HttpRetriever interface {
|
type WebmentionOut struct {
|
||||||
Get(url string) ([]byte, error)
|
Target string `yaml:"target"`
|
||||||
|
Supported bool `yaml:"supported"`
|
||||||
|
ScannedAt time.Time `yaml:"scanned_at"`
|
||||||
|
LastSentAt time.Time `yaml:"last_sent_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MicroformatParser interface {
|
type HttpClient interface {
|
||||||
ParseHEntry(data []byte) (ParsedHEntry, 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 OwlHttpRetriever struct{}
|
type HtmlParser interface {
|
||||||
|
ParseHEntry(resp *http.Response) (ParsedHEntry, error)
|
||||||
|
ParseLinks(resp *http.Response) ([]string, error)
|
||||||
|
ParseLinksFromString(string) ([]string, error)
|
||||||
|
GetWebmentionEndpoint(resp *http.Response) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
type OwlMicroformatParser struct{}
|
type OwlHttpClient = http.Client
|
||||||
|
|
||||||
|
type OwlHtmlParser struct{}
|
||||||
|
|
||||||
type ParsedHEntry struct {
|
type ParsedHEntry struct {
|
||||||
Title string
|
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) {
|
func collectText(n *html.Node, buf *bytes.Buffer) {
|
||||||
|
|
||||||
if n.Type == html.TextNode {
|
if n.Type == html.TextNode {
|
||||||
buf.WriteString(n.Data)
|
buf.WriteString(n.Data)
|
||||||
}
|
}
|
||||||
|
@ -53,8 +57,21 @@ func collectText(n *html.Node, buf *bytes.Buffer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (OwlMicroformatParser) ParseHEntry(data []byte) (ParsedHEntry, error) {
|
func readResponseBody(resp *http.Response) (string, error) {
|
||||||
doc, err := html.Parse(strings.NewReader(string(data)))
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return ParsedHEntry{}, err
|
||||||
|
}
|
||||||
|
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ParsedHEntry{}, err
|
return ParsedHEntry{}, err
|
||||||
}
|
}
|
||||||
|
@ -95,3 +112,110 @@ func (OwlMicroformatParser) ParseHEntry(data []byte) (ParsedHEntry, error) {
|
||||||
}
|
}
|
||||||
return findHFeed(doc)
|
return findHFeed(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var findLinks func(*html.Node) ([]string, error)
|
||||||
|
findLinks = func(n *html.Node) ([]string, error) {
|
||||||
|
links := make([]string, 0)
|
||||||
|
if n.Type == html.ElementNode && n.Data == "a" {
|
||||||
|
for _, attr := range n.Attr {
|
||||||
|
if attr.Key == "href" {
|
||||||
|
links = append(links, attr.Val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||||
|
childLinks, _ := findLinks(c)
|
||||||
|
links = append(links, childLinks...)
|
||||||
|
}
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
return findLinks(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OwlHtmlParser) GetWebmentionEndpoint(resp *http.Response) (string, error) {
|
||||||
|
//request url
|
||||||
|
requestUrl := resp.Request.URL
|
||||||
|
|
||||||
|
// Check link headers
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlStr, err := readResponseBody(resp)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
doc, err := html.Parse(strings.NewReader(htmlStr))
|
||||||
|
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" || n.Data == "a") {
|
||||||
|
for _, attr := range n.Attr {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
package owl_test
|
package owl_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"h4kor/owl-blogs"
|
"h4kor/owl-blogs"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"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
|
// https://www.w3.org/TR/webmention/#h-webmention-verification
|
||||||
//
|
//
|
||||||
|
|
||||||
func TestParseValidHEntry(t *testing.T) {
|
func TestParseValidHEntry(t *testing.T) {
|
||||||
html := []byte("<div class=\"h-entry\"><div class=\"p-name\">Foo</div></div>")
|
html := []byte("<div class=\"h-entry\"><div class=\"p-name\">Foo</div></div>")
|
||||||
parser := &owl.OwlMicroformatParser{}
|
parser := &owl.OwlHtmlParser{}
|
||||||
entry, err := parser.ParseHEntry(html)
|
entry, err := parser.ParseHEntry(&http.Response{Body: io.NopCloser(bytes.NewReader(html))})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unable to parse feed: %v", err)
|
t.Errorf("Unable to parse feed: %v", err)
|
||||||
|
@ -24,8 +38,8 @@ func TestParseValidHEntry(t *testing.T) {
|
||||||
|
|
||||||
func TestParseValidHEntryWithoutTitle(t *testing.T) {
|
func TestParseValidHEntryWithoutTitle(t *testing.T) {
|
||||||
html := []byte("<div class=\"h-entry\"></div><div class=\"p-name\">Foo</div>")
|
html := []byte("<div class=\"h-entry\"></div><div class=\"p-name\">Foo</div>")
|
||||||
parser := &owl.OwlMicroformatParser{}
|
parser := &owl.OwlHtmlParser{}
|
||||||
entry, err := parser.ParseHEntry(html)
|
entry, err := parser.ParseHEntry(&http.Response{Body: io.NopCloser(bytes.NewReader(html))})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unable to parse feed: %v", err)
|
t.Errorf("Unable to parse feed: %v", err)
|
||||||
|
@ -34,3 +48,142 @@ func TestParseValidHEntryWithoutTitle(t *testing.T) {
|
||||||
t.Errorf("Wrong Title. Expected %v, got %v", "Foo", entry.Title)
|
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(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 TestGetWebmentionEndpointLinkA(t *testing.T) {
|
||||||
|
html := []byte("<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 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{}
|
||||||
|
resp := constructResponse(html)
|
||||||
|
resp.Header = http.Header{"Link": []string{"<http://example.com/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 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{}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue