Compare commits

...

43 Commits

Author SHA1 Message Date
Jan-Lukas Else 5bac6cf0f1 Update dependencies 2020-09-14 15:51:07 +02:00
Jan-Lukas Else ee68238f30 Revert "Simplify image compression"
This reverts commit 0f2ac430ce.
2020-07-31 07:21:32 +02:00
Jan-Lukas Else 0f2ac430ce Simplify image compression 2020-07-21 21:40:39 +02:00
Jan-Lukas Else 05e6f383c2 Remove Telegram 2020-07-21 20:50:27 +02:00
Jan-Lukas Else 22355825ed Update dependencies 2020-07-21 20:42:56 +02:00
Jan-Lukas Else 491ce51bd4 Add alternative way to add image alt text 2020-07-21 20:31:50 +02:00
Jan-Lukas Else 7cbaf6ad31 Remove webmentions 2020-07-21 20:31:20 +02:00
Jan-Lukas Else 9aebddd251 Update dependencies 2020-06-09 12:00:30 +02:00
Jan-Lukas Else ed21c00622 Remove CDN purgin 2020-05-17 12:06:00 +02:00
Jan-Lukas Else c0c480c660 Update dependencies 2020-05-14 22:35:50 +02:00
Jan-Lukas Else 0292344035 Fix BunnyCDN media upload 2020-05-14 22:32:00 +02:00
Jan-Lukas Else ce0baf0086 Return post location with cache bypass 2020-05-11 09:05:26 +02:00
Jan-Lukas Else 79b28111f5 Add drone CI 2020-04-26 15:00:57 +02:00
Jan-Lukas Else e716188dff Remove sending of webmentions 2020-04-25 12:19:57 +02:00
Jan-Lukas Else 8a72be6fc2 Improvements 2020-04-20 23:04:01 +02:00
Jan-Lukas Else 580724e9f9 Prevent empty commits and webmention spam 2020-04-20 21:46:59 +02:00
Jan-Lukas Else 09e3b77453 Use two more references 2020-04-20 20:59:11 +02:00
Jan-Lukas Else cfc8911b77 Use mutex for file operations 2020-04-20 20:53:45 +02:00
Jan-Lukas Else 9027f6734b Add support for series 2020-03-31 17:02:05 +02:00
Jan-Lukas Else d747ceddf3 Add support for deleted mentions 2020-03-26 18:32:14 +01:00
Jan-Lukas Else 7c1e6baa3f Upgrades 2020-03-26 17:59:55 +01:00
Jan-Lukas Else cbf53ccb23 Use yaml for config, build filename and slug using configurable templates 2020-03-26 15:51:09 +01:00
Jan-Lukas Else 442395a83c Simplify config request 2020-03-26 12:03:48 +01:00
Jan-Lukas Else 0faf4c8e39 Use simple http request to send Telegram message 2020-03-26 11:53:12 +01:00
Jan-Lukas Else 686a53707d Improve and refactor code that generates hugo post file 2020-03-26 11:08:36 +01:00
Jan-Lukas Else 4997f1092e Remove source query, simplify code 2020-03-26 10:49:13 +01:00
Jan-Lukas Else 7e819f4017 Support alternative text for photos 2020-03-20 18:28:57 +01:00
Jan-Lukas Else f4bac41d11 Fix 2020-03-20 15:32:36 +01:00
Jan-Lukas Else 82d9057b10 Changes
* Use Go 1.14
* Remove jsonpub and microblog.pub and Gitea support
* Use generic git library, so all Git providers are supported
2020-03-20 14:58:11 +01:00
Jan-Lukas Else 0ee80b3798 Add Jsonpub hook 2020-03-04 17:41:44 +01:00
Jan-Lukas Else af67b7ebbb Boost instead of Post on Microblog.pub when ActivityStreams configured 2020-02-19 19:27:19 +01:00
Jan-Lukas Else 126db98c64 Scale down images to 2000 with Tinify 2020-01-23 23:38:33 +01:00
Jan-Lukas Else 1fb3f28aea low case file extension 2020-01-19 11:01:24 +01:00
Jan-Lukas Else 9ca7da9ae5 Add support for image compression 2020-01-19 10:55:18 +01:00
Jan-Lukas Else 962818c931 Add support for images and audio 2020-01-19 08:36:10 +01:00
Jan-Lukas Else 108571ac15 Update sleep timers 2020-01-16 13:10:37 +01:00
Jan-Lukas Else 85adb26acd Fix panic 2020-01-12 18:26:21 +01:00
Jan-Lukas Else 2872afbc28 Add support for posting German posts 2020-01-12 18:17:40 +01:00
Jan-Lukas Else fb76567cef Tweak link filtering when sending Webmention 2020-01-12 18:13:18 +01:00
Jan-Lukas Else 1c3cc6480f Make Media Storage optional 2020-01-12 17:48:05 +01:00
Jan-Lukas Else f71738cde4 Allow empty content 2020-01-12 17:30:32 +01:00
Jan-Lukas Else 2136265825 Fix mediaendpoint auth 2020-01-10 11:14:47 +01:00
Jan-Lukas Else 9a41fb90b9 Accept webmention without trailing slash when blogUrl has it 2020-01-10 11:14:35 +01:00
23 changed files with 825 additions and 995 deletions

20
.drone.yml Normal file
View File

@ -0,0 +1,20 @@
kind: pipeline
name: default
steps:
- name: publish
image: plugins/docker
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: quay.io/jlelse/hugo-micropub
registry: quay.io
tags: latest
when:
branch:
- master
event:
exclude:
- pull_request

4
.gitignore vendored
View File

@ -1,2 +1,4 @@
.idea .idea
*.iml *.iml
config.yml
hugo-micropub

View File

@ -1,9 +1,9 @@
FROM golang:1.13-alpine as build FROM golang:1.15-alpine3.12 as build
ADD . /app ADD . /app
WORKDIR /app WORKDIR /app
RUN go build RUN go build
FROM alpine:3.11 FROM alpine:3.12
RUN apk add --no-cache tzdata ca-certificates RUN apk add --no-cache tzdata ca-certificates
COPY --from=build /app/hugo-micropub /bin/ COPY --from=build /app/hugo-micropub /bin/
EXPOSE 5555 EXPOSE 5555

27
cdn.go
View File

@ -1,27 +0,0 @@
package main
import (
"net/http"
)
type Cdn interface {
// Purge url from CDN
Purge(url string)
}
// BunnyCDN
type BunnyCdn struct {
// Access Key
key string
}
var bunnyCdnUrl = "https://bunnycdn.com"
func (cdn *BunnyCdn) Purge(url string) {
client := &http.Client{}
req, _ := http.NewRequest("POST", bunnyCdnUrl+"/api/purge?url="+url, nil)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("AccessKey", cdn.key)
_, _ = client.Do(req)
}

View File

@ -1,30 +0,0 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestBunnyCdn_Purge(t *testing.T) {
cdn := &BunnyCdn{
key: "testkey",
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if expectedUrl := "/api/purge?url=https://test.url/test"; r.URL.String() != expectedUrl {
t.Errorf("Wrong URL: Got %s, but expected %s", r.URL.String(), expectedUrl)
}
if expectedContentType := "application/json"; r.Header.Get("Content-Type") != expectedContentType {
t.Errorf("Wrong Content-Type Header: Got %s, but expected %s", r.Header.Get("Content-Type"), expectedContentType)
}
if expectedAccept := "application/json"; r.Header.Get("Accept") != expectedAccept {
t.Errorf("Wrong Accept Header: Got %s, but expected %s", r.Header.Get("Accept"), expectedAccept)
}
if r.Header.Get("AccessKey") != cdn.key {
t.Errorf("Wrong AccessKey Header: Got %s, but expected %s", r.Header.Get("AccessKey"), cdn.key)
}
}))
defer ts.Close()
bunnyCdnUrl = ts.URL
cdn.Purge("https://test.url/test")
}

198
config.go
View File

@ -2,20 +2,23 @@ package main
import ( import (
"errors" "errors"
"github.com/caarlos0/env/v6"
"log" "log"
"os"
"strings"
"sync"
"gopkg.in/yaml.v3"
) )
var ( var (
BlogUrl string BlogUrl string
IgnoredWebmentionUrls []string MediaEndpointUrl string
SyndicationTargets []SyndicationTarget SyndicationTargets []SyndicationTarget
SelectedStorage Storage SelectedStorage Storage
SelectedMediaStorage MediaStorage SelectedMediaStorage MediaStorage
SelectedCdn Cdn SelectedImageCompression ImageCompression
SelectedSocials Socials DefaultLanguage string
SelectedNotificationServices NotificationServices Languages map[string]Language
MediaEndpointUrl string
) )
type SyndicationTarget struct { type SyndicationTarget struct {
@ -23,34 +26,101 @@ type SyndicationTarget struct {
Name string `json:"name"` Name string `json:"name"`
} }
type config struct { type YamlConfig struct {
BlogUrl string `env:"BLOG_URL,required"` BlogUrl string `yaml:"blogUrl"`
BaseUrl string `env:"BASE_URL,required"` BaseUrl string `yaml:"baseUrl"`
MediaUrl string `env:"MEDIA_URL"` MediaUrl string `yaml:"mediaUrl"`
GiteaEndpoint string `env:"GITEA_ENDPOINT"` DefaultLanguage string `yaml:"defaultLang"`
GiteaToken string `env:"GITEA_TOKEN"` Languages map[string]Language `yaml:"languages"`
BunnyCdnKey string `env:"BUNNY_CDN_KEY"` Git GitConfig `yaml:"git"`
BunnyCdnStorageKey string `env:"BUNNY_CDN_STORAGE_KEY"` BunnyCdn BunnyCdnConfig `yaml:"bunnyCdn"`
BunnyCdnStorageName string `env:"BUNNY_CDN_STORAGE_NAME"` Tinify TinifyConfig `yaml:"tinify"`
MicroblogUrl string `env:"MICROBLOG_URL"` SyndicationTargets []string `yaml:"syndication"`
MicroblogToken string `env:"MICROBLOG_TOKEN"` }
TelegramUserId int64 `env:"TELEGRAM_USER_ID"`
TelegramBotToken string `env:"TELEGRAM_BOT_TOKEN"` type BunnyCdnConfig struct {
IgnoredWebmentionUrls []string `env:"WEBMENTION_IGNORED" envSeparator:","` StorageKey string `yaml:"storageKey"`
SyndicationTargets []string `env:"SYNDICATION" envSeparator:","` StorageName string `yaml:"storageName"`
}
type TinifyConfig struct {
Key string `yaml:"key"`
}
type GitConfig struct {
Filepath string `yaml:"filepath"`
Url string `yaml:"url"`
Username string `yaml:"username"`
Password string `yaml:"password"`
AuthorName string `yaml:"authorName"`
AuthorEmail string `yaml:"authorEmail"`
}
type Language struct {
BlogUrl string `yaml:"blogUrl"`
ContentDir string `yaml:"contentDir"`
DefaultSection string `yaml:"defaultSection"`
Sections map[string]Section `yaml:"sections"`
}
type Section struct {
FilenameTemplate string `yaml:"file"`
LocationTemplate string `yaml:"location"`
} }
func initConfig() (err error) { func initConfig() (err error) {
cfg := config{} configFileName, configSet := os.LookupEnv("CONFIG")
if err := env.Parse(&cfg); err != nil { if !configSet {
return errors.New("failed to parse config, probably not all required env vars set") configFileName = "config.yml"
}
configFile, err := os.Open(configFileName)
if err != nil {
return errors.New("failed to open config file")
}
cfg := YamlConfig{}
err = yaml.NewDecoder(configFile).Decode(&cfg)
if err != nil {
return errors.New("failed to parse yaml")
} }
// Blog URL (required) // Blog URL (required)
if len(cfg.BlogUrl) < 1 {
return errors.New("blogUrl not configured")
}
if !strings.HasSuffix(cfg.BlogUrl, "/") {
return errors.New("missing trailing slash in configured blogUrl")
}
BlogUrl = cfg.BlogUrl BlogUrl = cfg.BlogUrl
// Media endpoint // Media endpoint (required)
MediaEndpointUrl = cfg.BaseUrl + "/media" if len(cfg.BaseUrl) < 1 {
// Ignored Webmention URLs (optional) return errors.New("baseUrl not configured")
IgnoredWebmentionUrls = cfg.IgnoredWebmentionUrls }
if len(cfg.MediaUrl) < 1 {
return errors.New("mediaUrl not configured")
}
if !strings.HasSuffix(cfg.BaseUrl, "/") {
return errors.New("missing trailing slash in configured baseUrl")
}
MediaEndpointUrl = cfg.BaseUrl + "media"
// Languages (required)
if len(cfg.DefaultLanguage) < 1 {
return errors.New("no default language configured")
}
DefaultLanguage = cfg.DefaultLanguage
if len(cfg.Languages) > 0 {
for _, lang := range cfg.Languages {
if len(lang.ContentDir) < 1 || len(lang.DefaultSection) < 1 || len(lang.Sections) < 1 {
return errors.New("language not completely configured")
}
for _, section := range lang.Sections {
if len(section.FilenameTemplate) < 1 || len(section.LocationTemplate) < 1 {
return errors.New("section not completely configured")
}
}
}
Languages = cfg.Languages
} else {
return errors.New("no languages configured")
}
// Syndication Targets (optional) // Syndication Targets (optional)
targets := make([]SyndicationTarget, 0) targets := make([]SyndicationTarget, 0)
for _, url := range cfg.SyndicationTargets { for _, url := range cfg.SyndicationTargets {
@ -62,11 +132,16 @@ func initConfig() (err error) {
SyndicationTargets = targets SyndicationTargets = targets
// Find selected storage // Find selected storage
SelectedStorage = func() Storage { SelectedStorage = func() Storage {
// Gitea // Git
if len(cfg.GiteaEndpoint) > 0 && len(cfg.GiteaToken) >= 0 { if len(cfg.Git.Filepath) > 0 && len(cfg.Git.Url) > 0 && len(cfg.Git.Username) > 0 && len(cfg.Git.Password) > 0 && len(cfg.Git.AuthorName) > 0 && len(cfg.Git.AuthorEmail) > 0 {
return &Gitea{ return &Git{
endpoint: cfg.GiteaEndpoint, filepath: cfg.Git.Filepath,
token: cfg.GiteaToken, url: cfg.Git.Url,
username: cfg.Git.Username,
password: cfg.Git.Password,
name: cfg.Git.AuthorName,
email: cfg.Git.AuthorEmail,
lock: &sync.Mutex{},
} }
} }
return nil return nil
@ -74,61 +149,34 @@ func initConfig() (err error) {
if SelectedStorage == nil { if SelectedStorage == nil {
return errors.New("no storage configured") return errors.New("no storage configured")
} }
// Find selected media storage // Find selected media storage (Optional)
SelectedMediaStorage = func() MediaStorage { SelectedMediaStorage = func() MediaStorage {
// BunnyCDN // BunnyCDN
if len(cfg.BunnyCdnStorageKey) > 0 && len(cfg.BunnyCdnStorageName) > 0 && len(cfg.MediaUrl) > 0 { // MEDIA_URL needs trailing slash too
if len(cfg.BunnyCdn.StorageKey) > 0 && len(cfg.BunnyCdn.StorageName) > 0 && len(cfg.MediaUrl) > 0 && strings.HasSuffix(cfg.MediaUrl, "/") {
return &BunnyCdnStorage{ return &BunnyCdnStorage{
key: cfg.BunnyCdnStorageKey, key: cfg.BunnyCdn.StorageKey,
storageZoneName: cfg.BunnyCdnStorageName, storageZoneName: cfg.BunnyCdn.StorageName,
baseLocation: cfg.MediaUrl, baseLocation: cfg.MediaUrl,
} }
} }
return nil return nil
}() }()
if SelectedMediaStorage == nil { if SelectedMediaStorage == nil {
return errors.New("no media storage configured") log.Println("no media storage configured")
} }
// Find selected CDN (optional) // Find configured image compression service (optional)
SelectedCdn = func() Cdn { SelectedImageCompression = func() ImageCompression {
// BunnyCDN (optional) // Tinify
if len(cfg.BunnyCdnKey) > 0 { if len(cfg.Tinify.Key) > 0 {
return &BunnyCdn{key: cfg.BunnyCdnKey} return &Tinify{
key: cfg.Tinify.Key,
}
} }
return nil return nil
}() }()
if SelectedCdn == nil { if SelectedImageCompression == nil {
log.Println("no CDN configured") log.Println("no image compression configured")
}
// Find configured social networks (optional)
SelectedSocials = func() Socials {
var socials []Social = nil
// Microblog.pub
if len(cfg.MicroblogUrl) > 0 && len(cfg.MicroblogToken) > 0 {
socials = append(socials, &MicroblogPub{
url: cfg.MicroblogUrl,
token: cfg.MicroblogToken,
})
}
return socials
}()
if SelectedSocials == nil {
log.Println("no social networks configured")
}
// Find configured notification services (optional)
SelectedNotificationServices = func() NotificationServices {
var notificationServices []NotificationService = nil
// Telegram
if cfg.TelegramUserId > 0 && len(cfg.TelegramBotToken) > 0 {
notificationServices = append(notificationServices, &Telegram{
userId: cfg.TelegramUserId,
botToken: cfg.TelegramBotToken,
})
}
return notificationServices
}()
if SelectedNotificationServices == nil {
log.Println("No notification services configured")
} }
return nil return nil
} }

368
entry.go
View File

@ -6,44 +6,49 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url"
"strings" "strings"
"text/template"
"time" "time"
) )
type Entry struct { type Entry struct {
content string content string
title string title string
date string date string
lastmod string lastmod string
section string section string
tags []string tags []string
link string series []string
slug string link string
replyLink string slug string
replyTitle string replyLink string
likeLink string replyTitle string
likeTitle string likeLink string
syndicate []string likeTitle string
filename string syndicate []string
location string language string
token 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) { func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
if contentType == WwwForm { if contentType == WwwForm {
bodyString, err := parseRequestBody(r) err := r.ParseForm()
if err != nil { if err != nil {
return nil, err return nil, errors.New("failed to parse Form")
} }
bodyValues, err := url.ParseQuery(bodyString) return createEntryFromValueMap(r.Form)
if err != nil {
return nil, errors.New("failed to parse query")
}
return createEntryFromValueMap(bodyValues)
} else if contentType == Multipart { } else if contentType == Multipart {
err := r.ParseMultipartForm(1024 * 1024 * 16) err := r.ParseMultipartForm(1024 * 1024 * 16)
if err != nil { if err != nil {
@ -51,12 +56,9 @@ func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
} }
return createEntryFromValueMap(r.MultipartForm.Value) return createEntryFromValueMap(r.MultipartForm.Value)
} else if contentType == Json { } else if contentType == Json {
bodyString, err := parseRequestBody(r) decoder := json.NewDecoder(r.Body)
if err != nil {
return nil, err
}
parsedMfItem := &MicroformatItem{} parsedMfItem := &MicroformatItem{}
err = json.Unmarshal([]byte(bodyString), &parsedMfItem) err := decoder.Decode(&parsedMfItem)
if err != nil { if err != nil {
return nil, errors.New("failed to parse Json") return nil, errors.New("failed to parse Json")
} }
@ -66,100 +68,126 @@ func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
} }
} }
func parseRequestBody(r *http.Request) (string, error) {
defer r.Body.Close()
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
return "", errors.New("failed to read body")
}
return string(bodyBytes), nil
}
func createEntryFromValueMap(values map[string][]string) (*Entry, error) { func createEntryFromValueMap(values map[string][]string) (*Entry, error) {
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") { if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
return nil, errors.New("only entry type is supported so far") return nil, errors.New("only entry type is supported so far")
} }
if _, ok := values["content"]; ok { entry := &Entry{}
entry := &Entry{ if content, ok := values["content"]; ok {
content: values["content"][0], 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
} else {
entry.tags = nil
}
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
} else {
entry.syndicate = nil
}
if token, ok := values["access_token"]; ok {
entry.token = "Bearer " + token[0]
}
err := computeExtraSettings(entry)
if err != nil {
return nil, err
}
return entry, nil
} }
return nil, errors.New("error parsing the entry") 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) { func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) {
if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" { if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" {
return nil, errors.New("only entry type is supported so far") 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 { if mfEntry.Properties != nil && len(mfEntry.Properties.Content) == 1 && len(mfEntry.Properties.Content[0]) > 0 {
entry := &Entry{ entry.content = mfEntry.Properties.Content[0]
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
}
err := computeExtraSettings(entry)
if err != nil {
return nil, err
}
return entry, nil
} }
return nil, errors.New("error parsing the entry") 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 { func computeExtraSettings(entry *Entry) error {
@ -183,6 +211,9 @@ func computeExtraSettings(entry *Entry) error {
} else if strings.HasPrefix(text, "tags: ") { } else if strings.HasPrefix(text, "tags: ") {
// Tags // Tags
entry.tags = strings.Split(strings.TrimPrefix(text, "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: ") { } else if strings.HasPrefix(text, "link: ") {
// Link // Link
entry.link = strings.TrimPrefix(text, "link: ") entry.link = strings.TrimPrefix(text, "link: ")
@ -198,28 +229,96 @@ func computeExtraSettings(entry *Entry) error {
} else if strings.HasPrefix(text, "like-title: ") { } else if strings.HasPrefix(text, "like-title: ") {
// Like title // Like title
entry.likeTitle = strings.TrimPrefix(text, "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 { } else {
_, _ = fmt.Fprintln(&filteredContent, text) _, _ = fmt.Fprintln(&filteredContent, text)
} }
} }
entry.content = filteredContent.String() 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 // Compute slug if empty
if len(entry.slug) == 0 || entry.slug == "" { if len(entry.slug) == 0 || entry.slug == "" {
random := generateRandomString(now, 5) random := generateRandomString(now, 5)
entry.slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random) 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 // Compute filename and location
if len(entry.section) < 1 { lang := Languages[entry.language]
entry.section = "micro" 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) entry.section = strings.ToLower(entry.section)
if entry.section == "thoughts" || entry.section == "links" || entry.section == "micro" { section := lang.Sections[entry.section]
entry.filename = fmt.Sprintf("content/%v/%02d/%02d/%v.md", entry.section, now.Year(), int(now.Month()), entry.slug) pathVars := struct {
entry.location = fmt.Sprintf("%v%v/%02d/%02d/%v/", BlogUrl, entry.section, now.Year(), int(now.Month()), entry.slug) LocalContentFolder string
} else { LocalBlogUrl string
entry.filename = fmt.Sprintf("content/%v/%v.md", entry.section, entry.slug) Year int
entry.location = fmt.Sprintf("%v%v/%v/", BlogUrl, entry.section, entry.slug) 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 return nil
} }
@ -245,34 +344,3 @@ func WriteEntry(entry *Entry) (location string, err error) {
location = entry.location location = entry.location
return return
} }
func analyzeURL(url string) (filePath string, section string, slug string, err error) {
if !strings.HasPrefix(url, BlogUrl) {
return
}
path := strings.TrimSuffix(strings.TrimPrefix(url, BlogUrl), "/")
pathParts := strings.Split(path, "/")
filePath = "content/" + path + ".md"
section = pathParts[0]
slug = pathParts[len(pathParts)-1]
return
}
func ReadEntry(url string) (entry *Entry, err error) {
filePath, section, slug, err := analyzeURL(url)
if err != nil {
return
}
fileContent, _, exists, err := SelectedStorage.ReadFile(filePath)
if err != nil || !exists {
return
}
entry, err = ReadHugoPost(fileContent)
if entry != nil {
entry.location = url
entry.filename = filePath
entry.section = section
entry.slug = slug
}
return
}

24
go.mod
View File

@ -1,11 +1,21 @@
module codeberg.org/jlelse/hugo-micropub module git.jlel.se/jlelse/hugo-micropub
go 1.13 go 1.14
require ( require (
github.com/caarlos0/env/v6 v6.1.0 codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect github.com/gliderlabs/ssh v0.3.0 // indirect
gopkg.in/yaml.v2 v2.2.7 github.com/go-git/go-git/v5 v5.1.0
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62 github.com/google/go-cmp v0.5.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.6.1 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
golang.org/x/text v0.3.3 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
) )

123
go.sum
View File

@ -1,26 +1,115 @@
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0 h1:pJX79kTd01NtxEnzhfd4OU2SY9fquKVoO47DUeNKe+8=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0/go.mod h1:X6cM4Sn0aL/4VQ/ku11yxmiV0WIk5XAaYEPHQLQjFFM=
github.com/caarlos0/env/v6 v6.1.0 h1:4FbM+HmZA/Q5wdSrH2kj0KQXm7xnhuO8y3TuOTnOvqc= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/caarlos0/env/v6 v6.1.0/go.mod h1:iUA6X3VCAOwDhoqvgKlTGjjwJzQseIJaFYApUqQkt+8= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible h1:OT02onvXX618RBcjxeUA4H7d1PSm5Apg4IET72VgVlE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/gliderlabs/ssh v0.3.0 h1:7GcKy4erEljCE/QeQ2jTVpu+3f3zkpZOxOJjFYkMqYU=
github.com/gliderlabs/ssh v0.3.0/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk=
github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 h1:czFLhve3vsQetD6JOJ8NZZvGQIXlnN3/yXxbT6/awxI= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62 h1:jqC8A1S2/9WjXSOK/Nl2rYwVgxU7DCnZ/zpOTL1BErI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62/go.mod h1:p+ZRAsZS2pzZ6kX3GKWYurf3WZI2ygj7VbR8NM8qwfM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

75
imagecompression.go Normal file
View File

@ -0,0 +1,75 @@
package main
import (
"errors"
"io/ioutil"
"os"
"sort"
"strings"
tfgo "codeberg.org/jlelse/tinify"
)
type ImageCompression interface {
Compress(url string) (location string, err error)
}
// Tinify
type Tinify struct {
// API Key
key string
}
func (t *Tinify) Compress(url string) (location string, err error) {
fileExtension := func() string {
spliced := strings.Split(url, ".")
return spliced[len(spliced)-1]
}()
supportedTypes := []string{"jpg", "jpeg", "png"}
sort.Strings(supportedTypes)
i := sort.SearchStrings(supportedTypes, strings.ToLower(fileExtension))
if !(i < len(supportedTypes) && supportedTypes[i] == strings.ToLower(fileExtension)) {
err = errors.New("file not supported")
return
}
tfgo.SetKey(t.key)
s, e := tfgo.FromUrl(url)
if e != nil {
err = errors.New("failed to compress file")
return
}
e = s.Resize(&tfgo.ResizeOption{
Method: tfgo.ResizeMethodScale,
Width: 2000,
})
if e != nil {
err = errors.New("failed to resize file")
return
}
file, e := ioutil.TempFile("", "tiny-*."+fileExtension)
if e != nil {
err = errors.New("failed to create temporary file")
return
}
defer func() {
_ = file.Close()
_ = os.Remove(file.Name())
}()
e = s.ToFile(file.Name())
if e != nil {
err = errors.New("failed to save compressed file")
return
}
hashFile, e := os.Open(file.Name())
defer func() { _ = hashFile.Close() }()
if e != nil {
err = errors.New("failed to open temporary file")
return
}
fileName, err := getSHA256(hashFile)
if err != nil {
return
}
location, err = SelectedMediaStorage.Upload(fileName+"."+fileExtension, file)
return
}

View File

@ -3,7 +3,6 @@ package main
import ( import (
"log" "log"
"net/http" "net/http"
"strings"
"time" "time"
) )
@ -15,9 +14,7 @@ func main() {
log.Println("Starting micropub server...") log.Println("Starting micropub server...")
log.Println("Current time: " + time.Now().Format(time.RFC3339)) log.Println("Current time: " + time.Now().Format(time.RFC3339))
log.Println("Blog URL: " + BlogUrl) log.Println("Blog URL: " + BlogUrl)
log.Println("Ignored URLs for Webmention: " + strings.Join(IgnoredWebmentionUrls, ", "))
http.HandleFunc("/micropub", HandleMicroPub) http.HandleFunc("/micropub", HandleMicroPub)
http.HandleFunc("/media", HandleMedia) http.HandleFunc("/media", HandleMedia)
http.HandleFunc("/webmention", HandleWebmention)
log.Fatal(http.ListenAndServe(":5555", nil)) log.Fatal(http.ListenAndServe(":5555", nil))
} }

View File

@ -1,9 +1,6 @@
package main package main
import ( import (
"crypto/sha256"
"fmt"
"io"
"mime" "mime"
"net/http" "net/http"
"path/filepath" "path/filepath"
@ -11,6 +8,11 @@ import (
) )
func HandleMedia(w http.ResponseWriter, r *http.Request) { func HandleMedia(w http.ResponseWriter, r *http.Request) {
if SelectedMediaStorage == nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("No media storage configured"))
return
}
if r.Method != "POST" { if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
_, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request")) _, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request"))
@ -28,8 +30,8 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
return return
} }
authCode := r.Header.Get("authorization") authCode := r.Header.Get("authorization")
if formAuth := r.FormValue("authorization"); len(authCode) == 0 && len(formAuth) > 0 { if formAuth := r.FormValue("access_token"); len(authCode) == 0 && len(formAuth) > 0 {
authCode = formAuth authCode = "Bearer " + formAuth
} }
if CheckAuthorization(authCode) { if CheckAuthorization(authCode) {
file, header, err := r.FormFile("file") file, header, err := r.FormFile("file")
@ -38,15 +40,15 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("Failed to get file")) _, _ = w.Write([]byte("Failed to get file"))
return return
} }
defer func() { _ = file.Close() }()
hashFile, _, _ := r.FormFile("file") hashFile, _, _ := r.FormFile("file")
h := sha256.New()
defer func() { _ = hashFile.Close() }() defer func() { _ = hashFile.Close() }()
if _, err := io.Copy(h, hashFile); err != nil { fileName, err := getSHA256(hashFile)
if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Failed to calculate hash of file")) _, _ = w.Write([]byte(err.Error()))
return return
} }
fileName := fmt.Sprintf("%x", h.Sum(nil))
fileExtension := filepath.Ext(header.Filename) fileExtension := filepath.Ext(header.Filename)
if len(fileExtension) == 0 { if len(fileExtension) == 0 {
// Find correct file extension if original filename does not contain one // Find correct file extension if original filename does not contain one
@ -58,13 +60,19 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
} }
} }
} }
fileName += fileExtension fileName += strings.ToLower(fileExtension)
location, err := SelectedMediaStorage.Upload(fileName, file) location, err := SelectedMediaStorage.Upload(fileName, file)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Failed to upload file")) _, _ = w.Write([]byte("Failed to upload original file"))
return return
} }
if SelectedImageCompression != nil {
compressedLocation, err := SelectedImageCompression.Compress(location)
if err == nil && len(compressedLocation) > 0 {
location = compressedLocation
}
}
w.Header().Add("Location", location) w.Header().Add("Location", location)
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
} else { } else {

View File

@ -1,7 +1,10 @@
package main package main
import ( import (
"crypto/sha256"
"errors" "errors"
"fmt"
"io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
@ -23,9 +26,9 @@ type BunnyCdnStorage struct {
var bunnyCdnStorageUrl = "https://storage.bunnycdn.com" var bunnyCdnStorageUrl = "https://storage.bunnycdn.com"
func (b BunnyCdnStorage) Upload(fileName string, file multipart.File) (location string, err error) { func (b *BunnyCdnStorage) Upload(fileName string, file multipart.File) (location string, err error) {
client := &http.Client{} client := http.DefaultClient
req, _ := http.NewRequest("PUT", bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape("/"+fileName), file) req, _ := http.NewRequest(http.MethodPut, bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape(fileName), file)
req.Header.Add("AccessKey", b.key) req.Header.Add("AccessKey", b.key)
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil || resp.StatusCode != 201 { if err != nil || resp.StatusCode != 201 {
@ -33,3 +36,12 @@ func (b BunnyCdnStorage) Upload(fileName string, file multipart.File) (location
} }
return b.baseLocation + fileName, nil return b.baseLocation + fileName, nil
} }
func getSHA256(file multipart.File) (filename string, err error) {
h := sha256.New()
if _, e := io.Copy(h, file); e != nil {
err = errors.New("failed to calculate hash of file")
return
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

View File

@ -6,15 +6,17 @@ type MicroformatItem struct {
} }
type MicroformatProperties struct { type MicroformatProperties struct {
Name []string `json:"name,omitempty"` Name []string `json:"name,omitempty"`
Published []string `json:"published,omitempty"` Published []string `json:"published,omitempty"`
Updated []string `json:"updated,omitempty"` Updated []string `json:"updated,omitempty"`
Category []string `json:"category,omitempty"` Category []string `json:"category,omitempty"`
Content []string `json:"content,omitempty"` Content []string `json:"content,omitempty"`
Url []string `json:"url,omitempty"` Url []string `json:"url,omitempty"`
InReplyTo []string `json:"in-reply-to,omitempty"` InReplyTo []string `json:"in-reply-to,omitempty"`
LikeOf []string `json:"like-of,omitempty"` LikeOf []string `json:"like-of,omitempty"`
BookmarkOf []string `json:"bookmark-of,omitempty"` BookmarkOf []string `json:"bookmark-of,omitempty"`
MpSlug []string `json:"mp-slug,omitempty"` MpSlug []string `json:"mp-slug,omitempty"`
MpSyndicateTo []string `json:"mp-syndicate-to,omitempty"` MpSyndicateTo []string `json:"mp-syndicate-to,omitempty"`
} Photo []interface{} `json:"photo,omitempty"`
Audio []string `json:"audio,omitempty"`
}

View File

@ -3,8 +3,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv"
"time"
) )
type MicropubConfig struct { type MicropubConfig struct {
@ -18,29 +16,10 @@ func HandleMicroPub(w http.ResponseWriter, r *http.Request) {
if q := r.URL.Query().Get("q"); q == "config" || q == "syndicate-to" { if q := r.URL.Query().Get("q"); q == "config" || q == "syndicate-to" {
w.Header().Add("Content-type", "application/json") w.Header().Add("Content-type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
jsonBytes, err := json.Marshal(&MicropubConfig{ _ = json.NewEncoder(w).Encode(&MicropubConfig{
SyndicateTo: SyndicationTargets, SyndicateTo: SyndicationTargets,
MediaEndpoint: MediaEndpointUrl, MediaEndpoint: MediaEndpointUrl,
}) })
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
_, _ = w.Write(jsonBytes)
return
} else if url := r.URL.Query().Get("url"); q == "source" {
limit := r.URL.Query().Get("limit")
limitInt, err := strconv.Atoi(limit)
jsonBytes, err := QueryURL(url, limitInt)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
w.Header().Add("Content-type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(jsonBytes)
return return
} else { } else {
w.Header().Add("Content-type", "application/json") w.Header().Add("Content-type", "application/json")
@ -83,23 +62,8 @@ func HandleMicroPub(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("There was an error committing the entry to the repository")) _, _ = w.Write([]byte("There was an error committing the entry to the repository"))
return return
} else { } else {
w.Header().Add("Location", location) w.Header().Add("Location", location+"?cache=0")
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)
// Purge CDN in 10 seconds, send webmentions, post to social media
go func() {
if SelectedCdn != nil {
time.Sleep(10 * time.Second)
SelectedCdn.Purge(location)
}
time.Sleep(10 * time.Second)
// Send webmentions
go SendWebmentions(location)
go func() {
if SelectedSocials != nil {
SelectedSocials.Post(location, entry.title)
}
}()
}()
return return
} }
} else { } else {

View File

@ -1,40 +0,0 @@
package main
import (
"fmt"
telegramBotApi "github.com/go-telegram-bot-api/telegram-bot-api"
)
type NotificationService interface {
Post(message string)
}
type NotificationServices []NotificationService
// Post to all Notification Services
func (notificationServices *NotificationServices) Post(message string) {
for _, notificationService := range *notificationServices {
notificationService.Post(message)
}
}
// Telegram
type Telegram struct {
userId int64
botToken string
}
func (t *Telegram) Post(message string) {
bot, err := telegramBotApi.NewBotAPI(t.botToken)
if err != nil {
fmt.Println("Failed to setup Telegram bot")
return
}
msg := telegramBotApi.NewMessage(t.userId, message)
_, err = bot.Send(msg)
if err != nil {
fmt.Println("Failed to send Telegram message")
return
}
return
}

135
post.go
View File

@ -3,130 +3,81 @@ package main
import ( import (
"bytes" "bytes"
"errors" "errors"
"gopkg.in/yaml.v2"
"strings" "gopkg.in/yaml.v3"
) )
type HugoFrontmatter struct { type HugoFrontMatter struct {
Title string `yaml:"title,omitempty"` Title string `yaml:"title,omitempty"`
Date string `yaml:"date,omitempty"` Published string `yaml:"date,omitempty"`
Lastmod string `yaml:"lastmod,omitempty"` Updated string `yaml:"lastmod,omitempty"`
Tags []string `yaml:"tags,omitempty"` Tags []string `yaml:"tags,omitempty"`
ExternalURL string `yaml:"externalURL,omitempty"` Series []string `yaml:"series,omitempty"`
Indieweb HugoFrontmatterIndieweb `yaml:"indieweb,omitempty"` ExternalURL string `yaml:"externalURL,omitempty"`
Syndicate []string `yaml:"syndicate,omitempty"` IndieWeb HugoFrontMatterIndieWeb `yaml:"indieweb,omitempty"`
Syndicate []string `yaml:"syndicate,omitempty"`
TranslationKey string `yaml:"translationKey,omitempty"`
Images []string `yaml:"images,omitempty"`
Audio string `yaml:"audio,omitempty"`
} }
type HugoFrontmatterIndieweb struct { type HugoFrontMatterIndieWeb struct {
Reply HugoFrontmatterReply `yaml:"reply,omitempty"` Reply HugoFrontMatterReply `yaml:"reply,omitempty"`
Like HugoFrontmatterLike `yaml:"like,omitempty"` Like HugoFrontMatterLike `yaml:"like,omitempty"`
} }
type HugoFrontmatterReply struct { type HugoFrontMatterReply struct {
Link string `yaml:"link,omitempty"` Link string `yaml:"link,omitempty"`
Title string `yaml:"title,omitempty"` Title string `yaml:"title,omitempty"`
} }
type HugoFrontmatterLike struct { type HugoFrontMatterLike struct {
Link string `yaml:"link,omitempty"` Link string `yaml:"link,omitempty"`
Title string `yaml:"title,omitempty"` Title string `yaml:"title,omitempty"`
} }
func writeFrontMatter(entry *Entry) (frontmatter string, err error) { func writeFrontMatter(entry *Entry) (string, error) {
var buff bytes.Buffer frontMatter := &HugoFrontMatter{
writeFrontmatter := &HugoFrontmatter{
Title: entry.title, Title: entry.title,
Date: entry.date, Published: entry.date,
Lastmod: entry.lastmod, Updated: entry.lastmod,
Tags: entry.tags, Tags: entry.tags,
Series: entry.series,
ExternalURL: entry.link, ExternalURL: entry.link,
Indieweb: HugoFrontmatterIndieweb{ IndieWeb: HugoFrontMatterIndieWeb{
Reply: HugoFrontmatterReply{ Reply: HugoFrontMatterReply{
Link: entry.replyLink, Link: entry.replyLink,
Title: entry.replyTitle, Title: entry.replyTitle,
}, },
Like: HugoFrontmatterLike{ Like: HugoFrontMatterLike{
Link: entry.likeLink, Link: entry.likeLink,
Title: entry.likeTitle, Title: entry.likeTitle,
}, },
}, },
Syndicate: entry.syndicate, Syndicate: entry.syndicate,
TranslationKey: entry.translationKey,
Audio: entry.audio,
} }
yamlBytes, err := yaml.Marshal(&writeFrontmatter) for _, image := range entry.images {
frontMatter.Images = append(frontMatter.Images, image.url)
}
writer := new(bytes.Buffer)
writer.WriteString("---\n")
err := yaml.NewEncoder(writer).Encode(&frontMatter)
if err != nil { if err != nil {
err = errors.New("failed marshaling frontmatter") return "", errors.New("failed encoding frontmatter")
return
} }
buff.WriteString("---\n") writer.WriteString("---\n")
buff.Write(yamlBytes) return writer.String(), nil
buff.WriteString("---\n")
frontmatter = buff.String()
return
} }
func WriteHugoPost(entry *Entry) (string, error) { func WriteHugoPost(entry *Entry) (string, error) {
var buff bytes.Buffer buff := new(bytes.Buffer)
frontmatter, err := writeFrontMatter(entry) f, err := writeFrontMatter(entry)
if err != nil { if err != nil {
return "", err return "", err
} }
buff.WriteString(frontmatter) buff.WriteString(f)
if len(entry.content) > 0 { buff.WriteString(entry.content)
buff.WriteString(entry.content)
}
return buff.String(), nil return buff.String(), nil
} }
func readFrontMatter(frontmatter string, entry *Entry) (err error) {
parsedFrontmatter := &HugoFrontmatter{}
err = yaml.Unmarshal([]byte(frontmatter), &parsedFrontmatter)
if err != nil {
err = errors.New("failed parsing frontmatter")
}
if len(parsedFrontmatter.Title) > 0 {
entry.title = parsedFrontmatter.Title
}
if len(parsedFrontmatter.Date) > 0 {
entry.date = parsedFrontmatter.Date
}
if len(parsedFrontmatter.Lastmod) > 0 {
entry.lastmod = parsedFrontmatter.Lastmod
}
if len(parsedFrontmatter.Tags) > 0 {
entry.tags = parsedFrontmatter.Tags
}
if len(parsedFrontmatter.ExternalURL) > 0 {
entry.link = parsedFrontmatter.ExternalURL
}
if len(parsedFrontmatter.Indieweb.Reply.Link) > 0 {
entry.replyLink = parsedFrontmatter.Indieweb.Reply.Link
}
if len(parsedFrontmatter.Indieweb.Reply.Title) > 0 {
entry.replyTitle = parsedFrontmatter.Indieweb.Reply.Title
}
if len(parsedFrontmatter.Indieweb.Like.Link) > 0 {
entry.replyLink = parsedFrontmatter.Indieweb.Like.Link
}
if len(parsedFrontmatter.Indieweb.Like.Title) > 0 {
entry.replyTitle = parsedFrontmatter.Indieweb.Like.Title
}
if len(parsedFrontmatter.Syndicate) > 0 {
entry.syndicate = parsedFrontmatter.Syndicate
}
return
}
func ReadHugoPost(fileContent string) (entry *Entry, err error) {
parts := strings.Split(fileContent, "---\n")
if len(parts) != 3 {
err = errors.New("empty frontmatter or content")
return
}
entry = new(Entry)
err = readFrontMatter(parts[1], entry)
if err != nil {
return
}
entry.content = strings.TrimSuffix(parts[2], "\n")
return
}

View File

@ -1,94 +0,0 @@
package main
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
)
type ItemList struct {
Items []*MicroformatItem `json:"items"`
}
func QueryURL(url string, limit int) ([]byte, error) {
if len(url) == 0 {
url = BlogUrl
}
if url == BlogUrl {
allPosts, err := allPosts(url)
if err != nil {
return nil, err
}
itemList := &ItemList{}
for i, postURL := range allPosts {
if limit != 0 && i == limit {
break
}
item, _ := getItem(postURL)
itemList.Items = append(itemList.Items, item)
}
jsonBytes, err := json.Marshal(itemList)
if err != nil {
err = errors.New("failed to marshal json")
return nil, err
}
return jsonBytes, err
} else {
item, err := getItem(url)
if err != nil {
return nil, err
}
jsonBytes, err := json.Marshal(item)
if err != nil {
err = errors.New("failed to marshal json")
return nil, err
}
return jsonBytes, err
}
}
func getItem(url string) (item *MicroformatItem, err error) {
entry, err := ReadEntry(url)
if err != nil {
return
}
item = &MicroformatItem{
Type: []string{"h-entry"},
Properties: &MicroformatProperties{
Name: []string{entry.title},
Published: []string{entry.date},
Updated: []string{entry.lastmod},
Category: entry.tags,
Content: []string{entry.content},
Url: []string{entry.location},
},
}
return
}
func allPosts(url string) ([]string, error) {
jsonFeed := &struct {
Items []struct {
Url string `json:"url"`
} `json:"items"`
}{}
resp, err := http.Get(url + "feed.json")
if err != nil {
return nil, errors.New("failed to get json feed")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.New("failed to read json feed")
}
err = json.Unmarshal(body, &jsonFeed)
if err != nil {
return nil, errors.New("failed to parse json feed")
}
var allUrls []string
for _, item := range jsonFeed.Items {
allUrls = append(allUrls, item.Url)
}
return allUrls, nil
}

View File

@ -1,46 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
)
type Social interface {
Post(location string, text string)
}
type Socials []Social
// Post to all socials
func (socials *Socials) Post(location string, text string) {
for _, social := range *socials {
social.Post(location, text)
}
}
// Microblog.pub
type MicroblogPub struct {
url string
token string
}
func (social *MicroblogPub) Post(location string, text string) {
if len(text) == 0 {
text = location
}
note := &struct {
Content string `json:"content"`
}{
Content: "[" + text + "](" + location + ")",
}
bytesRepresentation, err := json.Marshal(note)
if err != nil {
return
}
client := &http.Client{}
req, _ := http.NewRequest("POST", social.url+"api/new_note", bytes.NewBuffer(bytesRepresentation))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", "Bearer "+social.token)
_, _ = client.Do(req)
}

View File

@ -1,134 +1,184 @@
package main package main
import ( import (
"bytes"
"encoding/base64"
"encoding/json"
"errors" "errors"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"io/ioutil" "io/ioutil"
"net/http" "os"
"net/url" "path"
"strings" "sync"
"time"
) )
type Storage interface { type Storage interface {
CreateFile(path string, file string, message string) (err error) CreateFile(path string, file string, message string) (err error)
UpdateFile(path string, file string, message string) (err error) UpdateFile(path string, file string, message string) (err error)
ReadFile(path string) (content string, sha string, exists bool, err error) DeleteFile(path string, message string) (err error)
} }
// Gitea type Git struct {
type Gitea struct { filepath string
endpoint string url string
token string username string
password string
name string
email string
lock *sync.Mutex
} }
type giteaCommitRequest struct { func (g *Git) init() (r *git.Repository, w *git.Worktree, err error) {
Content string `json:"content"` // Open repo
Message string `json:"message"` r, err = git.PlainOpen(g.filepath)
SHA string `json:"sha,omitempty"` if err == nil {
} // Try to get work tree
w, err = r.Worktree()
type giteaReadResponse struct { if err == nil {
Type string `json:"type"` // Try to pull
Content string `json:"content"` err = w.Pull(&git.PullOptions{
SHA string `json:"sha"` Auth: &gitHttp.BasicAuth{
} Username: g.username,
Password: g.password,
type giteaErrorResponse struct { },
Message string `json:"message"` SingleBranch: true,
} })
if err == git.NoErrAlreadyUpToDate {
func (gitea *Gitea) CreateFile(path string, file string, message string) (err error) { err = nil
request := &giteaCommitRequest{
Content: base64.StdEncoding.EncodeToString([]byte(file)),
Message: message,
}
bytesRepresentation, err := json.Marshal(request)
if err != nil {
return errors.New("failed to marshal json before committing")
}
resp, err := http.Post(gitea.endpoint+url.QueryEscape(path)+"?access_token="+gitea.token, "application/json", bytes.NewBuffer(bytesRepresentation))
if err != nil || resp.StatusCode != 201 {
return errors.New("failed to create file in repo")
}
return nil
}
func (gitea *Gitea) UpdateFile(path string, file string, message string) (err error) {
_, sha, exists, err := gitea.ReadFile(path)
if err != nil {
return err
}
if !exists {
// File doesn't exist, create it
return gitea.CreateFile(path, file, message)
}
request := &giteaCommitRequest{
Content: base64.StdEncoding.EncodeToString([]byte(file)),
Message: message,
SHA: sha,
}
bytesRepresentation, err := json.Marshal(request)
if err != nil {
return errors.New("failed to marshal json before committing")
}
req, err := http.NewRequest(http.MethodPut, gitea.endpoint+url.QueryEscape(path)+"?access_token="+gitea.token, bytes.NewBuffer(bytesRepresentation))
if err != nil {
return errors.New("error making update request")
}
req.Header.Set("Content-type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 200 {
return errors.New("failed to update file in repo")
}
return nil
}
func (gitea *Gitea) ReadFile(path string) (content string, sha string, exists bool, err error) {
exists = false
resp, err := http.Get(gitea.endpoint + url.QueryEscape(path) + "?access_token=" + gitea.token)
if err != nil || resp.StatusCode != 200 {
if resp != nil {
defer resp.Body.Close()
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
err = errors.New("failed reading Gitea error response")
return
}
errorResponse := &giteaErrorResponse{}
marshalErr := json.Unmarshal(body, &errorResponse)
if marshalErr != nil {
err = errors.New("failed parsing Gitea error response")
return
}
exists = !strings.Contains(errorResponse.Message, "does not exist")
if !exists {
return
} }
} }
err = errors.New("failed to read file in repo")
return
} }
exists = true
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
err = errors.New("failed reading file in repo") // Delete old things
return g.destroyRepo()
// Clone
r, err = git.PlainClone(g.filepath, false, &git.CloneOptions{
Auth: &gitHttp.BasicAuth{
Username: g.username,
Password: g.password,
},
URL: g.url,
Depth: 1,
SingleBranch: true,
})
if err != nil {
err = errors.New("failed to clone repo")
}
if err == nil {
w, err = r.Worktree()
if err != nil {
err = errors.New("failed to get work tree")
}
}
} }
readResponse := &giteaReadResponse{}
err = json.Unmarshal(body, &readResponse)
if err != nil {
err = errors.New("failed parsing Gitea response")
return
}
decodedContentBytes, err := base64.StdEncoding.DecodeString(readResponse.Content)
if err != nil {
err = errors.New("failed decoding file content")
}
content = string(decodedContentBytes)
sha = readResponse.SHA
return return
} }
func (g *Git) destroyRepo() {
_ = os.RemoveAll(g.filepath)
}
func (g *Git) push(repository *git.Repository) error {
err := repository.Push(&git.PushOptions{
Auth: &gitHttp.BasicAuth{
Username: g.username,
Password: g.password,
},
})
if err == nil || err == git.NoErrAlreadyUpToDate {
return nil
} else {
// Destroy repo to prevent errors when trying to create same post again
g.destroyRepo()
return errors.New("failed to push to remote")
}
}
func (g *Git) CreateFile(filepath string, file string, message string) (err error) {
g.lock.Lock()
defer g.lock.Unlock()
_, _, err = g.init()
if err != nil {
err = errors.New("failed to initialize repo")
return
}
joinedPath := path.Join(g.filepath, filepath)
if _, e := os.Stat(joinedPath); e == nil {
return errors.New("file already exists")
} else {
return g.unsafeUpdateFile(filepath, file, message)
}
}
func (g *Git) UpdateFile(filepath string, file string, message string) error {
g.lock.Lock()
defer g.lock.Unlock()
return g.unsafeUpdateFile(filepath, file, message)
}
func (g *Git) unsafeUpdateFile(filepath string, file string, message string) error {
repo, w, err := g.init()
if err != nil {
return errors.New("failed to initialize repo")
}
joinedPath := path.Join(g.filepath, filepath)
_ = os.MkdirAll(path.Dir(joinedPath), 0755)
err = ioutil.WriteFile(joinedPath, []byte(file), 0644)
if err != nil {
return errors.New("failed to write to file")
}
status, err := w.Status()
if err == nil && status.IsClean() {
// No file changes, prevent empty commit
return nil
} else {
err = nil
}
_, err = w.Add(filepath)
if err != nil {
return errors.New("failed to stage file")
}
if len(message) == 0 {
message = "Add " + filepath
}
_, err = w.Commit(message, &git.CommitOptions{
Author: &object.Signature{
Name: g.name,
Email: g.email,
When: time.Now(),
},
})
if err != nil {
return errors.New("failed to commit file")
}
return g.push(repo)
}
func (g *Git) DeleteFile(filepath string, message string) (err error) {
g.lock.Lock()
defer g.lock.Unlock()
repo, w, err := g.init()
if err != nil {
return errors.New("failed to initialize repo")
}
joinedPath := path.Join(g.filepath, filepath)
err = os.Remove(joinedPath)
if err != nil {
return errors.New("failed to delete file")
}
_, err = w.Add(filepath)
if err != nil {
return errors.New("failed to stage deletion")
}
_, err = w.Commit(message, &git.CommitOptions{
Author: &object.Signature{
Name: g.name,
Email: g.email,
When: time.Now(),
},
})
if err != nil {
return errors.New("failed to commit deletion")
}
return g.push(repo)
}

View File

@ -3,7 +3,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil"
"net/http" "net/http"
"strings" "strings"
) )
@ -34,7 +33,7 @@ func checkAccess(token string) (bool, error) {
return false, errors.New("token string is empty") return false, errors.New("token string is empty")
} }
// form the request to check the token // form the request to check the token
client := &http.Client{} client := http.DefaultClient
req, err := http.NewRequest("GET", indieAuthTokenUrl, nil) req, err := http.NewRequest("GET", indieAuthTokenUrl, nil)
if err != nil { if err != nil {
return false, errors.New("error making the request for checking token access") return false, errors.New("error making the request for checking token access")
@ -46,14 +45,10 @@ func checkAccess(token string) (bool, error) {
if err != nil { if err != nil {
return false, errors.New("error sending the request for checking token access") return false, errors.New("error sending the request for checking token access")
} }
defer res.Body.Close()
// parse the response // parse the response
body, err := ioutil.ReadAll(res.Body) indieAuthRes := &IndieAuthRes{}
if err != nil { err = json.NewDecoder(res.Body).Decode(&indieAuthRes)
return false, errors.New("error parsing the response for checking token access") res.Body.Close()
}
var indieAuthRes = new(IndieAuthRes)
err = json.Unmarshal(body, &indieAuthRes)
if err != nil { if err != nil {
return false, errors.New("Error parsing the response into json for checking token access " + err.Error()) return false, errors.New("Error parsing the response into json for checking token access " + err.Error())
} }

View File

@ -1,190 +0,0 @@
package main
import (
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
"willnorris.com/go/webmention"
)
type Mention struct {
Source string `json:"source"`
Target string `json:"target"`
Date string `json:"date"`
}
func SendWebmentions(url string) {
client := webmention.New(nil)
dl, err := client.DiscoverLinks(url, ".h-entry")
if err != nil {
return
}
// Send Webmentions
for _, link := range filterLinks(dl) {
endpoint, err := client.DiscoverEndpoint(link)
if err != nil || len(endpoint) < 1 {
continue
}
_, err = client.SendWebmention(endpoint, url, link)
if err != nil {
log.Println("Sent webmention to " + link + " failed")
continue
}
log.Println("Sent webmention to " + link)
}
}
func filterLinks(links []string) []string {
var filteredLinks []string
LINKFILTER:
for _, link := range links {
if strings.HasPrefix(link, BlogUrl) {
continue
}
for _, ignoredURL := range IgnoredWebmentionUrls {
if strings.HasPrefix(link, ignoredURL) {
continue LINKFILTER
}
}
filteredLinks = append(filteredLinks, link)
}
return filteredLinks
}
func HandleWebmention(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
_, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request"))
return
}
sourceUrl, err := url.Parse(r.FormValue("source"))
if err != nil || !(sourceUrl.Scheme == "http" || sourceUrl.Scheme == "https") {
err = errors.New("failed to parse webmention source URL")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
targetUrl, err := url.Parse(r.FormValue("target"))
if err != nil || !(sourceUrl.Scheme == "http" || sourceUrl.Scheme == "https") {
err = errors.New("failed to parse webmention target URL")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
// Check if urls don't equal
if sourceUrl.String() == targetUrl.String() {
err = errors.New("source and target equal")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
// Check if target is blog
if !strings.HasPrefix(targetUrl.String(), BlogUrl) {
err = errors.New("wrong webmention target")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
// Check response code for source
respCode, err := responseCodeForSource(sourceUrl.String())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}
if respCode < 200 || respCode >= 300 {
if respCode == 410 {
// Delete mention, because source is gone
// TODO: Implement deletion
returnSuccess(targetUrl.String(), w, r)
return
} else {
err = errors.New("source returned error")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
}
// Check if source mentions target
if !sourceMentionsTarget(sourceUrl.String(), targetUrl.String()) {
err = errors.New("source doesn't mention target")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(err.Error()))
return
}
mention := &Mention{
Source: sourceUrl.String(),
Target: targetUrl.String(),
Date: time.Now().Format(time.RFC3339),
}
err = saveWebmention(mention)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(err.Error()))
return
}
returnSuccess(targetUrl.String(), w, r)
go func() {
if SelectedNotificationServices != nil {
SelectedNotificationServices.Post("New webmention: " + sourceUrl.String())
}
}()
return
}
func responseCodeForSource(source string) (int, error) {
client := &http.Client{}
resp, err := client.Get(source)
if err != nil || resp == nil {
return 0, err
}
return resp.StatusCode, nil
}
func sourceMentionsTarget(source string, target string) bool {
client := webmention.New(nil)
dl, err := client.DiscoverLinks(source, "")
if err != nil {
return false
}
for _, link := range dl {
if link == target {
return true
}
}
return false
}
func saveWebmention(mention *Mention) (err error) {
bytesRepresentation, err := json.Marshal(mention)
if err != nil {
return errors.New("failed to marshal json before committing")
}
filePath := fmt.Sprintf("data/mentions/%x/%x.json", md5.Sum([]byte(strings.ReplaceAll(mention.Target, "/", ""))), md5.Sum([]byte(mention.Source)))
err = SelectedStorage.UpdateFile(filePath, string(bytesRepresentation), "New webmention from "+mention.Source)
return
}
func returnSuccess(target string, w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.Header.Get("Accept"), "text/html") {
// Redirect browser
w.Header().Add("Location", target)
w.WriteHeader(http.StatusSeeOther)
} else {
w.WriteHeader(http.StatusOK)
}
// Purge CDN in 10 seconds
go func() {
if SelectedCdn != nil {
time.Sleep(10 * time.Second)
SelectedCdn.Purge(target)
}
}()
return
}

View File

@ -1,34 +0,0 @@
package main
import (
"net/http"
"net/http/httptest"
"strconv"
"testing"
)
func Test_responseCodeForSource(t *testing.T) {
for _, code := range []int{200, 404} {
t.Run(strconv.Itoa(code), func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(code)
}))
defer ts.Close()
got, err := responseCodeForSource(ts.URL)
if err != nil || got != code {
t.Errorf("Wrong response code: Got %d, but expected %d", got, code)
}
})
}
t.Run("Error", func(t *testing.T) {
ts := httptest.NewUnstartedServer(nil)
defer ts.Close()
got, err := responseCodeForSource(ts.URL)
if err == nil {
t.Errorf("Error is nil")
}
if got != 0 {
t.Errorf("Wrong response code: Got %d, but expected %d", got, 0)
}
})
}