Compare commits
43 Commits
102f555cbc
...
5bac6cf0f1
Author | SHA1 | Date |
---|---|---|
Jan-Lukas Else | 5bac6cf0f1 | |
Jan-Lukas Else | ee68238f30 | |
Jan-Lukas Else | 0f2ac430ce | |
Jan-Lukas Else | 05e6f383c2 | |
Jan-Lukas Else | 22355825ed | |
Jan-Lukas Else | 491ce51bd4 | |
Jan-Lukas Else | 7cbaf6ad31 | |
Jan-Lukas Else | 9aebddd251 | |
Jan-Lukas Else | ed21c00622 | |
Jan-Lukas Else | c0c480c660 | |
Jan-Lukas Else | 0292344035 | |
Jan-Lukas Else | ce0baf0086 | |
Jan-Lukas Else | 79b28111f5 | |
Jan-Lukas Else | e716188dff | |
Jan-Lukas Else | 8a72be6fc2 | |
Jan-Lukas Else | 580724e9f9 | |
Jan-Lukas Else | 09e3b77453 | |
Jan-Lukas Else | cfc8911b77 | |
Jan-Lukas Else | 9027f6734b | |
Jan-Lukas Else | d747ceddf3 | |
Jan-Lukas Else | 7c1e6baa3f | |
Jan-Lukas Else | cbf53ccb23 | |
Jan-Lukas Else | 442395a83c | |
Jan-Lukas Else | 0faf4c8e39 | |
Jan-Lukas Else | 686a53707d | |
Jan-Lukas Else | 4997f1092e | |
Jan-Lukas Else | 7e819f4017 | |
Jan-Lukas Else | f4bac41d11 | |
Jan-Lukas Else | 82d9057b10 | |
Jan-Lukas Else | 0ee80b3798 | |
Jan-Lukas Else | af67b7ebbb | |
Jan-Lukas Else | 126db98c64 | |
Jan-Lukas Else | 1fb3f28aea | |
Jan-Lukas Else | 9ca7da9ae5 | |
Jan-Lukas Else | 962818c931 | |
Jan-Lukas Else | 108571ac15 | |
Jan-Lukas Else | 85adb26acd | |
Jan-Lukas Else | 2872afbc28 | |
Jan-Lukas Else | fb76567cef | |
Jan-Lukas Else | 1c3cc6480f | |
Jan-Lukas Else | f71738cde4 | |
Jan-Lukas Else | 2136265825 | |
Jan-Lukas Else | 9a41fb90b9 |
|
@ -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
|
|
@ -1,2 +1,4 @@
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
|
config.yml
|
||||||
|
hugo-micropub
|
|
@ -1,9 +1,9 @@
|
||||||
FROM golang:1.13-alpine as build
|
FROM golang:1.15-alpine3.12 as build
|
||||||
ADD . /app
|
ADD . /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN go build
|
RUN go build
|
||||||
|
|
||||||
FROM alpine:3.11
|
FROM alpine:3.12
|
||||||
RUN apk add --no-cache tzdata ca-certificates
|
RUN apk add --no-cache tzdata ca-certificates
|
||||||
COPY --from=build /app/hugo-micropub /bin/
|
COPY --from=build /app/hugo-micropub /bin/
|
||||||
EXPOSE 5555
|
EXPOSE 5555
|
||||||
|
|
27
cdn.go
27
cdn.go
|
@ -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)
|
|
||||||
}
|
|
30
cdn_test.go
30
cdn_test.go
|
@ -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
198
config.go
|
@ -2,20 +2,23 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/caarlos0/env/v6"
|
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
BlogUrl string
|
BlogUrl string
|
||||||
IgnoredWebmentionUrls []string
|
MediaEndpointUrl string
|
||||||
SyndicationTargets []SyndicationTarget
|
SyndicationTargets []SyndicationTarget
|
||||||
SelectedStorage Storage
|
SelectedStorage Storage
|
||||||
SelectedMediaStorage MediaStorage
|
SelectedMediaStorage MediaStorage
|
||||||
SelectedCdn Cdn
|
SelectedImageCompression ImageCompression
|
||||||
SelectedSocials Socials
|
DefaultLanguage string
|
||||||
SelectedNotificationServices NotificationServices
|
Languages map[string]Language
|
||||||
MediaEndpointUrl string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SyndicationTarget struct {
|
type SyndicationTarget struct {
|
||||||
|
@ -23,34 +26,101 @@ type SyndicationTarget struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type YamlConfig struct {
|
||||||
BlogUrl string `env:"BLOG_URL,required"`
|
BlogUrl string `yaml:"blogUrl"`
|
||||||
BaseUrl string `env:"BASE_URL,required"`
|
BaseUrl string `yaml:"baseUrl"`
|
||||||
MediaUrl string `env:"MEDIA_URL"`
|
MediaUrl string `yaml:"mediaUrl"`
|
||||||
GiteaEndpoint string `env:"GITEA_ENDPOINT"`
|
DefaultLanguage string `yaml:"defaultLang"`
|
||||||
GiteaToken string `env:"GITEA_TOKEN"`
|
Languages map[string]Language `yaml:"languages"`
|
||||||
BunnyCdnKey string `env:"BUNNY_CDN_KEY"`
|
Git GitConfig `yaml:"git"`
|
||||||
BunnyCdnStorageKey string `env:"BUNNY_CDN_STORAGE_KEY"`
|
BunnyCdn BunnyCdnConfig `yaml:"bunnyCdn"`
|
||||||
BunnyCdnStorageName string `env:"BUNNY_CDN_STORAGE_NAME"`
|
Tinify TinifyConfig `yaml:"tinify"`
|
||||||
MicroblogUrl string `env:"MICROBLOG_URL"`
|
SyndicationTargets []string `yaml:"syndication"`
|
||||||
MicroblogToken string `env:"MICROBLOG_TOKEN"`
|
}
|
||||||
TelegramUserId int64 `env:"TELEGRAM_USER_ID"`
|
|
||||||
TelegramBotToken string `env:"TELEGRAM_BOT_TOKEN"`
|
type BunnyCdnConfig struct {
|
||||||
IgnoredWebmentionUrls []string `env:"WEBMENTION_IGNORED" envSeparator:","`
|
StorageKey string `yaml:"storageKey"`
|
||||||
SyndicationTargets []string `env:"SYNDICATION" envSeparator:","`
|
StorageName string `yaml:"storageName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TinifyConfig struct {
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GitConfig struct {
|
||||||
|
Filepath string `yaml:"filepath"`
|
||||||
|
Url string `yaml:"url"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
AuthorName string `yaml:"authorName"`
|
||||||
|
AuthorEmail string `yaml:"authorEmail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Language struct {
|
||||||
|
BlogUrl string `yaml:"blogUrl"`
|
||||||
|
ContentDir string `yaml:"contentDir"`
|
||||||
|
DefaultSection string `yaml:"defaultSection"`
|
||||||
|
Sections map[string]Section `yaml:"sections"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Section struct {
|
||||||
|
FilenameTemplate string `yaml:"file"`
|
||||||
|
LocationTemplate string `yaml:"location"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfig() (err error) {
|
func initConfig() (err error) {
|
||||||
cfg := config{}
|
configFileName, configSet := os.LookupEnv("CONFIG")
|
||||||
if err := env.Parse(&cfg); err != nil {
|
if !configSet {
|
||||||
return errors.New("failed to parse config, probably not all required env vars set")
|
configFileName = "config.yml"
|
||||||
|
}
|
||||||
|
configFile, err := os.Open(configFileName)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to open config file")
|
||||||
|
}
|
||||||
|
cfg := YamlConfig{}
|
||||||
|
err = yaml.NewDecoder(configFile).Decode(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to parse yaml")
|
||||||
}
|
}
|
||||||
// Blog URL (required)
|
// Blog URL (required)
|
||||||
|
if len(cfg.BlogUrl) < 1 {
|
||||||
|
return errors.New("blogUrl not configured")
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(cfg.BlogUrl, "/") {
|
||||||
|
return errors.New("missing trailing slash in configured blogUrl")
|
||||||
|
}
|
||||||
BlogUrl = cfg.BlogUrl
|
BlogUrl = cfg.BlogUrl
|
||||||
// Media endpoint
|
// Media endpoint (required)
|
||||||
MediaEndpointUrl = cfg.BaseUrl + "/media"
|
if len(cfg.BaseUrl) < 1 {
|
||||||
// Ignored Webmention URLs (optional)
|
return errors.New("baseUrl not configured")
|
||||||
IgnoredWebmentionUrls = cfg.IgnoredWebmentionUrls
|
}
|
||||||
|
if len(cfg.MediaUrl) < 1 {
|
||||||
|
return errors.New("mediaUrl not configured")
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(cfg.BaseUrl, "/") {
|
||||||
|
return errors.New("missing trailing slash in configured baseUrl")
|
||||||
|
}
|
||||||
|
MediaEndpointUrl = cfg.BaseUrl + "media"
|
||||||
|
// Languages (required)
|
||||||
|
if len(cfg.DefaultLanguage) < 1 {
|
||||||
|
return errors.New("no default language configured")
|
||||||
|
}
|
||||||
|
DefaultLanguage = cfg.DefaultLanguage
|
||||||
|
if len(cfg.Languages) > 0 {
|
||||||
|
for _, lang := range cfg.Languages {
|
||||||
|
if len(lang.ContentDir) < 1 || len(lang.DefaultSection) < 1 || len(lang.Sections) < 1 {
|
||||||
|
return errors.New("language not completely configured")
|
||||||
|
}
|
||||||
|
for _, section := range lang.Sections {
|
||||||
|
if len(section.FilenameTemplate) < 1 || len(section.LocationTemplate) < 1 {
|
||||||
|
return errors.New("section not completely configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Languages = cfg.Languages
|
||||||
|
} else {
|
||||||
|
return errors.New("no languages configured")
|
||||||
|
}
|
||||||
// Syndication Targets (optional)
|
// Syndication Targets (optional)
|
||||||
targets := make([]SyndicationTarget, 0)
|
targets := make([]SyndicationTarget, 0)
|
||||||
for _, url := range cfg.SyndicationTargets {
|
for _, url := range cfg.SyndicationTargets {
|
||||||
|
@ -62,11 +132,16 @@ func initConfig() (err error) {
|
||||||
SyndicationTargets = targets
|
SyndicationTargets = targets
|
||||||
// Find selected storage
|
// Find selected storage
|
||||||
SelectedStorage = func() Storage {
|
SelectedStorage = func() Storage {
|
||||||
// Gitea
|
// Git
|
||||||
if len(cfg.GiteaEndpoint) > 0 && len(cfg.GiteaToken) >= 0 {
|
if len(cfg.Git.Filepath) > 0 && len(cfg.Git.Url) > 0 && len(cfg.Git.Username) > 0 && len(cfg.Git.Password) > 0 && len(cfg.Git.AuthorName) > 0 && len(cfg.Git.AuthorEmail) > 0 {
|
||||||
return &Gitea{
|
return &Git{
|
||||||
endpoint: cfg.GiteaEndpoint,
|
filepath: cfg.Git.Filepath,
|
||||||
token: cfg.GiteaToken,
|
url: cfg.Git.Url,
|
||||||
|
username: cfg.Git.Username,
|
||||||
|
password: cfg.Git.Password,
|
||||||
|
name: cfg.Git.AuthorName,
|
||||||
|
email: cfg.Git.AuthorEmail,
|
||||||
|
lock: &sync.Mutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -74,61 +149,34 @@ func initConfig() (err error) {
|
||||||
if SelectedStorage == nil {
|
if SelectedStorage == nil {
|
||||||
return errors.New("no storage configured")
|
return errors.New("no storage configured")
|
||||||
}
|
}
|
||||||
// Find selected media storage
|
// Find selected media storage (Optional)
|
||||||
SelectedMediaStorage = func() MediaStorage {
|
SelectedMediaStorage = func() MediaStorage {
|
||||||
// BunnyCDN
|
// BunnyCDN
|
||||||
if len(cfg.BunnyCdnStorageKey) > 0 && len(cfg.BunnyCdnStorageName) > 0 && len(cfg.MediaUrl) > 0 {
|
// MEDIA_URL needs trailing slash too
|
||||||
|
if len(cfg.BunnyCdn.StorageKey) > 0 && len(cfg.BunnyCdn.StorageName) > 0 && len(cfg.MediaUrl) > 0 && strings.HasSuffix(cfg.MediaUrl, "/") {
|
||||||
return &BunnyCdnStorage{
|
return &BunnyCdnStorage{
|
||||||
key: cfg.BunnyCdnStorageKey,
|
key: cfg.BunnyCdn.StorageKey,
|
||||||
storageZoneName: cfg.BunnyCdnStorageName,
|
storageZoneName: cfg.BunnyCdn.StorageName,
|
||||||
baseLocation: cfg.MediaUrl,
|
baseLocation: cfg.MediaUrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if SelectedMediaStorage == nil {
|
if SelectedMediaStorage == nil {
|
||||||
return errors.New("no media storage configured")
|
log.Println("no media storage configured")
|
||||||
}
|
}
|
||||||
// Find selected CDN (optional)
|
// Find configured image compression service (optional)
|
||||||
SelectedCdn = func() Cdn {
|
SelectedImageCompression = func() ImageCompression {
|
||||||
// BunnyCDN (optional)
|
// Tinify
|
||||||
if len(cfg.BunnyCdnKey) > 0 {
|
if len(cfg.Tinify.Key) > 0 {
|
||||||
return &BunnyCdn{key: cfg.BunnyCdnKey}
|
return &Tinify{
|
||||||
|
key: cfg.Tinify.Key,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if SelectedCdn == nil {
|
if SelectedImageCompression == nil {
|
||||||
log.Println("no CDN configured")
|
log.Println("no image compression configured")
|
||||||
}
|
|
||||||
// Find configured social networks (optional)
|
|
||||||
SelectedSocials = func() Socials {
|
|
||||||
var socials []Social = nil
|
|
||||||
// Microblog.pub
|
|
||||||
if len(cfg.MicroblogUrl) > 0 && len(cfg.MicroblogToken) > 0 {
|
|
||||||
socials = append(socials, &MicroblogPub{
|
|
||||||
url: cfg.MicroblogUrl,
|
|
||||||
token: cfg.MicroblogToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return socials
|
|
||||||
}()
|
|
||||||
if SelectedSocials == nil {
|
|
||||||
log.Println("no social networks configured")
|
|
||||||
}
|
|
||||||
// Find configured notification services (optional)
|
|
||||||
SelectedNotificationServices = func() NotificationServices {
|
|
||||||
var notificationServices []NotificationService = nil
|
|
||||||
// Telegram
|
|
||||||
if cfg.TelegramUserId > 0 && len(cfg.TelegramBotToken) > 0 {
|
|
||||||
notificationServices = append(notificationServices, &Telegram{
|
|
||||||
userId: cfg.TelegramUserId,
|
|
||||||
botToken: cfg.TelegramBotToken,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return notificationServices
|
|
||||||
}()
|
|
||||||
if SelectedNotificationServices == nil {
|
|
||||||
log.Println("No notification services configured")
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
368
entry.go
368
entry.go
|
@ -6,44 +6,49 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Entry struct {
|
type Entry struct {
|
||||||
content string
|
content string
|
||||||
title string
|
title string
|
||||||
date string
|
date string
|
||||||
lastmod string
|
lastmod string
|
||||||
section string
|
section string
|
||||||
tags []string
|
tags []string
|
||||||
link string
|
series []string
|
||||||
slug string
|
link string
|
||||||
replyLink string
|
slug string
|
||||||
replyTitle string
|
replyLink string
|
||||||
likeLink string
|
replyTitle string
|
||||||
likeTitle string
|
likeLink string
|
||||||
syndicate []string
|
likeTitle string
|
||||||
filename string
|
syndicate []string
|
||||||
location string
|
language string
|
||||||
token string
|
translationKey string
|
||||||
|
images []Image
|
||||||
|
audio string
|
||||||
|
filename string
|
||||||
|
location string
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
url string
|
||||||
|
alt string
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
|
func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
|
||||||
if contentType == WwwForm {
|
if contentType == WwwForm {
|
||||||
bodyString, err := parseRequestBody(r)
|
err := r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, errors.New("failed to parse Form")
|
||||||
}
|
}
|
||||||
bodyValues, err := url.ParseQuery(bodyString)
|
return createEntryFromValueMap(r.Form)
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("failed to parse query")
|
|
||||||
}
|
|
||||||
return createEntryFromValueMap(bodyValues)
|
|
||||||
} else if contentType == Multipart {
|
} else if contentType == Multipart {
|
||||||
err := r.ParseMultipartForm(1024 * 1024 * 16)
|
err := r.ParseMultipartForm(1024 * 1024 * 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -51,12 +56,9 @@ func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
|
||||||
}
|
}
|
||||||
return createEntryFromValueMap(r.MultipartForm.Value)
|
return createEntryFromValueMap(r.MultipartForm.Value)
|
||||||
} else if contentType == Json {
|
} else if contentType == Json {
|
||||||
bodyString, err := parseRequestBody(r)
|
decoder := json.NewDecoder(r.Body)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
parsedMfItem := &MicroformatItem{}
|
parsedMfItem := &MicroformatItem{}
|
||||||
err = json.Unmarshal([]byte(bodyString), &parsedMfItem)
|
err := decoder.Decode(&parsedMfItem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to parse Json")
|
return nil, errors.New("failed to parse Json")
|
||||||
}
|
}
|
||||||
|
@ -66,100 +68,126 @@ func CreateEntry(contentType ContentType, r *http.Request) (*Entry, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseRequestBody(r *http.Request) (string, error) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
bodyBytes, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("failed to read body")
|
|
||||||
}
|
|
||||||
return string(bodyBytes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createEntryFromValueMap(values map[string][]string) (*Entry, error) {
|
func createEntryFromValueMap(values map[string][]string) (*Entry, error) {
|
||||||
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
|
if h, ok := values["h"]; ok && (len(h) != 1 || h[0] != "entry") {
|
||||||
return nil, errors.New("only entry type is supported so far")
|
return nil, errors.New("only entry type is supported so far")
|
||||||
}
|
}
|
||||||
if _, ok := values["content"]; ok {
|
entry := &Entry{}
|
||||||
entry := &Entry{
|
if content, ok := values["content"]; ok {
|
||||||
content: values["content"][0],
|
entry.content = content[0]
|
||||||
}
|
|
||||||
if name, ok := values["name"]; ok {
|
|
||||||
entry.title = name[0]
|
|
||||||
}
|
|
||||||
if category, ok := values["category"]; ok {
|
|
||||||
entry.tags = category
|
|
||||||
} else if categories, ok := values["category[]"]; ok {
|
|
||||||
entry.tags = categories
|
|
||||||
} else {
|
|
||||||
entry.tags = nil
|
|
||||||
}
|
|
||||||
if slug, ok := values["mp-slug"]; ok && len(slug) > 0 && slug[0] != "" {
|
|
||||||
entry.slug = slug[0]
|
|
||||||
}
|
|
||||||
if inReplyTo, ok := values["in-reply-to"]; ok {
|
|
||||||
entry.replyLink = inReplyTo[0]
|
|
||||||
}
|
|
||||||
if likeOf, ok := values["like-of"]; ok {
|
|
||||||
entry.likeLink = likeOf[0]
|
|
||||||
}
|
|
||||||
if bookmarkOf, ok := values["bookmark-of"]; ok {
|
|
||||||
entry.link = bookmarkOf[0]
|
|
||||||
}
|
|
||||||
if syndicate, ok := values["mp-syndicate-to"]; ok {
|
|
||||||
entry.syndicate = syndicate
|
|
||||||
} else if syndicates, ok := values["mp-syndicate-to[]"]; ok {
|
|
||||||
entry.syndicate = syndicates
|
|
||||||
} else {
|
|
||||||
entry.syndicate = nil
|
|
||||||
}
|
|
||||||
if token, ok := values["access_token"]; ok {
|
|
||||||
entry.token = "Bearer " + token[0]
|
|
||||||
}
|
|
||||||
err := computeExtraSettings(entry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return entry, nil
|
|
||||||
}
|
}
|
||||||
return nil, errors.New("error parsing the entry")
|
if name, ok := values["name"]; ok {
|
||||||
|
entry.title = name[0]
|
||||||
|
}
|
||||||
|
if category, ok := values["category"]; ok {
|
||||||
|
entry.tags = category
|
||||||
|
} else if categories, ok := values["category[]"]; ok {
|
||||||
|
entry.tags = categories
|
||||||
|
}
|
||||||
|
if slug, ok := values["mp-slug"]; ok && len(slug) > 0 && slug[0] != "" {
|
||||||
|
entry.slug = slug[0]
|
||||||
|
}
|
||||||
|
if inReplyTo, ok := values["in-reply-to"]; ok {
|
||||||
|
entry.replyLink = inReplyTo[0]
|
||||||
|
}
|
||||||
|
if likeOf, ok := values["like-of"]; ok {
|
||||||
|
entry.likeLink = likeOf[0]
|
||||||
|
}
|
||||||
|
if bookmarkOf, ok := values["bookmark-of"]; ok {
|
||||||
|
entry.link = bookmarkOf[0]
|
||||||
|
}
|
||||||
|
if syndicate, ok := values["mp-syndicate-to"]; ok {
|
||||||
|
entry.syndicate = syndicate
|
||||||
|
} else if syndicates, ok := values["mp-syndicate-to[]"]; ok {
|
||||||
|
entry.syndicate = syndicates
|
||||||
|
}
|
||||||
|
if photo, ok := values["photo"]; ok {
|
||||||
|
entry.images = append(entry.images, Image{url: photo[0]})
|
||||||
|
} else if photos, ok := values["photo[]"]; ok {
|
||||||
|
for _, photo := range photos {
|
||||||
|
entry.images = append(entry.images, Image{url: photo})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if photoAlt, ok := values["mp-photo-alt"]; ok {
|
||||||
|
if len(entry.images) > 0 {
|
||||||
|
entry.images[0].alt = photoAlt[0]
|
||||||
|
}
|
||||||
|
} else if photoAlts, ok := values["mp-photo-alt[]"]; ok {
|
||||||
|
for i, photoAlt := range photoAlts {
|
||||||
|
if len(entry.images) > i {
|
||||||
|
entry.images[i].alt = photoAlt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if audio, ok := values["audio"]; ok {
|
||||||
|
entry.audio = audio[0]
|
||||||
|
} else if audio, ok := values["audio[]"]; ok {
|
||||||
|
entry.audio = audio[0]
|
||||||
|
}
|
||||||
|
if token, ok := values["access_token"]; ok {
|
||||||
|
entry.token = "Bearer " + token[0]
|
||||||
|
}
|
||||||
|
err := computeExtraSettings(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) {
|
func createEntryFromMicroformat(mfEntry *MicroformatItem) (*Entry, error) {
|
||||||
if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" {
|
if len(mfEntry.Type) != 1 || mfEntry.Type[0] != "h-entry" {
|
||||||
return nil, errors.New("only entry type is supported so far")
|
return nil, errors.New("only entry type is supported so far")
|
||||||
}
|
}
|
||||||
|
entry := &Entry{}
|
||||||
if mfEntry.Properties != nil && len(mfEntry.Properties.Content) == 1 && len(mfEntry.Properties.Content[0]) > 0 {
|
if mfEntry.Properties != nil && len(mfEntry.Properties.Content) == 1 && len(mfEntry.Properties.Content[0]) > 0 {
|
||||||
entry := &Entry{
|
entry.content = mfEntry.Properties.Content[0]
|
||||||
content: mfEntry.Properties.Content[0],
|
|
||||||
}
|
|
||||||
if len(mfEntry.Properties.Name) == 1 {
|
|
||||||
entry.title = mfEntry.Properties.Name[0]
|
|
||||||
}
|
|
||||||
if len(mfEntry.Properties.Category) > 0 {
|
|
||||||
entry.tags = mfEntry.Properties.Category
|
|
||||||
}
|
|
||||||
if len(mfEntry.Properties.MpSlug) == 1 && len(mfEntry.Properties.MpSlug[0]) > 0 {
|
|
||||||
entry.slug = mfEntry.Properties.MpSlug[0]
|
|
||||||
}
|
|
||||||
if len(mfEntry.Properties.InReplyTo) == 1 {
|
|
||||||
entry.replyLink = mfEntry.Properties.InReplyTo[0]
|
|
||||||
}
|
|
||||||
if len(mfEntry.Properties.LikeOf) == 1 {
|
|
||||||
entry.likeLink = mfEntry.Properties.LikeOf[0]
|
|
||||||
}
|
|
||||||
if len(mfEntry.Properties.BookmarkOf) == 1 {
|
|
||||||
entry.link = mfEntry.Properties.BookmarkOf[0]
|
|
||||||
}
|
|
||||||
if len(mfEntry.Properties.MpSyndicateTo) > 0 {
|
|
||||||
entry.syndicate = mfEntry.Properties.MpSyndicateTo
|
|
||||||
}
|
|
||||||
err := computeExtraSettings(entry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return entry, nil
|
|
||||||
}
|
}
|
||||||
return nil, errors.New("error parsing the entry")
|
if len(mfEntry.Properties.Name) == 1 {
|
||||||
|
entry.title = mfEntry.Properties.Name[0]
|
||||||
|
}
|
||||||
|
if len(mfEntry.Properties.Category) > 0 {
|
||||||
|
entry.tags = mfEntry.Properties.Category
|
||||||
|
}
|
||||||
|
if len(mfEntry.Properties.MpSlug) == 1 && len(mfEntry.Properties.MpSlug[0]) > 0 {
|
||||||
|
entry.slug = mfEntry.Properties.MpSlug[0]
|
||||||
|
}
|
||||||
|
if len(mfEntry.Properties.InReplyTo) == 1 {
|
||||||
|
entry.replyLink = mfEntry.Properties.InReplyTo[0]
|
||||||
|
}
|
||||||
|
if len(mfEntry.Properties.LikeOf) == 1 {
|
||||||
|
entry.likeLink = mfEntry.Properties.LikeOf[0]
|
||||||
|
}
|
||||||
|
if len(mfEntry.Properties.BookmarkOf) == 1 {
|
||||||
|
entry.link = mfEntry.Properties.BookmarkOf[0]
|
||||||
|
}
|
||||||
|
if len(mfEntry.Properties.MpSyndicateTo) > 0 {
|
||||||
|
entry.syndicate = mfEntry.Properties.MpSyndicateTo
|
||||||
|
}
|
||||||
|
if len(mfEntry.Properties.Photo) > 0 {
|
||||||
|
for _, photo := range mfEntry.Properties.Photo {
|
||||||
|
if theString, justString := photo.(string); justString {
|
||||||
|
entry.images = append(entry.images, Image{url: theString})
|
||||||
|
} else if thePhoto, isPhoto := photo.(map[string]interface{}); isPhoto {
|
||||||
|
image := Image{}
|
||||||
|
// Micropub spec says "value" is correct, but not sure about that
|
||||||
|
if photoUrl, ok := thePhoto["value"].(string); ok {
|
||||||
|
image.url = photoUrl
|
||||||
|
}
|
||||||
|
if alt, ok := thePhoto["alt"].(string); ok {
|
||||||
|
image.alt = alt
|
||||||
|
}
|
||||||
|
entry.images = append(entry.images, image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mfEntry.Properties.Audio) > 0 {
|
||||||
|
entry.audio = mfEntry.Properties.Audio[0]
|
||||||
|
}
|
||||||
|
err := computeExtraSettings(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeExtraSettings(entry *Entry) error {
|
func computeExtraSettings(entry *Entry) error {
|
||||||
|
@ -183,6 +211,9 @@ func computeExtraSettings(entry *Entry) error {
|
||||||
} else if strings.HasPrefix(text, "tags: ") {
|
} else if strings.HasPrefix(text, "tags: ") {
|
||||||
// Tags
|
// Tags
|
||||||
entry.tags = strings.Split(strings.TrimPrefix(text, "tags: "), ",")
|
entry.tags = strings.Split(strings.TrimPrefix(text, "tags: "), ",")
|
||||||
|
} else if strings.HasPrefix(text, "series: ") {
|
||||||
|
// Series
|
||||||
|
entry.series = strings.Split(strings.TrimPrefix(text, "series: "), ",")
|
||||||
} else if strings.HasPrefix(text, "link: ") {
|
} else if strings.HasPrefix(text, "link: ") {
|
||||||
// Link
|
// Link
|
||||||
entry.link = strings.TrimPrefix(text, "link: ")
|
entry.link = strings.TrimPrefix(text, "link: ")
|
||||||
|
@ -198,28 +229,96 @@ func computeExtraSettings(entry *Entry) error {
|
||||||
} else if strings.HasPrefix(text, "like-title: ") {
|
} else if strings.HasPrefix(text, "like-title: ") {
|
||||||
// Like title
|
// Like title
|
||||||
entry.likeTitle = strings.TrimPrefix(text, "like-title: ")
|
entry.likeTitle = strings.TrimPrefix(text, "like-title: ")
|
||||||
|
} else if strings.HasPrefix(text, "language: ") {
|
||||||
|
// Language
|
||||||
|
entry.language = strings.TrimPrefix(text, "language: ")
|
||||||
|
} else if strings.HasPrefix(text, "translationkey: ") {
|
||||||
|
// Translation key
|
||||||
|
entry.translationKey = strings.TrimPrefix(text, "translationkey: ")
|
||||||
|
} else if strings.HasPrefix(text, "images: ") {
|
||||||
|
// Images
|
||||||
|
for _, image := range strings.Split(strings.TrimPrefix(text, "images: "), ",") {
|
||||||
|
entry.images = append(entry.images, Image{url: image})
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(text, "image-alts: ") {
|
||||||
|
// Image alt
|
||||||
|
for i, alt := range strings.Split(strings.TrimPrefix(text, "image-alts: "), ",") {
|
||||||
|
entry.images[i].alt = alt
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(text, "audio: ") {
|
||||||
|
// Audio
|
||||||
|
entry.audio = strings.TrimPrefix(text, "audio: ")
|
||||||
} else {
|
} else {
|
||||||
_, _ = fmt.Fprintln(&filteredContent, text)
|
_, _ = fmt.Fprintln(&filteredContent, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entry.content = filteredContent.String()
|
entry.content = filteredContent.String()
|
||||||
|
// Check if content contains images or add them
|
||||||
|
for _, image := range entry.images {
|
||||||
|
if !strings.Contains(entry.content, image.url) {
|
||||||
|
if len(image.alt) > 0 {
|
||||||
|
entry.content += "\n![" + image.alt + "](" + image.url + " \"" + image.alt + "\")\n"
|
||||||
|
} else {
|
||||||
|
entry.content += "\n![](" + image.url + ")\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Compute slug if empty
|
// Compute slug if empty
|
||||||
if len(entry.slug) == 0 || entry.slug == "" {
|
if len(entry.slug) == 0 || entry.slug == "" {
|
||||||
random := generateRandomString(now, 5)
|
random := generateRandomString(now, 5)
|
||||||
entry.slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random)
|
entry.slug = fmt.Sprintf("%v-%02d-%02d-%v", now.Year(), int(now.Month()), now.Day(), random)
|
||||||
}
|
}
|
||||||
|
// Set language
|
||||||
|
if len(entry.language) == 0 {
|
||||||
|
entry.language = DefaultLanguage
|
||||||
|
}
|
||||||
// Compute filename and location
|
// Compute filename and location
|
||||||
if len(entry.section) < 1 {
|
lang := Languages[entry.language]
|
||||||
entry.section = "micro"
|
contentFolder := lang.ContentDir
|
||||||
|
localizedBlogUrl := BlogUrl
|
||||||
|
if len(lang.BlogUrl) != 0 {
|
||||||
|
localizedBlogUrl = lang.BlogUrl
|
||||||
|
}
|
||||||
|
if len(entry.section) == 0 {
|
||||||
|
entry.section = lang.DefaultSection
|
||||||
}
|
}
|
||||||
entry.section = strings.ToLower(entry.section)
|
entry.section = strings.ToLower(entry.section)
|
||||||
if entry.section == "thoughts" || entry.section == "links" || entry.section == "micro" {
|
section := lang.Sections[entry.section]
|
||||||
entry.filename = fmt.Sprintf("content/%v/%02d/%02d/%v.md", entry.section, now.Year(), int(now.Month()), entry.slug)
|
pathVars := struct {
|
||||||
entry.location = fmt.Sprintf("%v%v/%02d/%02d/%v/", BlogUrl, entry.section, now.Year(), int(now.Month()), entry.slug)
|
LocalContentFolder string
|
||||||
} else {
|
LocalBlogUrl string
|
||||||
entry.filename = fmt.Sprintf("content/%v/%v.md", entry.section, entry.slug)
|
Year int
|
||||||
entry.location = fmt.Sprintf("%v%v/%v/", BlogUrl, entry.section, entry.slug)
|
Month int
|
||||||
|
Slug string
|
||||||
|
Section string
|
||||||
|
}{
|
||||||
|
LocalContentFolder: contentFolder,
|
||||||
|
LocalBlogUrl: localizedBlogUrl,
|
||||||
|
Year: now.Year(),
|
||||||
|
Month: int(now.Month()),
|
||||||
|
Slug: entry.slug,
|
||||||
|
Section: entry.section,
|
||||||
}
|
}
|
||||||
|
filenameTmpl, err := template.New("filename").Parse(section.FilenameTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to parse filename template")
|
||||||
|
}
|
||||||
|
filename := new(bytes.Buffer)
|
||||||
|
err = filenameTmpl.Execute(filename, pathVars)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to execute filename template")
|
||||||
|
}
|
||||||
|
entry.filename = filename.String()
|
||||||
|
locationTmpl, err := template.New("location").Parse(section.LocationTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to parse location template")
|
||||||
|
}
|
||||||
|
location := new(bytes.Buffer)
|
||||||
|
err = locationTmpl.Execute(location, pathVars)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to execute location template")
|
||||||
|
}
|
||||||
|
entry.location = location.String()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,34 +344,3 @@ func WriteEntry(entry *Entry) (location string, err error) {
|
||||||
location = entry.location
|
location = entry.location
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func analyzeURL(url string) (filePath string, section string, slug string, err error) {
|
|
||||||
if !strings.HasPrefix(url, BlogUrl) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
path := strings.TrimSuffix(strings.TrimPrefix(url, BlogUrl), "/")
|
|
||||||
pathParts := strings.Split(path, "/")
|
|
||||||
filePath = "content/" + path + ".md"
|
|
||||||
section = pathParts[0]
|
|
||||||
slug = pathParts[len(pathParts)-1]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadEntry(url string) (entry *Entry, err error) {
|
|
||||||
filePath, section, slug, err := analyzeURL(url)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileContent, _, exists, err := SelectedStorage.ReadFile(filePath)
|
|
||||||
if err != nil || !exists {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
entry, err = ReadHugoPost(fileContent)
|
|
||||||
if entry != nil {
|
|
||||||
entry.location = url
|
|
||||||
entry.filename = filePath
|
|
||||||
entry.section = section
|
|
||||||
entry.slug = slug
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
24
go.mod
24
go.mod
|
@ -1,11 +1,21 @@
|
||||||
module codeberg.org/jlelse/hugo-micropub
|
module git.jlel.se/jlelse/hugo-micropub
|
||||||
|
|
||||||
go 1.13
|
go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/caarlos0/env/v6 v6.1.0
|
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
github.com/gliderlabs/ssh v0.3.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.7
|
github.com/go-git/go-git/v5 v5.1.0
|
||||||
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62
|
github.com/google/go-cmp v0.5.2 // indirect
|
||||||
|
github.com/imdario/mergo v0.3.11 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/stretchr/testify v1.6.1 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
|
||||||
|
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect
|
||||||
|
golang.org/x/text v0.3.3 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
|
||||||
)
|
)
|
||||||
|
|
123
go.sum
123
go.sum
|
@ -1,26 +1,115 @@
|
||||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0 h1:pJX79kTd01NtxEnzhfd4OU2SY9fquKVoO47DUeNKe+8=
|
||||||
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
codeberg.org/jlelse/tinify v0.0.0-20200123222407-7fc9c21822b0/go.mod h1:X6cM4Sn0aL/4VQ/ku11yxmiV0WIk5XAaYEPHQLQjFFM=
|
||||||
github.com/caarlos0/env/v6 v6.1.0 h1:4FbM+HmZA/Q5wdSrH2kj0KQXm7xnhuO8y3TuOTnOvqc=
|
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||||
github.com/caarlos0/env/v6 v6.1.0/go.mod h1:iUA6X3VCAOwDhoqvgKlTGjjwJzQseIJaFYApUqQkt+8=
|
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible h1:OT02onvXX618RBcjxeUA4H7d1PSm5Apg4IET72VgVlE=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||||
|
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||||
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||||
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
|
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||||
|
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||||
|
github.com/gliderlabs/ssh v0.3.0 h1:7GcKy4erEljCE/QeQ2jTVpu+3f3zkpZOxOJjFYkMqYU=
|
||||||
|
github.com/gliderlabs/ssh v0.3.0/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||||
|
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||||
|
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||||
|
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
|
||||||
|
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||||
|
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
|
||||||
|
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
|
||||||
|
github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk=
|
||||||
|
github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM=
|
||||||
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||||
|
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||||
|
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||||
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||||
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
|
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 h1:czFLhve3vsQetD6JOJ8NZZvGQIXlnN3/yXxbT6/awxI=
|
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
|
||||||
|
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
||||||
|
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM=
|
||||||
|
golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62 h1:jqC8A1S2/9WjXSOK/Nl2rYwVgxU7DCnZ/zpOTL1BErI=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62/go.mod h1:p+ZRAsZS2pzZ6kX3GKWYurf3WZI2ygj7VbR8NM8qwfM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
3
main.go
3
main.go
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,9 +14,7 @@ func main() {
|
||||||
log.Println("Starting micropub server...")
|
log.Println("Starting micropub server...")
|
||||||
log.Println("Current time: " + time.Now().Format(time.RFC3339))
|
log.Println("Current time: " + time.Now().Format(time.RFC3339))
|
||||||
log.Println("Blog URL: " + BlogUrl)
|
log.Println("Blog URL: " + BlogUrl)
|
||||||
log.Println("Ignored URLs for Webmention: " + strings.Join(IgnoredWebmentionUrls, ", "))
|
|
||||||
http.HandleFunc("/micropub", HandleMicroPub)
|
http.HandleFunc("/micropub", HandleMicroPub)
|
||||||
http.HandleFunc("/media", HandleMedia)
|
http.HandleFunc("/media", HandleMedia)
|
||||||
http.HandleFunc("/webmention", HandleWebmention)
|
|
||||||
log.Fatal(http.ListenAndServe(":5555", nil))
|
log.Fatal(http.ListenAndServe(":5555", nil))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -11,6 +8,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if SelectedMediaStorage == nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = w.Write([]byte("No media storage configured"))
|
||||||
|
return
|
||||||
|
}
|
||||||
if r.Method != "POST" {
|
if r.Method != "POST" {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
_, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request"))
|
_, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request"))
|
||||||
|
@ -28,8 +30,8 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
authCode := r.Header.Get("authorization")
|
authCode := r.Header.Get("authorization")
|
||||||
if formAuth := r.FormValue("authorization"); len(authCode) == 0 && len(formAuth) > 0 {
|
if formAuth := r.FormValue("access_token"); len(authCode) == 0 && len(formAuth) > 0 {
|
||||||
authCode = formAuth
|
authCode = "Bearer " + formAuth
|
||||||
}
|
}
|
||||||
if CheckAuthorization(authCode) {
|
if CheckAuthorization(authCode) {
|
||||||
file, header, err := r.FormFile("file")
|
file, header, err := r.FormFile("file")
|
||||||
|
@ -38,15 +40,15 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte("Failed to get file"))
|
_, _ = w.Write([]byte("Failed to get file"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer func() { _ = file.Close() }()
|
||||||
hashFile, _, _ := r.FormFile("file")
|
hashFile, _, _ := r.FormFile("file")
|
||||||
h := sha256.New()
|
|
||||||
defer func() { _ = hashFile.Close() }()
|
defer func() { _ = hashFile.Close() }()
|
||||||
if _, err := io.Copy(h, hashFile); err != nil {
|
fileName, err := getSHA256(hashFile)
|
||||||
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, _ = w.Write([]byte("Failed to calculate hash of file"))
|
_, _ = w.Write([]byte(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileName := fmt.Sprintf("%x", h.Sum(nil))
|
|
||||||
fileExtension := filepath.Ext(header.Filename)
|
fileExtension := filepath.Ext(header.Filename)
|
||||||
if len(fileExtension) == 0 {
|
if len(fileExtension) == 0 {
|
||||||
// Find correct file extension if original filename does not contain one
|
// Find correct file extension if original filename does not contain one
|
||||||
|
@ -58,13 +60,19 @@ func HandleMedia(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileName += fileExtension
|
fileName += strings.ToLower(fileExtension)
|
||||||
location, err := SelectedMediaStorage.Upload(fileName, file)
|
location, err := SelectedMediaStorage.Upload(fileName, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
_, _ = w.Write([]byte("Failed to upload file"))
|
_, _ = w.Write([]byte("Failed to upload original file"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if SelectedImageCompression != nil {
|
||||||
|
compressedLocation, err := SelectedImageCompression.Compress(location)
|
||||||
|
if err == nil && len(compressedLocation) > 0 {
|
||||||
|
location = compressedLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
w.Header().Add("Location", location)
|
w.Header().Add("Location", location)
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -23,9 +26,9 @@ type BunnyCdnStorage struct {
|
||||||
|
|
||||||
var bunnyCdnStorageUrl = "https://storage.bunnycdn.com"
|
var bunnyCdnStorageUrl = "https://storage.bunnycdn.com"
|
||||||
|
|
||||||
func (b BunnyCdnStorage) Upload(fileName string, file multipart.File) (location string, err error) {
|
func (b *BunnyCdnStorage) Upload(fileName string, file multipart.File) (location string, err error) {
|
||||||
client := &http.Client{}
|
client := http.DefaultClient
|
||||||
req, _ := http.NewRequest("PUT", bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape("/"+fileName), file)
|
req, _ := http.NewRequest(http.MethodPut, bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape(fileName), file)
|
||||||
req.Header.Add("AccessKey", b.key)
|
req.Header.Add("AccessKey", b.key)
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil || resp.StatusCode != 201 {
|
if err != nil || resp.StatusCode != 201 {
|
||||||
|
@ -33,3 +36,12 @@ func (b BunnyCdnStorage) Upload(fileName string, file multipart.File) (location
|
||||||
}
|
}
|
||||||
return b.baseLocation + fileName, nil
|
return b.baseLocation + fileName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSHA256(file multipart.File) (filename string, err error) {
|
||||||
|
h := sha256.New()
|
||||||
|
if _, e := io.Copy(h, file); e != nil {
|
||||||
|
err = errors.New("failed to calculate hash of file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
|
@ -6,15 +6,17 @@ type MicroformatItem struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type MicroformatProperties struct {
|
type MicroformatProperties struct {
|
||||||
Name []string `json:"name,omitempty"`
|
Name []string `json:"name,omitempty"`
|
||||||
Published []string `json:"published,omitempty"`
|
Published []string `json:"published,omitempty"`
|
||||||
Updated []string `json:"updated,omitempty"`
|
Updated []string `json:"updated,omitempty"`
|
||||||
Category []string `json:"category,omitempty"`
|
Category []string `json:"category,omitempty"`
|
||||||
Content []string `json:"content,omitempty"`
|
Content []string `json:"content,omitempty"`
|
||||||
Url []string `json:"url,omitempty"`
|
Url []string `json:"url,omitempty"`
|
||||||
InReplyTo []string `json:"in-reply-to,omitempty"`
|
InReplyTo []string `json:"in-reply-to,omitempty"`
|
||||||
LikeOf []string `json:"like-of,omitempty"`
|
LikeOf []string `json:"like-of,omitempty"`
|
||||||
BookmarkOf []string `json:"bookmark-of,omitempty"`
|
BookmarkOf []string `json:"bookmark-of,omitempty"`
|
||||||
MpSlug []string `json:"mp-slug,omitempty"`
|
MpSlug []string `json:"mp-slug,omitempty"`
|
||||||
MpSyndicateTo []string `json:"mp-syndicate-to,omitempty"`
|
MpSyndicateTo []string `json:"mp-syndicate-to,omitempty"`
|
||||||
}
|
Photo []interface{} `json:"photo,omitempty"`
|
||||||
|
Audio []string `json:"audio,omitempty"`
|
||||||
|
}
|
40
micropub.go
40
micropub.go
|
@ -3,8 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MicropubConfig struct {
|
type MicropubConfig struct {
|
||||||
|
@ -18,29 +16,10 @@ func HandleMicroPub(w http.ResponseWriter, r *http.Request) {
|
||||||
if q := r.URL.Query().Get("q"); q == "config" || q == "syndicate-to" {
|
if q := r.URL.Query().Get("q"); q == "config" || q == "syndicate-to" {
|
||||||
w.Header().Add("Content-type", "application/json")
|
w.Header().Add("Content-type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
jsonBytes, err := json.Marshal(&MicropubConfig{
|
_ = json.NewEncoder(w).Encode(&MicropubConfig{
|
||||||
SyndicateTo: SyndicationTargets,
|
SyndicateTo: SyndicationTargets,
|
||||||
MediaEndpoint: MediaEndpointUrl,
|
MediaEndpoint: MediaEndpointUrl,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
_, _ = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, _ = w.Write(jsonBytes)
|
|
||||||
return
|
|
||||||
} else if url := r.URL.Query().Get("url"); q == "source" {
|
|
||||||
limit := r.URL.Query().Get("limit")
|
|
||||||
limitInt, err := strconv.Atoi(limit)
|
|
||||||
jsonBytes, err := QueryURL(url, limitInt)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
_, _ = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Add("Content-type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write(jsonBytes)
|
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
w.Header().Add("Content-type", "application/json")
|
w.Header().Add("Content-type", "application/json")
|
||||||
|
@ -83,23 +62,8 @@ func HandleMicroPub(w http.ResponseWriter, r *http.Request) {
|
||||||
_, _ = w.Write([]byte("There was an error committing the entry to the repository"))
|
_, _ = w.Write([]byte("There was an error committing the entry to the repository"))
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
w.Header().Add("Location", location)
|
w.Header().Add("Location", location+"?cache=0")
|
||||||
w.WriteHeader(http.StatusAccepted)
|
w.WriteHeader(http.StatusAccepted)
|
||||||
// Purge CDN in 10 seconds, send webmentions, post to social media
|
|
||||||
go func() {
|
|
||||||
if SelectedCdn != nil {
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
SelectedCdn.Purge(location)
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Second)
|
|
||||||
// Send webmentions
|
|
||||||
go SendWebmentions(location)
|
|
||||||
go func() {
|
|
||||||
if SelectedSocials != nil {
|
|
||||||
SelectedSocials.Post(location, entry.title)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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
135
post.go
|
@ -3,130 +3,81 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"strings"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HugoFrontmatter struct {
|
type HugoFrontMatter struct {
|
||||||
Title string `yaml:"title,omitempty"`
|
Title string `yaml:"title,omitempty"`
|
||||||
Date string `yaml:"date,omitempty"`
|
Published string `yaml:"date,omitempty"`
|
||||||
Lastmod string `yaml:"lastmod,omitempty"`
|
Updated string `yaml:"lastmod,omitempty"`
|
||||||
Tags []string `yaml:"tags,omitempty"`
|
Tags []string `yaml:"tags,omitempty"`
|
||||||
ExternalURL string `yaml:"externalURL,omitempty"`
|
Series []string `yaml:"series,omitempty"`
|
||||||
Indieweb HugoFrontmatterIndieweb `yaml:"indieweb,omitempty"`
|
ExternalURL string `yaml:"externalURL,omitempty"`
|
||||||
Syndicate []string `yaml:"syndicate,omitempty"`
|
IndieWeb HugoFrontMatterIndieWeb `yaml:"indieweb,omitempty"`
|
||||||
|
Syndicate []string `yaml:"syndicate,omitempty"`
|
||||||
|
TranslationKey string `yaml:"translationKey,omitempty"`
|
||||||
|
Images []string `yaml:"images,omitempty"`
|
||||||
|
Audio string `yaml:"audio,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HugoFrontmatterIndieweb struct {
|
type HugoFrontMatterIndieWeb struct {
|
||||||
Reply HugoFrontmatterReply `yaml:"reply,omitempty"`
|
Reply HugoFrontMatterReply `yaml:"reply,omitempty"`
|
||||||
Like HugoFrontmatterLike `yaml:"like,omitempty"`
|
Like HugoFrontMatterLike `yaml:"like,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HugoFrontmatterReply struct {
|
type HugoFrontMatterReply struct {
|
||||||
Link string `yaml:"link,omitempty"`
|
Link string `yaml:"link,omitempty"`
|
||||||
Title string `yaml:"title,omitempty"`
|
Title string `yaml:"title,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HugoFrontmatterLike struct {
|
type HugoFrontMatterLike struct {
|
||||||
Link string `yaml:"link,omitempty"`
|
Link string `yaml:"link,omitempty"`
|
||||||
Title string `yaml:"title,omitempty"`
|
Title string `yaml:"title,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeFrontMatter(entry *Entry) (frontmatter string, err error) {
|
func writeFrontMatter(entry *Entry) (string, error) {
|
||||||
var buff bytes.Buffer
|
frontMatter := &HugoFrontMatter{
|
||||||
writeFrontmatter := &HugoFrontmatter{
|
|
||||||
Title: entry.title,
|
Title: entry.title,
|
||||||
Date: entry.date,
|
Published: entry.date,
|
||||||
Lastmod: entry.lastmod,
|
Updated: entry.lastmod,
|
||||||
Tags: entry.tags,
|
Tags: entry.tags,
|
||||||
|
Series: entry.series,
|
||||||
ExternalURL: entry.link,
|
ExternalURL: entry.link,
|
||||||
Indieweb: HugoFrontmatterIndieweb{
|
IndieWeb: HugoFrontMatterIndieWeb{
|
||||||
Reply: HugoFrontmatterReply{
|
Reply: HugoFrontMatterReply{
|
||||||
Link: entry.replyLink,
|
Link: entry.replyLink,
|
||||||
Title: entry.replyTitle,
|
Title: entry.replyTitle,
|
||||||
},
|
},
|
||||||
Like: HugoFrontmatterLike{
|
Like: HugoFrontMatterLike{
|
||||||
Link: entry.likeLink,
|
Link: entry.likeLink,
|
||||||
Title: entry.likeTitle,
|
Title: entry.likeTitle,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Syndicate: entry.syndicate,
|
Syndicate: entry.syndicate,
|
||||||
|
TranslationKey: entry.translationKey,
|
||||||
|
Audio: entry.audio,
|
||||||
}
|
}
|
||||||
yamlBytes, err := yaml.Marshal(&writeFrontmatter)
|
for _, image := range entry.images {
|
||||||
|
frontMatter.Images = append(frontMatter.Images, image.url)
|
||||||
|
}
|
||||||
|
writer := new(bytes.Buffer)
|
||||||
|
writer.WriteString("---\n")
|
||||||
|
err := yaml.NewEncoder(writer).Encode(&frontMatter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.New("failed marshaling frontmatter")
|
return "", errors.New("failed encoding frontmatter")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
buff.WriteString("---\n")
|
writer.WriteString("---\n")
|
||||||
buff.Write(yamlBytes)
|
return writer.String(), nil
|
||||||
buff.WriteString("---\n")
|
|
||||||
frontmatter = buff.String()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteHugoPost(entry *Entry) (string, error) {
|
func WriteHugoPost(entry *Entry) (string, error) {
|
||||||
var buff bytes.Buffer
|
buff := new(bytes.Buffer)
|
||||||
frontmatter, err := writeFrontMatter(entry)
|
f, err := writeFrontMatter(entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
buff.WriteString(frontmatter)
|
buff.WriteString(f)
|
||||||
if len(entry.content) > 0 {
|
buff.WriteString(entry.content)
|
||||||
buff.WriteString(entry.content)
|
|
||||||
}
|
|
||||||
return buff.String(), nil
|
return buff.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readFrontMatter(frontmatter string, entry *Entry) (err error) {
|
|
||||||
parsedFrontmatter := &HugoFrontmatter{}
|
|
||||||
err = yaml.Unmarshal([]byte(frontmatter), &parsedFrontmatter)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.New("failed parsing frontmatter")
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Title) > 0 {
|
|
||||||
entry.title = parsedFrontmatter.Title
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Date) > 0 {
|
|
||||||
entry.date = parsedFrontmatter.Date
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Lastmod) > 0 {
|
|
||||||
entry.lastmod = parsedFrontmatter.Lastmod
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Tags) > 0 {
|
|
||||||
entry.tags = parsedFrontmatter.Tags
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.ExternalURL) > 0 {
|
|
||||||
entry.link = parsedFrontmatter.ExternalURL
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Indieweb.Reply.Link) > 0 {
|
|
||||||
entry.replyLink = parsedFrontmatter.Indieweb.Reply.Link
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Indieweb.Reply.Title) > 0 {
|
|
||||||
entry.replyTitle = parsedFrontmatter.Indieweb.Reply.Title
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Indieweb.Like.Link) > 0 {
|
|
||||||
entry.replyLink = parsedFrontmatter.Indieweb.Like.Link
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Indieweb.Like.Title) > 0 {
|
|
||||||
entry.replyTitle = parsedFrontmatter.Indieweb.Like.Title
|
|
||||||
}
|
|
||||||
if len(parsedFrontmatter.Syndicate) > 0 {
|
|
||||||
entry.syndicate = parsedFrontmatter.Syndicate
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadHugoPost(fileContent string) (entry *Entry, err error) {
|
|
||||||
parts := strings.Split(fileContent, "---\n")
|
|
||||||
if len(parts) != 3 {
|
|
||||||
err = errors.New("empty frontmatter or content")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
entry = new(Entry)
|
|
||||||
err = readFrontMatter(parts[1], entry)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
entry.content = strings.TrimSuffix(parts[2], "\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
94
query.go
94
query.go
|
@ -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
|
|
||||||
}
|
|
46
social.go
46
social.go
|
@ -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)
|
|
||||||
}
|
|
278
storage.go
278
storage.go
|
@ -1,134 +1,184 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
|
gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"os"
|
||||||
"net/url"
|
"path"
|
||||||
"strings"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
CreateFile(path string, file string, message string) (err error)
|
CreateFile(path string, file string, message string) (err error)
|
||||||
UpdateFile(path string, file string, message string) (err error)
|
UpdateFile(path string, file string, message string) (err error)
|
||||||
ReadFile(path string) (content string, sha string, exists bool, err error)
|
DeleteFile(path string, message string) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gitea
|
type Git struct {
|
||||||
type Gitea struct {
|
filepath string
|
||||||
endpoint string
|
url string
|
||||||
token string
|
username string
|
||||||
|
password string
|
||||||
|
name string
|
||||||
|
email string
|
||||||
|
lock *sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type giteaCommitRequest struct {
|
func (g *Git) init() (r *git.Repository, w *git.Worktree, err error) {
|
||||||
Content string `json:"content"`
|
// Open repo
|
||||||
Message string `json:"message"`
|
r, err = git.PlainOpen(g.filepath)
|
||||||
SHA string `json:"sha,omitempty"`
|
if err == nil {
|
||||||
}
|
// Try to get work tree
|
||||||
|
w, err = r.Worktree()
|
||||||
type giteaReadResponse struct {
|
if err == nil {
|
||||||
Type string `json:"type"`
|
// Try to pull
|
||||||
Content string `json:"content"`
|
err = w.Pull(&git.PullOptions{
|
||||||
SHA string `json:"sha"`
|
Auth: &gitHttp.BasicAuth{
|
||||||
}
|
Username: g.username,
|
||||||
|
Password: g.password,
|
||||||
type giteaErrorResponse struct {
|
},
|
||||||
Message string `json:"message"`
|
SingleBranch: true,
|
||||||
}
|
})
|
||||||
|
if err == git.NoErrAlreadyUpToDate {
|
||||||
func (gitea *Gitea) CreateFile(path string, file string, message string) (err error) {
|
err = nil
|
||||||
request := &giteaCommitRequest{
|
|
||||||
Content: base64.StdEncoding.EncodeToString([]byte(file)),
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
bytesRepresentation, err := json.Marshal(request)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("failed to marshal json before committing")
|
|
||||||
}
|
|
||||||
resp, err := http.Post(gitea.endpoint+url.QueryEscape(path)+"?access_token="+gitea.token, "application/json", bytes.NewBuffer(bytesRepresentation))
|
|
||||||
if err != nil || resp.StatusCode != 201 {
|
|
||||||
return errors.New("failed to create file in repo")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gitea *Gitea) UpdateFile(path string, file string, message string) (err error) {
|
|
||||||
_, sha, exists, err := gitea.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
// File doesn't exist, create it
|
|
||||||
return gitea.CreateFile(path, file, message)
|
|
||||||
}
|
|
||||||
request := &giteaCommitRequest{
|
|
||||||
Content: base64.StdEncoding.EncodeToString([]byte(file)),
|
|
||||||
Message: message,
|
|
||||||
SHA: sha,
|
|
||||||
}
|
|
||||||
bytesRepresentation, err := json.Marshal(request)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("failed to marshal json before committing")
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(http.MethodPut, gitea.endpoint+url.QueryEscape(path)+"?access_token="+gitea.token, bytes.NewBuffer(bytesRepresentation))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("error making update request")
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-type", "application/json")
|
|
||||||
client := &http.Client{}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil || resp.StatusCode != 200 {
|
|
||||||
return errors.New("failed to update file in repo")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gitea *Gitea) ReadFile(path string) (content string, sha string, exists bool, err error) {
|
|
||||||
exists = false
|
|
||||||
resp, err := http.Get(gitea.endpoint + url.QueryEscape(path) + "?access_token=" + gitea.token)
|
|
||||||
if err != nil || resp.StatusCode != 200 {
|
|
||||||
if resp != nil {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, readErr := ioutil.ReadAll(resp.Body)
|
|
||||||
if readErr != nil {
|
|
||||||
err = errors.New("failed reading Gitea error response")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
errorResponse := &giteaErrorResponse{}
|
|
||||||
marshalErr := json.Unmarshal(body, &errorResponse)
|
|
||||||
if marshalErr != nil {
|
|
||||||
err = errors.New("failed parsing Gitea error response")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
exists = !strings.Contains(errorResponse.Message, "does not exist")
|
|
||||||
if !exists {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = errors.New("failed to read file in repo")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
exists = true
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.New("failed reading file in repo")
|
// Delete old things
|
||||||
return
|
g.destroyRepo()
|
||||||
|
// Clone
|
||||||
|
r, err = git.PlainClone(g.filepath, false, &git.CloneOptions{
|
||||||
|
Auth: &gitHttp.BasicAuth{
|
||||||
|
Username: g.username,
|
||||||
|
Password: g.password,
|
||||||
|
},
|
||||||
|
URL: g.url,
|
||||||
|
Depth: 1,
|
||||||
|
SingleBranch: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("failed to clone repo")
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
w, err = r.Worktree()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("failed to get work tree")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
readResponse := &giteaReadResponse{}
|
|
||||||
err = json.Unmarshal(body, &readResponse)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.New("failed parsing Gitea response")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
decodedContentBytes, err := base64.StdEncoding.DecodeString(readResponse.Content)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.New("failed decoding file content")
|
|
||||||
}
|
|
||||||
content = string(decodedContentBytes)
|
|
||||||
sha = readResponse.SHA
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Git) destroyRepo() {
|
||||||
|
_ = os.RemoveAll(g.filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Git) push(repository *git.Repository) error {
|
||||||
|
err := repository.Push(&git.PushOptions{
|
||||||
|
Auth: &gitHttp.BasicAuth{
|
||||||
|
Username: g.username,
|
||||||
|
Password: g.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil || err == git.NoErrAlreadyUpToDate {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
// Destroy repo to prevent errors when trying to create same post again
|
||||||
|
g.destroyRepo()
|
||||||
|
return errors.New("failed to push to remote")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Git) CreateFile(filepath string, file string, message string) (err error) {
|
||||||
|
g.lock.Lock()
|
||||||
|
defer g.lock.Unlock()
|
||||||
|
_, _, err = g.init()
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("failed to initialize repo")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
joinedPath := path.Join(g.filepath, filepath)
|
||||||
|
if _, e := os.Stat(joinedPath); e == nil {
|
||||||
|
return errors.New("file already exists")
|
||||||
|
} else {
|
||||||
|
return g.unsafeUpdateFile(filepath, file, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Git) UpdateFile(filepath string, file string, message string) error {
|
||||||
|
g.lock.Lock()
|
||||||
|
defer g.lock.Unlock()
|
||||||
|
return g.unsafeUpdateFile(filepath, file, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Git) unsafeUpdateFile(filepath string, file string, message string) error {
|
||||||
|
repo, w, err := g.init()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to initialize repo")
|
||||||
|
}
|
||||||
|
joinedPath := path.Join(g.filepath, filepath)
|
||||||
|
_ = os.MkdirAll(path.Dir(joinedPath), 0755)
|
||||||
|
err = ioutil.WriteFile(joinedPath, []byte(file), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to write to file")
|
||||||
|
}
|
||||||
|
status, err := w.Status()
|
||||||
|
if err == nil && status.IsClean() {
|
||||||
|
// No file changes, prevent empty commit
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
_, err = w.Add(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to stage file")
|
||||||
|
}
|
||||||
|
if len(message) == 0 {
|
||||||
|
message = "Add " + filepath
|
||||||
|
}
|
||||||
|
_, err = w.Commit(message, &git.CommitOptions{
|
||||||
|
Author: &object.Signature{
|
||||||
|
Name: g.name,
|
||||||
|
Email: g.email,
|
||||||
|
When: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to commit file")
|
||||||
|
}
|
||||||
|
return g.push(repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Git) DeleteFile(filepath string, message string) (err error) {
|
||||||
|
g.lock.Lock()
|
||||||
|
defer g.lock.Unlock()
|
||||||
|
repo, w, err := g.init()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to initialize repo")
|
||||||
|
}
|
||||||
|
joinedPath := path.Join(g.filepath, filepath)
|
||||||
|
err = os.Remove(joinedPath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to delete file")
|
||||||
|
}
|
||||||
|
_, err = w.Add(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to stage deletion")
|
||||||
|
}
|
||||||
|
_, err = w.Commit(message, &git.CommitOptions{
|
||||||
|
Author: &object.Signature{
|
||||||
|
Name: g.name,
|
||||||
|
Email: g.email,
|
||||||
|
When: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to commit deletion")
|
||||||
|
}
|
||||||
|
return g.push(repo)
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -34,7 +33,7 @@ func checkAccess(token string) (bool, error) {
|
||||||
return false, errors.New("token string is empty")
|
return false, errors.New("token string is empty")
|
||||||
}
|
}
|
||||||
// form the request to check the token
|
// form the request to check the token
|
||||||
client := &http.Client{}
|
client := http.DefaultClient
|
||||||
req, err := http.NewRequest("GET", indieAuthTokenUrl, nil)
|
req, err := http.NewRequest("GET", indieAuthTokenUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.New("error making the request for checking token access")
|
return false, errors.New("error making the request for checking token access")
|
||||||
|
@ -46,14 +45,10 @@ func checkAccess(token string) (bool, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.New("error sending the request for checking token access")
|
return false, errors.New("error sending the request for checking token access")
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
|
||||||
// parse the response
|
// parse the response
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
indieAuthRes := &IndieAuthRes{}
|
||||||
if err != nil {
|
err = json.NewDecoder(res.Body).Decode(&indieAuthRes)
|
||||||
return false, errors.New("error parsing the response for checking token access")
|
res.Body.Close()
|
||||||
}
|
|
||||||
var indieAuthRes = new(IndieAuthRes)
|
|
||||||
err = json.Unmarshal(body, &indieAuthRes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errors.New("Error parsing the response into json for checking token access " + err.Error())
|
return false, errors.New("Error parsing the response into json for checking token access " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
190
webmention.go
190
webmention.go
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
Loading…
Reference in New Issue