Compare commits

...

2 Commits

Author SHA1 Message Date
Niko Abeler a38ebb61a9 form to edit registered configs 2023-07-22 21:12:53 +02:00
Niko Abeler 68e8f84220 WIP activity pub PoC 2023-07-22 20:34:17 +02:00
10 changed files with 469 additions and 2 deletions

33
app/config_register.go Normal file
View File

@ -0,0 +1,33 @@
package app
type ConfigRegister struct {
configs map[string]interface{}
}
type RegisteredConfig struct {
Name string
Config interface{}
}
func NewConfigRegister() *ConfigRegister {
return &ConfigRegister{configs: map[string]interface{}{}}
}
func (r *ConfigRegister) Register(name string, config interface{}) {
r.configs[name] = config
}
func (r *ConfigRegister) Configs() []RegisteredConfig {
var configs []RegisteredConfig
for name, config := range r.configs {
configs = append(configs, RegisteredConfig{
Name: name,
Config: config,
})
}
return configs
}
func (r *ConfigRegister) GetConfig(name string) interface{} {
return r.configs[name]
}

25
app/entry_creation_bus.go Normal file
View File

@ -0,0 +1,25 @@
package app
import "owl-blogs/domain/model"
type EntryCreationSubscriber interface {
NotifyEntryCreation(entry model.Entry)
}
type EntryCreationBus struct {
subscribers []EntryCreationSubscriber
}
func NewEntryCreationBus() *EntryCreationBus {
return &EntryCreationBus{subscribers: make([]EntryCreationSubscriber, 0)}
}
func (b *EntryCreationBus) Subscribe(subscriber EntryCreationSubscriber) {
b.subscribers = append(b.subscribers, subscriber)
}
func (b *EntryCreationBus) Notify(entry model.Entry) {
for _, subscriber := range b.subscribers {
subscriber.NotifyEntryCreation(entry)
}
}

View File

@ -44,7 +44,12 @@ func App(db infra.Database) *web.WebApp {
binaryService := app.NewBinaryFileService(binRepo)
authorService := app.NewAuthorService(authorRepo, siteConfigRepo)
return web.NewWebApp(entryService, registry, binaryService, authorService, siteConfigRepo)
configRegister := app.NewConfigRegister()
return web.NewWebApp(
entryService, registry, binaryService,
authorService, siteConfigRepo, configRegister,
)
}

View File

@ -1,6 +1,6 @@
{{define "base"}}
<!doctype html>
<html lang='en'>
<html lang='en' data-theme="light">
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -0,0 +1,9 @@
{{define "title"}}Admin{{end}}
{{define "main"}}
<ul>
{{ range . }}
<li><a href="/admin/config/{{.Name}}">{{.Name}}</a></li>
{{ end }}
</ul>
{{end}}

View File

@ -0,0 +1,11 @@
{{define "title"}}Editor{{end}}
{{define "main"}}
<a href="/editor">Back</a>
<br>
<br>
{{.}}
{{end}}

122
web/activity_pub_handler.go Normal file
View File

@ -0,0 +1,122 @@
package web
import (
"owl-blogs/app/repository"
"owl-blogs/config"
"owl-blogs/domain/model"
"github.com/gofiber/fiber/v2"
)
const ACT_PUB_CONF_NAME = "activity_pub"
type ActivityPubServer struct {
configRepo repository.ConfigRepository
}
type ActivityPubConfig struct {
PreferredUsername string `owl:"inputType=text"`
PublicKeyPem string `owl:"inputType=text widget=textarea"`
PrivateKeyPem string `owl:"inputType=text widget=textarea"`
}
type WebfingerResponse struct {
Subject string `json:"subject"`
Links []ActivityPubLink `json:"links"`
}
type ActivityPubLink struct {
Rel string `json:"rel"`
Type string `json:"type"`
Href string `json:"href"`
}
type ActivityPubActor struct {
Context []string `json:"@context"`
ID string `json:"id"`
Type string `json:"type"`
PreferredUsername string `json:"preferredUsername"`
Inbox string `json:"inbox"`
Oubox string `json:"outbox"`
Followers string `json:"followers"`
PublicKey ActivityPubPublicKey `json:"publicKey"`
}
type ActivityPubPublicKey struct {
ID string `json:"id"`
Owner string `json:"owner"`
PublicKeyPem string `json:"publicKeyPem"`
}
type ActivityPubOrderedCollection struct {
Context []string `json:"@context"`
ID string `json:"id"`
Type string `json:"type"`
TotalItems int `json:"totalItems"`
First string `json:"first"`
Last string `json:"last"`
}
func NewActivityPubServer(configRepo repository.ConfigRepository) *ActivityPubServer {
return &ActivityPubServer{
configRepo: configRepo,
}
}
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
webfinger := WebfingerResponse{
Subject: ctx.Query("resource"),
Links: []ActivityPubLink{
{
Rel: "self",
Type: "application/activity+json",
Href: siteConfig.FullUrl + "/activitypub/actor",
},
},
}
return ctx.JSON(webfinger)
}
func (s *ActivityPubServer) Router(router fiber.Router) {
router.Get("/actor", s.HandleActor)
}
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
siteConfig := model.SiteConfig{}
apConfig := ActivityPubConfig{}
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
actor := ActivityPubActor{
Context: []string{
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
},
ID: siteConfig.FullUrl + "/activitypub/actor",
Type: "Person",
PreferredUsername: apConfig.PreferredUsername,
Inbox: siteConfig.FullUrl + "/activitypub/inbox",
Oubox: siteConfig.FullUrl + "/activitypub/outbox",
Followers: siteConfig.FullUrl + "/activitypub/followers",
PublicKey: ActivityPubPublicKey{
ID: siteConfig.FullUrl + "/activitypub/actor#main-key",
Owner: siteConfig.FullUrl + "/activitypub/actor",
PublicKeyPem: apConfig.PublicKeyPem,
},
}
return ctx.JSON(actor)
}

75
web/admin_handler.go Normal file
View File

@ -0,0 +1,75 @@
package web
import (
"owl-blogs/app"
"owl-blogs/app/repository"
"owl-blogs/render"
"owl-blogs/web/forms"
"github.com/gofiber/fiber/v2"
)
type adminHandler struct {
configRepo repository.ConfigRepository
configRegister *app.ConfigRegister
}
func NewAdminHandler(configRepo repository.ConfigRepository, configRegister *app.ConfigRegister) *adminHandler {
return &adminHandler{
configRepo: configRepo,
configRegister: configRegister,
}
}
func (h *adminHandler) Handle(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
siteConfig := getSiteConfig(h.configRepo)
configs := h.configRegister.Configs()
return render.RenderTemplateWithBase(c, siteConfig, "views/admin", configs)
}
func (h *adminHandler) HandleConfigGet(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
configName := c.Params("config")
config := h.configRegister.GetConfig(configName)
if config == nil {
return c.SendStatus(404)
}
err := h.configRepo.Get(configName, config)
if err != nil {
return err
}
siteConfig := getSiteConfig(h.configRepo)
form := forms.NewForm(config, nil)
htmlForm, err := form.HtmlForm()
if err != nil {
return err
}
return render.RenderTemplateWithBase(c, siteConfig, "views/admin_config", htmlForm)
}
func (h *adminHandler) HandleConfigPost(c *fiber.Ctx) error {
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
configName := c.Params("config")
config := h.configRegister.GetConfig(configName)
if config == nil {
return c.SendStatus(404)
}
form := forms.NewForm(config, nil)
newConfig, err := form.Parse(c)
if err != nil {
return err
}
h.configRepo.Update(configName, newConfig)
return c.Redirect("")
}

View File

@ -29,6 +29,7 @@ func NewWebApp(
binService *app.BinaryService,
authorService *app.AuthorService,
configRepo repository.ConfigRepository,
configRegister *app.ConfigRegister,
) *WebApp {
app := fiber.New()
@ -45,6 +46,14 @@ func NewWebApp(
app.Get("/auth/login", loginHandler.HandleGet)
app.Post("/auth/login", loginHandler.HandlePost)
// admin
adminHandler := NewAdminHandler(configRepo, configRegister)
admin := app.Group("/admin")
admin.Use(middleware.NewAuthMiddleware(authorService).Handle)
admin.Get("/", adminHandler.Handle)
admin.Get("/config/:config/", adminHandler.HandleConfigGet)
admin.Post("/config/:config/", adminHandler.HandleConfigPost)
// Editor
editor := app.Group("/editor")
editor.Use(middleware.NewAuthMiddleware(authorService).Handle)
@ -89,6 +98,13 @@ func NewWebApp(
app.Get("/index.xml", rssHandler.Handle)
// Posts
app.Get("/posts/:post/", entryHandler.Handle)
// ActivityPub
// activityPubServer := NewActivityPubServer(configRepo)
configRegister.Register(ACT_PUB_CONF_NAME, &ActivityPubConfig{})
// app.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
// app.Route("/activitypub", activityPubServer.Router)
// Webmention
// app.Post("/webmention/", userWebmentionHandler(repo))
// Micropub

171
web/forms/form.go Normal file
View File

@ -0,0 +1,171 @@
package forms
import (
"fmt"
"mime/multipart"
"owl-blogs/app"
"reflect"
"strings"
)
type HttpFormData interface {
// FormFile returns the first file by key from a MultipartForm.
FormFile(key string) (*multipart.FileHeader, error)
// FormValue returns the first value by key from a MultipartForm.
// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.
// Defaults to the empty string "" if the form value doesn't exist.
// If a default value is given, it will return that value if the form value does not exist.
// Returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting instead.
FormValue(key string, defaultValue ...string) string
}
type Form struct {
data interface{}
binSvc *app.BinaryService
}
type FormFieldParams struct {
InputType string
Widget string
}
type FormField struct {
Name string
Value string
Params FormFieldParams
}
func NewForm(data interface{}, binaryService *app.BinaryService) *Form {
return &Form{
data: data,
binSvc: binaryService,
}
}
func (s *FormFieldParams) ApplyTag(tagKey string, tagValue string) error {
switch tagKey {
case "inputType":
s.InputType = tagValue
case "widget":
s.Widget = tagValue
default:
return fmt.Errorf("unknown tag key: %v", tagKey)
}
return nil
}
func (s *FormField) Html() string {
html := ""
html += fmt.Sprintf("<label for=\"%v\">%v</label>\n", s.Name, s.Name)
if s.Params.InputType == "text" && s.Params.Widget == "textarea" {
html += fmt.Sprintf("<textarea name=\"%v\" id=\"%v\" rows=\"20\">%v</textarea>\n", s.Name, s.Name, s.Value)
} else {
html += fmt.Sprintf("<input type=\"%v\" name=\"%v\" id=\"%v\" value=\"%v\" />\n", s.Params.InputType, s.Name, s.Name, s.Value)
}
return html
}
func FieldToFormField(field reflect.StructField, value string) (FormField, error) {
formField := FormField{
Name: field.Name,
Value: value,
Params: FormFieldParams{},
}
tag := field.Tag.Get("owl")
for _, param := range strings.Split(tag, " ") {
parts := strings.Split(param, "=")
if len(parts) != 2 {
continue
}
err := formField.Params.ApplyTag(parts[0], parts[1])
if err != nil {
return FormField{}, err
}
}
return formField, nil
}
func StructToFormFields(data interface{}) ([]FormField, error) {
dataValue := reflect.Indirect(reflect.ValueOf(data))
dataType := reflect.TypeOf(data).Elem()
numFields := dataType.NumField()
fields := []FormField{}
for i := 0; i < numFields; i++ {
field, err := FieldToFormField(dataType.Field(i), dataValue.FieldByIndex([]int{i}).String())
if err != nil {
return nil, err
}
fields = append(fields, field)
}
return fields, nil
}
func (s *Form) HtmlForm() (string, error) {
fields, err := StructToFormFields(s.data)
if err != nil {
return "", err
}
html := "<form method=\"POST\" enctype=\"multipart/form-data\">\n"
for _, field := range fields {
html += field.Html()
}
html += "<input type=\"submit\" value=\"Submit\" />\n"
html += "</form>\n"
return html, nil
}
func (s *Form) Parse(ctx HttpFormData) (interface{}, error) {
if ctx == nil {
return nil, fmt.Errorf("nil context")
}
dataVal := reflect.ValueOf(s.data)
if dataVal.Kind() != reflect.Ptr {
return nil, fmt.Errorf("meta data is not a pointer")
}
fields, err := StructToFormFields(s.data)
if err != nil {
return nil, err
}
for _, field := range fields {
fieldName := field.Name
if field.Params.InputType == "file" {
file, err := ctx.FormFile(fieldName)
if err != nil {
return nil, err
}
fileData, err := file.Open()
if err != nil {
return nil, err
}
defer fileData.Close()
fileBytes := make([]byte, file.Size)
_, err = fileData.Read(fileBytes)
if err != nil {
return nil, err
}
binaryFile, err := s.binSvc.Create(file.Filename, fileBytes)
if err != nil {
return nil, err
}
metaField := dataVal.Elem().FieldByName(fieldName)
if metaField.IsValid() {
metaField.SetString(binaryFile.Id)
}
} else {
formValue := ctx.FormValue(fieldName)
metaField := dataVal.Elem().FieldByName(fieldName)
if metaField.IsValid() {
metaField.SetString(formValue)
}
}
}
return s.data, nil
}