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
*.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
WORKDIR /app
RUN go build
FROM alpine:3.11
FROM alpine:3.12
RUN apk add --no-cache tzdata ca-certificates
COPY --from=build /app/hugo-micropub /bin/
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 (
"errors"
"github.com/caarlos0/env/v6"
"log"
"os"
"strings"
"sync"
"gopkg.in/yaml.v3"
)
var (
BlogUrl string
IgnoredWebmentionUrls []string
MediaEndpointUrl string
SyndicationTargets []SyndicationTarget
SelectedStorage Storage
SelectedMediaStorage MediaStorage
SelectedCdn Cdn
SelectedSocials Socials
SelectedNotificationServices NotificationServices
MediaEndpointUrl string
SelectedImageCompression ImageCompression
DefaultLanguage string
Languages map[string]Language
)
type SyndicationTarget struct {
@ -23,34 +26,101 @@ type SyndicationTarget struct {
Name string `json:"name"`
}
type config struct {
BlogUrl string `env:"BLOG_URL,required"`
BaseUrl string `env:"BASE_URL,required"`
MediaUrl string `env:"MEDIA_URL"`
GiteaEndpoint string `env:"GITEA_ENDPOINT"`
GiteaToken string `env:"GITEA_TOKEN"`
BunnyCdnKey string `env:"BUNNY_CDN_KEY"`
BunnyCdnStorageKey string `env:"BUNNY_CDN_STORAGE_KEY"`
BunnyCdnStorageName string `env:"BUNNY_CDN_STORAGE_NAME"`
MicroblogUrl string `env:"MICROBLOG_URL"`
MicroblogToken string `env:"MICROBLOG_TOKEN"`
TelegramUserId int64 `env:"TELEGRAM_USER_ID"`
TelegramBotToken string `env:"TELEGRAM_BOT_TOKEN"`
IgnoredWebmentionUrls []string `env:"WEBMENTION_IGNORED" envSeparator:","`
SyndicationTargets []string `env:"SYNDICATION" envSeparator:","`
type YamlConfig struct {
BlogUrl string `yaml:"blogUrl"`
BaseUrl string `yaml:"baseUrl"`
MediaUrl string `yaml:"mediaUrl"`
DefaultLanguage string `yaml:"defaultLang"`
Languages map[string]Language `yaml:"languages"`
Git GitConfig `yaml:"git"`
BunnyCdn BunnyCdnConfig `yaml:"bunnyCdn"`
Tinify TinifyConfig `yaml:"tinify"`
SyndicationTargets []string `yaml:"syndication"`
}
type BunnyCdnConfig struct {
StorageKey string `yaml:"storageKey"`
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) {
cfg := config{}
if err := env.Parse(&cfg); err != nil {
return errors.New("failed to parse config, probably not all required env vars set")
configFileName, configSet := os.LookupEnv("CONFIG")
if !configSet {
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)
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
// Media endpoint
MediaEndpointUrl = cfg.BaseUrl + "/media"
// Ignored Webmention URLs (optional)
IgnoredWebmentionUrls = cfg.IgnoredWebmentionUrls
// Media endpoint (required)
if len(cfg.BaseUrl) < 1 {
return errors.New("baseUrl not configured")
}
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)
targets := make([]SyndicationTarget, 0)
for _, url := range cfg.SyndicationTargets {
@ -62,11 +132,16 @@ func initConfig() (err error) {
SyndicationTargets = targets
// Find selected storage
SelectedStorage = func() Storage {
// Gitea
if len(cfg.GiteaEndpoint) > 0 && len(cfg.GiteaToken) >= 0 {
return &Gitea{
endpoint: cfg.GiteaEndpoint,
token: cfg.GiteaToken,
// Git
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 &Git{
filepath: cfg.Git.Filepath,
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
@ -74,61 +149,34 @@ func initConfig() (err error) {
if SelectedStorage == nil {
return errors.New("no storage configured")
}
// Find selected media storage
// Find selected media storage (Optional)
SelectedMediaStorage = func() MediaStorage {
// 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{
key: cfg.BunnyCdnStorageKey,
storageZoneName: cfg.BunnyCdnStorageName,
key: cfg.BunnyCdn.StorageKey,
storageZoneName: cfg.BunnyCdn.StorageName,
baseLocation: cfg.MediaUrl,
}
}
return nil
}()
if SelectedMediaStorage == nil {
return errors.New("no media storage configured")
log.Println("no media storage configured")
}
// Find selected CDN (optional)
SelectedCdn = func() Cdn {
// BunnyCDN (optional)
if len(cfg.BunnyCdnKey) > 0 {
return &BunnyCdn{key: cfg.BunnyCdnKey}
// Find configured image compression service (optional)
SelectedImageCompression = func() ImageCompression {
// Tinify
if len(cfg.Tinify.Key) > 0 {
return &Tinify{
key: cfg.Tinify.Key,
}
}
return nil
}()
if SelectedCdn == nil {
log.Println("no CDN 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")
if SelectedImageCompression == nil {
log.Println("no image compression configured")
}
return nil
}

368
entry.go
View File

@ -6,44 +6,49 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"strings"
"text/template"
"time"
)
type Entry struct {
content string
title string
date string
lastmod string
section string
tags []string
link string
slug string
replyLink string
replyTitle string
likeLink string
likeTitle string
syndicate []string
filename string
location string
token string
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 {
bodyString, err := parseRequestBody(r)
err := r.ParseForm()
if err != nil {
return nil, err
return nil, errors.New("failed to parse Form")
}
bodyValues, err := url.ParseQuery(bodyString)
if err != nil {
return nil, errors.New("failed to parse query")
}
return createEntryFromValueMap(bodyValues)
return createEntryFromValueMap(r.Form)
} else if contentType == Multipart {
err := r.ParseMultipartForm(1024 * 1024 * 16)
if err != nil {
@ -51,12 +56,9 @@ func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
}
return createEntryFromValueMap(r.MultipartForm.Value)
} else if contentType == Json {
bodyString, err := parseRequestBody(r)
if err != nil {
return nil, err
}
decoder := json.NewDecoder(r.Body)
parsedMfItem := &MicroformatItem{}
err = json.Unmarshal([]byte(bodyString), &parsedMfItem)
err := decoder.Decode(&parsedMfItem)
if err != nil {
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) {
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
return nil, errors.New("only entry type is supported so far")
}
if _, ok := values["content"]; ok {
entry := &Entry{
content: values["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
entry := &Entry{}
if content, ok := values["content"]; ok {
entry.content = content[0]
}
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) {
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 := &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
}
err := computeExtraSettings(entry)
if err != nil {
return nil, err
}
return entry, nil
entry.content = mfEntry.Properties.Content[0]
}
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 {
@ -183,6 +211,9 @@ func computeExtraSettings(entry *Entry) error {
} 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: ")
@ -198,28 +229,96 @@ func computeExtraSettings(entry *Entry) error {
} 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
if len(entry.section) < 1 {
entry.section = "micro"
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)
if entry.section == "thoughts" || entry.section == "links" || entry.section == "micro" {
entry.filename = fmt.Sprintf("content/%v/%02d/%02d/%v.md", entry.section, now.Year(), int(now.Month()), entry.slug)
entry.location = fmt.Sprintf("%v%v/%02d/%02d/%v/", BlogUrl, entry.section, now.Year(), int(now.Month()), entry.slug)
} else {
entry.filename = fmt.Sprintf("content/%v/%v.md", entry.section, entry.slug)
entry.location = fmt.Sprintf("%v%v/%v/", BlogUrl, entry.section, entry.slug)
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
}
@ -245,34 +344,3 @@ func WriteEntry(entry *Entry) (location string, err error) {
location = entry.location
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 (
github.com/caarlos0/env/v6 v6.1.0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
gopkg.in/yaml.v2 v2.2.7
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/gliderlabs/ssh v0.3.0 // indirect
github.com/go-git/go-git/v5 v5.1.0
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=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/caarlos0/env/v6 v6.1.0 h1:4FbM+HmZA/Q5wdSrH2kj0KQXm7xnhuO8y3TuOTnOvqc=
github.com/caarlos0/env/v6 v6.1.0/go.mod h1:iUA6X3VCAOwDhoqvgKlTGjjwJzQseIJaFYApUqQkt+8=
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0 h1:pJX79kTd01NtxEnzhfd4OU2SY9fquKVoO47DUeNKe+8=
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0/go.mod h1:X6cM4Sn0aL/4VQ/ku11yxmiV0WIk5XAaYEPHQLQjFFM=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
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/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible h1:OT02onvXX618RBcjxeUA4H7d1PSm5Apg4IET72VgVlE=
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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/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/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 h1:czFLhve3vsQetD6JOJ8NZZvGQIXlnN3/yXxbT6/awxI=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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 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.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62 h1:jqC8A1S2/9WjXSOK/Nl2rYwVgxU7DCnZ/zpOTL1BErI=
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62/go.mod h1:p+ZRAsZS2pzZ6kX3GKWYurf3WZI2ygj7VbR8NM8qwfM=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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 (
"log"
"net/http"
"strings"
"time"
)
@ -15,9 +14,7 @@ func main() {
log.Println("Starting micropub server...")
log.Println("Current time: " + time.Now().Format(time.RFC3339))
log.Println("Blog URL: " + BlogUrl)
log.Println("Ignored URLs for Webmention: " + strings.Join(IgnoredWebmentionUrls, ", "))
http.HandleFunc("/micropub", HandleMicroPub)
http.HandleFunc("/media", HandleMedia)
http.HandleFunc("/webmention", HandleWebmention)
log.Fatal(http.ListenAndServe(":5555", nil))
}

View File

@ -1,9 +1,6 @@
package main
import (
"crypto/sha256"
"fmt"
"io"
"mime"
"net/http"
"path/filepath"
@ -11,6 +8,11 @@ import (
)
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" {
w.WriteHeader(http.StatusMethodNotAllowed)
_, _ = 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
}
authCode := r.Header.Get("authorization")
if formAuth := r.FormValue("authorization"); len(authCode) == 0 && len(formAuth) > 0 {
authCode = formAuth
if formAuth := r.FormValue("access_token"); len(authCode) == 0 && len(formAuth) > 0 {
authCode = "Bearer " + formAuth
}
if CheckAuthorization(authCode) {
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"))
return
}
defer func() { _ = file.Close() }()
hashFile, _, _ := r.FormFile("file")
h := sha256.New()
defer func() { _ = hashFile.Close() }()
if _, err := io.Copy(h, hashFile); err != nil {
fileName, err := getSHA256(hashFile)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Failed to calculate hash of file"))
_, _ = w.Write([]byte(err.Error()))
return
}
fileName := fmt.Sprintf("%x", h.Sum(nil))
fileExtension := filepath.Ext(header.Filename)
if len(fileExtension) == 0 {
// 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)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Failed to upload file"))
_, _ = w.Write([]byte("Failed to upload original file"))
return
}
if SelectedImageCompression != nil {
compressedLocation, err := SelectedImageCompression.Compress(location)
if err == nil && len(compressedLocation) > 0 {
location = compressedLocation
}
}
w.Header().Add("Location", location)
w.WriteHeader(http.StatusCreated)
} else {

View File

@ -1,7 +1,10 @@
package main
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
@ -23,9 +26,9 @@ type BunnyCdnStorage struct {
var bunnyCdnStorageUrl = "https://storage.bunnycdn.com"
func (b BunnyCdnStorage) Upload(fileName string, file multipart.File) (location string, err error) {
client := &http.Client{}
req, _ := http.NewRequest("PUT", bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape("/"+fileName), file)
func (b *BunnyCdnStorage) Upload(fileName string, file multipart.File) (location string, err error) {
client := http.DefaultClient
req, _ := http.NewRequest(http.MethodPut, bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape(fileName), file)
req.Header.Add("AccessKey", b.key)
resp, err := client.Do(req)
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
}
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 {
Name []string `json:"name,omitempty"`
Published []string `json:"published,omitempty"`
Updated []string `json:"updated,omitempty"`
Category []string `json:"category,omitempty"`
Content []string `json:"content,omitempty"`
Url []string `json:"url,omitempty"`
InReplyTo []string `json:"in-reply-to,omitempty"`
LikeOf []string `json:"like-of,omitempty"`
BookmarkOf []string `json:"bookmark-of,omitempty"`
MpSlug []string `json:"mp-slug,omitempty"`
MpSyndicateTo []string `json:"mp-syndicate-to,omitempty"`
}
Name []string `json:"name,omitempty"`
Published []string `json:"published,omitempty"`
Updated []string `json:"updated,omitempty"`
Category []string `json:"category,omitempty"`
Content []string `json:"content,omitempty"`
Url []string `json:"url,omitempty"`
InReplyTo []string `json:"in-reply-to,omitempty"`
LikeOf []string `json:"like-of,omitempty"`
BookmarkOf []string `json:"bookmark-of,omitempty"`
MpSlug []string `json:"mp-slug,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 (
"encoding/json"
"net/http"
"strconv"
"time"
)
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" {
w.Header().Add("Content-type", "application/json")
w.WriteHeader(http.StatusOK)
jsonBytes, err := json.Marshal(&MicropubConfig{
_ = json.NewEncoder(w).Encode(&MicropubConfig{
SyndicateTo: SyndicationTargets,
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
} else {
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"))
return
} else {
w.Header().Add("Location", location)
w.Header().Add("Location", location+"?cache=0")
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
}
} 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 (
"bytes"
"errors"
"gopkg.in/yaml.v2"
"strings"
"gopkg.in/yaml.v3"
)
type HugoFrontmatter struct {
Title string `yaml:"title,omitempty"`
Date string `yaml:"date,omitempty"`
Lastmod string `yaml:"lastmod,omitempty"`
Tags []string `yaml:"tags,omitempty"`
ExternalURL string `yaml:"externalURL,omitempty"`
Indieweb HugoFrontmatterIndieweb `yaml:"indieweb,omitempty"`
Syndicate []string `yaml:"syndicate,omitempty"`
type HugoFrontMatter struct {
Title string `yaml:"title,omitempty"`
Published string `yaml:"date,omitempty"`
Updated string `yaml:"lastmod,omitempty"`
Tags []string `yaml:"tags,omitempty"`
Series []string `yaml:"series,omitempty"`
ExternalURL string `yaml:"externalURL,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 {
Reply HugoFrontmatterReply `yaml:"reply,omitempty"`
Like HugoFrontmatterLike `yaml:"like,omitempty"`
type HugoFrontMatterIndieWeb struct {
Reply HugoFrontMatterReply `yaml:"reply,omitempty"`
Like HugoFrontMatterLike `yaml:"like,omitempty"`
}
type HugoFrontmatterReply struct {
type HugoFrontMatterReply struct {
Link string `yaml:"link,omitempty"`
Title string `yaml:"title,omitempty"`
}
type HugoFrontmatterLike struct {
type HugoFrontMatterLike struct {
Link string `yaml:"link,omitempty"`
Title string `yaml:"title,omitempty"`
}
func writeFrontMatter(entry *Entry) (frontmatter string, err error) {
var buff bytes.Buffer
writeFrontmatter := &HugoFrontmatter{
func writeFrontMatter(entry *Entry) (string, error) {
frontMatter := &HugoFrontMatter{
Title: entry.title,
Date: entry.date,
Lastmod: entry.lastmod,
Published: entry.date,
Updated: entry.lastmod,
Tags: entry.tags,
Series: entry.series,
ExternalURL: entry.link,
Indieweb: HugoFrontmatterIndieweb{
Reply: HugoFrontmatterReply{
IndieWeb: HugoFrontMatterIndieWeb{
Reply: HugoFrontMatterReply{
Link: entry.replyLink,
Title: entry.replyTitle,
},
Like: HugoFrontmatterLike{
Like: HugoFrontMatterLike{
Link: entry.likeLink,
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 {
err = errors.New("failed marshaling frontmatter")
return
return "", errors.New("failed encoding frontmatter")
}
buff.WriteString("---\n")
buff.Write(yamlBytes)
buff.WriteString("---\n")
frontmatter = buff.String()
return
writer.WriteString("---\n")
return writer.String(), nil
}
func WriteHugoPost(entry *Entry) (string, error) {
var buff bytes.Buffer
frontmatter, err := writeFrontMatter(entry)
buff := new(bytes.Buffer)
f, err := writeFrontMatter(entry)
if err != nil {
return "", err
}
buff.WriteString(frontmatter)
if len(entry.content) > 0 {
buff.WriteString(entry.content)
}
buff.WriteString(f)
buff.WriteString(entry.content)
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
import (
"bytes"
"encoding/base64"
"encoding/json"
"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"
"net/http"
"net/url"
"strings"
"os"
"path"
"sync"
"time"
)
type Storage interface {
CreateFile(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 Gitea struct {
endpoint string
token string
type Git struct {
filepath string
url string
username string
password string
name string
email string
lock *sync.Mutex
}
type giteaCommitRequest struct {
Content string `json:"content"`
Message string `json:"message"`
SHA string `json:"sha,omitempty"`
}
type giteaReadResponse struct {
Type string `json:"type"`
Content string `json:"content"`
SHA string `json:"sha"`
}
type giteaErrorResponse struct {
Message string `json:"message"`
}
func (gitea *Gitea) CreateFile(path string, file string, message string) (err error) {
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
func (g *Git) init() (r *git.Repository, w *git.Worktree, err error) {
// Open repo
r, err = git.PlainOpen(g.filepath)
if err == nil {
// Try to get work tree
w, err = r.Worktree()
if err == nil {
// Try to pull
err = w.Pull(&git.PullOptions{
Auth: &gitHttp.BasicAuth{
Username: g.username,
Password: g.password,
},
SingleBranch: true,
})
if err == git.NoErrAlreadyUpToDate {
err = nil
}
}
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 {
err = errors.New("failed reading file in repo")
return
// Delete old things
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
}
}
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 (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"strings"
)
@ -34,7 +33,7 @@ func checkAccess(token string) (bool, error) {
return false, errors.New("token string is empty")
}
// form the request to check the token
client := &http.Client{}
client := http.DefaultClient
req, err := http.NewRequest("GET", indieAuthTokenUrl, nil)
if err != nil {
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 {
return false, errors.New("error sending the request for checking token access")
}
defer res.Body.Close()
// parse the response
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return false, errors.New("error parsing the response for checking token access")
}
var indieAuthRes = new(IndieAuthRes)
err = json.Unmarshal(body, &indieAuthRes)
indieAuthRes := &IndieAuthRes{}
err = json.NewDecoder(res.Body).Decode(&indieAuthRes)
res.Body.Close()
if err != nil {
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)
}
})
}