owl-blogs/post.go

405 lines
8.5 KiB
Go
Raw Normal View History

2022-08-03 14:55:48 +00:00
package owl
2022-07-21 17:02:37 +00:00
import (
2022-07-21 17:44:07 +00:00
"bytes"
2022-08-23 15:59:17 +00:00
"crypto/sha256"
"encoding/base64"
2022-09-01 19:34:33 +00:00
"fmt"
2022-07-21 17:02:37 +00:00
"io/ioutil"
2022-09-04 15:10:40 +00:00
"net/url"
2022-08-23 15:59:17 +00:00
"os"
2022-07-21 17:02:37 +00:00
"path"
2022-09-01 19:53:06 +00:00
"sort"
2022-09-04 15:10:40 +00:00
"time"
2022-07-21 17:44:07 +00:00
"github.com/yuin/goldmark"
2022-07-27 19:26:37 +00:00
"github.com/yuin/goldmark/extension"
2022-07-21 17:44:07 +00:00
"github.com/yuin/goldmark/parser"
2022-08-21 09:31:48 +00:00
"github.com/yuin/goldmark/renderer/html"
"gopkg.in/yaml.v2"
2022-07-21 17:02:37 +00:00
)
type Post struct {
user *User
id string
title string
metaLoaded bool
meta PostMeta
2022-07-21 17:02:37 +00:00
}
type PostMeta struct {
Title string `yaml:"title"`
Aliases []string `yaml:"aliases"`
Date string `yaml:"date"`
2022-08-20 20:35:51 +00:00
Draft bool `yaml:"draft"`
}
2022-09-04 13:03:16 +00:00
type PostStatus struct {
Webmentions []WebmentionOut
}
2022-08-03 17:41:13 +00:00
func (post Post) Id() string {
return post.id
}
2022-07-21 17:02:37 +00:00
func (post Post) Dir() string {
return path.Join(post.user.Dir(), "public", post.id)
}
2022-09-04 13:32:37 +00:00
func (post Post) StatusFile() string {
return path.Join(post.Dir(), "status.yml")
}
2022-08-01 17:50:29 +00:00
func (post Post) MediaDir() string {
return path.Join(post.Dir(), "media")
}
2022-08-23 15:59:17 +00:00
func (post Post) WebmentionDir() string {
return path.Join(post.Dir(), "webmention")
}
2022-07-24 13:34:52 +00:00
func (post Post) UrlPath() string {
2022-08-03 17:41:13 +00:00
return post.user.UrlPath() + "posts/" + post.id + "/"
}
2022-08-13 16:47:27 +00:00
func (post Post) FullUrl() string {
return post.user.FullUrl() + "posts/" + post.id + "/"
}
2022-08-03 17:41:13 +00:00
func (post Post) UrlMediaPath(filename string) string {
return post.UrlPath() + "media/" + filename
2022-07-23 15:19:47 +00:00
}
2022-07-21 17:44:07 +00:00
func (post Post) Title() string {
return post.title
}
2022-07-21 17:02:37 +00:00
func (post Post) ContentFile() string {
return path.Join(post.Dir(), "index.md")
}
func (post *Post) Meta() PostMeta {
if !post.metaLoaded {
post.LoadMeta()
}
return post.meta
}
2022-07-21 17:02:37 +00:00
func (post Post) Content() []byte {
// read file
data, _ := ioutil.ReadFile(post.ContentFile())
return data
}
2022-07-21 17:44:07 +00:00
2022-09-04 13:32:37 +00:00
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 {
2022-07-21 17:44:07 +00:00
data := post.Content()
// trim yaml block
// TODO this can be done nicer
trimmedData := bytes.TrimSpace(data)
// check first line is ---
if string(trimmedData[0:4]) == "---\n" {
trimmedData = trimmedData[4:]
// find --- end
end := bytes.Index(trimmedData, []byte("\n---\n"))
if end != -1 {
data = trimmedData[end+5:]
}
}
2022-08-21 09:31:48 +00:00
options := goldmark.WithRendererOptions()
if config, _ := post.user.repo.Config(); config.AllowRawHtml {
2022-08-21 09:31:48 +00:00
options = goldmark.WithRendererOptions(
html.WithUnsafe(),
)
}
2022-07-21 17:44:07 +00:00
markdown := goldmark.New(
2022-08-21 09:31:48 +00:00
options,
2022-07-21 17:44:07 +00:00
goldmark.WithExtensions(
// meta.Meta,
2022-07-27 19:26:37 +00:00
extension.GFM,
2022-07-21 17:44:07 +00:00
),
)
var buf bytes.Buffer
context := parser.NewContext()
if err := markdown.Convert(data, &buf, parser.WithContext(context)); err != nil {
panic(err)
}
return buf
2022-07-21 17:44:07 +00:00
}
2022-08-06 17:38:13 +00:00
func (post Post) Aliases() []string {
return post.Meta().Aliases
}
func (post *Post) LoadMeta() error {
data := post.Content()
// get yaml metadata block
meta := PostMeta{}
trimmedData := bytes.TrimSpace(data)
// check first line is ---
if string(trimmedData[0:4]) == "---\n" {
trimmedData = trimmedData[4:]
// find --- end
end := bytes.Index(trimmedData, []byte("\n---\n"))
if end != -1 {
metaData := trimmedData[:end]
err := yaml.Unmarshal(metaData, &meta)
if err != nil {
return err
}
}
}
post.meta = meta
return nil
2022-08-06 17:38:13 +00:00
}
2022-08-23 15:59:17 +00:00
2022-09-01 19:34:33 +00:00
func (post *Post) WebmentionFile(source string) string {
2022-08-31 18:43:01 +00:00
2022-08-23 15:59:17 +00:00
hash := sha256.Sum256([]byte(source))
hashStr := base64.URLEncoding.EncodeToString(hash[:])
2022-09-01 19:34:33 +00:00
return path.Join(post.WebmentionDir(), hashStr+".yml")
}
2022-09-04 13:03:16 +00:00
func (post *Post) PersistWebmention(webmention WebmentionIn) error {
2022-09-01 19:34:33 +00:00
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
// write to file
fileName := post.WebmentionFile(webmention.Source)
data, err := yaml.Marshal(webmention)
if err != nil {
return err
}
return os.WriteFile(fileName, data, 0644)
}
2022-09-04 13:03:16 +00:00
func (post *Post) Webmention(source string) (WebmentionIn, error) {
2022-09-01 19:34:33 +00:00
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
// Check if file exists
fileName := post.WebmentionFile(source)
if !fileExists(fileName) {
// return error if file doesn't exist
2022-09-04 13:03:16 +00:00
return WebmentionIn{}, fmt.Errorf("Webmention file not found: %s", source)
2022-09-01 19:34:33 +00:00
}
data, err := os.ReadFile(fileName)
if err != nil {
2022-09-04 13:03:16 +00:00
return WebmentionIn{}, err
2022-09-01 19:34:33 +00:00
}
2022-09-04 13:03:16 +00:00
mention := WebmentionIn{}
2022-09-01 19:34:33 +00:00
err = yaml.Unmarshal(data, &mention)
if err != nil {
2022-09-04 13:03:16 +00:00
return WebmentionIn{}, err
2022-09-01 19:34:33 +00:00
}
return mention, nil
}
func (post *Post) AddWebmention(source string) error {
// Check if file already exists
2022-09-01 19:34:33 +00:00
_, err := post.Webmention(source)
if err != nil {
2022-09-04 13:03:16 +00:00
webmention := WebmentionIn{
2022-09-01 19:34:33 +00:00
Source: source,
}
defer post.EnrichWebmention(source)
return post.PersistWebmention(webmention)
}
2022-09-01 19:34:33 +00:00
return nil
}
2022-09-04 13:32:37 +00:00
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)
}
2022-09-04 15:10:40 +00:00
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)
}
2022-09-01 19:34:33 +00:00
func (post *Post) EnrichWebmention(source string) error {
2022-09-04 15:10:40 +00:00
html, err := post.user.repo.HttpClient.Get(source)
if err == nil {
2022-09-01 19:34:33 +00:00
webmention, err := post.Webmention(source)
if err != nil {
return err
}
entry, err := post.user.repo.Parser.ParseHEntry(html)
if err == nil {
2022-09-01 19:34:33 +00:00
webmention.Title = entry.Title
return post.PersistWebmention(webmention)
}
}
2022-09-01 19:34:33 +00:00
return err
2022-08-23 15:59:17 +00:00
}
2022-09-04 13:03:16 +00:00
func (post *Post) Webmentions() []WebmentionIn {
2022-08-31 18:43:01 +00:00
// ensure dir exists
os.MkdirAll(post.WebmentionDir(), 0755)
2022-09-01 19:34:33 +00:00
files := listDir(post.WebmentionDir())
2022-09-04 13:03:16 +00:00
webmentions := []WebmentionIn{}
2022-09-01 19:34:33 +00:00
for _, file := range files {
data, err := os.ReadFile(path.Join(post.WebmentionDir(), file))
if err != nil {
continue
}
2022-09-04 13:03:16 +00:00
mention := WebmentionIn{}
2022-09-01 19:34:33 +00:00
err = yaml.Unmarshal(data, &mention)
if err != nil {
continue
}
webmentions = append(webmentions, mention)
}
2022-08-31 18:43:01 +00:00
2022-09-01 19:34:33 +00:00
return webmentions
2022-08-23 15:59:17 +00:00
}
2022-09-01 19:53:06 +00:00
2022-09-04 13:03:16 +00:00
func (post *Post) ApprovedWebmentions() []WebmentionIn {
2022-09-01 19:53:06 +00:00
webmentions := post.Webmentions()
2022-09-04 13:03:16 +00:00
approved := []WebmentionIn{}
2022-09-01 19:53:06 +00:00
for _, webmention := range webmentions {
if webmention.ApprovalStatus == "approved" {
approved = append(approved, webmention)
}
}
// sort by retrieved date
sort.Slice(approved, func(i, j int) bool {
return approved[i].RetrievedAt.After(approved[j].RetrievedAt)
})
return approved
}
2022-09-04 13:32:37 +00:00
func (post *Post) OutgoingWebmentions() []WebmentionOut {
status := post.Status()
return status.Webmentions
}
// ScanForLinks scans the post content for links and adds them to the
// `status.yml` file for the post. The links are not scanned by this function.
func (post *Post) ScanForLinks() error {
// this could be done in markdown parsing, but I don't want to
// rely on goldmark for this (yet)
postHtml, err := renderPostContent(post)
if err != nil {
return err
}
links, _ := post.user.repo.Parser.ParseLinks([]byte(postHtml))
for _, link := range links {
post.AddOutgoingWebmention(link)
}
return nil
}
2022-09-04 15:10:40 +00:00
func (post *Post) SendWebmention(webmention WebmentionOut) error {
defer post.UpdateOutgoingWebmention(&webmention)
webmention.ScannedAt = time.Now()
html, err := post.user.repo.HttpClient.Get(webmention.Target)
if err != nil {
// TODO handle error
webmention.Supported = false
return err
}
endpoint, err := post.user.repo.Parser.GetWebmentionEndpoint(html)
if err != nil {
// TODO handle error
webmention.Supported = false
return err
}
webmention.Supported = true
// send webmention
payload := url.Values{}
payload.Set("source", post.FullUrl())
payload.Set("target", webmention.Target)
_, err = post.user.repo.HttpClient.Post(endpoint, payload)
if err != nil {
// TODO handle error
return err
}
// update webmention status
webmention.LastSentAt = time.Now()
return nil
}