Add Media Endpoint support

kcoram/uplift
Jan-Lukas Else 2020-01-01 13:44:44 +01:00
parent 35770cecc9
commit 222afd3957
8 changed files with 183 additions and 138 deletions

193
config.go
View File

@ -2,10 +2,8 @@ package main
import ( import (
"errors" "errors"
"github.com/caarlos0/env/v6"
"log" "log"
"os"
"strconv"
"strings"
) )
var ( var (
@ -13,9 +11,11 @@ var (
IgnoredWebmentionUrls []string IgnoredWebmentionUrls []string
SyndicationTargets []SyndicationTarget SyndicationTargets []SyndicationTarget
SelectedStorage Storage SelectedStorage Storage
SelectedMediaStorage MediaStorage
SelectedCdn Cdn SelectedCdn Cdn
SelectedSocials Socials SelectedSocials Socials
SelectedNotificationServices NotificationServices SelectedNotificationServices NotificationServices
MediaEndpointUrl string
) )
type SyndicationTarget struct { type SyndicationTarget struct {
@ -23,80 +23,106 @@ type SyndicationTarget struct {
Name string `json:"name"` Name string `json:"name"`
} }
func initConfig() { type config struct {
BlogUrl string `env:"BLOG_URL,required"`
BaseUrl string `env:"BASE_URL,required"`
MediaUrl string `env:"MEDIA_URL"`
GiteaEndpoint string `env:"GITEA_ENDPOINT"`
GiteaToken string `env:"GITEA_TOKEN"`
BunnyCdnKey string `env:"BUNNY_CDN_KEY"`
BunnyCdnStorageKey string `env:"BUNNY_CDN_STORAGE_KEY"`
BunnyCdnStorageName string `env:"BUNNY_CDN_STORAGE_NAME"`
MicroblogUrl string `env:"MICROBLOG_URL"`
MicroblogToken string `env:"MICROBLOG_TOKEN"`
TelegramUserId int64 `env:"TELEGRAM_USER_ID"`
TelegramBotToken string `env:"TELEGRAM_BOT_TOKEN"`
IgnoredWebmentionUrls []string `env:"WEBMENTION_IGNORED" envSeparator:","`
SyndicationTargets []string `env:"SYNDICATION" envSeparator:","`
}
func initConfig() (err error) {
cfg := config{}
if err := env.Parse(&cfg); err != nil {
return errors.New("failed to parse config, probably not all required env vars set")
}
// Blog URL (required) // Blog URL (required)
blogUrl, err := blogUrl() BlogUrl = cfg.BlogUrl
if err != nil { // Media endpoint
log.Fatal(err) MediaEndpointUrl = cfg.BaseUrl + "/media"
}
BlogUrl = blogUrl
// Ignored Webmention URLs (optional) // Ignored Webmention URLs (optional)
ignoredWebmentionUrls, err := ignoredWebmentionUrls() IgnoredWebmentionUrls = cfg.IgnoredWebmentionUrls
if err != nil {
log.Println(err)
}
IgnoredWebmentionUrls = ignoredWebmentionUrls
// Syndication Targets (optional) // Syndication Targets (optional)
syndicationTargets, err := syndicationTargets() targets := make([]SyndicationTarget, 0)
if err != nil { for _, url := range cfg.SyndicationTargets {
log.Println(err) targets = append(targets, SyndicationTarget{
Uid: url,
Name: url,
})
} }
SyndicationTargets = syndicationTargets SyndicationTargets = targets
// Find selected storage // Find selected storage
SelectedStorage = func() Storage { SelectedStorage = func() Storage {
// Gitea // Gitea
giteaEndpoint, err1 := giteaEndpoint() if len(cfg.GiteaEndpoint) > 0 && len(cfg.GiteaToken) >= 0 {
giteaToken, err2 := giteaToken()
if err1 == nil && err2 == nil {
return &Gitea{ return &Gitea{
endpoint: giteaEndpoint, endpoint: cfg.GiteaEndpoint,
token: giteaToken, token: cfg.GiteaToken,
} }
} }
return nil return nil
}() }()
if SelectedStorage == nil { if SelectedStorage == nil {
log.Fatal("No storage configured") return errors.New("no storage configured")
}
// Find selected media storage
SelectedMediaStorage = func() MediaStorage {
// BunnyCDN
if len(cfg.BunnyCdnStorageKey) > 0 && len(cfg.BunnyCdnStorageName) > 0 && len(cfg.MediaUrl) > 0 {
return &BunnyCdnStorage{
key: cfg.BunnyCdnStorageKey,
storageZoneName: cfg.BunnyCdnStorageName,
baseLocation: cfg.MediaUrl,
}
}
return nil
}()
if SelectedMediaStorage == nil {
return errors.New("no media storage configured")
} }
// Find selected CDN (optional) // Find selected CDN (optional)
SelectedCdn = func() Cdn { SelectedCdn = func() Cdn {
// BunnyCDN (optional) // BunnyCDN (optional)
bunnyCdnKey, err := bunnyCdnKey() if len(cfg.BunnyCdnKey) > 0 {
if err == nil { return &BunnyCdn{key: cfg.BunnyCdnKey}
return &BunnyCdn{key: bunnyCdnKey}
} }
return nil return nil
}() }()
if SelectedCdn == nil { if SelectedCdn == nil {
log.Println("No CDN configured") log.Println("no CDN configured")
} }
// Find configured social networks (optional) // Find configured social networks (optional)
SelectedSocials = func() Socials { SelectedSocials = func() Socials {
var socials []Social = nil var socials []Social = nil
// Microblog.pub // Microblog.pub
microblogUrl, err1 := microblogUrl() if len(cfg.MicroblogUrl) > 0 && len(cfg.MicroblogToken) > 0 {
microblogToken, err2 := microblogToken()
if err1 == nil && err2 == nil {
socials = append(socials, &MicroblogPub{ socials = append(socials, &MicroblogPub{
url: microblogUrl, url: cfg.MicroblogUrl,
token: microblogToken, token: cfg.MicroblogToken,
}) })
} }
return socials return socials
}() }()
if SelectedSocials == nil { if SelectedSocials == nil {
log.Println("No social networks configured") log.Println("no social networks configured")
} }
// Find configured notification services (optional) // Find configured notification services (optional)
SelectedNotificationServices = func() NotificationServices { SelectedNotificationServices = func() NotificationServices {
var notificationServices []NotificationService = nil var notificationServices []NotificationService = nil
// Telegram // Telegram
telegramUserId, err1 := telegramUserId() if cfg.TelegramUserId > 0 && len(cfg.TelegramBotToken) > 0 {
telegramBotToken, err2 := telegramBotToken()
if err1 == nil && err2 == nil {
notificationServices = append(notificationServices, &Telegram{ notificationServices = append(notificationServices, &Telegram{
userId: telegramUserId, userId: cfg.TelegramUserId,
botToken: telegramBotToken, botToken: cfg.TelegramBotToken,
}) })
} }
return notificationServices return notificationServices
@ -104,92 +130,5 @@ func initConfig() {
if SelectedNotificationServices == nil { if SelectedNotificationServices == nil {
log.Println("No notification services configured") log.Println("No notification services configured")
} }
} return nil
func giteaEndpoint() (string, error) {
giteaEndpoint := os.Getenv("GITEA_ENDPOINT")
if len(giteaEndpoint) == 0 || giteaEndpoint == "" {
return "", errors.New("GITEA_ENDPOINT not specified")
}
return giteaEndpoint, nil
}
func giteaToken() (string, error) {
giteaToken := os.Getenv("GITEA_TOKEN")
if len(giteaToken) == 0 || giteaToken == "" {
return "", errors.New("GITEA_TOKEN not specified")
}
return giteaToken, nil
}
func blogUrl() (string, error) {
blogURL := os.Getenv("BLOG_URL")
if len(blogURL) == 0 || blogURL == "" {
return "", errors.New("BLOG_URL not specified")
}
return blogURL, nil
}
func bunnyCdnKey() (string, error) {
bunnyCDNKey := os.Getenv("BUNNY_CDN_KEY")
if len(bunnyCDNKey) == 0 || bunnyCDNKey == "" {
return "", errors.New("BUNNY_CDN_KEY not specified, BunnyCDN features are deactivated")
}
return bunnyCDNKey, nil
}
func microblogUrl() (string, error) {
microblogUrl := os.Getenv("MICROBLOG_URL")
if len(microblogUrl) == 0 || microblogUrl == "" {
return "", errors.New("MICROBLOG_URL not specified, microblog.pub features are deactivated")
}
return microblogUrl, nil
}
func microblogToken() (string, error) {
microblogToken := os.Getenv("MICROBLOG_TOKEN")
if len(microblogToken) == 0 || microblogToken == "" {
return "", errors.New("MICROBLOG_TOKEN not specified, microblog.pub features are deactivated")
}
return microblogToken, nil
}
func telegramUserId() (int64, error) {
telegramUserIdString := os.Getenv("TELEGRAM_USER_ID")
telegramUserId, err := strconv.ParseInt(telegramUserIdString, 10, 64)
if err != nil || len(telegramUserIdString) == 0 || telegramUserIdString == "" {
return 0, errors.New("TELEGRAM_USER_ID not specified, Telegram features are deactivated")
}
return telegramUserId, nil
}
func telegramBotToken() (string, error) {
telegramBotToken := os.Getenv("TELEGRAM_BOT_TOKEN")
if len(telegramBotToken) == 0 || telegramBotToken == "" {
return "", errors.New("TELEGRAM_BOT_TOKEN not specified, Telegram features are deactivated")
}
return telegramBotToken, nil
}
func ignoredWebmentionUrls() ([]string, error) {
webmentionIgnored := os.Getenv("WEBMENTION_IGNORED")
if len(webmentionIgnored) == 0 {
return make([]string, 0), errors.New("WEBMENTION_IGNORED not set, no URLs are ignored on Webmention sending")
}
return strings.Split(webmentionIgnored, ","), nil
}
func syndicationTargets() ([]SyndicationTarget, error) {
syndication := os.Getenv("SYNDICATION")
targets := make([]SyndicationTarget, 0)
if len(syndication) == 0 {
return targets, errors.New("SYNDICATION not set, no targets are returned when querying for syndication targets")
}
for _, url := range strings.Split(syndication, ",") {
targets = append(targets, SyndicationTarget{
Uid: url,
Name: url,
})
}
return targets, nil
} }

1
go.mod
View File

@ -3,6 +3,7 @@ module codeberg.org/jlelse/hugo-micropub
go 1.13 go 1.13
require ( require (
github.com/caarlos0/env/v6 v6.1.0
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible
github.com/technoweenie/multipartstreamer v1.0.1 // indirect github.com/technoweenie/multipartstreamer v1.0.1 // indirect
gopkg.in/yaml.v2 v2.2.7 gopkg.in/yaml.v2 v2.2.7

8
go.sum
View File

@ -1,9 +1,16 @@
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/caarlos0/env/v6 v6.1.0 h1:4FbM+HmZA/Q5wdSrH2kj0KQXm7xnhuO8y3TuOTnOvqc=
github.com/caarlos0/env/v6 v6.1.0/go.mod h1:iUA6X3VCAOwDhoqvgKlTGjjwJzQseIJaFYApUqQkt+8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible h1:OT02onvXX618RBcjxeUA4H7d1PSm5Apg4IET72VgVlE= github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible h1:OT02onvXX618RBcjxeUA4H7d1PSm5Apg4IET72VgVlE=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
@ -12,6 +19,7 @@ golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 h1:czFLhve3vsQetD6JOJ8NZZvGQ
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62 h1:jqC8A1S2/9WjXSOK/Nl2rYwVgxU7DCnZ/zpOTL1BErI= willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62 h1:jqC8A1S2/9WjXSOK/Nl2rYwVgxU7DCnZ/zpOTL1BErI=

View File

@ -8,12 +8,16 @@ import (
) )
func main() { func main() {
initConfig() err := initConfig()
if err != nil {
log.Fatal(err)
}
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, ", ")) log.Println("Ignored URLs for Webmention: " + strings.Join(IgnoredWebmentionUrls, ", "))
http.HandleFunc("/micropub", HandleMicroPub) http.HandleFunc("/micropub", HandleMicroPub)
http.HandleFunc("/media", HandleMedia)
http.HandleFunc("/webmention", HandleWebmention) http.HandleFunc("/webmention", HandleWebmention)
log.Fatal(http.ListenAndServe(":5555", nil)) log.Fatal(http.ListenAndServe(":5555", nil))
} }

58
mediaendpoint.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"net/http"
"strings"
"time"
)
func HandleMedia(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
}
if contentType := r.Header.Get("content-type"); !strings.Contains(contentType, "multipart/form-data") {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("Wrong Content-Type"))
return
}
err := r.ParseMultipartForm(0)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Failed to parse Multipart"))
return
}
authCode := r.Header.Get("authorization")
if formAuth := r.FormValue("authorization"); len(authCode) == 0 && len(formAuth) > 0 {
authCode = formAuth
}
if CheckAuthorization(authCode) {
file, header, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Failed to get file"))
return
}
fileName := generateRandomString(time.Now(), 15)
mimeType := header.Header.Get("content-type")
originalName := header.Filename
if strings.Contains(mimeType, "jpeg") || strings.Contains(originalName, ".jpeg") || strings.Contains(originalName, ".jpg") {
fileName += ".jpg"
} else if strings.Contains(mimeType, "png") || strings.Contains(originalName, ".png") {
fileName += ".png"
}
location, err := SelectedMediaStorage.Upload(fileName, file)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Failed to upload file"))
return
}
w.Header().Add("Location", location)
w.WriteHeader(http.StatusCreated)
} else {
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte("Forbidden, there was a problem with the provided access token"))
return
}
}

35
mediastorage.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"errors"
"mime/multipart"
"net/http"
"net/url"
)
type MediaStorage interface {
Upload(fileName string, file multipart.File) (location string, err error)
}
// BunnyCDN
type BunnyCdnStorage struct {
// Access Key
key string
// Storage zone name
storageZoneName string
// Base location
baseLocation string
}
var bunnyCdnStorageUrl = "https://storage.bunnycdn.com"
func (b BunnyCdnStorage) Upload(fileName string, file multipart.File) (location string, err error) {
client := &http.Client{}
req, _ := http.NewRequest("PUT", bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape("/"+fileName), file)
req.Header.Add("AccessKey", b.key)
resp, err := client.Do(req)
if err != nil || resp.StatusCode != 201 {
return "", errors.New("failed to upload file to BunnyCDN")
}
return b.baseLocation + fileName, nil
}

View File

@ -8,7 +8,8 @@ import (
) )
type MicropubConfig struct { type MicropubConfig struct {
SyndicateTo []SyndicationTarget `json:"syndicate-to,omitempty"` SyndicateTo []SyndicationTarget `json:"syndicate-to,omitempty"`
MediaEndpoint string `json:"media-endpoint,omitempty"`
} }
func HandleMicroPub(w http.ResponseWriter, r *http.Request) { func HandleMicroPub(w http.ResponseWriter, r *http.Request) {
@ -18,7 +19,8 @@ func HandleMicroPub(w http.ResponseWriter, r *http.Request) {
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{ jsonBytes, err := json.Marshal(&MicropubConfig{
SyndicateTo: SyndicationTargets, SyndicateTo: SyndicationTargets,
MediaEndpoint: MediaEndpointUrl,
}) })
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
@ -71,7 +73,10 @@ func HandleMicroPub(w http.ResponseWriter, r *http.Request) {
} }
return return
} }
if CheckAuthorization(entry, r.Header.Get("authorization")) { if authHeader := r.Header.Get("authorization"); len(entry.token) == 0 && len(authHeader) > 0 {
entry.token = authHeader
}
if CheckAuthorization(entry.token) {
location, err := WriteEntry(entry) location, err := WriteEntry(entry)
if err != nil { if err != nil {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)

View File

@ -75,13 +75,8 @@ func checkAccess(token string) (bool, error) {
return true, nil return true, nil
} }
func CheckAuthorization(entry *Entry, token string) bool { func CheckAuthorization(token string) bool {
if len(token) < 1 { // there is no token provided if ok, err := checkAccess(token); ok {
return false
} else {
entry.token = token
}
if ok, err := checkAccess(entry.token); ok {
return true return true
} else if err != nil { } else if err != nil {
return false return false