Compare commits
No commits in common. "7a70be98393acb56da102ebebfffd5ea28724b0c" and "41c2286311c80ad8b48ffed6c5c047a2d756918e" have entirely different histories.
7a70be9839
...
41c2286311
18
README.md
18
README.md
|
@ -23,8 +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
|
||||
\- webmentions.yml
|
||||
-- Used to track incoming and outgoing webmentions
|
||||
\- 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
|
||||
|
@ -37,10 +38,6 @@ Each directory in the `/users/` directory of a repository is considered a user.
|
|||
\- VERSION
|
||||
-- Contains the version string.
|
||||
-- Used to determine compatibility in the future
|
||||
\- media/
|
||||
-- All this files will be publicly available. To be used for general files
|
||||
\- avatar.{png, jpg, jpeg, gif}
|
||||
-- The avatar for the user
|
||||
\- config.yml
|
||||
-- Contains settings global to the user.
|
||||
-- For example: page title and style options
|
||||
|
@ -67,15 +64,10 @@ Actual post
|
|||
```
|
||||
|
||||
|
||||
#### webmentions.yml
|
||||
#### status.yml
|
||||
|
||||
```
|
||||
incoming:
|
||||
- source: https://example.com/post
|
||||
title: Example Post
|
||||
ApprovalStatus: ["", "approved", "rejected"]
|
||||
retrieved_at: 2021-08-13T17:07:00Z
|
||||
outgoing:
|
||||
webmentions:
|
||||
- target: https://example.com/post
|
||||
supported: true
|
||||
scanned_at: 2021-08-13T17:07:00Z
|
||||
|
|
|
@ -237,26 +237,6 @@ func postMediaHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Requ
|
|||
}
|
||||
}
|
||||
|
||||
func userMediaHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
filepath := ps.ByName("filepath")
|
||||
|
||||
user, err := getUserFromRepo(repo, ps)
|
||||
if err != nil {
|
||||
println("Error getting user: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
filepath = path.Join(user.MediaDir(), filepath)
|
||||
if _, err := os.Stat(filepath); err != nil {
|
||||
println("Error getting file: ", err.Error())
|
||||
notFoundHandler(repo)(w, r)
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, filepath)
|
||||
}
|
||||
}
|
||||
|
||||
func notFoundHandler(repo *owl.Repository) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
|
|
@ -14,11 +14,10 @@ func Router(repo *owl.Repository) http.Handler {
|
|||
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
|
||||
router.GET("/", repoIndexHandler(repo))
|
||||
router.GET("/user/:user/", userIndexHandler(repo))
|
||||
router.GET("/user/:user/media/*filepath", userMediaHandler(repo))
|
||||
router.POST("/user/:user/webmention/", userWebmentionHandler(repo))
|
||||
router.GET("/user/:user/index.xml", userRSSHandler(repo))
|
||||
router.GET("/user/:user/posts/:post/", postHandler(repo))
|
||||
router.GET("/user/:user/posts/:post/media/*filepath", postMediaHandler(repo))
|
||||
router.POST("/user/:user/webmention/", userWebmentionHandler(repo))
|
||||
router.NotFound = http.HandlerFunc(notFoundHandler(repo))
|
||||
return router
|
||||
}
|
||||
|
@ -27,11 +26,10 @@ func SingleUserRouter(repo *owl.Repository) http.Handler {
|
|||
router := httprouter.New()
|
||||
router.ServeFiles("/static/*filepath", http.Dir(repo.StaticDir()))
|
||||
router.GET("/", userIndexHandler(repo))
|
||||
router.GET("/media/*filepath", userMediaHandler(repo))
|
||||
router.POST("/webmention/", userWebmentionHandler(repo))
|
||||
router.GET("/index.xml", userRSSHandler(repo))
|
||||
router.GET("/posts/:post/", postHandler(repo))
|
||||
router.GET("/posts/:post/media/*filepath", postMediaHandler(repo))
|
||||
router.POST("/webmention/", userWebmentionHandler(repo))
|
||||
router.NotFound = http.HandlerFunc(notFoundHandler(repo))
|
||||
return router
|
||||
}
|
||||
|
|
|
@ -62,9 +62,9 @@ var webmentionCmd = &cobra.Command{
|
|||
for _, webmention := range webmentions {
|
||||
go func(webmention owl.WebmentionOut) {
|
||||
defer wg.Done()
|
||||
sendErr := post.SendWebmention(webmention)
|
||||
if sendErr != nil {
|
||||
println("Error sending webmentions: ", sendErr.Error())
|
||||
err = post.SendWebmention(webmention)
|
||||
if err != nil {
|
||||
println("Error sending webmentions: ", err.Error())
|
||||
} else {
|
||||
println("Webmention sent to ", webmention.Target)
|
||||
}
|
||||
|
|
|
@ -18,26 +18,6 @@
|
|||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
float: left;
|
||||
margin-right: 1rem;
|
||||
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
order: 0;
|
||||
}
|
||||
|
||||
.header-profile {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
hgroup h2 a { color: inherit; }
|
||||
</style>
|
||||
|
@ -45,17 +25,24 @@
|
|||
|
||||
<body>
|
||||
<header>
|
||||
<div class="container header h-card">
|
||||
<hgroup class="header-title">
|
||||
<h2><a class="p-name u-url" href="{{ .User.UrlPath }}">{{ .UserConfig.Title }}</a></h2>
|
||||
<h3 class="p-note">{{ .UserConfig.SubTitle }}</h3>
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li>
|
||||
<hgroup>
|
||||
<h2><a href="{{ .User.UrlPath }}">{{ .UserConfig.Title }}</a></h2>
|
||||
<h3>{{ .UserConfig.SubTitle }}</h3>
|
||||
</hgroup>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="header-profile">
|
||||
{{ if .User.AvatarUrl }}
|
||||
<img class="u-logo avatar" src="{{ .User.AvatarUrl }}" alt="{{ .UserConfig.Title }}" width="100" height="100" />
|
||||
{{ end }}
|
||||
<div style="float: right; list-style: none;">
|
||||
</nav>
|
||||
</header>
|
||||
<main class="container">
|
||||
{{ .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>
|
||||
|
@ -64,15 +51,8 @@
|
|||
<li><a href="https://github.com/{{.UserConfig.GitHubHandle}}" rel="me">@{{.UserConfig.GitHubHandle}} on GitHub</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
<main class="container">
|
||||
{{ .Content }}
|
||||
</main>
|
||||
<footer class="container">
|
||||
</ul>
|
||||
</nav>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<small>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Meta.Date}}">
|
||||
{{.Meta.FormattedDate}}
|
||||
{{.Meta.Date}}
|
||||
</time>
|
||||
</small>
|
||||
</hgroup>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<small>
|
||||
Published:
|
||||
<time class="dt-published" datetime="{{.Post.Meta.Date}}">
|
||||
{{.Post.Meta.FormattedDate}}
|
||||
{{.Post.Meta.Date}}
|
||||
</time>
|
||||
<a class="u-url" href="{{.Post.FullUrl}}">#</a>
|
||||
</small>
|
||||
|
|
61
post.go
61
post.go
|
@ -2,7 +2,6 @@ package owl
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -30,64 +29,8 @@ type Post struct {
|
|||
type PostMeta struct {
|
||||
Title string `yaml:"title"`
|
||||
Aliases []string `yaml:"aliases"`
|
||||
Date time.Time `yaml:"date"`
|
||||
Draft bool `yaml:"draft"`
|
||||
}
|
||||
|
||||
func (pm PostMeta) FormattedDate() string {
|
||||
return pm.Date.Format("02-01-2006 15:04:05")
|
||||
}
|
||||
|
||||
func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type T struct {
|
||||
Title string `yaml:"title"`
|
||||
Aliases []string `yaml:"aliases"`
|
||||
Draft bool `yaml:"draft"`
|
||||
}
|
||||
type S struct {
|
||||
Date string `yaml:"date"`
|
||||
}
|
||||
|
||||
var t T
|
||||
var s S
|
||||
if err := unmarshal(&t); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unmarshal(&s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pm.Title = t.Title
|
||||
pm.Aliases = t.Aliases
|
||||
pm.Draft = t.Draft
|
||||
|
||||
possibleFormats := []string{
|
||||
"2006-01-02",
|
||||
time.Layout,
|
||||
time.ANSIC,
|
||||
time.UnixDate,
|
||||
time.RubyDate,
|
||||
time.RFC822,
|
||||
time.RFC822Z,
|
||||
time.RFC850,
|
||||
time.RFC1123,
|
||||
time.RFC1123Z,
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
time.Stamp,
|
||||
time.StampMilli,
|
||||
time.StampMicro,
|
||||
time.StampNano,
|
||||
}
|
||||
|
||||
for _, format := range possibleFormats {
|
||||
if t, err := time.Parse(format, s.Date); err == nil {
|
||||
pm.Date = t
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
Draft bool `yaml:"draft"`
|
||||
}
|
||||
|
||||
type PostWebmetions struct {
|
||||
|
@ -365,7 +308,7 @@ func (post *Post) SendWebmention(webmention WebmentionOut) error {
|
|||
|
||||
// if last scan is less than 7 days ago, don't send webmention
|
||||
if webmention.ScannedAt.After(time.Now().Add(-7*24*time.Hour)) && !webmention.Supported {
|
||||
return errors.New("did not scan. Last scan was less than 7 days ago")
|
||||
return nil
|
||||
}
|
||||
|
||||
webmention.ScannedAt = time.Now()
|
||||
|
|
65
post_test.go
65
post_test.go
|
@ -156,7 +156,7 @@ func TestLoadMeta(t *testing.T) {
|
|||
t.Errorf("Expected title: %v, got %v", []string{"foo/bar/"}, post.Meta().Aliases)
|
||||
}
|
||||
|
||||
if post.Meta().Date.Format(time.RFC1123Z) != "Wed, 17 Aug 2022 10:50:02 +0000" {
|
||||
if post.Meta().Date != "Wed, 17 Aug 2022 10:50:02 +0000" {
|
||||
t.Errorf("Expected title: %s, got %s", "Wed, 17 Aug 2022 10:50:02 +0000", post.Meta().Title)
|
||||
}
|
||||
|
||||
|
@ -392,8 +392,8 @@ func TestSendWebmentionOnlyScansOncePerWeek(t *testing.T) {
|
|||
webmention = webmentions[0]
|
||||
|
||||
err := post.SendWebmention(webmention)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got nil")
|
||||
if err != nil {
|
||||
t.Errorf("Error sending webmention: %v", err)
|
||||
}
|
||||
|
||||
webmentions = post.OutgoingWebmentions()
|
||||
|
@ -550,62 +550,3 @@ func TestComplexParallelWebmentions(t *testing.T) {
|
|||
t.Errorf("Expected 20 webmentions, got %d", len(outs))
|
||||
}
|
||||
}
|
||||
func TestComplexParallelSimulatedProcessesWebmentions(t *testing.T) {
|
||||
repoName := testRepoName()
|
||||
repo, _ := owl.CreateRepository(repoName, owl.RepoConfig{})
|
||||
repo.HttpClient = &MockHttpClient{}
|
||||
repo.Parser = &MockParseLinksHtmlParser{
|
||||
Links: []string{
|
||||
"http://example.com/1",
|
||||
"http://example.com/2",
|
||||
"http://example.com/3",
|
||||
},
|
||||
}
|
||||
user, _ := repo.CreateUser("testuser")
|
||||
post, _ := user.CreateNewPost("testpost")
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(40)
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
fRepo, _ := owl.OpenRepository(repoName)
|
||||
fUser, _ := fRepo.GetUser("testuser")
|
||||
fPost, err := fUser.GetPost(post.Id())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
fPost.AddIncomingWebmention("http://example.com/" + strconv.Itoa(k))
|
||||
}(i)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
fRepo, _ := owl.OpenRepository(repoName)
|
||||
fUser, _ := fRepo.GetUser("testuser")
|
||||
fPost, err := fUser.GetPost(post.Id())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
webmention := owl.WebmentionOut{
|
||||
Target: "http://example.com/" + strconv.Itoa(k),
|
||||
}
|
||||
fPost.SendWebmention(webmention)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
ins := post.IncomingWebmentions()
|
||||
|
||||
if len(ins) != 20 {
|
||||
t.Errorf("Expected 20 webmentions, got %d", len(ins))
|
||||
}
|
||||
|
||||
outs := post.OutgoingWebmentions()
|
||||
|
||||
if len(outs) != 20 {
|
||||
t.Errorf("Expected 20 webmentions, got %d", len(outs))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -251,13 +251,3 @@ func TestRenderIncludesFullUrl(t *testing.T) {
|
|||
t.Error("Expected: " + post.FullUrl())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddAvatarIfExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
os.WriteFile(path.Join(user.MediaDir(), "avatar.png"), []byte("test"), 0644)
|
||||
|
||||
result, _ := owl.RenderIndexPage(user)
|
||||
if !strings.Contains(result, "avatar.png") {
|
||||
t.Error("Avatar not rendered. Got: " + result)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -136,10 +136,9 @@ func (repo *Repository) CreateUser(name string) (User, error) {
|
|||
// creates repo/name folder if it doesn't exist
|
||||
user_dir := new_user.Dir()
|
||||
os.Mkdir(user_dir, 0755)
|
||||
// create folders
|
||||
os.Mkdir(path.Join(user_dir, "meta"), 0755)
|
||||
// create public folder
|
||||
os.Mkdir(path.Join(user_dir, "public"), 0755)
|
||||
os.Mkdir(path.Join(user_dir, "media"), 0755)
|
||||
|
||||
// create Meta files
|
||||
os.WriteFile(path.Join(user_dir, "meta", "VERSION"), []byte(VERSION), 0644)
|
||||
|
|
3
rss.go
3
rss.go
|
@ -3,7 +3,6 @@ package owl
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RSS struct {
|
||||
|
@ -47,7 +46,7 @@ func RenderRSSFeed(user User) (string, error) {
|
|||
Guid: post.FullUrl(),
|
||||
Title: post.Title(),
|
||||
Link: post.FullUrl(),
|
||||
PubDate: meta.Date.Format(time.RFC1123Z),
|
||||
PubDate: meta.Date,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ func TestRenderRSSFeedPostData(t *testing.T) {
|
|||
if !strings.Contains(res, post.FullUrl()) {
|
||||
t.Error("SubTitle not rendered. Got: " + res)
|
||||
}
|
||||
if !strings.Contains(res, "Thu, 01 Jan 2015 00:00:00 +0000") {
|
||||
if !strings.Contains(res, "2015-01-01") {
|
||||
t.Error("Date not rendered. Got: " + res)
|
||||
}
|
||||
}
|
||||
|
|
40
user.go
40
user.go
|
@ -43,11 +43,6 @@ func (user User) WebmentionUrl() string {
|
|||
return url
|
||||
}
|
||||
|
||||
func (user User) MediaUrl() string {
|
||||
url, _ := url.JoinPath(user.UrlPath(), "media")
|
||||
return url
|
||||
}
|
||||
|
||||
func (user User) PostDir() string {
|
||||
return path.Join(user.Dir(), "public")
|
||||
}
|
||||
|
@ -56,10 +51,6 @@ func (user User) MetaDir() string {
|
|||
return path.Join(user.Dir(), "meta")
|
||||
}
|
||||
|
||||
func (user User) MediaDir() string {
|
||||
return path.Join(user.Dir(), "media")
|
||||
}
|
||||
|
||||
func (user User) ConfigFile() string {
|
||||
return path.Join(user.MetaDir(), "config.yml")
|
||||
}
|
||||
|
@ -68,16 +59,6 @@ func (user User) Name() string {
|
|||
return user.name
|
||||
}
|
||||
|
||||
func (user User) AvatarUrl() string {
|
||||
for _, ext := range []string{".jpg", ".jpeg", ".png", ".gif"} {
|
||||
if fileExists(path.Join(user.MediaDir(), "avatar"+ext)) {
|
||||
url, _ := url.JoinPath(user.MediaUrl(), "avatar"+ext)
|
||||
return url
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (user User) Posts() ([]*Post, error) {
|
||||
postFiles := listDir(path.Join(user.Dir(), "public"))
|
||||
posts := make([]*Post, 0)
|
||||
|
@ -110,7 +91,12 @@ func (user User) Posts() ([]*Post, error) {
|
|||
postDates := make([]PostWithDate, len(posts))
|
||||
for i, post := range posts {
|
||||
meta := post.Meta()
|
||||
postDates[i] = PostWithDate{post: post, date: meta.Date}
|
||||
date, err := time.Parse(time.RFC1123Z, meta.Date)
|
||||
if err != nil {
|
||||
// invalid date -> use 1970-01-01
|
||||
date = time.Time{}
|
||||
}
|
||||
postDates[i] = PostWithDate{post: post, date: date}
|
||||
}
|
||||
|
||||
// sort posts by date
|
||||
|
@ -157,21 +143,11 @@ func (user User) CreateNewPost(title string) (Post, error) {
|
|||
}
|
||||
}
|
||||
post := Post{user: &user, id: folder_name, title: title}
|
||||
meta := PostMeta{
|
||||
Title: title,
|
||||
Date: time.Now(),
|
||||
Aliases: []string{},
|
||||
Draft: false,
|
||||
}
|
||||
|
||||
initial_content := ""
|
||||
initial_content += "---\n"
|
||||
// write meta
|
||||
meta_bytes, err := yaml.Marshal(meta)
|
||||
if err != nil {
|
||||
return Post{}, err
|
||||
}
|
||||
initial_content += string(meta_bytes)
|
||||
initial_content += "title: " + title + "\n"
|
||||
initial_content += "date: " + time.Now().UTC().Format(time.RFC1123Z) + "\n"
|
||||
initial_content += "---\n"
|
||||
initial_content += "\n"
|
||||
initial_content += "Write your post here.\n"
|
||||
|
|
19
user_test.go
19
user_test.go
|
@ -42,8 +42,8 @@ func TestCreateNewPostAddsDateToMetaBlock(t *testing.T) {
|
|||
posts, _ := user.Posts()
|
||||
post, _ := user.GetPost(posts[0].Id())
|
||||
meta := post.Meta()
|
||||
if meta.Date.IsZero() {
|
||||
t.Errorf("Found no date. Got: %v", meta.Date)
|
||||
if meta.Date == "" {
|
||||
t.Error("Found no date. Got: " + meta.Date)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -302,18 +302,3 @@ func TestPostsSortedByPublishingDateBrokenAtBottom(t *testing.T) {
|
|||
t.Error("Wrong Id, Got: " + posts[1].Id())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvatarEmptyIfNotExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
if user.AvatarUrl() != "" {
|
||||
t.Error("Avatar should be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvatarSetIfFileExist(t *testing.T) {
|
||||
user := getTestUser()
|
||||
os.WriteFile(path.Join(user.MediaDir(), "avatar.png"), []byte("test"), 0644)
|
||||
if user.AvatarUrl() == "" {
|
||||
t.Error("Avatar should not be empty")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue