owl-blogs/post.go

467 lines
10 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-09-10 12:22:06 +00:00
"errors"
2022-09-04 15:10:40 +00:00
"net/url"
2022-10-13 19:00:28 +00:00
"os"
2022-07-21 17:02:37 +00:00
"path"
2022-09-01 19:53:06 +00:00
"sort"
2022-09-09 19:14:49 +00:00
"sync"
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
)
2022-12-05 20:09:23 +00:00
type GenericPost struct {
user *User
id string
metaLoaded bool
meta PostMeta
2022-09-09 19:14:49 +00:00
wmLock sync.Mutex
2022-07-21 17:02:37 +00:00
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) TemplateDir() string {
2022-12-05 18:51:42 +00:00
return "article"
}
2022-12-05 20:09:23 +00:00
type Post interface {
2022-12-05 18:51:42 +00:00
TemplateDir() string
2022-12-05 20:09:23 +00:00
// Actual Data
2022-12-05 18:51:42 +00:00
User() *User
2022-12-05 20:09:23 +00:00
Id() string
2022-12-05 18:51:42 +00:00
Title() string
Meta() PostMeta
Content() []byte
RenderedContent() string
Aliases() []string
2022-12-05 20:09:23 +00:00
// Filesystem
Dir() string
MediaDir() string
ContentFile() string
// Urls
UrlPath() string
FullUrl() string
UrlMediaPath(filename string) string
// Webmentions Support
2022-12-05 18:51:42 +00:00
IncomingWebmentions() []WebmentionIn
OutgoingWebmentions() []WebmentionOut
PersistIncomingWebmention(webmention WebmentionIn) error
PersistOutgoingWebmention(webmention *WebmentionOut) error
AddIncomingWebmention(source string) error
EnrichWebmention(webmention WebmentionIn) error
ApprovedIncomingWebmentions() []WebmentionIn
ScanForLinks() error
SendWebmention(webmention WebmentionOut) error
}
2022-12-05 19:31:48 +00:00
type ReplyData struct {
2022-10-10 18:59:06 +00:00
Url string `yaml:"url"`
Text string `yaml:"text"`
}
2022-12-05 19:31:48 +00:00
type BookmarkData struct {
2022-12-04 14:45:51 +00:00
Url string `yaml:"url"`
Text string `yaml:"text"`
}
2022-10-10 18:59:06 +00:00
type PostMeta struct {
2022-12-05 19:31:48 +00:00
Type string `yaml:"type"`
Title string `yaml:"title"`
Description string `yaml:"description"`
Aliases []string `yaml:"aliases"`
Date time.Time `yaml:"date"`
Draft bool `yaml:"draft"`
Reply ReplyData `yaml:"reply"`
Bookmark BookmarkData `yaml:"bookmark"`
2022-09-11 15:25:26 +00:00
}
2022-09-11 15:34:50 +00:00
func (pm PostMeta) FormattedDate() string {
return pm.Date.Format("02-01-2006 15:04:05")
}
2022-09-11 15:25:26 +00:00
func (pm *PostMeta) UnmarshalYAML(unmarshal func(interface{}) error) error {
type T struct {
2022-12-05 19:31:48 +00:00
Type string `yaml:"type"`
Title string `yaml:"title"`
Description string `yaml:"description"`
Aliases []string `yaml:"aliases"`
Draft bool `yaml:"draft"`
Reply ReplyData `yaml:"reply"`
Bookmark BookmarkData `yaml:"bookmark"`
2022-09-11 15:25:26 +00:00
}
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
}
2022-11-29 19:36:50 +00:00
pm.Type = t.Type
if pm.Type == "" {
pm.Type = "article"
}
2022-09-11 15:25:26 +00:00
pm.Title = t.Title
2022-10-19 19:14:31 +00:00
pm.Description = t.Description
2022-09-11 15:25:26 +00:00
pm.Aliases = t.Aliases
pm.Draft = t.Draft
2022-10-10 18:59:06 +00:00
pm.Reply = t.Reply
2022-12-04 14:45:51 +00:00
pm.Bookmark = t.Bookmark
2022-09-11 15:25:26 +00:00
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
}
type PostWebmetions struct {
Incoming []WebmentionIn `ymal:"incoming"`
Outgoing []WebmentionOut `ymal:"outgoing"`
2022-09-04 13:03:16 +00:00
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) Id() string {
2022-08-03 17:41:13 +00:00
return post.id
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) User() *User {
2022-10-07 17:51:13 +00:00
return post.user
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) Dir() string {
2022-07-21 17:02:37 +00:00
return path.Join(post.user.Dir(), "public", post.id)
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) IncomingWebmentionsFile() string {
return path.Join(post.Dir(), "incoming_webmentions.yml")
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) OutgoingWebmentionsFile() string {
return path.Join(post.Dir(), "outgoing_webmentions.yml")
2022-09-04 13:32:37 +00:00
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) MediaDir() string {
2022-08-01 17:50:29 +00:00
return path.Join(post.Dir(), "media")
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) UrlPath() string {
2022-08-03 17:41:13 +00:00
return post.user.UrlPath() + "posts/" + post.id + "/"
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) FullUrl() string {
2022-08-13 16:47:27 +00:00
return post.user.FullUrl() + "posts/" + post.id + "/"
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) UrlMediaPath(filename string) string {
2022-08-03 17:41:13 +00:00
return post.UrlPath() + "media/" + filename
2022-07-23 15:19:47 +00:00
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) Title() string {
2022-12-05 18:11:48 +00:00
return post.Meta().Title
2022-07-21 17:44:07 +00:00
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) ContentFile() string {
2022-07-21 17:02:37 +00:00
return path.Join(post.Dir(), "index.md")
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) Meta() PostMeta {
if !post.metaLoaded {
post.LoadMeta()
}
return post.meta
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) Content() []byte {
2022-07-21 17:02:37 +00:00
// read file
2022-10-13 19:00:28 +00:00
data, _ := os.ReadFile(post.ContentFile())
2022-07-21 17:02:37 +00:00
return data
}
2022-07-21 17:44:07 +00:00
2022-12-05 20:09:23 +00:00
func (post *GenericPost) RenderedContent() string {
2022-07-21 17:44:07 +00:00
data := post.Content()
// trim yaml block
// TODO this can be done nicer
trimmedData := bytes.TrimSpace(data)
2022-12-05 17:46:00 +00:00
// ensure that data ends with a newline
trimmedData = append(trimmedData, []byte("\n")...)
// 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)
}
2022-12-01 18:37:13 +00:00
return buf.String()
2022-07-21 17:44:07 +00:00
}
2022-08-06 17:38:13 +00:00
2022-12-05 20:09:23 +00:00
func (post *GenericPost) Aliases() []string {
return post.Meta().Aliases
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) LoadMeta() error {
data := post.Content()
// get yaml metadata block
meta := PostMeta{}
trimmedData := bytes.TrimSpace(data)
2022-12-05 19:47:52 +00:00
// ensure that data ends with a newline
trimmedData = append(trimmedData, []byte("\n")...)
// check first line is ---
if string(trimmedData[0:4]) == "---\n" {
trimmedData = trimmedData[4:]
// find --- end
2022-10-10 18:59:06 +00:00
end := bytes.Index(trimmedData, []byte("---\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-12-05 20:09:23 +00:00
func (post *GenericPost) IncomingWebmentions() []WebmentionIn {
2022-09-10 11:44:25 +00:00
// return parsed webmentions
fileName := post.IncomingWebmentionsFile()
2022-09-10 11:44:25 +00:00
if !fileExists(fileName) {
return []WebmentionIn{}
2022-09-10 11:44:25 +00:00
}
webmentions := []WebmentionIn{}
loadFromYaml(fileName, &webmentions)
2022-09-10 11:44:25 +00:00
return webmentions
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) OutgoingWebmentions() []WebmentionOut {
// return parsed webmentions
fileName := post.OutgoingWebmentionsFile()
if !fileExists(fileName) {
return []WebmentionOut{}
}
2022-09-10 11:44:25 +00:00
webmentions := []WebmentionOut{}
loadFromYaml(fileName, &webmentions)
2022-09-10 11:44:25 +00:00
return webmentions
2022-09-10 11:44:25 +00:00
}
2022-09-10 12:04:13 +00:00
// PersistWebmentionOutgoing persists incoming webmention
2022-12-05 20:09:23 +00:00
func (post *GenericPost) PersistIncomingWebmention(webmention WebmentionIn) error {
2022-09-10 12:04:13 +00:00
post.wmLock.Lock()
defer post.wmLock.Unlock()
wms := post.IncomingWebmentions()
2022-09-01 19:34:33 +00:00
// if target is not in status, add it
replaced := false
for i, t := range wms {
if t.Source == webmention.Source {
wms[i].UpdateWith(webmention)
replaced = true
break
}
2022-09-01 19:34:33 +00:00
}
if !replaced {
wms = append(wms, webmention)
}
err := saveToYaml(post.IncomingWebmentionsFile(), wms)
if err != nil {
return err
}
return nil
2022-09-10 11:44:25 +00:00
}
// PersistOutgoingWebmention persists a webmention to the webmention file.
2022-12-05 20:09:23 +00:00
func (post *GenericPost) PersistOutgoingWebmention(webmention *WebmentionOut) error {
2022-09-10 11:44:25 +00:00
post.wmLock.Lock()
defer post.wmLock.Unlock()
wms := post.OutgoingWebmentions()
2022-09-10 11:44:25 +00:00
// if target is not in webmention, add it
replaced := false
for i, t := range wms {
2022-09-10 11:44:25 +00:00
if t.Target == webmention.Target {
wms[i].UpdateWith(*webmention)
2022-09-10 11:44:25 +00:00
replaced = true
break
}
}
if !replaced {
wms = append(wms, *webmention)
2022-09-10 11:44:25 +00:00
}
err := saveToYaml(post.OutgoingWebmentionsFile(), wms)
if err != nil {
return err
}
return nil
}
2022-09-01 19:34:33 +00:00
2022-12-05 20:09:23 +00:00
func (post *GenericPost) AddIncomingWebmention(source string) error {
// Check if file already exists
2022-09-10 12:04:13 +00:00
wm := WebmentionIn{
Source: source,
}
2022-09-01 19:34:33 +00:00
2022-09-10 12:04:13 +00:00
defer func() {
go post.EnrichWebmention(wm)
}()
return post.PersistIncomingWebmention(wm)
}
2022-09-09 19:14:49 +00:00
2022-12-05 20:09:23 +00:00
func (post *GenericPost) EnrichWebmention(webmention WebmentionIn) error {
2022-09-10 12:04:13 +00:00
resp, err := post.user.repo.HttpClient.Get(webmention.Source)
if err == nil {
entry, err := post.user.repo.Parser.ParseHEntry(resp)
if err == nil {
2022-09-01 19:34:33 +00:00
webmention.Title = entry.Title
return post.PersistIncomingWebmention(webmention)
}
}
2022-09-01 19:34:33 +00:00
return err
2022-08-23 15:59:17 +00:00
}
2022-12-05 20:09:23 +00:00
func (post *GenericPost) ApprovedIncomingWebmentions() []WebmentionIn {
webmentions := post.IncomingWebmentions()
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
// 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.
2022-12-05 20:09:23 +00:00
func (post *GenericPost) ScanForLinks() error {
2022-09-04 13:32:37 +00:00
// this could be done in markdown parsing, but I don't want to
// rely on goldmark for this (yet)
2022-09-06 18:00:12 +00:00
postHtml := post.RenderedContent()
2022-12-01 18:37:13 +00:00
links, _ := post.user.repo.Parser.ParseLinksFromString(postHtml)
2022-10-10 19:06:33 +00:00
// add reply url if set
if post.Meta().Reply.Url != "" {
links = append(links, post.Meta().Reply.Url)
}
2022-09-04 13:32:37 +00:00
for _, link := range links {
2022-09-10 11:44:25 +00:00
post.PersistOutgoingWebmention(&WebmentionOut{
Target: link,
2022-09-10 12:04:13 +00:00
})
2022-09-04 13:32:37 +00:00
}
return nil
}
2022-09-04 15:10:40 +00:00
2022-12-05 20:09:23 +00:00
func (post *GenericPost) SendWebmention(webmention WebmentionOut) error {
2022-09-10 12:04:13 +00:00
defer post.PersistOutgoingWebmention(&webmention)
2022-09-10 12:16:22 +00:00
// 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 {
2022-09-10 12:22:06 +00:00
return errors.New("did not scan. Last scan was less than 7 days ago")
2022-09-10 12:16:22 +00:00
}
2022-09-04 15:10:40 +00:00
webmention.ScannedAt = time.Now()
resp, err := post.user.repo.HttpClient.Get(webmention.Target)
2022-09-04 15:10:40 +00:00
if err != nil {
webmention.Supported = false
return err
}
2022-09-06 17:47:15 +00:00
endpoint, err := post.user.repo.Parser.GetWebmentionEndpoint(resp)
2022-09-04 15:10:40 +00:00
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)
2022-09-04 15:10:40 +00:00
if err != nil {
return err
}
// update webmention status
webmention.LastSentAt = time.Now()
return nil
}