Merge pull request 'Sending Webmentions' (#10) from webmention into master

Reviewed-on: #10
This commit is contained in:
h4kor 2022-09-06 19:49:00 +00:00
commit 1fc6b9e9d2
33 changed files with 1051 additions and 345 deletions

View File

@ -1,3 +1,6 @@
##
## Build Container
##
FROM golang:1.19-alpine as build
@ -12,9 +15,12 @@ RUN go mod download
COPY . .
RUN go build -o ./out/owl-web ./cmd/owl-web
RUN go build -o ./out/owl-cli ./cmd/owl-cli
RUN go build -o ./out/owl ./cmd/owl
##
## Run Container
##
FROM alpine:3.9
RUN apk add ca-certificates
@ -24,4 +30,4 @@ COPY --from=build /tmp/owl/out/ /bin/
EXPOSE 8080
# Run the binary program produced by `go install`
CMD ["/bin/owl-web"]
ENTRYPOINT ["/bin/owl"]

View File

@ -23,6 +23,9 @@ Each directory in the `/users/` directory of a repository is considered a user.
-- This will be rendered as the blog post.
-- Must be present for the blog post to be valid.
-- All other folders will be ignored
\- status.yml
-- Used to track various process status related to the post,
-- such as if a webmention was sent.
\- media/
-- Contains all media files used in the blog post.
-- All files in this folder will be publicly available
@ -59,3 +62,14 @@ aliases:
Actual post
```
#### status.yml
```
webmentions:
- target: https://example.com/post
supported: true
scanned_at: 2021-08-13T17:07:00Z
last_sent_at: 2021-08-13T17:07:00Z
```

View File

@ -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)
}
}

38
cmd/owl/init.go Normal file
View File

@ -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)
}
},
}

32
cmd/owl/main.go Normal file
View File

@ -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()
}

50
cmd/owl/new_post.go Normal file
View File

@ -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)
}
},
}

38
cmd/owl/new_user.go Normal file
View File

@ -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)
}
},
}

24
cmd/owl/web.go Normal file
View File

@ -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)
},
}

View File

@ -1,7 +1,8 @@
package main_test
package web_test
import (
main "h4kor/owl-blogs/cmd/owl-web"
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
"net/http"
"net/http/httptest"
"os"
@ -9,7 +10,7 @@ import (
)
func TestRedirectOnAliases(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -47,7 +48,7 @@ func TestRedirectOnAliases(t *testing.T) {
}
func TestNoRedirectOnNonExistingAliases(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -78,7 +79,7 @@ func TestNoRedirectOnNonExistingAliases(t *testing.T) {
}
func TestNoRedirectIfValidPostUrl(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
post2, _ := user.CreateNewPost("post-2")
@ -109,7 +110,7 @@ func TestNoRedirectIfValidPostUrl(t *testing.T) {
}
func TestRedirectIfInvalidPostUrl(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -139,7 +140,7 @@ func TestRedirectIfInvalidPostUrl(t *testing.T) {
}
func TestRedirectIfInvalidUserUrl(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -169,7 +170,7 @@ func TestRedirectIfInvalidUserUrl(t *testing.T) {
}
func TestRedirectIfInvalidMediaUrl(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -199,9 +200,8 @@ func TestRedirectIfInvalidMediaUrl(t *testing.T) {
}
func TestDeepAliasInSingleUserMode(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{SingleUser: "test-1"})
user, _ := repo.CreateUser("test-1")
repo.SetSingleUser(user)
post, _ := user.CreateNewPost("post-1")
content := "---\n"

View File

@ -1,4 +1,4 @@
package main
package web
import (
"h4kor/owl-blogs"
@ -11,8 +11,8 @@ import (
)
func getUserFromRepo(repo *owl.Repository, ps httprouter.Params) (owl.User, error) {
if repo.SingleUserName() != "" {
return repo.GetUser(repo.SingleUserName())
if config, _ := repo.Config(); config.SingleUser != "" {
return repo.GetUser(config.SingleUser)
}
userName := ps.ByName("user")
user, err := repo.GetUser(userName)

View File

@ -1,8 +1,8 @@
package main_test
package web_test
import (
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl-web"
main "h4kor/owl-blogs/cmd/owl/web"
"math/rand"
"net/http"
"net/http/httptest"
@ -27,13 +27,13 @@ func testRepoName() string {
return "/tmp/" + randomName()
}
func getTestRepo() owl.Repository {
repo, _ := owl.CreateRepository(testRepoName())
func getTestRepo(config owl.RepoConfig) owl.Repository {
repo, _ := owl.CreateRepository(testRepoName(), config)
return repo
}
func TestMultiUserRepoIndexHandler(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
repo.CreateUser("user_1")
repo.CreateUser("user_2")
@ -64,7 +64,7 @@ func TestMultiUserRepoIndexHandler(t *testing.T) {
}
func TestMultiUserUserIndexHandler(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
user.CreateNewPost("post-1")
@ -91,7 +91,7 @@ func TestMultiUserUserIndexHandler(t *testing.T) {
}
func TestMultiUserPostHandler(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -112,7 +112,7 @@ func TestMultiUserPostHandler(t *testing.T) {
}
func TestMultiUserPostMediaHandler(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")

View File

@ -1,7 +1,8 @@
package main_test
package web_test
import (
main "h4kor/owl-blogs/cmd/owl-web"
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
"net/http"
"net/http/httptest"
"os"
@ -9,7 +10,7 @@ import (
)
func TestPostHandlerReturns404OnDrafts(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")

View File

@ -1,7 +1,8 @@
package main_test
package web_test
import (
main "h4kor/owl-blogs/cmd/owl-web"
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl/web"
"net/http"
"net/http/httptest"
"strings"
@ -9,7 +10,7 @@ import (
)
func TestMultiUserUserRssIndexHandler(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
user.CreateNewPost("post-1")

View File

@ -1,4 +1,4 @@
package main
package web
import (
"h4kor/owl-blogs"
@ -34,63 +34,10 @@ func SingleUserRouter(repo *owl.Repository) http.Handler {
return router
}
func main() {
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
}
func StartServer(repoPath string, port int) {
var repo owl.Repository
var err error
if singleUserName != "" {
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)
repo, err = owl.OpenRepository(repoPath)
if err != nil {
println("Error opening repository: ", err.Error())
@ -98,13 +45,12 @@ func main() {
}
var router http.Handler
if singleUserName == "" {
println("Multi user mode Router used")
router = Router(&repo)
} else {
println("Single user mode Router used")
if config, _ := repo.Config(); config.SingleUser != "" {
router = SingleUserRouter(&repo)
} else {
router = Router(&repo)
}
println("Listening on port", port)
http.ListenAndServe(":"+strconv.Itoa(port), router)

View File

@ -1,8 +1,8 @@
package main_test
package web_test
import (
owl "h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl-web"
main "h4kor/owl-blogs/cmd/owl/web"
"net/http"
"net/http/httptest"
"os"
@ -12,9 +12,8 @@ import (
)
func getSingleUserTestRepo() (owl.Repository, owl.User) {
repo, _ := owl.CreateRepository(testRepoName())
repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{SingleUser: "test-1"})
user, _ := repo.CreateUser("test-1")
repo.SetSingleUser(user)
return repo, user
}

View File

@ -1,8 +1,8 @@
package main_test
package web_test
import (
"h4kor/owl-blogs"
main "h4kor/owl-blogs/cmd/owl-web"
main "h4kor/owl-blogs/cmd/owl/web"
"net/http"
"net/http/httptest"
"net/url"
@ -42,7 +42,7 @@ func assertStatus(t *testing.T, rr *httptest.ResponseRecorder, expStatus int) {
}
func TestWebmentionHandleAccepts(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -60,7 +60,7 @@ func TestWebmentionHandleAccepts(t *testing.T) {
func TestWebmentionWrittenToPost(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -89,7 +89,7 @@ func TestWebmentionWrittenToPost(t *testing.T) {
// (Most commonly this means checking that the source and target schemes are http or https).
func TestWebmentionSourceValidation(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -106,7 +106,7 @@ func TestWebmentionSourceValidation(t *testing.T) {
func TestWebmentionTargetValidation(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -125,7 +125,7 @@ func TestWebmentionTargetValidation(t *testing.T) {
func TestWebmentionSameTargetAndSource(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")
@ -144,7 +144,7 @@ func TestWebmentionSameTargetAndSource(t *testing.T) {
// This check SHOULD happen synchronously to reject invalid Webmentions before more in-depth verification begins.
// What a "valid resource" means is up to the receiver.
func TestValidationOfTarget(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("test-1")
post, _ := user.CreateNewPost("post-1")

70
cmd/owl/webmention.go Normal file
View File

@ -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)
}
}
}
}
},
}

View File

@ -5,14 +5,20 @@
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Title }}</title>
<title>{{ .Title }} - {{ .UserConfig.Title }}</title>
<link rel="stylesheet" href="/static/pico.min.css">
<link rel="webmention" href="{{ .User.WebmentionUrl }}">
<style>
header {
background-color: {{.HeaderColor}};
background-color: {{.UserConfig.HeaderColor}};
}
footer {
border-top: dashed 2px;
border-color: #ccc;
}
hgroup h2 a { color: inherit; }
</style>
</head>
@ -23,8 +29,8 @@
<ul>
<li>
<hgroup>
<h2><a href="{{ .User.UrlPath }}">{{ .UserTitle }}</a></h2>
<h3>{{ .UserSubtitle }}</h3>
<h2><a href="{{ .User.UrlPath }}">{{ .UserConfig.Title }}</a></h2>
<h3>{{ .UserConfig.SubTitle }}</h3>
</hgroup>
</li>
</ul>
@ -35,6 +41,13 @@
{{ .Content }}
</main>
<footer class="container">
<nav>
<ul>
{{ if .UserConfig.TwitterHandle}}
<li><a href="https://twitter.com/{{.UserConfig.TwitterHandle}}" rel="me">@{{.UserConfig.TwitterHandle}} on Twitter</a></li>
{{ end }}
</ul>
</nav>
</footer>
</body>

View File

@ -1 +0,0 @@
domain: "http://localhost:8080"

View File

@ -6,6 +6,7 @@
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
{{.Post.Meta.Date}}
</time>
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
</small>
</hgroup>
<hr>

15
go.mod
View File

@ -3,9 +3,14 @@ module h4kor/owl-blogs
go 1.18
require (
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/yuin/goldmark v1.4.13 // indirect
github.com/yuin/goldmark-meta v1.1.0 // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
github.com/julienschmidt/httprouter v1.3.0
github.com/spf13/cobra v1.5.0
github.com/yuin/goldmark v1.4.13
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)

16
go.sum
View File

@ -1,11 +1,19 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@ -2,20 +2,44 @@ package owl_test
import (
"h4kor/owl-blogs"
"io"
"math/rand"
"net/http"
"net/url"
"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
}
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) {
return []byte(""), nil
func (*MockHttpClient) Get(url string) (resp *http.Response, err error) {
return &http.Response{}, nil
}
func (*MockHttpClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) {
return &http.Response{}, nil
}
func (*MockHttpClient) PostForm(url string, data url.Values) (resp *http.Response, err error) {
return &http.Response{}, nil
}
func randomName() string {
@ -37,13 +61,13 @@ func randomUserName() string {
}
func getTestUser() owl.User {
repo, _ := owl.CreateRepository(testRepoName())
repo, _ := owl.CreateRepository(testRepoName(), owl.RepoConfig{})
user, _ := repo.CreateUser(randomUserName())
return user
}
func getTestRepo() owl.Repository {
repo, _ := owl.CreateRepository(testRepoName())
func getTestRepo(config owl.RepoConfig) owl.Repository {
repo, _ := owl.CreateRepository(testRepoName(), config)
return repo
}

168
post.go
View File

@ -6,9 +6,11 @@ import (
"encoding/base64"
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"sort"
"time"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
@ -32,6 +34,10 @@ type PostMeta struct {
Draft bool `yaml:"draft"`
}
type PostStatus struct {
Webmentions []WebmentionOut
}
func (post Post) Id() string {
return post.id
}
@ -40,6 +46,10 @@ func (post Post) Dir() string {
return path.Join(post.user.Dir(), "public", post.id)
}
func (post Post) StatusFile() string {
return path.Join(post.Dir(), "status.yml")
}
func (post Post) MediaDir() string {
return path.Join(post.Dir(), "media")
}
@ -81,6 +91,42 @@ func (post Post) Content() []byte {
return data
}
func (post Post) Status() PostStatus {
// read status file
// return parsed webmentions
fileName := post.StatusFile()
if !fileExists(fileName) {
return PostStatus{}
}
data, err := os.ReadFile(fileName)
if err != nil {
return PostStatus{}
}
status := PostStatus{}
err = yaml.Unmarshal(data, &status)
if err != nil {
return PostStatus{}
}
return status
}
func (post Post) PersistStatus(status PostStatus) error {
data, err := yaml.Marshal(status)
if err != nil {
return err
}
err = os.WriteFile(post.StatusFile(), data, 0644)
if err != nil {
return err
}
return nil
}
func (post Post) RenderedContent() bytes.Buffer {
data := post.Content()
@ -98,7 +144,7 @@ func (post Post) RenderedContent() bytes.Buffer {
}
options := goldmark.WithRendererOptions()
if post.user.repo.AllowRawHtml() {
if config, _ := post.user.repo.Config(); config.AllowRawHtml {
options = goldmark.WithRendererOptions(
html.WithUnsafe(),
)
@ -156,7 +202,7 @@ func (post *Post) WebmentionFile(source string) string {
return path.Join(post.WebmentionDir(), hashStr+".yml")
}
func (post *Post) PersistWebmention(webmention Webmention) error {
func (post *Post) PersistWebmention(webmention WebmentionIn) error {
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
@ -169,7 +215,7 @@ func (post *Post) PersistWebmention(webmention Webmention) error {
return os.WriteFile(fileName, data, 0644)
}
func (post *Post) Webmention(source string) (Webmention, error) {
func (post *Post) Webmention(source string) (WebmentionIn, error) {
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
@ -177,18 +223,18 @@ func (post *Post) Webmention(source string) (Webmention, error) {
fileName := post.WebmentionFile(source)
if !fileExists(fileName) {
// return error if file doesn't exist
return Webmention{}, fmt.Errorf("Webmention file not found: %s", source)
return WebmentionIn{}, fmt.Errorf("Webmention file not found: %s", source)
}
data, err := os.ReadFile(fileName)
if err != nil {
return Webmention{}, err
return WebmentionIn{}, err
}
mention := Webmention{}
mention := WebmentionIn{}
err = yaml.Unmarshal(data, &mention)
if err != nil {
return Webmention{}, err
return WebmentionIn{}, err
}
return mention, nil
@ -198,7 +244,7 @@ func (post *Post) AddWebmention(source string) error {
// Check if file already exists
_, err := post.Webmention(source)
if err != nil {
webmention := Webmention{
webmention := WebmentionIn{
Source: source,
}
defer post.EnrichWebmention(source)
@ -207,14 +253,55 @@ func (post *Post) AddWebmention(source string) error {
return nil
}
func (post *Post) AddOutgoingWebmention(target string) error {
status := post.Status()
// Check if file already exists
_, err := post.Webmention(target)
if err != nil {
webmention := WebmentionOut{
Target: target,
}
// if target is not in status, add it
for _, t := range status.Webmentions {
if t.Target == webmention.Target {
return nil
}
}
status.Webmentions = append(status.Webmentions, webmention)
}
return post.PersistStatus(status)
}
func (post *Post) UpdateOutgoingWebmention(webmention *WebmentionOut) error {
status := post.Status()
// if target is not in status, add it
replaced := false
for i, t := range status.Webmentions {
if t.Target == webmention.Target {
status.Webmentions[i] = *webmention
replaced = true
break
}
}
if !replaced {
status.Webmentions = append(status.Webmentions, *webmention)
}
return post.PersistStatus(status)
}
func (post *Post) EnrichWebmention(source string) error {
html, err := post.user.repo.Retriever.Get(source)
resp, err := post.user.repo.HttpClient.Get(source)
if err == nil {
webmention, err := post.Webmention(source)
if err != nil {
return err
}
entry, err := post.user.repo.Parser.ParseHEntry(html)
entry, err := post.user.repo.Parser.ParseHEntry(resp)
if err == nil {
webmention.Title = entry.Title
return post.PersistWebmention(webmention)
@ -223,17 +310,17 @@ func (post *Post) EnrichWebmention(source string) error {
return err
}
func (post *Post) Webmentions() []Webmention {
func (post *Post) Webmentions() []WebmentionIn {
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
files := listDir(post.WebmentionDir())
webmentions := []Webmention{}
webmentions := []WebmentionIn{}
for _, file := range files {
data, err := os.ReadFile(path.Join(post.WebmentionDir(), file))
if err != nil {
continue
}
mention := Webmention{}
mention := WebmentionIn{}
err = yaml.Unmarshal(data, &mention)
if err != nil {
continue
@ -244,9 +331,9 @@ func (post *Post) Webmentions() []Webmention {
return webmentions
}
func (post *Post) ApprovedWebmentions() []Webmention {
func (post *Post) ApprovedWebmentions() []WebmentionIn {
webmentions := post.Webmentions()
approved := []Webmention{}
approved := []WebmentionIn{}
for _, webmention := range webmentions {
if webmention.ApprovalStatus == "approved" {
approved = append(approved, webmention)
@ -259,3 +346,54 @@ func (post *Post) ApprovedWebmentions() []Webmention {
})
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
}

View File

@ -89,7 +89,7 @@ func TestDraftInMetaData(t *testing.T) {
}
func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
content := "---\n"
@ -107,8 +107,7 @@ func TestNoRawHTMLIfDisallowedByRepo(t *testing.T) {
}
func TestRawHTMLIfAllowedByRepo(t *testing.T) {
repo := getTestRepo()
repo.SetAllowRawHtml(true)
repo := getTestRepo(owl.RepoConfig{AllowRawHtml: true})
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
content := "---\n"
@ -126,8 +125,7 @@ func TestRawHTMLIfAllowedByRepo(t *testing.T) {
}
func TestLoadMeta(t *testing.T) {
repo := getTestRepo()
repo.SetAllowRawHtml(true)
repo := getTestRepo(owl.RepoConfig{AllowRawHtml: true})
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
@ -170,10 +168,10 @@ func TestLoadMeta(t *testing.T) {
///
func TestPersistWebmention(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
webmention := owl.Webmention{
webmention := owl.WebmentionIn{
Source: "http://example.com/source",
}
err := post.PersistWebmention(webmention)
@ -191,9 +189,9 @@ func TestPersistWebmention(t *testing.T) {
}
func TestAddWebmentionCreatesFile(t *testing.T) {
repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{}
repo.Parser = &MockMicroformatParser{}
repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
@ -209,9 +207,9 @@ func TestAddWebmentionCreatesFile(t *testing.T) {
}
func TestAddWebmentionNotOverwritingFile(t *testing.T) {
repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{}
repo.Parser = &MockMicroformatParser{}
repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
@ -239,9 +237,9 @@ func TestAddWebmentionNotOverwritingFile(t *testing.T) {
}
func TestAddWebmentionAddsParsedTitle(t *testing.T) {
repo := getTestRepo()
repo.Retriever = &MockHttpRetriever{}
repo.Parser = &MockMicroformatParser{}
repo := getTestRepo(owl.RepoConfig{})
repo.HttpClient = &MockHttpClient{}
repo.Parser = &MockHtmlParser{}
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
@ -262,28 +260,28 @@ func TestAddWebmentionAddsParsedTitle(t *testing.T) {
}
func TestApprovedWebmentions(t *testing.T) {
repo := getTestRepo()
repo := getTestRepo(owl.RepoConfig{})
user, _ := repo.CreateUser("testuser")
post, _ := user.CreateNewPost("testpost")
webmention := owl.Webmention{
webmention := owl.WebmentionIn{
Source: "http://example.com/source",
ApprovalStatus: "approved",
RetrievedAt: time.Now(),
}
post.PersistWebmention(webmention)
webmention = owl.Webmention{
webmention = owl.WebmentionIn{
Source: "http://example.com/source2",
ApprovalStatus: "",
RetrievedAt: time.Now().Add(time.Hour * -1),
}
post.PersistWebmention(webmention)
webmention = owl.Webmention{
webmention = owl.WebmentionIn{
Source: "http://example.com/source3",
ApprovalStatus: "approved",
RetrievedAt: time.Now().Add(time.Hour * -2),
}
post.PersistWebmention(webmention)
webmention = owl.Webmention{
webmention = owl.WebmentionIn{
Source: "http://example.com/source4",
ApprovalStatus: "rejected",
RetrievedAt: time.Now().Add(time.Hour * -3),
@ -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)
<