Compare commits
3 Commits
master
...
kcoram/upl
Author | SHA1 | Date |
---|---|---|
Kevin C. Coram | 6aeb4836a2 | |
Kevin C. Coram | 802109cf31 | |
Kevin C. Coram | e0fc3147f7 |
20
.drone.yml
20
.drone.yml
|
@ -1,20 +0,0 @@
|
|||
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
|
|
@ -1,4 +1,3 @@
|
|||
.idea
|
||||
*.iml
|
||||
config.yml
|
||||
hugo-micropub
|
|
@ -1,9 +1,9 @@
|
|||
FROM golang:1.15-alpine3.12 as build
|
||||
FROM golang:1.13-alpine as build
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
RUN go build
|
||||
|
||||
FROM alpine:3.12
|
||||
FROM alpine:3.11
|
||||
RUN apk add --no-cache tzdata ca-certificates
|
||||
COPY --from=build /app/hugo-micropub /bin/
|
||||
EXPOSE 5555
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
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)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
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")
|
||||
}
|
196
config.go
196
config.go
|
@ -2,23 +2,20 @@ package main
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/caarlos0/env/v6"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
BlogUrl string
|
||||
MediaEndpointUrl string
|
||||
IgnoredWebmentionUrls []string
|
||||
SyndicationTargets []SyndicationTarget
|
||||
SelectedStorage Storage
|
||||
SelectedMediaStorage MediaStorage
|
||||
SelectedImageCompression ImageCompression
|
||||
DefaultLanguage string
|
||||
Languages map[string]Language
|
||||
SelectedCdn Cdn
|
||||
SelectedSocials Socials
|
||||
SelectedNotificationServices NotificationServices
|
||||
MediaEndpointUrl string
|
||||
)
|
||||
|
||||
type SyndicationTarget struct {
|
||||
|
@ -26,101 +23,34 @@ type SyndicationTarget struct {
|
|||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
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"`
|
||||
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:","`
|
||||
}
|
||||
|
||||
func initConfig() (err error) {
|
||||
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")
|
||||
cfg := config{}
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
return errors.New("failed to parse config, probably not all required env vars set")
|
||||
}
|
||||
// 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 (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")
|
||||
}
|
||||
// Media endpoint
|
||||
MediaEndpointUrl = cfg.BaseUrl + "/media"
|
||||
// Ignored Webmention URLs (optional)
|
||||
IgnoredWebmentionUrls = cfg.IgnoredWebmentionUrls
|
||||
// Syndication Targets (optional)
|
||||
targets := make([]SyndicationTarget, 0)
|
||||
for _, url := range cfg.SyndicationTargets {
|
||||
|
@ -132,16 +62,11 @@ func initConfig() (err error) {
|
|||
SyndicationTargets = targets
|
||||
// Find selected storage
|
||||
SelectedStorage = func() Storage {
|
||||
// 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{},
|
||||
// Gitea
|
||||
if len(cfg.GiteaEndpoint) > 0 && len(cfg.GiteaToken) >= 0 {
|
||||
return &Gitea{
|
||||
endpoint: cfg.GiteaEndpoint,
|
||||
token: cfg.GiteaToken,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -149,14 +74,13 @@ func initConfig() (err error) {
|
|||
if SelectedStorage == nil {
|
||||
return errors.New("no storage configured")
|
||||
}
|
||||
// Find selected media storage (Optional)
|
||||
// Find selected media storage
|
||||
SelectedMediaStorage = func() MediaStorage {
|
||||
// BunnyCDN
|
||||
// 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, "/") {
|
||||
if len(cfg.BunnyCdnStorageKey) > 0 && len(cfg.BunnyCdnStorageName) > 0 && len(cfg.MediaUrl) > 0 {
|
||||
return &BunnyCdnStorage{
|
||||
key: cfg.BunnyCdn.StorageKey,
|
||||
storageZoneName: cfg.BunnyCdn.StorageName,
|
||||
key: cfg.BunnyCdnStorageKey,
|
||||
storageZoneName: cfg.BunnyCdnStorageName,
|
||||
baseLocation: cfg.MediaUrl,
|
||||
}
|
||||
}
|
||||
|
@ -165,18 +89,46 @@ func initConfig() (err error) {
|
|||
if SelectedMediaStorage == nil {
|
||||
log.Println("no media storage configured")
|
||||
}
|
||||
// Find configured image compression service (optional)
|
||||
SelectedImageCompression = func() ImageCompression {
|
||||
// Tinify
|
||||
if len(cfg.Tinify.Key) > 0 {
|
||||
return &Tinify{
|
||||
key: cfg.Tinify.Key,
|
||||
}
|
||||
// Find selected CDN (optional)
|
||||
SelectedCdn = func() Cdn {
|
||||
// BunnyCDN (optional)
|
||||
if len(cfg.BunnyCdnKey) > 0 {
|
||||
return &BunnyCdn{key: cfg.BunnyCdnKey}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if SelectedImageCompression == nil {
|
||||
log.Println("no image compression configured")
|
||||
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")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
222
entry.go
222
entry.go
|
@ -6,10 +6,11 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -20,7 +21,6 @@ type Entry struct {
|
|||
lastmod string
|
||||
section string
|
||||
tags []string
|
||||
series []string
|
||||
link string
|
||||
slug string
|
||||
replyLink string
|
||||
|
@ -28,27 +28,22 @@ type Entry struct {
|
|||
likeLink string
|
||||
likeTitle string
|
||||
syndicate []string
|
||||
language string
|
||||
translationKey string
|
||||
images []Image
|
||||
audio string
|
||||
filename string
|
||||
location string
|
||||
token string
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
url string
|
||||
alt string
|
||||
}
|
||||
|
||||
func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
|
||||
if contentType == WwwForm {
|
||||
err := r.ParseForm()
|
||||
bodyString, err := parseRequestBody(r)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse Form")
|
||||
return nil, err
|
||||
}
|
||||
return createEntryFromValueMap(r.Form)
|
||||
bodyValues, err := url.ParseQuery(bodyString)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse query")
|
||||
}
|
||||
return createEntryFromValueMap(bodyValues)
|
||||
} else if contentType == Multipart {
|
||||
err := r.ParseMultipartForm(1024 * 1024 * 16)
|
||||
if err != nil {
|
||||
|
@ -56,9 +51,12 @@ func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
|
|||
}
|
||||
return createEntryFromValueMap(r.MultipartForm.Value)
|
||||
} else if contentType == Json {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
bodyString, err := parseRequestBody(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parsedMfItem := &MicroformatItem{}
|
||||
err := decoder.Decode(&parsedMfItem)
|
||||
err = json.Unmarshal([]byte(bodyString), &parsedMfItem)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to parse Json")
|
||||
}
|
||||
|
@ -68,10 +66,29 @@ 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 validEntry(values map[string][]string) bool {
|
||||
if _, ok := values["content"]; ok {
|
||||
return true
|
||||
} else if _, ok := values["like-of"]; ok {
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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 validEntry(values) {
|
||||
entry := &Entry{}
|
||||
if content, ok := values["content"]; ok {
|
||||
entry.content = content[0]
|
||||
|
@ -83,6 +100,8 @@ func createEntryFromValueMap(values map[string][]string) (*Entry, error) {
|
|||
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]
|
||||
|
@ -100,29 +119,8 @@ func createEntryFromValueMap(values map[string][]string) (*Entry, error) {
|
|||
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]
|
||||
} else {
|
||||
entry.syndicate = nil
|
||||
}
|
||||
if token, ok := values["access_token"]; ok {
|
||||
entry.token = "Bearer " + token[0]
|
||||
|
@ -132,15 +130,17 @@ func createEntryFromValueMap(values map[string][]string) (*Entry, error) {
|
|||
return nil, err
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
return nil, errors.New("error parsing the entry")
|
||||
}
|
||||
|
||||
func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) {
|
||||
if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" {
|
||||
return nil, errors.New("only entry type is supported so far")
|
||||
}
|
||||
entry := &Entry{}
|
||||
if mfEntry.Properties != nil && len(mfEntry.Properties.Content) == 1 && len(mfEntry.Properties.Content[0]) > 0 {
|
||||
entry.content = mfEntry.Properties.Content[0]
|
||||
entry := &Entry{
|
||||
content: mfEntry.Properties.Content[0],
|
||||
}
|
||||
if len(mfEntry.Properties.Name) == 1 {
|
||||
entry.title = mfEntry.Properties.Name[0]
|
||||
|
@ -163,31 +163,13 @@ func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) {
|
|||
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
|
||||
}
|
||||
return nil, errors.New("error parsing the entry")
|
||||
}
|
||||
|
||||
func computeExtraSettings(entry *Entry) error {
|
||||
|
@ -211,9 +193,6 @@ 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: ")
|
||||
|
@ -229,96 +208,28 @@ 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
|
||||
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
|
||||
if len(entry.section) < 1 {
|
||||
entry.section = "micro"
|
||||
}
|
||||
entry.section = strings.ToLower(entry.section)
|
||||
section := lang.Sections[entry.section]
|
||||
pathVars := struct {
|
||||
LocalContentFolder string
|
||||
LocalBlogUrl string
|
||||
Year int
|
||||
Month int
|
||||
Slug string
|
||||
Section string
|
||||
}{
|
||||
LocalContentFolder: contentFolder,
|
||||
LocalBlogUrl: localizedBlogUrl,
|
||||
Year: now.Year(),
|
||||
Month: int(now.Month()),
|
||||
Slug: entry.slug,
|
||||
Section: entry.section,
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -344,3 +255,34 @@ 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
24
go.mod
|
@ -1,21 +1,11 @@
|
|||
module git.jlel.se/jlelse/hugo-micropub
|
||||
module codeberg.org/jlelse/hugo-micropub
|
||||
|
||||
go 1.14
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
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
|
||||
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
|
||||
)
|
||||
|
|
123
go.sum
123
go.sum
|
@ -1,115 +1,26 @@
|
|||
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/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=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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/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/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=
|
||||
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=
|
||||
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.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=
|
||||
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=
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
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
|
||||
}
|
5
main.go
5
main.go
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -14,7 +15,11 @@ 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)
|
||||
if SelectedMediaStorage != nil {
|
||||
http.HandleFunc("/media", HandleMedia)
|
||||
}
|
||||
http.HandleFunc("/webmention", HandleWebmention)
|
||||
log.Fatal(http.ListenAndServe(":5555", nil))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
@ -9,9 +12,9 @@ import (
|
|||
|
||||
func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
||||
if SelectedMediaStorage == nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("No media storage configured"))
|
||||
return
|
||||
// This shouldn't happen since the HTTP handler isn't set up if there is no SelectedMediaStorage
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("No media endpoint configured"))
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
|
@ -30,8 +33,8 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
authCode := r.Header.Get("authorization")
|
||||
if formAuth := r.FormValue("access_token"); len(authCode) == 0 && len(formAuth) > 0 {
|
||||
authCode = "Bearer " + formAuth
|
||||
if formAuth := r.FormValue("authorization"); len(authCode) == 0 && len(formAuth) > 0 {
|
||||
authCode = formAuth
|
||||
}
|
||||
if CheckAuthorization(authCode) {
|
||||
file, header, err := r.FormFile("file")
|
||||
|
@ -40,15 +43,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() }()
|
||||
fileName, err := getSHA256(hashFile)
|
||||
if err != nil {
|
||||
if _, err := io.Copy(h, hashFile); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(err.Error()))
|
||||
_, _ = w.Write([]byte("Failed to calculate hash of file"))
|
||||
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
|
||||
|
@ -60,19 +63,13 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
}
|
||||
fileName += strings.ToLower(fileExtension)
|
||||
fileName += fileExtension
|
||||
location, err := SelectedMediaStorage.Upload(fileName, file)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("Failed to upload original file"))
|
||||
_, _ = w.Write([]byte("Failed to upload 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 {
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -26,9 +23,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.DefaultClient
|
||||
req, _ := http.NewRequest(http.MethodPut, bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape(fileName), file)
|
||||
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)
|
||||
req.Header.Add("AccessKey", b.key)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil || resp.StatusCode != 201 {
|
||||
|
@ -36,12 +33,3 @@ 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
|
||||
}
|
||||
|
|
|
@ -17,6 +17,4 @@ type MicroformatProperties struct {
|
|||
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"`
|
||||
}
|
50
micropub.go
50
micropub.go
|
@ -3,6 +3,8 @@ package main
|
|||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MicropubConfig struct {
|
||||
|
@ -10,16 +12,43 @@ type MicropubConfig struct {
|
|||
MediaEndpoint string `json:"media-endpoint,omitempty"`
|
||||
}
|
||||
|
||||
func getMediaEndpoint() string {
|
||||
if SelectedMediaStorage != nil {
|
||||
return MediaEndpointUrl
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func HandleMicroPub(w http.ResponseWriter, r *http.Request) {
|
||||
// a handler for GET requests, used for troubleshooting
|
||||
if r.Method == "GET" {
|
||||
if q := r.URL.Query().Get("q"); q == "config" || q == "syndicate-to" {
|
||||
w.Header().Add("Content-type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(&MicropubConfig{
|
||||
jsonBytes, err := json.Marshal(&MicropubConfig{
|
||||
SyndicateTo: SyndicationTargets,
|
||||
MediaEndpoint: MediaEndpointUrl,
|
||||
MediaEndpoint: getMediaEndpoint(),
|
||||
})
|
||||
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")
|
||||
|
@ -62,8 +91,23 @@ 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+"?cache=0")
|
||||
w.Header().Add("Location", location)
|
||||
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 {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
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
|
||||
}
|
123
post.go
123
post.go
|
@ -3,81 +3,130 @@ package main
|
|||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"gopkg.in/yaml.v2"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HugoFrontMatter struct {
|
||||
type HugoFrontmatter struct {
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Published string `yaml:"date,omitempty"`
|
||||
Updated string `yaml:"lastmod,omitempty"`
|
||||
Date string `yaml:"date,omitempty"`
|
||||
Lastmod string `yaml:"lastmod,omitempty"`
|
||||
Tags []string `yaml:"tags,omitempty"`
|
||||
Series []string `yaml:"series,omitempty"`
|
||||
ExternalURL string `yaml:"externalURL,omitempty"`
|
||||
IndieWeb HugoFrontMatterIndieWeb `yaml:"indieweb,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) (string, error) {
|
||||
frontMatter := &HugoFrontMatter{
|
||||
func writeFrontMatter(entry *Entry) (frontmatter string, err error) {
|
||||
var buff bytes.Buffer
|
||||
writeFrontmatter := &HugoFrontmatter{
|
||||
Title: entry.title,
|
||||
Published: entry.date,
|
||||
Updated: entry.lastmod,
|
||||
Date: entry.date,
|
||||
Lastmod: 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,
|
||||
TranslationKey: entry.translationKey,
|
||||
Audio: entry.audio,
|
||||
}
|
||||
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)
|
||||
yamlBytes, err := yaml.Marshal(&writeFrontmatter)
|
||||
if err != nil {
|
||||
return "", errors.New("failed encoding frontmatter")
|
||||
err = errors.New("failed marshaling frontmatter")
|
||||
return
|
||||
}
|
||||
writer.WriteString("---\n")
|
||||
return writer.String(), nil
|
||||
buff.WriteString("---\n")
|
||||
buff.Write(yamlBytes)
|
||||
buff.WriteString("---\n")
|
||||
frontmatter = buff.String()
|
||||
return
|
||||
}
|
||||
|
||||
func WriteHugoPost(entry *Entry) (string, error) {
|
||||
buff := new(bytes.Buffer)
|
||||
f, err := writeFrontMatter(entry)
|
||||
var buff bytes.Buffer
|
||||
frontmatter, err := writeFrontMatter(entry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buff.WriteString(f)
|
||||
buff.WriteString(frontmatter)
|
||||
if len(entry.content) > 0 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
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)
|
||||
}
|
266
storage.go
266
storage.go
|
@ -1,184 +1,134 @@
|
|||
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"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Storage interface {
|
||||
CreateFile(path string, file string, message string) (err error)
|
||||
UpdateFile(path string, file string, message string) (err error)
|
||||
DeleteFile(path string, message string) (err error)
|
||||
ReadFile(path string) (content string, sha string, exists bool, err error)
|
||||
}
|
||||
|
||||
type Git struct {
|
||||
filepath string
|
||||
url string
|
||||
username string
|
||||
password string
|
||||
name string
|
||||
email string
|
||||
lock *sync.Mutex
|
||||
// Gitea
|
||||
type Gitea struct {
|
||||
endpoint string
|
||||
token string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// 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")
|
||||
return errors.New("failed to marshal json before committing")
|
||||
}
|
||||
if err == nil {
|
||||
w, err = r.Worktree()
|
||||
if err != nil {
|
||||
err = errors.New("failed to get work tree")
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
func (gitea *Gitea) UpdateFile(path string, file string, message string) (err error) {
|
||||
_, sha, exists, err := gitea.ReadFile(path)
|
||||
if err != nil {
|
||||
err = errors.New("failed to initialize repo")
|
||||
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
|
||||
}
|
||||
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)
|
||||
errorResponse := &giteaErrorResponse{}
|
||||
marshalErr := json.Unmarshal(body, &errorResponse)
|
||||
if marshalErr != nil {
|
||||
err = errors.New("failed parsing Gitea error response")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
err = errors.New("failed reading file in repo")
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
@ -33,7 +34,7 @@ func checkAccess(token string) (bool, error) {
|
|||
return false, errors.New("token string is empty")
|
||||
}
|
||||
// form the request to check the token
|
||||
client := http.DefaultClient
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", indieAuthTokenUrl, nil)
|
||||
if err != nil {
|
||||
return false, errors.New("error making the request for checking token access")
|
||||
|
@ -45,10 +46,14 @@ 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
|
||||
indieAuthRes := &IndieAuthRes{}
|
||||
err = json.NewDecoder(res.Body).Decode(&indieAuthRes)
|
||||
res.Body.Close()
|
||||
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)
|
||||
if err != nil {
|
||||
return false, errors.New("Error parsing the response into json for checking token access " + err.Error())
|
||||
}
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue