Micropub endpoint for my blog https://jlelse.blog/posts/hugo-micropub/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

346 lines
10 KiB

package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"strings"
"text/template"
"time"
)
type Entry struct {
content string
title string
date string
lastmod string
section string
tags []string
series []string
link string
slug string
replyLink string
replyTitle string
likeLink string
likeTitle string
syndicate []string
language string
translationKey string
images []Image
audio string
filename string
location string
token string
}
type Image struct {
url string
alt string
}
func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
if contentType == WwwForm {
err := r.ParseForm()
if err != nil {
return nil, errors.New("failed to parse Form")
}
return createEntryFromValueMap(r.Form)
} else if contentType == Multipart {
err := r.ParseMultipartForm(1024 * 1024 * 16)
if err != nil {
return nil, errors.New("failed to parse Multipart")
}
return createEntryFromValueMap(r.MultipartForm.Value)
} else if contentType == Json {
decoder := json.NewDecoder(r.Body)
parsedMfItem := &MicroformatItem{}
err := decoder.Decode(&parsedMfItem)
if err != nil {
return nil, errors.New("failed to parse Json")
}
return createEntryFromMicroformat(parsedMfItem)
} else {
return nil, errors.New("unsupported content-type")
}
}
func createEntryFromValueMap(values map[string][]string) (*Entry, error) {
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
return nil, errors.New("only entry type is supported so far")
}
entry := &Entry{}
if content, ok := values["content"]; ok {
entry.content = content[0]
}
if name, ok := values["name"]; ok {
entry.title = name[0]
}
if category, ok := values["category"]; ok {
entry.tags = category
} else if categories, ok := values["category[]"]; ok {
entry.tags = categories
}
if slug, ok := values["mp-slug"]; ok && len(slug) > 0 && slug[0] != "" {
entry.slug = slug[0]
}
if inReplyTo, ok := values["in-reply-to"]; ok {
entry.replyLink = inReplyTo[0]
}
if likeOf, ok := values["like-of"]; ok {
entry.likeLink = likeOf[0]
}
if bookmarkOf, ok := values["bookmark-of"]; ok {
entry.link = bookmarkOf[0]
}
if syndicate, ok := values["mp-syndicate-to"]; ok {
entry.syndicate = syndicate
} else if syndicates, ok := values["mp-syndicate-to[]"]; ok {
entry.syndicate = syndicates
}
if photo, ok := values["photo"]; ok {
entry.images = append(entry.images, Image{url: photo[0]})
} else if photos, ok := values["photo[]"]; ok {
for _, photo := range photos {
entry.images = append(entry.images, Image{url: photo})
}
}
if photoAlt, ok := values["mp-photo-alt"]; ok {
if len(entry.images) > 0 {
entry.images[0].alt = photoAlt[0]
}
} else if photoAlts, ok := values["mp-photo-alt[]"]; ok {
for i, photoAlt := range photoAlts {
if len(entry.images) > i {
entry.images[i].alt = photoAlt
}
}
}
if audio, ok := values["audio"]; ok {
entry.audio = audio[0]
} else if audio, ok := values["audio[]"]; ok {
entry.audio = audio[0]
}
if token, ok := values["access_token"]; ok {
entry.token = "Bearer " + token[0]
}
err := computeExtraSettings(entry)
if err != nil {
return nil, err
}
return entry, nil
}
func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) {
if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" {
return nil, errors.New("only entry type is supported so far")
}
entry := &Entry{}
if mfEntry.Properties != nil && len(mfEntry.Properties.Content) == 1 && len(mfEntry.Properties.Content[0]) > 0 {
entry.content = mfEntry.Properties.Content[0]
}
if len(mfEntry.Properties.Name) == 1 {
entry.title = mfEntry.Properties.Name[0]
}
if len(mfEntry.Properties.Category) > 0 {
entry.tags = mfEntry.Properties.Category
}
if len(mfEntry.Properties.MpSlug) == 1 && len(mfEntry.Properties.MpSlug[0]) > 0 {
entry.slug = mfEntry.Properties.MpSlug[0]
}
if len(mfEntry.Properties.InReplyTo) == 1 {
entry.replyLink = mfEntry.Properties.InReplyTo[0]
}
if len(mfEntry.Properties.LikeOf) == 1 {
entry.likeLink = mfEntry.Properties.LikeOf[0]
}
if len(mfEntry.Properties.BookmarkOf) == 1 {
entry.link = mfEntry.Properties.BookmarkOf[0]
}
if len(mfEntry.Properties.MpSyndicateTo) > 0 {
entry.syndicate = mfEntry.Properties.MpSyndicateTo
}
if len(mfEntry.Properties.Photo) > 0 {
for _, photo := range mfEntry.Properties.Photo {
if theString, justString := photo.(string); justString {
entry.images = append(entry.images, Image{url: theString})
} else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto {
image := Image{}
// Micropub spec says "value" is correct, but not sure about that
if photoUrl, ok := thePhoto["value"].(string); ok {
image.url = photoUrl
}
if alt, ok := thePhoto["alt"].(string); ok {
image.alt = alt
}
entry.images = append(entry.images, image)
}
}
}
if len(mfEntry.Properties.Audio) > 0 {
entry.audio = mfEntry.Properties.Audio[0]
}
err := computeExtraSettings(entry)
if err != nil {
return nil, err
}
return entry, nil
}
func computeExtraSettings(entry *Entry) error {
now := time.Now()
// Set date
entry.date = now.Format(time.RFC3339)
// Find settings hidden in content
var filteredContent bytes.Buffer
contentScanner := bufio.NewScanner(strings.NewReader(entry.content))
for contentScanner.Scan() {
text := contentScanner.Text()
if strings.HasPrefix(text, "section: ") {
// Section
entry.section = strings.TrimPrefix(text, "section: ")
} else if strings.HasPrefix(text, "title: ") {
// Title
entry.title = strings.TrimPrefix(text, "title: ")
} else if strings.HasPrefix(text, "slug: ") {
// Slug
entry.slug = strings.TrimPrefix(text, "slug: ")
} else if strings.HasPrefix(text, "tags: ") {
// Tags
entry.tags = strings.Split(strings.TrimPrefix(text, "tags: "), ",")
} else if strings.HasPrefix(text, "series: ") {
// Series
entry.series = strings.Split(strings.TrimPrefix(text, "series: "), ",")
} else if strings.HasPrefix(text, "link: ") {
// Link
entry.link = strings.TrimPrefix(text, "link: ")
} else if strings.HasPrefix(text, "reply-link: ") {
// Reply link
entry.replyLink = strings.TrimPrefix(text, "reply-link: ")
} else if strings.HasPrefix(text, "reply-title: ") {
// Reply title
entry.replyTitle = strings.TrimPrefix(text, "reply-title: ")
} else if strings.HasPrefix(text, "like-link: ") {
// Like link
entry.likeLink = strings.TrimPrefix(text, "like-link: ")
} else if strings.HasPrefix(text, "like-title: ") {
// Like title
entry.likeTitle = strings.TrimPrefix(text, "like-title: ")
} else if strings.HasPrefix(text, "language: ") {
// Language
entry.language = strings.TrimPrefix(text, "language: ")
} else if strings.HasPrefix(text, "translationkey: ") {
// Translation key
entry.translationKey = strings.TrimPrefix(text, "translationkey: ")
} else if strings.HasPrefix(text, "images: ") {
// Images
for _, image := range strings.Split(strings.TrimPrefix(text, "images: "), ",") {
entry.images = append(entry.images, Image{url: image})
}
} else if strings.HasPrefix(text, "image-alts: ") {
// Image alt
for i, alt := range strings.Split(strings.TrimPrefix(text, "image-alts: "), ",") {
entry.images[i].alt = alt
}
} else if strings.HasPrefix(text, "audio: ") {
// Audio
entry.audio = strings.TrimPrefix(text, "audio: ")
} else {
_, _ = fmt.Fprintln(&filteredContent, text)
}
}
entry.content = filteredContent.String()
// Check if content contains images or add them
for _, image := range entry.images {
if !strings.Contains(entry.content, image.url) {
if len(image.alt) > 0 {
entry.content += "\n![" + image.alt + "](" + image.url + " \"" + image.alt + "\")\n"
} else {
entry.content += "\n![](" + image.url + ")\n"
}
}
}
// Compute slug if empty
if len(entry.slug) == 0 || entry.slug == "" {
random := generateRandomString(now, 5)
entry.slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random)
}
// Set language
if len(entry.language) == 0 {
entry.language = DefaultLanguage
}
// Compute filename and location
lang := Languages[entry.language]
contentFolder := lang.ContentDir
localizedBlogUrl := BlogUrl
if len(lang.BlogUrl) != 0 {
localizedBlogUrl = lang.BlogUrl
}
if len(entry.section) == 0 {
entry.section = lang.DefaultSection
}
entry.section = strings.ToLower(entry.section)
section := lang.Sections[entry.section]
pathVars := struct {
LocalContentFolder string
LocalBlogUrl string
Year int
Month int
Slug string
Section string
}{
LocalContentFolder: contentFolder,
LocalBlogUrl: localizedBlogUrl,
Year: now.Year(),
Month: int(now.Month()),
Slug: entry.slug,
Section: entry.section,
}
filenameTmpl, err := template.New("filename").Parse(section.FilenameTemplate)
if err != nil {
return errors.New("failed to parse filename template")
}
filename := new(bytes.Buffer)
err = filenameTmpl.Execute(filename, pathVars)
if err != nil {
return errors.New("failed to execute filename template")
}
entry.filename = filename.String()
locationTmpl, err := template.New("location").Parse(section.LocationTemplate)
if err != nil {
return errors.New("failed to parse location template")
}
location := new(bytes.Buffer)
err = locationTmpl.Execute(location, pathVars)
if err != nil {
return errors.New("failed to execute location template")
}
entry.location = location.String()
return nil
}
func generateRandomString(now time.Time, n int) string {
rand.Seed(now.UnixNano())
letters := []rune("abcdefghijklmnopqrstuvwxyz")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
func WriteEntry(entry *Entry) (location string, err error) {
file, err := WriteHugoPost(entry)
if err != nil {
return
}
err = SelectedStorage.CreateFile(entry.filename, file, entry.title)
if err != nil {
return
}
location = entry.location
return
}