Compare commits
54 Commits
better_for
...
main
Author | SHA1 | Date |
---|---|---|
Niko Abeler | da06541e11 | |
Niko Abeler | 7b047a609f | |
Niko Abeler | a5f24427a1 | |
Niko Abeler | ec13fffbe9 | |
Niko Abeler | 686cd72ec2 | |
Niko Abeler | 29e875d2e5 | |
Niko Abeler | f958fa36fd | |
Niko Abeler | 1b347bcdac | |
Niko Abeler | c8b759a834 | |
Niko Abeler | 943bc10eaf | |
Niko Abeler | 4543e448ed | |
Niko Abeler | e0d6f4f223 | |
Niko Abeler | 49a602f68b | |
Niko Abeler | 596ab0047e | |
Niko Abeler | 5c05f48be3 | |
Niko Abeler | 3c924ac8a4 | |
Niko Abeler | 26737ea21d | |
Niko Abeler | 29ae7e717f | |
Niko Abeler | 741ccfac73 | |
Niko Abeler | 9cfbf0b9b7 | |
Niko Abeler | cba57ba708 | |
Niko Abeler | ced3907880 | |
Niko Abeler | 3cbf952ae6 | |
Niko Abeler | 0c8779def7 | |
Niko Abeler | 652f81805d | |
Niko Abeler | 624f19a1d9 | |
Niko Abeler | 10ca2bdcd9 | |
Niko Abeler | 633c0991e9 | |
Niko Abeler | 2bf2e409b6 | |
Niko Abeler | 390a58b404 | |
Niko Abeler | d7e5df2a95 | |
Niko Abeler | 3a3655b587 | |
Niko Abeler | cba07e961d | |
Niko Abeler | 1524820d5e | |
Niko Abeler | 91b82f0e57 | |
Niko Abeler | 2a4b76ee03 | |
Niko Abeler | 4bdb920c71 | |
Niko Abeler | ff10f6c5eb | |
Niko Abeler | bc7b146e91 | |
Niko Abeler | 7eb3bf0b44 | |
Niko Abeler | 5cc55a79ff | |
Niko Abeler | be62bcd627 | |
Niko Abeler | d794ad0865 | |
Niko Abeler | f5946ea823 | |
Niko Abeler | 8200e3384c | |
Niko Abeler | 0bf7c492c9 | |
Niko Abeler | 765698b1a6 | |
Niko Abeler | 1485a7efbe | |
Niko Abeler | bc50388f58 | |
Niko Abeler | 7ceb00799a | |
Niko Abeler | 6d115dd74e | |
Niko Abeler | 4941b5d027 | |
Niko Abeler | 2f81bf8678 | |
h4kor | c36b9abbcf |
|
@ -1,11 +1,11 @@
|
||||||
root = "."
|
root = "."
|
||||||
testdata_dir = "testdata"
|
testdata_dir = "testdata"
|
||||||
tmp_dir = "tmp"
|
tmp_dir = "/tmp"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
args_bin = ["web"]
|
args_bin = ["web"]
|
||||||
bin = "./tmp/main"
|
bin = "/tmp/main"
|
||||||
cmd = "go build -o ./tmp/main owl-blogs/cmd/owl"
|
cmd = "go build -buildvcs=false -o /tmp/main owl-blogs/cmd/owl"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
e2e_tests/
|
||||||
|
tmp/
|
||||||
|
*.db
|
|
@ -0,0 +1,45 @@
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-linux-amd64:
|
||||||
|
name: release linux/amd64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: E2E Test
|
||||||
|
run: |
|
||||||
|
cd e2e_tests
|
||||||
|
docker compose -f docker-compose.ci.yml up -d
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pytest
|
||||||
|
|
||||||
|
- name: Build Release
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 1
|
||||||
|
GOOS: linux
|
||||||
|
GOARCH: amd64
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
go build -o owl-linux-amd64 ./cmd/owl
|
||||||
|
gh release upload ${{github.event.release.tag_name}} owl-linux-amd64
|
|
@ -0,0 +1,38 @@
|
||||||
|
# This workflow will build a golang project
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||||
|
|
||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
|
|
||||||
|
- name: E2E Test
|
||||||
|
run: |
|
||||||
|
cd e2e_tests
|
||||||
|
docker compose -f docker-compose.ci.yml up -d
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pytest
|
|
@ -27,4 +27,7 @@ users/
|
||||||
|
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
tmp/
|
tmp/
|
||||||
|
|
||||||
|
venv/
|
||||||
|
*.pyc
|
|
@ -1,7 +1,7 @@
|
||||||
##
|
##
|
||||||
## Build Container
|
## Build Container
|
||||||
##
|
##
|
||||||
FROM golang:1.21-alpine as build
|
FROM golang:1.22-alpine as build
|
||||||
|
|
||||||
|
|
||||||
RUN apk add --no-cache --update git gcc g++
|
RUN apk add --no-cache --update git gcc g++
|
||||||
|
|
57
README.md
57
README.md
|
@ -2,7 +2,60 @@
|
||||||
|
|
||||||
# Owl Blogs
|
# Owl Blogs
|
||||||
|
|
||||||
A simple web server for blogs generated from Markdown files.
|
Owl-blogs is a blogging software focused on simplicity with IndieWeb and Fediverse support.
|
||||||
|
|
||||||
**_This project is not yet stable. Expect frequent breaking changes! Only use this if you are willing to regularly adjust your project accordingly._**
|
# Usage
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
To run the web server use the command:
|
||||||
|
|
||||||
|
```
|
||||||
|
owl web
|
||||||
|
```
|
||||||
|
|
||||||
|
The blog will run on port 3000 (http://localhost:3000)
|
||||||
|
|
||||||
|
To create a new account:
|
||||||
|
|
||||||
|
```
|
||||||
|
owl new-author -u <name> -p <password>
|
||||||
|
```
|
||||||
|
|
||||||
|
To retrieve a list of all commands run:
|
||||||
|
|
||||||
|
```
|
||||||
|
owl -h
|
||||||
|
```
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```
|
||||||
|
CGO_ENABLED=1 go build -o owl ./cmd/owl
|
||||||
|
```
|
||||||
|
|
||||||
|
For development with live reload use `air` ([has to be install first](https://github.com/cosmtrek/air))
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
The project has two test suites; "unit tests" written in go and "end-to-end tests" written in python.
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
|
||||||
|
```
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### End-to-End tests
|
||||||
|
|
||||||
|
- Start the docker compose setup in the `e2e_tests` directory.
|
||||||
|
- Install the python dependencies into a virtualenv
|
||||||
|
```
|
||||||
|
cd e2e_tests
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
- Run the e2e_tests with `pytest`
|
||||||
|
|
|
@ -0,0 +1,712 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/config"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
entrytypes "owl-blogs/entry_types"
|
||||||
|
"owl-blogs/interactions"
|
||||||
|
"owl-blogs/render"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
vocab "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
|
"github.com/go-fed/httpsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityPubConfig struct {
|
||||||
|
PreferredUsername string
|
||||||
|
PublicKeyPem string
|
||||||
|
PrivateKeyPem string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form implements app.AppConfig.
|
||||||
|
func (cfg *ActivityPubConfig) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
|
f, _ := render.RenderTemplateToString("forms/ActivityPubConfig", cfg)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFormData implements app.AppConfig.
|
||||||
|
func (cfg *ActivityPubConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
|
cfg.PreferredUsername = data.FormValue("PreferredUsername")
|
||||||
|
cfg.PublicKeyPem = data.FormValue("PublicKeyPem")
|
||||||
|
cfg.PrivateKeyPem = data.FormValue("PrivateKeyPem")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *ActivityPubConfig) PrivateKey() *rsa.PrivateKey {
|
||||||
|
block, _ := pem.Decode([]byte(cfg.PrivateKeyPem))
|
||||||
|
privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error x509.ParsePKCS1PrivateKey", "err", err)
|
||||||
|
}
|
||||||
|
return privKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityPubService struct {
|
||||||
|
followersRepo repository.FollowerRepository
|
||||||
|
configRepo repository.ConfigRepository
|
||||||
|
interactionRepository repository.InteractionRepository
|
||||||
|
entryService *EntryService
|
||||||
|
siteConfigServcie *SiteConfigService
|
||||||
|
binService *BinaryService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityPubService(
|
||||||
|
followersRepo repository.FollowerRepository,
|
||||||
|
configRepo repository.ConfigRepository,
|
||||||
|
interactionRepository repository.InteractionRepository,
|
||||||
|
entryService *EntryService,
|
||||||
|
siteConfigServcie *SiteConfigService,
|
||||||
|
binService *BinaryService,
|
||||||
|
bus *EventBus,
|
||||||
|
) *ActivityPubService {
|
||||||
|
service := &ActivityPubService{
|
||||||
|
followersRepo: followersRepo,
|
||||||
|
configRepo: configRepo,
|
||||||
|
interactionRepository: interactionRepository,
|
||||||
|
entryService: entryService,
|
||||||
|
binService: binService,
|
||||||
|
siteConfigServcie: siteConfigServcie,
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.Subscribe(service)
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) defaultConfig() ActivityPubConfig {
|
||||||
|
privKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
pubKey := privKey.Public().(*rsa.PublicKey)
|
||||||
|
|
||||||
|
pubKeyPem := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PUBLIC KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PublicKey(pubKey),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
privKeyPrm := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(privKey),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return ActivityPubConfig{
|
||||||
|
PreferredUsername: "blog",
|
||||||
|
PublicKeyPem: string(pubKeyPem),
|
||||||
|
PrivateKeyPem: string(privKeyPrm),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) GetApConfig() (ActivityPubConfig, error) {
|
||||||
|
apConfig := ActivityPubConfig{}
|
||||||
|
err := svc.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
if err != nil {
|
||||||
|
println("ERROR IN ACTIVITY PUB CONFIG")
|
||||||
|
return ActivityPubConfig{}, err
|
||||||
|
}
|
||||||
|
if reflect.ValueOf(apConfig).IsZero() {
|
||||||
|
cfg := svc.defaultConfig()
|
||||||
|
svc.configRepo.Update(config.ACT_PUB_CONF_NAME, cfg)
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
return apConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) ActorUrl() string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.FullUrl
|
||||||
|
}
|
||||||
|
func (svc *ActivityPubService) MainKeyUri() string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.FullUrl + "#main-key"
|
||||||
|
}
|
||||||
|
func (svc *ActivityPubService) InboxUrl() string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.FullUrl + "/activitypub/inbox"
|
||||||
|
}
|
||||||
|
func (svc *ActivityPubService) OutboxUrl() string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.FullUrl + "/activitypub/outbox"
|
||||||
|
}
|
||||||
|
func (svc *ActivityPubService) FollowersUrl() string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.FullUrl + "/activitypub/followers"
|
||||||
|
}
|
||||||
|
func (svc *ActivityPubService) AcccepId() string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.FullUrl + "#accept-" + strconv.FormatInt(time.Now().UnixNano(), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) HashtagId(hashtag string) string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.FullUrl + "/tags/" + strings.ReplaceAll(hashtag, "#", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) ActorName() string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) ActorIcon() vocab.Image {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
u := cfg.AvatarUrl
|
||||||
|
pUrl, _ := url.Parse(u)
|
||||||
|
parts := strings.Split(pUrl.Path, ".")
|
||||||
|
fullUrl, _ := url.JoinPath(cfg.FullUrl, u)
|
||||||
|
return vocab.Image{
|
||||||
|
Type: vocab.ImageType,
|
||||||
|
MediaType: vocab.MimeType("image/" + parts[len(parts)-1]),
|
||||||
|
URL: vocab.IRI(fullUrl),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) ActorSummary() string {
|
||||||
|
cfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
return cfg.SubTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) AddFollower(follower string) error {
|
||||||
|
return s.followersRepo.Add(follower)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) RemoveFollower(follower string) error {
|
||||||
|
return s.followersRepo.Remove(follower)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) AllFollowers() ([]string, error) {
|
||||||
|
return s.followersRepo.All()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) sign(privateKey *rsa.PrivateKey, pubKeyId string, body []byte, r *http.Request) error {
|
||||||
|
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256}
|
||||||
|
digestAlgorithm := httpsig.DigestSha256
|
||||||
|
// The "Date" and "Digest" headers must already be set on r, as well as r.URL.
|
||||||
|
headersToSign := []string{httpsig.RequestTarget, "host", "date"}
|
||||||
|
if body != nil {
|
||||||
|
headersToSign = append(headersToSign, "digest")
|
||||||
|
}
|
||||||
|
signer, _, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// To sign the digest, we need to give the signer a copy of the body...
|
||||||
|
// ...but it is optional, no digest will be signed if given "nil"
|
||||||
|
// If r were a http.ResponseWriter, call SignResponse instead.
|
||||||
|
err = signer.SignRequest(privateKey, pubKeyId, r, body)
|
||||||
|
|
||||||
|
slog.Info("Signed Request", "req", r.Header)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) GetActor(reqUrl string) (vocab.Actor, error) {
|
||||||
|
|
||||||
|
siteConfig := model.SiteConfig{}
|
||||||
|
apConfig := ActivityPubConfig{}
|
||||||
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
|
c := http.Client{}
|
||||||
|
|
||||||
|
parsedUrl, err := url.Parse(reqUrl)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("parse error", "err", err)
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", reqUrl, nil)
|
||||||
|
req.Header.Set("Accept", "application/ld+json")
|
||||||
|
req.Header.Set("Date", time.Now().Format(http.TimeFormat))
|
||||||
|
req.Header.Set("Host", parsedUrl.Host)
|
||||||
|
|
||||||
|
err = s.sign(apConfig.PrivateKey(), s.MainKeyUri(), nil, req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Signing error", "err", err)
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to retrieve sender actor", "err", err, "url", reqUrl)
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := vocab.UnmarshalJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return vocab.Actor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var actor vocab.Actor
|
||||||
|
|
||||||
|
err = vocab.OnActor(item, func(o *vocab.Actor) error {
|
||||||
|
actor = *o
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return actor, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) VerifySignature(r *http.Request, sender string) error {
|
||||||
|
siteConfig := model.SiteConfig{}
|
||||||
|
apConfig := ActivityPubConfig{}
|
||||||
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
|
slog.Info("verifying for", "sender", sender, "retriever", s.ActorUrl())
|
||||||
|
|
||||||
|
actor, err := s.GetActor(sender)
|
||||||
|
// actor does not have a pub key -> don't verify
|
||||||
|
if actor.PublicKey.PublicKeyPem == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("unable to retrieve actor for sig verification", "sender", sender)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
block, _ := pem.Decode([]byte(actor.PublicKey.PublicKeyPem))
|
||||||
|
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("unable to decode pub key pem", "pubKeyPem", actor.PublicKey.PublicKeyPem)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("retrieved pub key of sender", "actor", actor, "pubKey", pubKey)
|
||||||
|
|
||||||
|
verifier, err := httpsig.NewVerifier(r)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("invalid signature", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return verifier.Verify(pubKey, httpsig.RSA_SHA256)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) Accept(act *vocab.Activity) error {
|
||||||
|
actor, err := s.GetActor(act.Actor.GetID().String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accept := vocab.AcceptNew(vocab.IRI(s.AcccepId()), act)
|
||||||
|
data, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(vocab.ActivityBaseURI),
|
||||||
|
).Marshal(accept)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("marshalling error", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.sendObject(actor, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) AddLike(sender string, liked string, likeId string) error {
|
||||||
|
entry, err := s.entryService.FindByUrl(liked)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actor, err := s.GetActor(sender)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var like *interactions.Like
|
||||||
|
interaction, err := s.interactionRepository.FindById(likeId)
|
||||||
|
if err != nil {
|
||||||
|
interaction = &interactions.Like{}
|
||||||
|
}
|
||||||
|
like, ok := interaction.(*interactions.Like)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("existing interaction with same id is not a like")
|
||||||
|
}
|
||||||
|
existing := like.ID() != ""
|
||||||
|
|
||||||
|
likeMeta := interactions.LikeMetaData{
|
||||||
|
SenderUrl: sender,
|
||||||
|
SenderName: actor.Name.String(),
|
||||||
|
}
|
||||||
|
like.SetID(likeId)
|
||||||
|
like.SetMetaData(&likeMeta)
|
||||||
|
like.SetEntryID(entry.ID())
|
||||||
|
like.SetCreatedAt(time.Now())
|
||||||
|
if !existing {
|
||||||
|
return s.interactionRepository.Create(like)
|
||||||
|
} else {
|
||||||
|
return s.interactionRepository.Update(like)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) RemoveLike(id string) error {
|
||||||
|
interaction, err := s.interactionRepository.FindById(id)
|
||||||
|
if err != nil {
|
||||||
|
interaction = &interactions.Like{}
|
||||||
|
}
|
||||||
|
return s.interactionRepository.Delete(interaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) AddRepost(sender string, reposted string, respostId string) error {
|
||||||
|
entry, err := s.entryService.FindByUrl(reposted)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actor, err := s.GetActor(sender)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var repost *interactions.Repost
|
||||||
|
interaction, err := s.interactionRepository.FindById(respostId)
|
||||||
|
if err != nil {
|
||||||
|
interaction = &interactions.Repost{}
|
||||||
|
}
|
||||||
|
repost, ok := interaction.(*interactions.Repost)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("existing interaction with same id is not a like")
|
||||||
|
}
|
||||||
|
existing := repost.ID() != ""
|
||||||
|
|
||||||
|
repostMeta := interactions.RepostMetaData{
|
||||||
|
SenderUrl: sender,
|
||||||
|
SenderName: actor.Name.String(),
|
||||||
|
}
|
||||||
|
repost.SetID(respostId)
|
||||||
|
repost.SetMetaData(&repostMeta)
|
||||||
|
repost.SetEntryID(entry.ID())
|
||||||
|
repost.SetCreatedAt(time.Now())
|
||||||
|
if !existing {
|
||||||
|
return s.interactionRepository.Create(repost)
|
||||||
|
} else {
|
||||||
|
return s.interactionRepository.Update(repost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) RemoveRepost(id string) error {
|
||||||
|
interaction, err := s.interactionRepository.FindById(id)
|
||||||
|
if err != nil {
|
||||||
|
interaction = &interactions.Repost{}
|
||||||
|
}
|
||||||
|
return s.interactionRepository.Delete(interaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubService) sendObject(to vocab.Actor, data []byte) error {
|
||||||
|
siteConfig := model.SiteConfig{}
|
||||||
|
apConfig := ActivityPubConfig{}
|
||||||
|
s.configRepo.Get(config.ACT_PUB_CONF_NAME, &apConfig)
|
||||||
|
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
|
||||||
|
if to.Inbox == nil {
|
||||||
|
slog.Error("actor has no inbox", "actor", to)
|
||||||
|
return errors.New("actor has no inbox")
|
||||||
|
}
|
||||||
|
|
||||||
|
actorUrl, err := url.Parse(to.Inbox.GetID().String())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("parse error", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := http.Client{}
|
||||||
|
req, _ := http.NewRequest("POST", to.Inbox.GetID().String(), bytes.NewReader(data))
|
||||||
|
req.Header.Set("Accept", "application/ld+json")
|
||||||
|
req.Header.Set("Date", time.Now().Format(http.TimeFormat))
|
||||||
|
req.Header.Set("Host", actorUrl.Host)
|
||||||
|
err = s.sign(apConfig.PrivateKey(), s.MainKeyUri(), data, req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Signing error", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := c.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Sending error", "url", req.URL, "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("Request", "host", resp.Request.Header)
|
||||||
|
|
||||||
|
if resp.StatusCode > 299 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
slog.Error("Error sending Note", "method", resp.Request.Method, "url", resp.Request.URL, "status", resp.Status, "body", string(body))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
slog.Info("Sent Body", "body", string(data))
|
||||||
|
slog.Info("Retrieved", "status", resp.Status, "body", string(body))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Notifiers
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) NotifyEntryCreated(entry model.Entry) {
|
||||||
|
slog.Info("Processing Entry Create for ActivityPub")
|
||||||
|
followers, err := svc.AllFollowers()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Cannot retrieve followers")
|
||||||
|
}
|
||||||
|
|
||||||
|
object, err := svc.entryToObject(entry)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Cannot convert object", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
create := vocab.CreateNew(object.ID, object)
|
||||||
|
create.Actor = object.AttributedTo
|
||||||
|
create.To = object.To
|
||||||
|
create.Published = object.Published
|
||||||
|
data, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(vocab.ActivityBaseURI),
|
||||||
|
jsonld.Context{
|
||||||
|
jsonld.ContextElement{
|
||||||
|
Term: "toot",
|
||||||
|
IRI: jsonld.IRI("http://joinmastodon.org/ns#"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).Marshal(create)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("marshalling error", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, follower := range followers {
|
||||||
|
actor, err := svc.GetActor(follower)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Unable to retrieve follower actor", "err", err)
|
||||||
|
}
|
||||||
|
svc.sendObject(actor, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) NotifyEntryUpdated(entry model.Entry) {
|
||||||
|
slog.Info("Processing Entry Create for ActivityPub")
|
||||||
|
followers, err := svc.AllFollowers()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Cannot retrieve followers")
|
||||||
|
}
|
||||||
|
|
||||||
|
object, err := svc.entryToObject(entry)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Cannot convert object", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
update := vocab.UpdateNew(object.ID, object)
|
||||||
|
update.Actor = object.AttributedTo
|
||||||
|
update.To = object.To
|
||||||
|
update.Published = object.Published
|
||||||
|
data, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(vocab.ActivityBaseURI),
|
||||||
|
jsonld.Context{
|
||||||
|
jsonld.ContextElement{
|
||||||
|
Term: "toot",
|
||||||
|
IRI: jsonld.IRI("http://joinmastodon.org/ns#"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).Marshal(update)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("marshalling error", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, follower := range followers {
|
||||||
|
actor, err := svc.GetActor(follower)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Unable to retrieve follower actor", "err", err)
|
||||||
|
}
|
||||||
|
svc.sendObject(actor, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) NotifyEntryDeleted(entry model.Entry) {
|
||||||
|
obj, err := svc.entryToObject(entry)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error converting to object", "err", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
followers, err := svc.AllFollowers()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Cannot retrieve followers")
|
||||||
|
}
|
||||||
|
|
||||||
|
delete := vocab.DeleteNew(obj.ID, obj)
|
||||||
|
delete.Actor = obj.AttributedTo
|
||||||
|
delete.To = obj.To
|
||||||
|
delete.Published = time.Now()
|
||||||
|
data, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(vocab.ActivityBaseURI),
|
||||||
|
).Marshal(delete)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("marshalling error", "err", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, follower := range followers {
|
||||||
|
actor, err := svc.GetActor(follower)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Unable to retrieve follower actor", "err", err)
|
||||||
|
}
|
||||||
|
svc.sendObject(actor, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) entryToObject(entry model.Entry) (vocab.Object, error) {
|
||||||
|
// limit to notes for now
|
||||||
|
|
||||||
|
if noteEntry, ok := entry.(*entrytypes.Note); ok {
|
||||||
|
return svc.noteToObject(noteEntry), nil
|
||||||
|
}
|
||||||
|
if imageEntry, ok := entry.(*entrytypes.Image); ok {
|
||||||
|
return svc.imageToObject(imageEntry), nil
|
||||||
|
}
|
||||||
|
if articleEntry, ok := entry.(*entrytypes.Article); ok {
|
||||||
|
return svc.articleToObject(articleEntry), nil
|
||||||
|
}
|
||||||
|
if recipeEntry, ok := entry.(*entrytypes.Recipe); ok {
|
||||||
|
return svc.recipeToObject(recipeEntry), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Warn("entry type not yet supported for activity pub")
|
||||||
|
return vocab.Object{}, errors.New("entry type not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) noteToObject(noteEntry *entrytypes.Note) vocab.Object {
|
||||||
|
|
||||||
|
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
content := noteEntry.Content()
|
||||||
|
r := regexp.MustCompile("#[a-z0-9_]+")
|
||||||
|
matches := r.FindAllString(string(content), -1)
|
||||||
|
tags := vocab.ItemCollection{}
|
||||||
|
for _, hashtag := range matches {
|
||||||
|
tags.Append(vocab.Object{
|
||||||
|
ID: vocab.ID(svc.HashtagId(hashtag)),
|
||||||
|
Name: vocab.NaturalLanguageValues{{Value: vocab.Content(hashtag)}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
note := vocab.Note{
|
||||||
|
ID: vocab.ID(noteEntry.FullUrl(siteCfg)),
|
||||||
|
Type: "Note",
|
||||||
|
To: vocab.ItemCollection{
|
||||||
|
vocab.PublicNS,
|
||||||
|
vocab.IRI(svc.FollowersUrl()),
|
||||||
|
},
|
||||||
|
Published: *noteEntry.PublishedAt(),
|
||||||
|
AttributedTo: vocab.ID(svc.ActorUrl()),
|
||||||
|
Content: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(content)},
|
||||||
|
},
|
||||||
|
Tag: tags,
|
||||||
|
}
|
||||||
|
return note
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) imageToObject(imageEntry *entrytypes.Image) vocab.Object {
|
||||||
|
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
content := imageEntry.Content()
|
||||||
|
|
||||||
|
imgPath := imageEntry.ImageUrl()
|
||||||
|
fullImageUrl, _ := url.JoinPath(siteCfg.FullUrl, imgPath)
|
||||||
|
binaryFile, err := svc.binService.FindById(imageEntry.MetaData().(*entrytypes.ImageMetaData).ImageId)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("cannot get image file")
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments := vocab.ItemCollection{}
|
||||||
|
attachments = append(attachments, vocab.Document{
|
||||||
|
Type: vocab.DocumentType,
|
||||||
|
MediaType: vocab.MimeType(binaryFile.Mime()),
|
||||||
|
URL: vocab.ID(fullImageUrl),
|
||||||
|
Name: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(content)},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
image := vocab.Image{
|
||||||
|
ID: vocab.ID(imageEntry.FullUrl(siteCfg)),
|
||||||
|
Type: "Image",
|
||||||
|
To: vocab.ItemCollection{
|
||||||
|
vocab.PublicNS,
|
||||||
|
vocab.IRI(svc.FollowersUrl()),
|
||||||
|
},
|
||||||
|
Published: *imageEntry.PublishedAt(),
|
||||||
|
AttributedTo: vocab.ID(svc.ActorUrl()),
|
||||||
|
Name: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(imageEntry.Title())},
|
||||||
|
},
|
||||||
|
Content: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(imageEntry.Title() + "<br><br>" + string(content))},
|
||||||
|
},
|
||||||
|
Attachment: attachments,
|
||||||
|
// Tag: tags,
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) articleToObject(articleEntry *entrytypes.Article) vocab.Object {
|
||||||
|
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
content := articleEntry.Content()
|
||||||
|
|
||||||
|
image := vocab.Article{
|
||||||
|
ID: vocab.ID(articleEntry.FullUrl(siteCfg)),
|
||||||
|
Type: "Article",
|
||||||
|
To: vocab.ItemCollection{
|
||||||
|
vocab.PublicNS,
|
||||||
|
vocab.IRI(svc.FollowersUrl()),
|
||||||
|
},
|
||||||
|
Published: *articleEntry.PublishedAt(),
|
||||||
|
AttributedTo: vocab.ID(svc.ActorUrl()),
|
||||||
|
Name: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(articleEntry.Title())},
|
||||||
|
},
|
||||||
|
Content: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(string(content))},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *ActivityPubService) recipeToObject(recipeEntry *entrytypes.Recipe) vocab.Object {
|
||||||
|
siteCfg, _ := svc.siteConfigServcie.GetSiteConfig()
|
||||||
|
content := recipeEntry.Content()
|
||||||
|
|
||||||
|
image := vocab.Article{
|
||||||
|
ID: vocab.ID(recipeEntry.FullUrl(siteCfg)),
|
||||||
|
Type: "Article",
|
||||||
|
To: vocab.ItemCollection{
|
||||||
|
vocab.PublicNS,
|
||||||
|
vocab.IRI(svc.FollowersUrl()),
|
||||||
|
},
|
||||||
|
Published: *recipeEntry.PublishedAt(),
|
||||||
|
AttributedTo: vocab.ID(svc.ActorUrl()),
|
||||||
|
Name: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(recipeEntry.Title())},
|
||||||
|
},
|
||||||
|
Content: vocab.NaturalLanguageValues{
|
||||||
|
{Value: vocab.Content(string(content))},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app/repository"
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -12,12 +11,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthorService struct {
|
type AuthorService struct {
|
||||||
repo repository.AuthorRepository
|
repo repository.AuthorRepository
|
||||||
siteConfigRepo repository.ConfigRepository
|
siteConfigService *SiteConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthorService(repo repository.AuthorRepository, siteConfigRepo repository.ConfigRepository) *AuthorService {
|
func NewAuthorService(repo repository.AuthorRepository, siteConfigService *SiteConfigService) *AuthorService {
|
||||||
return &AuthorService{repo: repo, siteConfigRepo: siteConfigRepo}
|
return &AuthorService{repo: repo, siteConfigService: siteConfigService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashPassword(password string) (string, error) {
|
func hashPassword(password string) (string, error) {
|
||||||
|
@ -65,14 +64,13 @@ func (s *AuthorService) Authenticate(name string, password string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthorService) getSecretKey() string {
|
func (s *AuthorService) getSecretKey() string {
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := s.siteConfigService.GetSiteConfig()
|
||||||
err := s.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if siteConfig.Secret == "" {
|
if siteConfig.Secret == "" {
|
||||||
siteConfig.Secret = RandStringRunes(64)
|
siteConfig.Secret = RandStringRunes(64)
|
||||||
err = s.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
err = s.siteConfigService.UpdateSiteConfig(siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ func (c *testConfigRepo) Update(name string, result interface{}) error {
|
||||||
func getAutherService() *app.AuthorService {
|
func getAutherService() *app.AuthorService {
|
||||||
db := test.NewMockDb()
|
db := test.NewMockDb()
|
||||||
authorRepo := infra.NewDefaultAuthorRepo(db)
|
authorRepo := infra.NewDefaultAuthorRepo(db)
|
||||||
authorService := app.NewAuthorService(authorRepo, &testConfigRepo{})
|
authorService := app.NewAuthorService(authorRepo, app.NewSiteConfigService(&testConfigRepo{}))
|
||||||
return authorService
|
return authorService
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,11 @@ func (s *BinaryService) FindById(id string) (*model.BinaryFile, error) {
|
||||||
return s.repo.FindById(id)
|
return s.repo.FindById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BinaryService) ListIds() ([]string, error) {
|
// ListIds list all ids of binary files
|
||||||
return s.repo.ListIds()
|
// if filter is not empty, the list will be filter to all ids which include the filter filter substring
|
||||||
|
// ids and filters are compared in lower case
|
||||||
|
func (s *BinaryService) ListIds(filter string) ([]string, error) {
|
||||||
|
return s.repo.ListIds(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BinaryService) Delete(binary *model.BinaryFile) error {
|
func (s *BinaryService) Delete(binary *model.BinaryFile) error {
|
||||||
|
|
|
@ -3,8 +3,7 @@ package app
|
||||||
import "owl-blogs/domain/model"
|
import "owl-blogs/domain/model"
|
||||||
|
|
||||||
type AppConfig interface {
|
type AppConfig interface {
|
||||||
Form(binSvc model.BinaryStorageInterface) string
|
model.Formable
|
||||||
ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (AppConfig, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigRegister struct {
|
type ConfigRegister struct {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app/repository"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
|
@ -9,17 +10,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type EntryService struct {
|
type EntryService struct {
|
||||||
EntryRepository repository.EntryRepository
|
EntryRepository repository.EntryRepository
|
||||||
Bus *EventBus
|
siteConfigServcie *SiteConfigService
|
||||||
|
Bus *EventBus
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEntryService(
|
func NewEntryService(
|
||||||
entryRepository repository.EntryRepository,
|
entryRepository repository.EntryRepository,
|
||||||
|
siteConfigServcie *SiteConfigService,
|
||||||
bus *EventBus,
|
bus *EventBus,
|
||||||
) *EntryService {
|
) *EntryService {
|
||||||
return &EntryService{
|
return &EntryService{
|
||||||
EntryRepository: entryRepository,
|
EntryRepository: entryRepository,
|
||||||
Bus: bus,
|
siteConfigServcie: siteConfigServcie,
|
||||||
|
Bus: bus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +48,13 @@ func (s *EntryService) Create(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Bus.NotifyCreated(entry)
|
// only notify if the publishing date is set
|
||||||
|
// otherwise this is a draft.
|
||||||
|
// listeners might publish the entry to other services/platforms
|
||||||
|
// this should only happen for publshed content
|
||||||
|
if entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() {
|
||||||
|
s.Bus.NotifyCreated(entry)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +63,13 @@ func (s *EntryService) Update(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Bus.NotifyUpdated(entry)
|
// only notify if the publishing date is set
|
||||||
|
// otherwise this is a draft.
|
||||||
|
// listeners might publish the entry to other services/platforms
|
||||||
|
// this should only happen for publshed content
|
||||||
|
if entry.PublishedAt() != nil && !entry.PublishedAt().IsZero() {
|
||||||
|
s.Bus.NotifyUpdated(entry)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +78,9 @@ func (s *EntryService) Delete(entry model.Entry) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// deletes should always be notfied
|
||||||
|
// a published entry might be converted to a draft before deletion
|
||||||
|
// omitting the deletion in this case would prevent deletion on other platforms
|
||||||
s.Bus.NotifyDeleted(entry)
|
s.Bus.NotifyDeleted(entry)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -70,6 +89,19 @@ func (s *EntryService) FindById(id string) (model.Entry, error) {
|
||||||
return s.EntryRepository.FindById(id)
|
return s.EntryRepository.FindById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *EntryService) FindByUrl(url string) (model.Entry, error) {
|
||||||
|
cfg, _ := s.siteConfigServcie.GetSiteConfig()
|
||||||
|
if !strings.HasPrefix(url, cfg.FullUrl) {
|
||||||
|
return nil, errors.New("url does not belong to blog")
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(url, "/") {
|
||||||
|
url = url[:len(url)-1]
|
||||||
|
}
|
||||||
|
parts := strings.Split(url, "/")
|
||||||
|
id := parts[len(parts)-1]
|
||||||
|
return s.FindById(id)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *EntryService) filterEntries(entries []model.Entry, published bool, drafts bool) []model.Entry {
|
func (s *EntryService) filterEntries(entries []model.Entry, published bool, drafts bool) []model.Entry {
|
||||||
filteredEntries := make([]model.Entry, 0)
|
filteredEntries := make([]model.Entry, 0)
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
|
|
|
@ -14,7 +14,9 @@ func setupService() *app.EntryService {
|
||||||
register := app.NewEntryTypeRegistry()
|
register := app.NewEntryTypeRegistry()
|
||||||
register.Register(&test.MockEntry{})
|
register.Register(&test.MockEntry{})
|
||||||
repo := infra.NewEntryRepository(db, register)
|
repo := infra.NewEntryRepository(db, register)
|
||||||
service := app.NewEntryService(repo, app.NewEventBus())
|
cfgRepo := infra.NewConfigRepo(db)
|
||||||
|
cfgService := app.NewSiteConfigService(cfgRepo)
|
||||||
|
service := app.NewEntryService(repo, cfgService, app.NewEventBus())
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InteractionService struct {
|
||||||
|
repo repository.InteractionRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InteractionService) ListInteractions() ([]model.Interaction, error) {
|
||||||
|
return s.repo.ListAllInteractions()
|
||||||
|
}
|
|
@ -19,7 +19,10 @@ type BinaryRepository interface {
|
||||||
Create(name string, data []byte, entry model.Entry) (*model.BinaryFile, error)
|
Create(name string, data []byte, entry model.Entry) (*model.BinaryFile, error)
|
||||||
FindById(id string) (*model.BinaryFile, error)
|
FindById(id string) (*model.BinaryFile, error)
|
||||||
FindByNameForEntry(name string, entry model.Entry) (*model.BinaryFile, error)
|
FindByNameForEntry(name string, entry model.Entry) (*model.BinaryFile, error)
|
||||||
ListIds() ([]string, error)
|
// ListIds list all ids of binary files
|
||||||
|
// if filter is not empty, the list will be filter to all ids which include the filter filter substring
|
||||||
|
// ids and filters are compared in lower case
|
||||||
|
ListIds(filter string) ([]string, error)
|
||||||
Delete(binary *model.BinaryFile) error
|
Delete(binary *model.BinaryFile) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,4 +48,12 @@ type InteractionRepository interface {
|
||||||
Delete(interaction model.Interaction) error
|
Delete(interaction model.Interaction) error
|
||||||
FindById(id string) (model.Interaction, error)
|
FindById(id string) (model.Interaction, error)
|
||||||
FindAll(entryId string) ([]model.Interaction, error)
|
FindAll(entryId string) ([]model.Interaction, error)
|
||||||
|
// ListAllInteractions lists all interactions, sorted by creation date (descending)
|
||||||
|
ListAllInteractions() ([]model.Interaction, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FollowerRepository interface {
|
||||||
|
Add(follower string) error
|
||||||
|
Remove(follower string) error
|
||||||
|
All() ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/config"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SiteConfigService is a service to retrieve and store the site config
|
||||||
|
// Even though the site config is a standard config, it is handle by an extra service
|
||||||
|
// as it is used in many places.
|
||||||
|
// The SiteConfig contains global settings require by multiple parts of the app
|
||||||
|
type SiteConfigService struct {
|
||||||
|
repo repository.ConfigRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSiteConfigService(repo repository.ConfigRepository) *SiteConfigService {
|
||||||
|
return &SiteConfigService{
|
||||||
|
repo: repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *SiteConfigService) defaultConfig() model.SiteConfig {
|
||||||
|
return model.SiteConfig{
|
||||||
|
Title: "My Owl-Blog",
|
||||||
|
SubTitle: "A freshly created blog",
|
||||||
|
PrimaryColor: "#d37f12",
|
||||||
|
AuthorName: "",
|
||||||
|
Me: []model.MeLinks{},
|
||||||
|
Lists: []model.EntryList{},
|
||||||
|
PrimaryListInclude: []string{},
|
||||||
|
HeaderMenu: []model.MenuItem{},
|
||||||
|
FooterMenu: []model.MenuItem{},
|
||||||
|
Secret: "",
|
||||||
|
AvatarUrl: "",
|
||||||
|
FullUrl: "http://localhost:3000",
|
||||||
|
HtmlHeadExtra: "",
|
||||||
|
FooterExtra: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *SiteConfigService) GetSiteConfig() (model.SiteConfig, error) {
|
||||||
|
siteConfig := model.SiteConfig{}
|
||||||
|
err := svc.repo.Get(config.SITE_CONFIG, &siteConfig)
|
||||||
|
if err != nil {
|
||||||
|
println("ERROR IN SITE CONFIG")
|
||||||
|
return model.SiteConfig{}, err
|
||||||
|
}
|
||||||
|
if reflect.ValueOf(siteConfig).IsZero() {
|
||||||
|
return svc.defaultConfig(), nil
|
||||||
|
}
|
||||||
|
return siteConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *SiteConfigService) UpdateSiteConfig(cfg model.SiteConfig) error {
|
||||||
|
return svc.repo.Update(config.SITE_CONFIG, cfg)
|
||||||
|
}
|
|
@ -5,28 +5,27 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"owl-blogs/app/owlhttp"
|
"owl-blogs/app/owlhttp"
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app/repository"
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/interactions"
|
"owl-blogs/interactions"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebmentionService struct {
|
type WebmentionService struct {
|
||||||
ConfigRepo repository.ConfigRepository
|
siteConfigService *SiteConfigService
|
||||||
InteractionRepository repository.InteractionRepository
|
InteractionRepository repository.InteractionRepository
|
||||||
EntryRepository repository.EntryRepository
|
EntryRepository repository.EntryRepository
|
||||||
Http owlhttp.HttpClient
|
Http owlhttp.HttpClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebmentionService(
|
func NewWebmentionService(
|
||||||
configRepo repository.ConfigRepository,
|
siteConfigService *SiteConfigService,
|
||||||
interactionRepository repository.InteractionRepository,
|
interactionRepository repository.InteractionRepository,
|
||||||
entryRepository repository.EntryRepository,
|
entryRepository repository.EntryRepository,
|
||||||
http owlhttp.HttpClient,
|
http owlhttp.HttpClient,
|
||||||
bus *EventBus,
|
bus *EventBus,
|
||||||
) *WebmentionService {
|
) *WebmentionService {
|
||||||
svc := &WebmentionService{
|
svc := &WebmentionService{
|
||||||
ConfigRepo: configRepo,
|
siteConfigService: siteConfigService,
|
||||||
InteractionRepository: interactionRepository,
|
InteractionRepository: interactionRepository,
|
||||||
EntryRepository: entryRepository,
|
EntryRepository: entryRepository,
|
||||||
Http: http,
|
Http: http,
|
||||||
|
@ -104,8 +103,7 @@ func (s *WebmentionService) ScanForLinks(entry model.Entry) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WebmentionService) FullEntryUrl(entry model.Entry) string {
|
func (s *WebmentionService) FullEntryUrl(entry model.Entry) string {
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, _ := s.siteConfigService.GetSiteConfig()
|
||||||
s.ConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
url, _ := url.JoinPath(
|
url, _ := url.JoinPath(
|
||||||
siteConfig.FullUrl,
|
siteConfig.FullUrl,
|
||||||
|
|
|
@ -62,7 +62,7 @@ func getWebmentionService() *app.WebmentionService {
|
||||||
|
|
||||||
http := infra.OwlHttpClient{}
|
http := infra.OwlHttpClient{}
|
||||||
return app.NewWebmentionService(
|
return app.NewWebmentionService(
|
||||||
configRepo, interactionRepo, entryRepo, &http, bus,
|
app.NewSiteConfigService(configRepo), interactionRepo, entryRepo, &http, bus,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,6 @@ var importCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
v2Config.Title = v1Config.Title
|
v2Config.Title = v1Config.Title
|
||||||
v2Config.SubTitle = v1Config.SubTitle
|
v2Config.SubTitle = v1Config.SubTitle
|
||||||
v2Config.HeaderColor = v1Config.HeaderColor
|
|
||||||
v2Config.AuthorName = v1Config.AuthorName
|
v2Config.AuthorName = v1Config.AuthorName
|
||||||
v2Config.Me = mes
|
v2Config.Me = mes
|
||||||
v2Config.Lists = lists
|
v2Config.Lists = lists
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"owl-blogs/infra"
|
"owl-blogs/infra"
|
||||||
"owl-blogs/interactions"
|
"owl-blogs/interactions"
|
||||||
"owl-blogs/plugings"
|
"owl-blogs/plugings"
|
||||||
|
"owl-blogs/render"
|
||||||
"owl-blogs/web"
|
"owl-blogs/web"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -40,6 +41,8 @@ func App(db infra.Database) *web.WebApp {
|
||||||
|
|
||||||
interactionRegister := app.NewInteractionTypeRegistry()
|
interactionRegister := app.NewInteractionTypeRegistry()
|
||||||
interactionRegister.Register(&interactions.Webmention{})
|
interactionRegister.Register(&interactions.Webmention{})
|
||||||
|
interactionRegister.Register(&interactions.Like{})
|
||||||
|
interactionRegister.Register(&interactions.Repost{})
|
||||||
|
|
||||||
configRegister := app.NewConfigRegister()
|
configRegister := app.NewConfigRegister()
|
||||||
|
|
||||||
|
@ -49,6 +52,7 @@ func App(db infra.Database) *web.WebApp {
|
||||||
authorRepo := infra.NewDefaultAuthorRepo(db)
|
authorRepo := infra.NewDefaultAuthorRepo(db)
|
||||||
configRepo := infra.NewConfigRepo(db)
|
configRepo := infra.NewConfigRepo(db)
|
||||||
interactionRepo := infra.NewInteractionRepo(db, interactionRegister)
|
interactionRepo := infra.NewInteractionRepo(db, interactionRegister)
|
||||||
|
followersRepo := infra.NewFollowerRepository(db)
|
||||||
|
|
||||||
// Create External Services
|
// Create External Services
|
||||||
httpClient := &infra.OwlHttpClient{}
|
httpClient := &infra.OwlHttpClient{}
|
||||||
|
@ -57,12 +61,21 @@ func App(db infra.Database) *web.WebApp {
|
||||||
eventBus := app.NewEventBus()
|
eventBus := app.NewEventBus()
|
||||||
|
|
||||||
// Create Services
|
// Create Services
|
||||||
entryService := app.NewEntryService(entryRepo, eventBus)
|
siteConfigService := app.NewSiteConfigService(configRepo)
|
||||||
|
entryService := app.NewEntryService(entryRepo, siteConfigService, eventBus)
|
||||||
binaryService := app.NewBinaryFileService(binRepo)
|
binaryService := app.NewBinaryFileService(binRepo)
|
||||||
authorService := app.NewAuthorService(authorRepo, configRepo)
|
authorService := app.NewAuthorService(authorRepo, siteConfigService)
|
||||||
webmentionService := app.NewWebmentionService(
|
webmentionService := app.NewWebmentionService(
|
||||||
configRepo, interactionRepo, entryRepo, httpClient, eventBus,
|
siteConfigService, interactionRepo, entryRepo, httpClient, eventBus,
|
||||||
)
|
)
|
||||||
|
apService := app.NewActivityPubService(
|
||||||
|
followersRepo, configRepo, interactionRepo,
|
||||||
|
entryService, siteConfigService, binaryService,
|
||||||
|
eventBus,
|
||||||
|
)
|
||||||
|
|
||||||
|
// setup render functions
|
||||||
|
render.SiteConfigService = siteConfigService
|
||||||
|
|
||||||
// plugins
|
// plugins
|
||||||
plugings.NewEcho(eventBus)
|
plugings.NewEcho(eventBus)
|
||||||
|
@ -74,7 +87,8 @@ func App(db infra.Database) *web.WebApp {
|
||||||
return web.NewWebApp(
|
return web.NewWebApp(
|
||||||
entryService, entryRegister, binaryService,
|
entryService, entryRegister, binaryService,
|
||||||
authorService, configRepo, configRegister,
|
authorService, configRepo, configRegister,
|
||||||
webmentionService, interactionRepo,
|
siteConfigService, webmentionService, interactionRepo,
|
||||||
|
apService,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ package config
|
||||||
import "os"
|
import "os"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SITE_CONFIG = "site_config"
|
SITE_CONFIG = "site_config"
|
||||||
|
ACT_PUB_CONF_NAME = "activity_pub"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config interface {
|
type Config interface {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,16 +16,18 @@ type Entry interface {
|
||||||
|
|
||||||
// Optional: can return empty string
|
// Optional: can return empty string
|
||||||
Title() string
|
Title() string
|
||||||
|
ImageUrl() string
|
||||||
|
|
||||||
SetID(id string)
|
SetID(id string)
|
||||||
SetPublishedAt(publishedAt *time.Time)
|
SetPublishedAt(publishedAt *time.Time)
|
||||||
SetMetaData(metaData EntryMetaData)
|
SetMetaData(metaData EntryMetaData)
|
||||||
SetAuthorId(authorId string)
|
SetAuthorId(authorId string)
|
||||||
|
|
||||||
|
FullUrl(cfg SiteConfig) string
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntryMetaData interface {
|
type EntryMetaData interface {
|
||||||
Form(binSvc BinaryStorageInterface) string
|
Formable
|
||||||
ParseFormData(data HttpFormData, binSvc BinaryStorageInterface) (EntryMetaData, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntryBase struct {
|
type EntryBase struct {
|
||||||
|
@ -41,6 +44,10 @@ func (e *EntryBase) PublishedAt() *time.Time {
|
||||||
return e.publishedAt
|
return e.publishedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *EntryBase) ImageUrl() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (e *EntryBase) SetID(id string) {
|
func (e *EntryBase) SetID(id string) {
|
||||||
e.id = id
|
e.id = id
|
||||||
}
|
}
|
||||||
|
@ -56,3 +63,8 @@ func (e *EntryBase) AuthorId() string {
|
||||||
func (e *EntryBase) SetAuthorId(authorId string) {
|
func (e *EntryBase) SetAuthorId(authorId string) {
|
||||||
e.authorId = authorId
|
e.authorId = authorId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *EntryBase) FullUrl(cfg SiteConfig) string {
|
||||||
|
u, _ := url.JoinPath(cfg.FullUrl, "/posts/", e.ID(), "/")
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
package model_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEntryFullUrl(t *testing.T) {
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
Id string
|
||||||
|
Url string
|
||||||
|
Want string
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []testCase{
|
||||||
|
{Id: "foobar", Url: "https://example.com", Want: "https://example.com/posts/foobar/"},
|
||||||
|
{Id: "foobar", Url: "https://example.com/", Want: "https://example.com/posts/foobar/"},
|
||||||
|
{Id: "foobar", Url: "http://example.com", Want: "http://example.com/posts/foobar/"},
|
||||||
|
{Id: "foobar", Url: "http://example.com/", Want: "http://example.com/posts/foobar/"},
|
||||||
|
{Id: "bi-bar-buz", Url: "https://example.com", Want: "https://example.com/posts/bi-bar-buz/"},
|
||||||
|
{Id: "foobar", Url: "https://example.com/lol/", Want: "https://example.com/lol/posts/foobar/"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
e := model.EntryBase{}
|
||||||
|
e.SetID(test.Id)
|
||||||
|
cfg := model.SiteConfig{FullUrl: test.Url}
|
||||||
|
require.Equal(t, e.FullUrl(cfg), test.Want)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,6 +2,11 @@ package model
|
||||||
|
|
||||||
import "mime/multipart"
|
import "mime/multipart"
|
||||||
|
|
||||||
|
type Formable interface {
|
||||||
|
Form(binSvc BinaryStorageInterface) string
|
||||||
|
ParseFormData(data HttpFormData, binSvc BinaryStorageInterface) error
|
||||||
|
}
|
||||||
|
|
||||||
type HttpFormData interface {
|
type HttpFormData interface {
|
||||||
// FormFile returns the first file by key from a MultipartForm.
|
// FormFile returns the first file by key from a MultipartForm.
|
||||||
FormFile(key string) (*multipart.FileHeader, error)
|
FormFile(key string) (*multipart.FileHeader, error)
|
||||||
|
|
|
@ -22,7 +22,6 @@ type MenuItem struct {
|
||||||
type SiteConfig struct {
|
type SiteConfig struct {
|
||||||
Title string
|
Title string
|
||||||
SubTitle string
|
SubTitle string
|
||||||
HeaderColor string
|
|
||||||
PrimaryColor string
|
PrimaryColor string
|
||||||
AuthorName string
|
AuthorName string
|
||||||
Me []MeLinks
|
Me []MeLinks
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type SiteConfigInterface interface {
|
||||||
|
GetSiteConfig() (SiteConfig, error)
|
||||||
|
UpdateSiteConfig(cfg SiteConfig) error
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import pytest
|
||||||
|
from requests import Session
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from tests.fixtures import ACCT_NAME
|
||||||
|
|
||||||
|
|
||||||
|
class LiveServerSession(Session):
|
||||||
|
def __init__(self, base_url=None):
|
||||||
|
super().__init__()
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
def request(self, method, url, *args, **kwargs):
|
||||||
|
joined_url = urljoin(self.base_url, url)
|
||||||
|
return super().request(method, joined_url, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return LiveServerSession("http://localhost:3000")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def actor_url(client):
|
||||||
|
resp = client.get(f"/.well-known/webfinger?resource={ACCT_NAME}")
|
||||||
|
data = resp.json()
|
||||||
|
self_link = [x for x in data["links"] if x["rel"] == "self"][0]
|
||||||
|
return self_link["href"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def actor(client, actor_url):
|
||||||
|
resp = client.get(actor_url, headers={"Content-Type": "application/activity+json"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def inbox_url(actor):
|
||||||
|
return actor["inbox"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def outbox_url(actor):
|
||||||
|
return actor["outbox"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def followers_url(actor):
|
||||||
|
return actor["followers"]
|
|
@ -0,0 +1,12 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
command: web
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
mock_masto:
|
||||||
|
build: mock_masto
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
|
@ -0,0 +1,24 @@
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: Dockerfile.test
|
||||||
|
volumes:
|
||||||
|
- ../app:/go/owl/app
|
||||||
|
- ../assets:/go/owl/assets
|
||||||
|
- ../cmd:/go/owl/cmd
|
||||||
|
- ../config:/go/owl/config
|
||||||
|
- ../domain:/go/owl/domain
|
||||||
|
- ../entry_types:/go/owl/entry_types
|
||||||
|
- ../importer:/go/owl/importer
|
||||||
|
- ../infra:/go/owl/infra
|
||||||
|
- ../interactions:/go/owl/interactions
|
||||||
|
- ../plugings:/go/owl/plugings
|
||||||
|
- ../render:/go/owl/render
|
||||||
|
- ../web:/go/owl/web
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
mock_masto:
|
||||||
|
build: mock_masto
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
|
@ -0,0 +1,6 @@
|
||||||
|
FROM python:3.11
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
CMD [ "python", "main.py" ]
|
|
@ -0,0 +1,214 @@
|
||||||
|
import json
|
||||||
|
from flask import Flask, request
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
PRIV_KEY_PEM = """-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCni8P4bvkC/3Sx
|
||||||
|
NTrDw1qw0vWtJKZMsyJ3Mcs4+1apoVqOQhujUqGqFSiRT7Vmc7OEhB0vikdiTkCk
|
||||||
|
1PcoTd/rOa/0WbG8385JcLzaJfTIG+rrRtHwZ1TwxwBju43jpGEZjpbA0dqoXMkr
|
||||||
|
J1MyD7aPLoAiVe0ikw2czSZumv4ncemOtk0VG3b2TnIxo3CMKtUOWu8xT08MMIuo
|
||||||
|
3cZRnpI6Xr/ULzvjv8e3EjIpwRJqMPECtGsfdcjFmR0yFIrjrlmkZTiW31z/Dk7i
|
||||||
|
xRGD0ADy3WEQ3lA4l3mNZeyG4S0Wi4iYe9/wegESMZcakLoME7ks+KNS388Mdbcd
|
||||||
|
DKy9NmWvAgMBAAECggEABLQAA0hHhdWv6+Lc9xkpFuTvxTV4fuyvCf4u1eGlnstg
|
||||||
|
ZF/nW1/6w8XQ8WCgbJ4mKuZz1J14FYKxfoRaj8S9MA2Ff+wd+M77gRpAuDWajRzO
|
||||||
|
LQk8OW2yd7POXKkAzvln9F9eofkCFKR4zSpPGTenCJaQkuYrQEOKfUf7oofdRzQi
|
||||||
|
w9kmp3wAxM/EseHZpknYDCgDQV7MDQAaMD7kbynL2WfXPxebktwpRlKUwgtGrevj
|
||||||
|
gagQL8J/GX6wO3ymw9sln4BhlI2+3LuiMXQdQc1tamkXFCguCuOZCu/2VRdCHmiS
|
||||||
|
nnpu+FMspBHbvxO+RXo3Cu/S6jjJgoQxD2WZTE0gqQKBgQDM6AQdqBYjISdkI9Gl
|
||||||
|
6ZLLjwZRJSYpopujtX7pun61l9kUwQevaR2Z39rMWxX62DD6arazi/ygIUBw6Kgp
|
||||||
|
s/qBEb29ec+0cESdC8aJYb3dGvDzh/8C05p7ozxj8JZQcxq5W5jql/BELlSsUONO
|
||||||
|
jfqQv8RGZNSkD9uy6TxOr4eWIwKBgQDRUuO/XRDLt8Mp10mTshxTznSQ3gAJYKeG
|
||||||
|
0WfEC3kPEukHBQb8huqFcQDiQ71oBWuEdOQWgT3aBS6L+nIMyZMT5u+BejQm7/E5
|
||||||
|
pMM+z0VRpfFSsIrCvU8yKam0aemQGlKQAfhTct1gCg+wKnYsSQMlNHKWEfDbw9I/
|
||||||
|
cns/IN+dBQKBgQC6/Of0oFVDTZgC3GUPAO3C8QwUtM/0or1hUdk1Nck3shCZzeVT
|
||||||
|
f5tRtmSWpHCUbwGTJBsCEjdBcda6srXzCJkLe8Moy6ZtxR34KqzM5fM7eMB1nJ9s
|
||||||
|
Vunc9gPAN+cUF1ZF3H7ZZjoOHjGK5m3oW8xSl41np9Acv5P/2rP8Ilaa/QKBgQDJ
|
||||||
|
YwISfitGk8mEW8hB/L4cMykapztJyl/i6Vz31EHoKr1fL4sFMZg4QfwjtCBqD6zd
|
||||||
|
hshajoU/WHTr30wS2WxTXX9YBoZeX8KpPsdJioiagRioAYm+yfuDu2m2VZ+MMIb2
|
||||||
|
Xa7YOk6Zs5RcXL3M5YHNLaSAlUoxZTjGKhJBLhN1MQKBgQCbo3ngBl7Qjjx4WJ93
|
||||||
|
2WEEKvSDCv69eecNQDuKWKEiFqBN23LheNrN8DXMWFTtE4miY106dzQ0dUMh418x
|
||||||
|
K98rXSX3VvY4w48AznvPMKVLqesFjcvwnBdvk/NqXod20CMSpOEVj6W/nGoTBQt2
|
||||||
|
0PuW3IUym9KvO0WX9E+1Qw8mbw==
|
||||||
|
-----END PRIVATE KEY-----"""
|
||||||
|
|
||||||
|
PUB_KEY_PEM = """-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp4vD+G75Av90sTU6w8Na
|
||||||
|
sNL1rSSmTLMidzHLOPtWqaFajkIbo1KhqhUokU+1ZnOzhIQdL4pHYk5ApNT3KE3f
|
||||||
|
6zmv9FmxvN/OSXC82iX0yBvq60bR8GdU8McAY7uN46RhGY6WwNHaqFzJKydTMg+2
|
||||||
|
jy6AIlXtIpMNnM0mbpr+J3HpjrZNFRt29k5yMaNwjCrVDlrvMU9PDDCLqN3GUZ6S
|
||||||
|
Ol6/1C8747/HtxIyKcESajDxArRrH3XIxZkdMhSK465ZpGU4lt9c/w5O4sURg9AA
|
||||||
|
8t1hEN5QOJd5jWXshuEtFouImHvf8HoBEjGXGpC6DBO5LPijUt/PDHW3HQysvTZl
|
||||||
|
rwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----"""
|
||||||
|
|
||||||
|
INBOX = []
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/.well-known/webfinger")
|
||||||
|
def webfinger():
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"subject": "acct:h4kor@mock_masto",
|
||||||
|
"aliases": [
|
||||||
|
"http://mock_masto:8000/@h4kor",
|
||||||
|
"http://mock_masto:8000/users/h4kor",
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.net/rel/profile-page",
|
||||||
|
"type": "text/html",
|
||||||
|
"href": "http://mock_masto:8000/@h4kor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "self",
|
||||||
|
"type": "application/activity+json",
|
||||||
|
"href": "http://mock_masto:8000/users/h4kor",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||||
|
"template": "http://mock_masto:8000/authorize_interaction?uri={uri}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.net/rel/avatar",
|
||||||
|
"type": "image/png",
|
||||||
|
"href": "http://assets.mock_masto/accounts/avatars/000/082/056/original/a4be9944e3b03229.png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/users/h4kor")
|
||||||
|
def actor():
|
||||||
|
print("request to actor")
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"http://www.w3.org/ns/activitystreams",
|
||||||
|
"http://w3id.org/security/v1",
|
||||||
|
{
|
||||||
|
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"featured": {"@id": "toot:featured", "@type": "@id"},
|
||||||
|
"featuredTags": {"@id": "toot:featuredTags", "@type": "@id"},
|
||||||
|
"alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"},
|
||||||
|
"movedTo": {"@id": "as:movedTo", "@type": "@id"},
|
||||||
|
"schema": "http://schema.org#",
|
||||||
|
"PropertyValue": "schema:PropertyValue",
|
||||||
|
"value": "schema:value",
|
||||||
|
"discoverable": "toot:discoverable",
|
||||||
|
"Device": "toot:Device",
|
||||||
|
"Ed25519Signature": "toot:Ed25519Signature",
|
||||||
|
"Ed25519Key": "toot:Ed25519Key",
|
||||||
|
"Curve25519Key": "toot:Curve25519Key",
|
||||||
|
"EncryptedMessage": "toot:EncryptedMessage",
|
||||||
|
"publicKeyBase64": "toot:publicKeyBase64",
|
||||||
|
"deviceId": "toot:deviceId",
|
||||||
|
"claim": {"@type": "@id", "@id": "toot:claim"},
|
||||||
|
"fingerprintKey": {"@type": "@id", "@id": "toot:fingerprintKey"},
|
||||||
|
"identityKey": {"@type": "@id", "@id": "toot:identityKey"},
|
||||||
|
"devices": {"@type": "@id", "@id": "toot:devices"},
|
||||||
|
"messageFranking": "toot:messageFranking",
|
||||||
|
"messageType": "toot:messageType",
|
||||||
|
"cipherText": "toot:cipherText",
|
||||||
|
"suspended": "toot:suspended",
|
||||||
|
"memorial": "toot:memorial",
|
||||||
|
"indexable": "toot:indexable",
|
||||||
|
"Hashtag": "as:Hashtag",
|
||||||
|
"focalPoint": {"@container": "@list", "@id": "toot:focalPoint"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"id": "http://mock_masto:8000/users/h4kor",
|
||||||
|
"type": "Person",
|
||||||
|
"following": "http://mock_masto:8000/users/h4kor/following",
|
||||||
|
"followers": "http://mock_masto:8000/users/h4kor/followers",
|
||||||
|
"inbox": "http://mock_masto:8000/users/h4kor/inbox",
|
||||||
|
"outbox": "http://mock_masto:8000/users/h4kor/outbox",
|
||||||
|
"featured": "http://mock_masto:8000/users/h4kor/collections/featured",
|
||||||
|
"featuredTags": "http://mock_masto:8000/users/h4kor/collections/tags",
|
||||||
|
"preferredUsername": "h4kor",
|
||||||
|
"name": "Niko",
|
||||||
|
"summary": '<p>Teaching computers to do things with arguable efficiency.</p><p>he/him</p><p><a href="http://mock_masto:8000/tags/vegan" class="mention hashtag" rel="tag">#<span>vegan</span></a> <a href="http://mock_masto:8000/tags/cooking" class="mention hashtag" rel="tag">#<span>cooking</span></a> <a href="http://mock_masto:8000/tags/programming" class="mention hashtag" rel="tag">#<span>programming</span></a> <a href="http://mock_masto:8000/tags/politics" class="mention hashtag" rel="tag">#<span>politics</span></a> <a href="http://mock_masto:8000/tags/climate" class="mention hashtag" rel="tag">#<span>climate</span></a></p>',
|
||||||
|
"url": "http://mock_masto:8000/@h4kor",
|
||||||
|
"manuallyApprovesFollowers": False,
|
||||||
|
"discoverable": True,
|
||||||
|
"indexable": False,
|
||||||
|
"published": "2018-08-16T00:00:00Z",
|
||||||
|
"memorial": False,
|
||||||
|
"devices": "http://mock_masto:8000/users/h4kor/collections/devices",
|
||||||
|
"publicKey": {
|
||||||
|
"id": "http://mock_masto:8000/users/h4kor#main-key",
|
||||||
|
"owner": "http://mock_masto:8000/users/h4kor",
|
||||||
|
"publicKeyPem": PUB_KEY_PEM,
|
||||||
|
},
|
||||||
|
"tag": [
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto:8000/tags/politics",
|
||||||
|
"name": "#politics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto:8000/tags/climate",
|
||||||
|
"name": "#climate",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto:8000/tags/vegan",
|
||||||
|
"name": "#vegan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto:8000/tags/programming",
|
||||||
|
"name": "#programming",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Hashtag",
|
||||||
|
"href": "http://mock_masto:8000/tags/cooking",
|
||||||
|
"name": "#cooking",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"attachment": [
|
||||||
|
{
|
||||||
|
"type": "PropertyValue",
|
||||||
|
"name": "Me",
|
||||||
|
"value": '<a href="http://rerere.org" target="_blank" rel="nofollow noopener noreferrer me" translate="no"><span class="invisible">http://</span><span class="">rerere.org</span><span class="invisible"></span></a>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "PropertyValue",
|
||||||
|
"name": "Blog",
|
||||||
|
"value": '<a href="http://blog.libove.org" target="_blank" rel="nofollow noopener noreferrer me" translate="no"><span class="invisible">http://</span><span class="">blog.libove.org</span><span class="invisible"></span></a>',
|
||||||
|
},
|
||||||
|
{"type": "PropertyValue", "name": "Location", "value": "Münster"},
|
||||||
|
{
|
||||||
|
"type": "PropertyValue",
|
||||||
|
"name": "Current Project",
|
||||||
|
"value": '<a href="http://git.libove.org/h4kor/owl-blogs" target="_blank" rel="nofollow noopener noreferrer me" translate="no"><span class="invisible">http://</span><span class="">git.libove.org/h4kor/owl-blogs</span><span class="invisible"></span></a>',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"endpoints": {"sharedInbox": "http://mock_masto:8000/inbox"},
|
||||||
|
"icon": {
|
||||||
|
"type": "Image",
|
||||||
|
"mediaType": "image/png",
|
||||||
|
"url": "http://assets.mock_masto/accounts/avatars/000/082/056/original/a4be9944e3b03229.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/users/h4kor/inbox", methods=["POST"])
|
||||||
|
def inbox():
|
||||||
|
if request.method == "POST":
|
||||||
|
INBOX.append(json.loads(request.get_data()))
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/msgs")
|
||||||
|
def msgs():
|
||||||
|
return json.dumps(INBOX)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True, host="0.0.0.0", port="8000")
|
|
@ -0,0 +1 @@
|
||||||
|
Flask==3.0.3
|
|
@ -0,0 +1,17 @@
|
||||||
|
certifi==2024.2.2
|
||||||
|
cffi==1.16.0
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
cryptography==42.0.7
|
||||||
|
exceptiongroup==1.2.1
|
||||||
|
http-message-signatures==0.5.0
|
||||||
|
http_sfv==0.9.9
|
||||||
|
idna==3.7
|
||||||
|
iniconfig==2.0.0
|
||||||
|
packaging==24.0
|
||||||
|
pluggy==1.5.0
|
||||||
|
pycparser==2.22
|
||||||
|
pytest==8.2.0
|
||||||
|
requests==2.31.0
|
||||||
|
tomli==2.0.1
|
||||||
|
typing_extensions==4.11.0
|
||||||
|
urllib3==2.2.1
|
|
@ -0,0 +1,102 @@
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import json
|
||||||
|
from time import sleep
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import requests, base64, hashlib
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
|
||||||
|
ACCT_NAME = "acct:blog@localhost:3000"
|
||||||
|
|
||||||
|
PRIV_KEY_PEM = """-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCni8P4bvkC/3Sx
|
||||||
|
NTrDw1qw0vWtJKZMsyJ3Mcs4+1apoVqOQhujUqGqFSiRT7Vmc7OEhB0vikdiTkCk
|
||||||
|
1PcoTd/rOa/0WbG8385JcLzaJfTIG+rrRtHwZ1TwxwBju43jpGEZjpbA0dqoXMkr
|
||||||
|
J1MyD7aPLoAiVe0ikw2czSZumv4ncemOtk0VG3b2TnIxo3CMKtUOWu8xT08MMIuo
|
||||||
|
3cZRnpI6Xr/ULzvjv8e3EjIpwRJqMPECtGsfdcjFmR0yFIrjrlmkZTiW31z/Dk7i
|
||||||
|
xRGD0ADy3WEQ3lA4l3mNZeyG4S0Wi4iYe9/wegESMZcakLoME7ks+KNS388Mdbcd
|
||||||
|
DKy9NmWvAgMBAAECggEABLQAA0hHhdWv6+Lc9xkpFuTvxTV4fuyvCf4u1eGlnstg
|
||||||
|
ZF/nW1/6w8XQ8WCgbJ4mKuZz1J14FYKxfoRaj8S9MA2Ff+wd+M77gRpAuDWajRzO
|
||||||
|
LQk8OW2yd7POXKkAzvln9F9eofkCFKR4zSpPGTenCJaQkuYrQEOKfUf7oofdRzQi
|
||||||
|
w9kmp3wAxM/EseHZpknYDCgDQV7MDQAaMD7kbynL2WfXPxebktwpRlKUwgtGrevj
|
||||||
|
gagQL8J/GX6wO3ymw9sln4BhlI2+3LuiMXQdQc1tamkXFCguCuOZCu/2VRdCHmiS
|
||||||
|
nnpu+FMspBHbvxO+RXo3Cu/S6jjJgoQxD2WZTE0gqQKBgQDM6AQdqBYjISdkI9Gl
|
||||||
|
6ZLLjwZRJSYpopujtX7pun61l9kUwQevaR2Z39rMWxX62DD6arazi/ygIUBw6Kgp
|
||||||
|
s/qBEb29ec+0cESdC8aJYb3dGvDzh/8C05p7ozxj8JZQcxq5W5jql/BELlSsUONO
|
||||||
|
jfqQv8RGZNSkD9uy6TxOr4eWIwKBgQDRUuO/XRDLt8Mp10mTshxTznSQ3gAJYKeG
|
||||||
|
0WfEC3kPEukHBQb8huqFcQDiQ71oBWuEdOQWgT3aBS6L+nIMyZMT5u+BejQm7/E5
|
||||||
|
pMM+z0VRpfFSsIrCvU8yKam0aemQGlKQAfhTct1gCg+wKnYsSQMlNHKWEfDbw9I/
|
||||||
|
cns/IN+dBQKBgQC6/Of0oFVDTZgC3GUPAO3C8QwUtM/0or1hUdk1Nck3shCZzeVT
|
||||||
|
f5tRtmSWpHCUbwGTJBsCEjdBcda6srXzCJkLe8Moy6ZtxR34KqzM5fM7eMB1nJ9s
|
||||||
|
Vunc9gPAN+cUF1ZF3H7ZZjoOHjGK5m3oW8xSl41np9Acv5P/2rP8Ilaa/QKBgQDJ
|
||||||
|
YwISfitGk8mEW8hB/L4cMykapztJyl/i6Vz31EHoKr1fL4sFMZg4QfwjtCBqD6zd
|
||||||
|
hshajoU/WHTr30wS2WxTXX9YBoZeX8KpPsdJioiagRioAYm+yfuDu2m2VZ+MMIb2
|
||||||
|
Xa7YOk6Zs5RcXL3M5YHNLaSAlUoxZTjGKhJBLhN1MQKBgQCbo3ngBl7Qjjx4WJ93
|
||||||
|
2WEEKvSDCv69eecNQDuKWKEiFqBN23LheNrN8DXMWFTtE4miY106dzQ0dUMh418x
|
||||||
|
K98rXSX3VvY4w48AznvPMKVLqesFjcvwnBdvk/NqXod20CMSpOEVj6W/nGoTBQt2
|
||||||
|
0PuW3IUym9KvO0WX9E+1Qw8mbw==
|
||||||
|
-----END PRIVATE KEY-----"""
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_follow(client, inbox_url, actor_url):
|
||||||
|
req = sign(
|
||||||
|
"POST",
|
||||||
|
inbox_url,
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "http://mock_masto:8000/d0b5768b-a15b-4ed6-bc84-84c7e2b57588",
|
||||||
|
"type": "Follow",
|
||||||
|
"actor": "http://mock_masto:8000/users/h4kor",
|
||||||
|
"object": actor_url,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp = requests.Session().send(req)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def sign(method, url, data):
|
||||||
|
|
||||||
|
priv_key = load_pem_private_key(PRIV_KEY_PEM.encode(), None)
|
||||||
|
body = json.dumps(data).encode()
|
||||||
|
body_hash = hashlib.sha256(body).digest()
|
||||||
|
digest = "SHA-256=" + base64.b64encode(body_hash).decode()
|
||||||
|
date = datetime.now(tz=timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||||
|
host = "localhost:3000"
|
||||||
|
target = urlparse(url).path
|
||||||
|
to_sign = f"""(request-target): {method.lower()} {target}
|
||||||
|
host: {host}
|
||||||
|
date: {date}""".encode()
|
||||||
|
sig = priv_key.sign(
|
||||||
|
to_sign,
|
||||||
|
padding.PKCS1v15(),
|
||||||
|
hashes.SHA256(),
|
||||||
|
)
|
||||||
|
sig_str = base64.b64encode(sig).decode()
|
||||||
|
|
||||||
|
request = requests.Request(method, url, data=body)
|
||||||
|
request = request.prepare()
|
||||||
|
request.headers["Content-Digest"] = digest
|
||||||
|
request.headers["Host"] = host
|
||||||
|
request.headers["Date"] = date
|
||||||
|
request.headers["Signature"] = (
|
||||||
|
f'keyId="http://mock_masto:8000/users/h4kor#main-key",headers="(request-target) host date",signature="{sig_str}"'
|
||||||
|
)
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def msg_inc(n):
|
||||||
|
resp = requests.get("http://localhost:8000/msgs")
|
||||||
|
data = resp.json()
|
||||||
|
msgs = len(data)
|
||||||
|
yield
|
||||||
|
sleep(0.2)
|
||||||
|
resp = requests.get("http://localhost:8000/msgs")
|
||||||
|
data = resp.json()
|
||||||
|
assert msgs + n == len(
|
||||||
|
data
|
||||||
|
), f"prev: {msgs}, now: {len(data)}, expected: {msgs + n}"
|
|
@ -0,0 +1,88 @@
|
||||||
|
from pprint import pprint
|
||||||
|
from time import sleep
|
||||||
|
import requests
|
||||||
|
from .fixtures import ensure_follow, msg_inc, sign
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def test_actor(client, actor_url):
|
||||||
|
resp = client.get(actor_url, headers={"Content-Type": "application/activity+json"})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert "id" in data
|
||||||
|
assert "type" in data
|
||||||
|
assert "inbox" in data
|
||||||
|
assert "outbox" in data
|
||||||
|
assert "followers" in data
|
||||||
|
assert "preferredUsername" in data
|
||||||
|
assert "publicKey" in data
|
||||||
|
assert len(data["publicKey"])
|
||||||
|
|
||||||
|
pubKey = data["publicKey"]
|
||||||
|
assert "id" in pubKey
|
||||||
|
assert "owner" in pubKey
|
||||||
|
assert "publicKeyPem" in pubKey
|
||||||
|
|
||||||
|
assert pubKey["owner"] == data["id"]
|
||||||
|
assert pubKey["id"] != data["id"]
|
||||||
|
assert "-----BEGIN RSA PUBLIC KEY-----" in pubKey["publicKeyPem"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_following(client, inbox_url, followers_url, actor_url):
|
||||||
|
with msg_inc(1):
|
||||||
|
req = sign(
|
||||||
|
"POST",
|
||||||
|
inbox_url,
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "http://mock_masto:8000/d0b5768b-a15b-4ed6-bc84-84c7e2b57588",
|
||||||
|
"type": "Follow",
|
||||||
|
"actor": "http://mock_masto:8000/users/h4kor",
|
||||||
|
"object": actor_url,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp = requests.Session().send(req)
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
followers_url, headers={"Content-Type": "application/activity+json"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
pprint(data)
|
||||||
|
assert "items" in data
|
||||||
|
assert len(data["items"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_unfollow(client, inbox_url, followers_url, actor_url):
|
||||||
|
ensure_follow(client, inbox_url, actor_url)
|
||||||
|
sleep(0.5)
|
||||||
|
with msg_inc(1):
|
||||||
|
req = sign(
|
||||||
|
"POST",
|
||||||
|
inbox_url,
|
||||||
|
{
|
||||||
|
"@context": "https://www.w3.org/ns/activitystreams",
|
||||||
|
"id": "http://mock_masto:8000/users/h4kor#follows/3632040/undo",
|
||||||
|
"type": "Undo",
|
||||||
|
"actor": "http://mock_masto:8000/users/h4kor",
|
||||||
|
"object": {
|
||||||
|
"id": "http://mock_masto:8000/d0b5768b-a15b-4ed6-bc84-84c7e2b57588",
|
||||||
|
"type": "Follow",
|
||||||
|
"actor": "http://mock_masto:8000/users/h4kor",
|
||||||
|
"object": actor_url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp = requests.Session().send(req)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
resp = client.get(
|
||||||
|
followers_url, headers={"Content-Type": "application/activity+json"}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
pprint(data)
|
||||||
|
assert "totalItems" in data
|
||||||
|
assert data["totalItems"] == 0
|
|
@ -0,0 +1,27 @@
|
||||||
|
import pytest
|
||||||
|
from .fixtures import ACCT_NAME
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
["query", "status"],
|
||||||
|
[
|
||||||
|
["", 404],
|
||||||
|
["?foo=bar", 404],
|
||||||
|
["?resource=lol@bar.com", 404],
|
||||||
|
[f"?resource={ACCT_NAME}", 200],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_webfinger_status(client, query, status):
|
||||||
|
resp = client.get("/.well-known/webfinger" + query)
|
||||||
|
assert resp.status_code == status
|
||||||
|
|
||||||
|
|
||||||
|
def test_webfinger(client):
|
||||||
|
resp = client.get(f"/.well-known/webfinger?resource={ACCT_NAME}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.json()
|
||||||
|
assert data["subject"] == ACCT_NAME
|
||||||
|
assert len(data["links"]) > 0
|
||||||
|
self_link = [x for x in data["links"] if x["rel"] == "self"][0]
|
||||||
|
assert self_link["type"] == "application/activity+json"
|
||||||
|
assert "href" in self_link
|
|
@ -23,11 +23,10 @@ func (meta *ArticleMetaData) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements model.EntryMetaData.
|
// ParseFormData implements model.EntryMetaData.
|
||||||
func (*ArticleMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
func (meta *ArticleMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
return &ArticleMetaData{
|
meta.Title = data.FormValue("title")
|
||||||
Title: data.FormValue("title"),
|
meta.Content = data.FormValue("content")
|
||||||
Content: data.FormValue("content"),
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Article) Title() string {
|
func (e *Article) Title() string {
|
||||||
|
|
|
@ -24,12 +24,11 @@ func (meta *BookmarkMetaData) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements model.EntryMetaData.
|
// ParseFormData implements model.EntryMetaData.
|
||||||
func (*BookmarkMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
func (meta *BookmarkMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
return &BookmarkMetaData{
|
meta.Title = data.FormValue("title")
|
||||||
Title: data.FormValue("title"),
|
meta.Url = data.FormValue("url")
|
||||||
Url: data.FormValue("url"),
|
meta.Content = data.FormValue("content")
|
||||||
Content: data.FormValue("content"),
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Bookmark) Title() string {
|
func (e *Bookmark) Title() string {
|
||||||
|
|
|
@ -24,41 +24,44 @@ func (meta *ImageMetaData) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements model.EntryMetaData.
|
// ParseFormData implements model.EntryMetaData.
|
||||||
func (meta *ImageMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
func (meta *ImageMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
file, err := data.FormFile("image")
|
file, err := data.FormFile("image")
|
||||||
var imgId = meta.ImageId
|
var imgId = meta.ImageId
|
||||||
if err != nil && imgId == "" {
|
if err != nil && imgId == "" {
|
||||||
return nil, err
|
return err
|
||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
fileData, err := file.Open()
|
fileData, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
defer fileData.Close()
|
defer fileData.Close()
|
||||||
|
|
||||||
fileBytes := make([]byte, file.Size)
|
fileBytes := make([]byte, file.Size)
|
||||||
_, err = fileData.Read(fileBytes)
|
_, err = fileData.Read(fileBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
bin, err := binSvc.Create(file.Filename, fileBytes)
|
bin, err := binSvc.Create(file.Filename, fileBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
imgId = bin.Id
|
imgId = bin.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ImageMetaData{
|
meta.ImageId = imgId
|
||||||
ImageId: imgId,
|
meta.Title = data.FormValue("title")
|
||||||
Title: data.FormValue("title"),
|
meta.Content = data.FormValue("content")
|
||||||
Content: data.FormValue("content"),
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Image) Title() string {
|
func (e *Image) Title() string {
|
||||||
return e.meta.Title
|
return e.meta.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Image) ImageUrl() string {
|
||||||
|
return "/media/" + e.meta.ImageId
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Image) Content() model.EntryContent {
|
func (e *Image) Content() model.EntryContent {
|
||||||
str, err := render.RenderTemplateToString("entry/Image", e)
|
str, err := render.RenderTemplateToString("entry/Image", e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -22,10 +22,9 @@ func (meta *NoteMetaData) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements model.EntryMetaData.
|
// ParseFormData implements model.EntryMetaData.
|
||||||
func (*NoteMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
func (meta *NoteMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
return &NoteMetaData{
|
meta.Content = data.FormValue("content")
|
||||||
Content: data.FormValue("content"),
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Note) Title() string {
|
func (e *Note) Title() string {
|
||||||
|
|
|
@ -23,11 +23,10 @@ func (meta *PageMetaData) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements model.EntryMetaData.
|
// ParseFormData implements model.EntryMetaData.
|
||||||
func (*PageMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
func (meta *PageMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
return &PageMetaData{
|
meta.Title = data.FormValue("title")
|
||||||
Title: data.FormValue("title"),
|
meta.Content = data.FormValue("content")
|
||||||
Content: data.FormValue("content"),
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Page) Title() string {
|
func (e *Page) Title() string {
|
||||||
|
|
|
@ -27,7 +27,7 @@ func (meta *RecipeMetaData) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements model.EntryMetaData.
|
// ParseFormData implements model.EntryMetaData.
|
||||||
func (*RecipeMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
func (meta *RecipeMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
ings := strings.Split(data.FormValue("ingredients"), "\n")
|
ings := strings.Split(data.FormValue("ingredients"), "\n")
|
||||||
clean := make([]string, 0)
|
clean := make([]string, 0)
|
||||||
for _, ing := range ings {
|
for _, ing := range ings {
|
||||||
|
@ -35,13 +35,12 @@ func (*RecipeMetaData) ParseFormData(data model.HttpFormData, binSvc model.Binar
|
||||||
clean = append(clean, strings.TrimSpace(ing))
|
clean = append(clean, strings.TrimSpace(ing))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &RecipeMetaData{
|
meta.Title = data.FormValue("title")
|
||||||
Title: data.FormValue("title"),
|
meta.Yield = data.FormValue("yield")
|
||||||
Yield: data.FormValue("yield"),
|
meta.Duration = data.FormValue("duration")
|
||||||
Duration: data.FormValue("duration"),
|
meta.Ingredients = clean
|
||||||
Ingredients: clean,
|
meta.Content = data.FormValue("content")
|
||||||
Content: data.FormValue("content"),
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Recipe) Title() string {
|
func (e *Recipe) Title() string {
|
||||||
|
|
|
@ -24,12 +24,11 @@ func (meta *ReplyMetaData) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements model.EntryMetaData.
|
// ParseFormData implements model.EntryMetaData.
|
||||||
func (*ReplyMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
func (meta *ReplyMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
return &ReplyMetaData{
|
meta.Title = data.FormValue("title")
|
||||||
Title: data.FormValue("title"),
|
meta.Url = data.FormValue("url")
|
||||||
Url: data.FormValue("url"),
|
meta.Content = data.FormValue("content")
|
||||||
Content: data.FormValue("content"),
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Reply) Title() string {
|
func (e *Reply) Title() string {
|
||||||
|
|
51
go.mod
51
go.mod
|
@ -1,53 +1,50 @@
|
||||||
module owl-blogs
|
module owl-blogs
|
||||||
|
|
||||||
go 1.20
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-ap/activitypub v0.0.0-20230719093539-2b6a6f3a25ee
|
github.com/Davincible/goinsta/v3 v3.2.6
|
||||||
github.com/gofiber/fiber/v2 v2.47.0
|
github.com/go-ap/activitypub v0.0.0-20240408091739-ba76b44c2594
|
||||||
github.com/google/uuid v1.3.0
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||||
github.com/jmoiron/sqlx v1.3.5
|
github.com/gofiber/fiber/v2 v2.52.4
|
||||||
github.com/mattn/go-sqlite3 v1.14.19
|
github.com/google/uuid v1.6.0
|
||||||
github.com/spf13/cobra v1.7.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/yuin/goldmark v1.5.4
|
github.com/yuin/goldmark v1.7.1
|
||||||
golang.org/x/crypto v0.12.0
|
golang.org/x/crypto v0.23.0
|
||||||
golang.org/x/net v0.14.0
|
golang.org/x/net v0.25.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
|
||||||
github.com/Davincible/goinsta/v3 v3.2.6 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/chromedp/cdproto v0.0.0-20240501202034-ef67d660e9fd // indirect
|
||||||
github.com/chromedp/cdproto v0.0.0-20230808232040-5d0fb3432de3 // indirect
|
github.com/chromedp/chromedp v0.9.5 // indirect
|
||||||
github.com/chromedp/chromedp v0.9.2 // indirect
|
|
||||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-ap/errors v0.0.0-20221205040414-01c1adfc98ea // indirect
|
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 // indirect
|
||||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 // indirect
|
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.3.0 // indirect
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.16.3 // indirect
|
github.com/klauspost/compress v1.17.8 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||||
github.com/philhofer/fwd v1.1.2 // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/tinylib/msgp v1.1.8 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.47.0 // indirect
|
github.com/valyala/fasthttp v1.52.0 // indirect
|
||||||
github.com/valyala/fastjson v1.6.4 // indirect
|
github.com/valyala/fastjson v1.6.4 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.11.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
151
go.sum
151
go.sum
|
@ -1,149 +1,106 @@
|
||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
|
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg=
|
||||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs=
|
||||||
github.com/Davincible/goinsta/v3 v3.2.6 h1:+lNIWU6NABWd2VSGe83UQypnef+kzWwjmfgGihPbwD8=
|
github.com/Davincible/goinsta/v3 v3.2.6 h1:+lNIWU6NABWd2VSGe83UQypnef+kzWwjmfgGihPbwD8=
|
||||||
github.com/Davincible/goinsta/v3 v3.2.6/go.mod h1:jIDhrWZmttL/gtXj/mkCaZyeNdAAqW3UYjasOUW0YEw=
|
github.com/Davincible/goinsta/v3 v3.2.6/go.mod h1:jIDhrWZmttL/gtXj/mkCaZyeNdAAqW3UYjasOUW0YEw=
|
||||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/cdproto v0.0.0-20230808232040-5d0fb3432de3 h1:Qhw280TMvghvzNauTPMwEvaHUYWRJSoSlNVMev4lO7M=
|
github.com/chromedp/cdproto v0.0.0-20240501202034-ef67d660e9fd h1:5/HXKq8EaAWVmnl6Hnyl4SVq7FF5990DBW6AuTrWtVw=
|
||||||
github.com/chromedp/cdproto v0.0.0-20230808232040-5d0fb3432de3/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/chromedp/cdproto v0.0.0-20240501202034-ef67d660e9fd/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
github.com/chromedp/chromedp v0.9.2 h1:dKtNz4kApb06KuSXoTQIyUC2TrA0fhGDwNZf3bcgfKw=
|
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
|
||||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
|
||||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-ap/activitypub v0.0.0-20230719093539-2b6a6f3a25ee h1:1OMBlmSzLXftIj5z/D1s1Xr3FanVKtLFZPtdIFslh1A=
|
github.com/go-ap/activitypub v0.0.0-20240408091739-ba76b44c2594 h1:er3GvGCm7bJwHostjZlsRy7uiUuCquUVF9Fe0TrwiPI=
|
||||||
github.com/go-ap/activitypub v0.0.0-20230719093539-2b6a6f3a25ee/go.mod h1:qw0WNf+PTG69Xu6mVqUluDuKl1VwVYdgntOZQFBZQ48=
|
github.com/go-ap/activitypub v0.0.0-20240408091739-ba76b44c2594/go.mod h1:yRUfFCoZY6C1CWalauqEQ5xYgSckzEBEO/2MBC6BOME=
|
||||||
github.com/go-ap/errors v0.0.0-20221205040414-01c1adfc98ea h1:ywGtLGVjJjMrq4mu35Qmu+NtlhlTk/gTayE6Bb4tQZk=
|
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0 h1:H9MGShwybHLSln6K8RxHPMHiLcD86Lru+5TVW2TcXHY=
|
||||||
github.com/go-ap/errors v0.0.0-20221205040414-01c1adfc98ea/go.mod h1:SaTNjEEkp0q+w3pUS1ccyEL/lUrHteORlDq/e21mCc8=
|
github.com/go-ap/errors v0.0.0-20240304112515-6077fa9c17b0/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI=
|
||||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw=
|
||||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
github.com/gofiber/fiber/v2 v2.47.0 h1:EN5lHVCc+Pyqh5OEsk8fzRiifgwpbrP0rulQ4iNf3fs=
|
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
|
||||||
github.com/gofiber/fiber/v2 v2.47.0/go.mod h1:mbFMVN1lQuzziTkkakgtKKdjfsXSw9BKR5lmcNksUoU=
|
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
|
||||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
|
||||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
|
||||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
|
||||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
|
||||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
|
||||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
|
||||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
|
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
||||||
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||||
github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
|
||||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
|
||||||
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
|
||||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
|
||||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
|
||||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
|
|
@ -101,9 +101,15 @@ func (repo *DefaultBinaryFileRepo) FindByNameForEntry(name string, entry model.E
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListIds implements repository.BinaryRepository
|
// ListIds implements repository.BinaryRepository
|
||||||
func (repo *DefaultBinaryFileRepo) ListIds() ([]string, error) {
|
func (repo *DefaultBinaryFileRepo) ListIds(filter string) ([]string, error) {
|
||||||
|
filter = strings.TrimSpace(strings.ToLower(filter))
|
||||||
|
if filter == "" {
|
||||||
|
filter = "%"
|
||||||
|
} else {
|
||||||
|
filter = "%" + filter + "%"
|
||||||
|
}
|
||||||
var ids []string
|
var ids []string
|
||||||
err := repo.db.Select(&ids, "SELECT id FROM binary_files")
|
err := repo.db.Select(&ids, "SELECT id FROM binary_files WHERE LOWER(id) LIKE ?", filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sqlFollower struct {
|
||||||
|
Follwer string `db:"follower"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultFollowerRepo struct {
|
||||||
|
db *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFollowerRepository(db Database) repository.FollowerRepository {
|
||||||
|
sqlxdb := db.Get()
|
||||||
|
|
||||||
|
// Create tables if not exists
|
||||||
|
sqlxdb.MustExec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS followers (
|
||||||
|
follower TEXT PRIMARY KEY
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
|
||||||
|
return &DefaultFollowerRepo{
|
||||||
|
db: sqlxdb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add implements repository.FollowerRepository.
|
||||||
|
func (d *DefaultFollowerRepo) Add(follower string) error {
|
||||||
|
_, err := d.db.Exec("INSERT INTO followers (follower) VALUES (?) ON CONFLICT DO NOTHING", follower)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove implements repository.FollowerRepository.
|
||||||
|
func (d *DefaultFollowerRepo) Remove(follower string) error {
|
||||||
|
_, err := d.db.Exec("DELETE FROM followers WHERE follower = ?", follower)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// All implements repository.FollowerRepository.
|
||||||
|
func (d *DefaultFollowerRepo) All() ([]string, error) {
|
||||||
|
var followers []sqlFollower
|
||||||
|
err := d.db.Select(&followers, "SELECT * FROM followers")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := []string{}
|
||||||
|
for _, follower := range followers {
|
||||||
|
result = append(result, follower.Follwer)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package infra_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/infra"
|
||||||
|
"owl-blogs/test"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupFollowerRepo() repository.FollowerRepository {
|
||||||
|
db := test.NewMockDb()
|
||||||
|
repo := infra.NewFollowerRepository(db)
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFollower(t *testing.T) {
|
||||||
|
repo := setupFollowerRepo()
|
||||||
|
|
||||||
|
err := repo.Add("foo@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
followers, err := repo.All()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, followers, 1)
|
||||||
|
require.Equal(t, followers[0], "foo@example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoubleAddFollower(t *testing.T) {
|
||||||
|
repo := setupFollowerRepo()
|
||||||
|
|
||||||
|
err := repo.Add("foo@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = repo.Add("foo@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
followers, err := repo.All()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, followers, 1)
|
||||||
|
require.Equal(t, followers[0], "foo@example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultipleAddFollower(t *testing.T) {
|
||||||
|
repo := setupFollowerRepo()
|
||||||
|
|
||||||
|
err := repo.Add("foo@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = repo.Add("bar@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = repo.Add("baz@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
followers, err := repo.All()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, followers, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveFollower(t *testing.T) {
|
||||||
|
repo := setupFollowerRepo()
|
||||||
|
|
||||||
|
err := repo.Add("foo@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
followers, err := repo.All()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, followers, 1)
|
||||||
|
|
||||||
|
err = repo.Remove("foo@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
followers, err = repo.All()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, followers, 0)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveNonExistingFollower(t *testing.T) {
|
||||||
|
repo := setupFollowerRepo()
|
||||||
|
|
||||||
|
err := repo.Remove("foo@example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
followers, err := repo.All()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, followers, 0)
|
||||||
|
|
||||||
|
}
|
|
@ -89,7 +89,7 @@ func (repo *DefaultInteractionRepo) Delete(interaction model.Interaction) error
|
||||||
// FindAll implements repository.InteractionRepository.
|
// FindAll implements repository.InteractionRepository.
|
||||||
func (repo *DefaultInteractionRepo) FindAll(entryId string) ([]model.Interaction, error) {
|
func (repo *DefaultInteractionRepo) FindAll(entryId string) ([]model.Interaction, error) {
|
||||||
data := []sqlInteraction{}
|
data := []sqlInteraction{}
|
||||||
err := repo.db.Select(&data, "SELECT * FROM interactions WHERE entry_id = ?", entryId)
|
err := repo.db.Select(&data, "SELECT * FROM interactions WHERE entry_id = ? ORDER BY created_at DESC", entryId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -156,3 +156,23 @@ func (repo *DefaultInteractionRepo) sqlInteractionToInteraction(interaction sqlI
|
||||||
return i, nil
|
return i, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAllInteractions implements repository.InteractionRepository.
|
||||||
|
func (repo *DefaultInteractionRepo) ListAllInteractions() ([]model.Interaction, error) {
|
||||||
|
data := []sqlInteraction{}
|
||||||
|
err := repo.db.Select(&data, "SELECT * FROM interactions ORDER BY created_at DESC")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interactions := []model.Interaction{}
|
||||||
|
for _, d := range data {
|
||||||
|
i, err := repo.sqlInteractionToInteraction(d)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
interactions = append(interactions, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return interactions, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package interactions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Like struct {
|
||||||
|
model.InteractionBase
|
||||||
|
meta LikeMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type LikeMetaData struct {
|
||||||
|
SenderUrl string
|
||||||
|
SenderName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Like) Content() model.InteractionContent {
|
||||||
|
str, err := render.RenderTemplateToString("interaction/Like", i)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.InteractionContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Like) MetaData() interface{} {
|
||||||
|
return &i.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Like) SetMetaData(metaData interface{}) {
|
||||||
|
i.meta = *metaData.(*LikeMetaData)
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package interactions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repost struct {
|
||||||
|
model.InteractionBase
|
||||||
|
meta RepostMetaData
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepostMetaData struct {
|
||||||
|
SenderUrl string
|
||||||
|
SenderName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Repost) Content() model.InteractionContent {
|
||||||
|
str, err := render.RenderTemplateToString("interaction/Repost", i)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
return model.InteractionContent(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Repost) MetaData() interface{} {
|
||||||
|
return &i.meta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Repost) SetMetaData(metaData interface{}) {
|
||||||
|
i.meta = *metaData.(*RepostMetaData)
|
||||||
|
}
|
|
@ -28,11 +28,10 @@ func (cfg *InstagramConfig) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements app.AppConfig.
|
// ParseFormData implements app.AppConfig.
|
||||||
func (*InstagramConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (app.AppConfig, error) {
|
func (cfg *InstagramConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
return &InstagramConfig{
|
cfg.User = data.FormValue("User")
|
||||||
User: data.FormValue("User"),
|
cfg.Password = data.FormValue("Password")
|
||||||
Password: data.FormValue("Password"),
|
return nil
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterInstagram(
|
func RegisterInstagram(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ type TemplateData struct {
|
||||||
|
|
||||||
//go:embed templates
|
//go:embed templates
|
||||||
var templates embed.FS
|
var templates embed.FS
|
||||||
|
var SiteConfigService model.SiteConfigInterface
|
||||||
|
|
||||||
var funcMap = template.FuncMap{
|
var funcMap = template.FuncMap{
|
||||||
"markdown": func(text string) string {
|
"markdown": func(text string) string {
|
||||||
|
@ -29,6 +31,10 @@ var funcMap = template.FuncMap{
|
||||||
}
|
}
|
||||||
return html
|
return html
|
||||||
},
|
},
|
||||||
|
"urljoin": func(elems ...string) string {
|
||||||
|
r, _ := url.JoinPath(elems[0], elems[1:]...)
|
||||||
|
return r
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateTemplateWithBase(templateName string) (*template.Template, error) {
|
func CreateTemplateWithBase(templateName string) (*template.Template, error) {
|
||||||
|
@ -40,7 +46,7 @@ func CreateTemplateWithBase(templateName string) (*template.Template, error) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderTemplateWithBase(w io.Writer, siteConfig model.SiteConfig, templateName string, data interface{}) error {
|
func RenderTemplateWithBase(w io.Writer, templateName string, data interface{}) error {
|
||||||
|
|
||||||
t, err := CreateTemplateWithBase(templateName)
|
t, err := CreateTemplateWithBase(templateName)
|
||||||
|
|
||||||
|
@ -48,6 +54,11 @@ func RenderTemplateWithBase(w io.Writer, siteConfig model.SiteConfig, templateNa
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
siteConfig, err := SiteConfigService.GetSiteConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = t.ExecuteTemplate(w, "base", TemplateData{
|
err = t.ExecuteTemplate(w, "base", TemplateData{
|
||||||
Data: data,
|
Data: data,
|
||||||
SiteConfig: siteConfig,
|
SiteConfig: siteConfig,
|
||||||
|
|
|
@ -6,132 +6,49 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<title>{{template "title" .Data}} - {{ .SiteConfig.Title }}</title>
|
<title>{{template "title" .Data}} - {{ .SiteConfig.Title }}</title>
|
||||||
<meta property="og:title" content="{{template "title" .Data}}" />
|
<meta property="og:title" content="{{template "title" .Data}}" />
|
||||||
|
{{ template "head" .}}
|
||||||
|
|
||||||
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||||
<link rel="webmention" href="/webmention/" />
|
<link rel="webmention" href="/webmention/" />
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/index.xml">
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/index.xml">
|
||||||
|
|
||||||
<link rel='stylesheet' href='/static/pico.min.css'>
|
<link rel='stylesheet' href='/static/owl.css'>
|
||||||
<link rel='stylesheet' href='/static/style.css'>
|
<link rel='stylesheet' href='/static/style.css'>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--primary: {{.SiteConfig.PrimaryColor}};
|
--primary: {{.SiteConfig.PrimaryColor}};
|
||||||
--primary-hover: color-mix(in srgb,var(--primary),#000 20%);
|
|
||||||
--primary-focus: color-mix(in srgb,var(--primary),#fff 40%);
|
|
||||||
--primary-inverse: #FFF;
|
|
||||||
--background: {{.SiteConfig.HeaderColor}};
|
|
||||||
--background-dark: color-mix(in srgb,var(--background),#000 50%);
|
|
||||||
--background-light: color-mix(in srgb,var(--background),#fff 50%);
|
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
[data-theme="light"],
|
|
||||||
:root:not([data-theme="dark"]) {
|
|
||||||
--primary: {{.SiteConfig.PrimaryColor}};
|
|
||||||
--primary-hover: color-mix(in srgb,var(--primary),#000 20%);
|
|
||||||
--primary-focus: color-mix(in srgb,var(--primary),#fff 40%);
|
|
||||||
--primary-inverse: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
header {
|
|
||||||
background-color: var(--background);
|
|
||||||
padding-bottom: 1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
border-top: dashed 2px;
|
|
||||||
border-color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
float: left;
|
|
||||||
margin-right: 1rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
order: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-profile {
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
hgroup h2 a { color: inherit; }
|
|
||||||
|
|
||||||
.photo-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-grid-item {
|
|
||||||
flex: 1 0 25%;
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-grid-item img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
aspect-ratio: 1 / 1 ;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
main img {
|
|
||||||
max-height: 50vh;
|
|
||||||
margin: auto;
|
|
||||||
text-align: center;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
{{ .SiteConfig.HtmlHeadExtra }}
|
{{ .SiteConfig.HtmlHeadExtra }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header class="h-card">
|
||||||
<div class="container header h-card">
|
{{ if .SiteConfig.AvatarUrl }}
|
||||||
<hgroup class="header-title">
|
<img class="u-photo u-logo avatar" src="{{ .SiteConfig.AvatarUrl }}" alt="{{ .SiteConfig.Title }}" />
|
||||||
<h2><a class="p-name u-url" href="/">{{ .SiteConfig.Title }}</a></h2>
|
{{ end }}
|
||||||
<h3 class="p-note">{{ .SiteConfig.SubTitle }}</h3>
|
|
||||||
</hgroup>
|
|
||||||
|
|
||||||
<div class="header-profile">
|
<hgroup>
|
||||||
{{ if .SiteConfig.AvatarUrl }}
|
<h1><a class="p-name u-url" href="/">{{ .SiteConfig.Title }}</a></h1>
|
||||||
<img class="u-photo u-logo avatar" src="{{ .SiteConfig.AvatarUrl }}" alt="{{ .SiteConfig.Title }}" width="100" height="100" />
|
<p class="p-note">{{ .SiteConfig.SubTitle }}</p>
|
||||||
{{ end }}
|
</hgroup>
|
||||||
<div style="float: right; list-style: none;">
|
<nav>
|
||||||
{{ range $me := .SiteConfig.Me }}
|
<ul>
|
||||||
<li><a href="{{$me.Url}}" rel="me">{{$me.Name}}</a>
|
{{ range $link := .SiteConfig.HeaderMenu }}
|
||||||
</li>
|
{{ if $link.List }}
|
||||||
{{ end }}
|
<li><a href="/lists/{{ $link.List }}">{{ $link.Title }}</a></li>
|
||||||
</div>
|
{{ else if $link.Post }}
|
||||||
</div>
|
<li><a href="/posts/{{ $link.Post }}">{{ $link.Title }}</a></li>
|
||||||
</div>
|
{{ else }}
|
||||||
<div class="container">
|
<li><a href="{{ $link.Url }}">{{ $link.Title }}</a></li>
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
{{ range $link := .SiteConfig.HeaderMenu }}
|
|
||||||
{{ if $link.List }}
|
|
||||||
<li><a href="/lists/{{ $link.List }}">{{ $link.Title }}</a></li>
|
|
||||||
{{ else if $link.Post }}
|
|
||||||
<li><a href="/posts/{{ $link.Post }}">{{ $link.Title }}</a></li>
|
|
||||||
{{ else }}
|
|
||||||
<li><a href="{{ $link.Url }}">{{ $link.Title }}</a></li>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
{{ end }}
|
||||||
</nav>
|
</ul>
|
||||||
</div>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<main class="container">
|
<main>
|
||||||
{{template "main" .Data}}
|
{{template "main" .Data}}
|
||||||
</main>
|
</main>
|
||||||
<footer class="container">
|
<footer>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a target="_blank" href="/index.xml">RSS Feed</a></li>
|
<li><a target="_blank" href="/index.xml">RSS Feed</a></li>
|
||||||
|
@ -144,16 +61,21 @@
|
||||||
<li><a href="{{ $link.Url }}">{{ $link.Title }}</a></li>
|
<li><a href="{{ $link.Url }}">{{ $link.Title }}</a></li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ range $me := .SiteConfig.Me }}
|
||||||
|
<li><a href="{{$me.Url}}" rel="me">{{$me.Name}}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<li><a href="/admin/">Editor</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
{{ .SiteConfig.FooterExtra}}
|
<div>
|
||||||
<small>
|
{{ .SiteConfig.FooterExtra}}
|
||||||
<nav>
|
</div>
|
||||||
<ul>
|
|
||||||
<li><a href="/admin/">Editor</a></li>
|
<div style="margin-top:var(--s2);">
|
||||||
</ul>
|
powered by <i><a href="https://github.com/H4kor/owl-blogs" target="_blank">owl-blogs</a></i>
|
||||||
</nav>
|
</a>
|
||||||
</small>
|
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -2,4 +2,13 @@
|
||||||
<input type="text" name="title" value="{{.Title}}" />
|
<input type="text" name="title" value="{{.Title}}" />
|
||||||
|
|
||||||
<label for="content">Content</label>
|
<label for="content">Content</label>
|
||||||
<textarea name="content" rows="16">{{.Content}}</textarea>
|
<textarea
|
||||||
|
id="contentField"
|
||||||
|
name="content"
|
||||||
|
rows="16"
|
||||||
|
>{{.Content}}</textarea>
|
||||||
|
|
||||||
|
<script src="/static/editor.js"></script>
|
||||||
|
<script>
|
||||||
|
addFileDrop("contentField")
|
||||||
|
</script>
|
|
@ -1,2 +1,7 @@
|
||||||
<label for="content">Content</label>
|
<label for="content">Content</label>
|
||||||
<textarea name="content" rows="8">{{.Content}}</textarea>
|
<textarea id="contentField" name="content" rows="8">{{.Content}}</textarea>
|
||||||
|
|
||||||
|
<script src="/static/editor.js"></script>
|
||||||
|
<script>
|
||||||
|
addFileDrop("contentField")
|
||||||
|
</script>
|
|
@ -2,4 +2,9 @@
|
||||||
<input type="text" name="title" value="{{.Title}}" />
|
<input type="text" name="title" value="{{.Title}}" />
|
||||||
|
|
||||||
<label for="content">Content</label>
|
<label for="content">Content</label>
|
||||||
<textarea name="content" rows="16">{{.Content}}</textarea>
|
<textarea id="contentField" name="content" rows="16">{{.Content}}</textarea>
|
||||||
|
|
||||||
|
<script src="/static/editor.js"></script>
|
||||||
|
<script>
|
||||||
|
addFileDrop("contentField")
|
||||||
|
</script>
|
|
@ -12,4 +12,9 @@
|
||||||
{{$i}}{{ end }}</textarea>
|
{{$i}}{{ end }}</textarea>
|
||||||
|
|
||||||
<label for="content">Content</label>
|
<label for="content">Content</label>
|
||||||
<textarea name="content" rows="16">{{.Content}}</textarea>
|
<textarea id="contentField" name="content" rows="16">{{.Content}}</textarea>
|
||||||
|
|
||||||
|
<script src="/static/editor.js"></script>
|
||||||
|
<script>
|
||||||
|
addFileDrop("contentField")
|
||||||
|
</script>
|
|
@ -5,4 +5,9 @@
|
||||||
<input type="text" name="url" value="{{.Url}}" placeholder="https://..." />
|
<input type="text" name="url" value="{{.Url}}" placeholder="https://..." />
|
||||||
|
|
||||||
<label for="content">Content</label>
|
<label for="content">Content</label>
|
||||||
<textarea name="content" rows="16">{{.Content}}</textarea></textarea>
|
<textarea id="contentField" name="content" rows="16">{{.Content}}</textarea></textarea>
|
||||||
|
|
||||||
|
<script src="/static/editor.js"></script>
|
||||||
|
<script>
|
||||||
|
addFileDrop("contentField")
|
||||||
|
</script>
|
|
@ -0,0 +1,3 @@
|
||||||
|
Liked by <a href="{{.MetaData.SenderUrl}}">
|
||||||
|
{{.MetaData.SenderName}}
|
||||||
|
</a>
|
|
@ -0,0 +1,3 @@
|
||||||
|
Reposted by <a href="{{.MetaData.SenderUrl}}">
|
||||||
|
{{.MetaData.SenderName}}
|
||||||
|
</a>
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Admin{{end}}
|
{{define "title"}}Admin{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
<h2 style="margin-bottom: 1rem;">Write</h2>
|
<h2 style="margin-bottom: 1rem;">Write</h2>
|
||||||
|
@ -13,8 +14,9 @@
|
||||||
|
|
||||||
<h2 style="margin-bottom: 1rem;">Content</h2>
|
<h2 style="margin-bottom: 1rem;">Content</h2>
|
||||||
<div class="action-tile-list">
|
<div class="action-tile-list">
|
||||||
<a class="action-tile" href="/admin/binaries/">Files</a>
|
|
||||||
<a class="action-tile" href="/admin/drafts/">Drafts</a>
|
<a class="action-tile" href="/admin/drafts/">Drafts</a>
|
||||||
|
<a class="action-tile" href="/admin/binaries/">Files</a>
|
||||||
|
<a class="action-tile" href="/admin/interactions/">Interactions</a>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{{define "title"}}Configuration{{end}}
|
{{define "title"}}Configuration{{end}}
|
||||||
|
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
<a href="/admin">← Back</a>
|
<a href="/admin">← Back</a>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Files{{end}}
|
{{define "title"}}Files{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
|
@ -8,14 +9,23 @@
|
||||||
|
|
||||||
<h2>Files</h2>
|
<h2>Files</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<h3>Upload File</h3>
|
||||||
<form action="/admin/binaries/new" method="post" enctype="multipart/form-data">
|
<form action="/admin/binaries/new" method="post" enctype="multipart/form-data">
|
||||||
<div>
|
<fieldset role="group">
|
||||||
<label for="file">File</label>
|
|
||||||
<input type="file" name="file" id="file">
|
<input type="file" name="file" id="file">
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input type="submit" value="Upload">
|
<input type="submit" value="Upload">
|
||||||
</div>
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h3>Files</h3>
|
||||||
|
<form action="" method="get">
|
||||||
|
<fieldset role="search">
|
||||||
|
<input type="filter" name="filter" id="filter" value="{{.Filter}}">
|
||||||
|
<input type="submit" value="Search">
|
||||||
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<table role="grid">
|
<table role="grid">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Index{{end}}
|
{{define "title"}}Index{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Editor{{end}}
|
{{define "title"}}Editor{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
{{define "title"}}{{.Entry.Title}}{{end}}
|
{{define "title"}}{{.Entry.Title}}{{end}}
|
||||||
|
|
||||||
|
{{define "head"}}
|
||||||
|
<meta property="og:url" content="{{ urljoin .SiteConfig.FullUrl "/posts/" .Data.Entry.ID }}/" />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
{{ if .Data.Entry.ImageUrl}}
|
||||||
|
<meta property="og:image" content="{{ urljoin .SiteConfig.FullUrl .Data.Entry.ImageUrl}}" />
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Data.Entry.PublishedAt }}
|
||||||
|
<meta property="og:article:published_time" content="{{.Data.Entry.PublishedAt.Format "2006-01-02T15:04:05" }}" />
|
||||||
|
{{ end }}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
{{ if not .Entry.PublishedAt }}
|
{{ if not .Entry.PublishedAt }}
|
||||||
|
@ -11,28 +22,29 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<hgroup>
|
{{if .Entry.Title}}
|
||||||
{{if .Entry.Title}}
|
<h1 class="p-name entry-title">{{.Entry.Title}}</h1>
|
||||||
<h1 class="p-name">{{.Entry.Title}}</h1>
|
{{else}}
|
||||||
{{end}}
|
<div style="padding-top:4rem;"></div>
|
||||||
<small>
|
{{end}}
|
||||||
<a class="u-url" href="">#</a>
|
<div class="entry-meta">
|
||||||
Published:
|
<a class="u-url" href="/posts/{{ .Entry.ID }}/">#</a>
|
||||||
{{ if .Entry.PublishedAt }}
|
Published:
|
||||||
<time class="dt-published" datetime="{{.Entry.PublishedAt.Format "2006-01-02T15:04:05" }}">
|
{{ if .Entry.PublishedAt }}
|
||||||
{{.Entry.PublishedAt.Format "2006-01-02" }}
|
<time class="dt-published" datetime="{{.Entry.PublishedAt.Format "2006-01-02T15:04:05" }}">
|
||||||
</time>
|
{{.Entry.PublishedAt.Format "2006-01-02" }}
|
||||||
{{ end }}
|
</time>
|
||||||
{{ if .Author.Name }}
|
{{ end }}
|
||||||
by
|
{{ if .Author.Name }}
|
||||||
<a class="p-author h-card" href="{{.Author.FullUrl}}">
|
by
|
||||||
{{ if .Author.AvatarUrl }}
|
<a class="p-author h-card" href="{{.Author.FullUrl}}">
|
||||||
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Author.AvatarUrl }}" alt="{{ .Author.Config.Title }}" />
|
{{ if .Author.AvatarUrl }}
|
||||||
{{ end }}
|
<img class="u-photo u-logo" style="height: 1em;" src="{{ .Author.AvatarUrl }}" alt="{{ .Author.Config.Title }}" />
|
||||||
{{.Author.Name}}
|
{{ end }}
|
||||||
</a>
|
{{.Author.Name}}
|
||||||
{{ end }}
|
</a>
|
||||||
</small>
|
{{ end }}
|
||||||
|
</div>
|
||||||
</hgroup>
|
</hgroup>
|
||||||
|
|
||||||
<div class="e-content">
|
<div class="e-content">
|
||||||
|
@ -77,7 +89,7 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div style="margin-bottom:1em;">
|
<div>
|
||||||
<a style="width:100%;" href="/editor/edit/{{.Entry.ID}}/" role="button" class="">Edit</a>
|
<a style="width:100%;" href="/editor/edit/{{.Entry.ID}}/" role="button" class="">Edit</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
{{define "title"}}Index{{end}}
|
{{define "title"}}Index{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
<div class="h-feed">
|
<div class="h-feed">
|
||||||
{{ range .Entries }}
|
{{ range .Entries }}
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<hgroup>
|
<h1 class="entry-title">
|
||||||
<h3>
|
<a class="u-url" href="/posts/{{ .ID }}/">
|
||||||
<a class="u-url" href="/posts/{{ .ID }}">
|
{{if .Title}}
|
||||||
{{if .Title}}
|
{{ .Title }}
|
||||||
{{ .Title }}
|
{{else}}
|
||||||
{{else}}
|
#
|
||||||
#
|
{{end}}
|
||||||
{{end}}
|
</a>
|
||||||
</a>
|
</h1>
|
||||||
</h3>
|
<div class="entry-meta">
|
||||||
<small style="font-size: 0.75em;">
|
Published:
|
||||||
{{ if .PublishedAt }}
|
{{ if .PublishedAt }}
|
||||||
<time class="dt-published" datetime="{{.PublishedAt.Format "2006-01-02T15:04:05" }}">
|
<time class="dt-published" datetime="{{.PublishedAt.Format "2006-01-02T15:04:05" }}">
|
||||||
{{.PublishedAt.Format "2006-01-02" }}
|
{{.PublishedAt.Format "2006-01-02" }}
|
||||||
</time>
|
</time>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</small>
|
</div>
|
||||||
</hgroup>
|
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
{{define "title"}}Interactions{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
|
{{define "main"}}
|
||||||
|
|
||||||
|
<a href="/admin">← Back</a>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<h2>Recent Interactions</h2>
|
||||||
|
|
||||||
|
<table role="grid">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Entry</th>
|
||||||
|
<th scope="col">Created At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{{ range .Interactions }}
|
||||||
|
<tr>
|
||||||
|
<td scope="row">
|
||||||
|
<a href="/posts/{{ .EntryID }}">{{ .EntryID }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ .CreatedAt.Format "2006-01-02 15:04" }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<nav class="row">
|
||||||
|
{{ if not .FirstPage }}
|
||||||
|
<div>
|
||||||
|
<a href="?page={{ .PrevPage }}">Prev</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div>Page {{.Page}}</div>
|
||||||
|
|
||||||
|
{{ if not .LastPage }}
|
||||||
|
<div>
|
||||||
|
<a href="?page={{ .NextPage }}">Next</a>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{end}}
|
|
@ -1,28 +1,28 @@
|
||||||
{{define "title"}}Index{{end}}
|
{{define "title"}}Index{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
<div class="h-feed">
|
<div class="h-feed">
|
||||||
{{ range .Entries }}
|
{{ range .Entries }}
|
||||||
<div class="h-entry">
|
<div class="h-entry">
|
||||||
<hgroup>
|
<h1 class="entry-title">
|
||||||
<h3>
|
<a class="u-url" href="/posts/{{ .ID }}/">
|
||||||
<a class="u-url" href="/posts/{{ .ID }}/">
|
{{if .Title}}
|
||||||
{{if .Title}}
|
{{ .Title }}
|
||||||
{{ .Title }}
|
{{else}}
|
||||||
{{else}}
|
#
|
||||||
#
|
{{end}}
|
||||||
{{end}}
|
</a>
|
||||||
</a>
|
</h1>
|
||||||
</h3>
|
<div class="entry-meta">
|
||||||
<small style="font-size: 0.75em;">
|
Published:
|
||||||
{{ if .PublishedAt }}
|
{{ if .PublishedAt }}
|
||||||
<time class="dt-published" datetime="{{.PublishedAt.Format "2006-01-02T15:04:05" }}">
|
<time class="dt-published" datetime="{{.PublishedAt.Format "2006-01-02T15:04:05" }}">
|
||||||
{{.PublishedAt.Format "2006-01-02" }}
|
{{.PublishedAt.Format "2006-01-02" }}
|
||||||
</time>
|
</time>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</small>
|
</div>
|
||||||
</hgroup>
|
|
||||||
{{ .Content }}
|
{{ .Content }}
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Editor List{{end}}
|
{{define "title"}}Editor List{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Editor{{end}}
|
{{define "title"}}Editor{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
|
@ -13,9 +14,6 @@
|
||||||
<label for="SubTitle">SubTitle</label>
|
<label for="SubTitle">SubTitle</label>
|
||||||
<input type="text" name="SubTitle" id="SubTitle" value="{{.SubTitle}}"/>
|
<input type="text" name="SubTitle" id="SubTitle" value="{{.SubTitle}}"/>
|
||||||
|
|
||||||
<label for="HeaderColor">HeaderColor</label>
|
|
||||||
<input type="color" name="HeaderColor" id="HeaderColor" value="{{.HeaderColor}}"/>
|
|
||||||
|
|
||||||
<label for="PrimaryColor">PrimaryColor</label>
|
<label for="PrimaryColor">PrimaryColor</label>
|
||||||
<input type="color" name="PrimaryColor" id="PrimaryColor" value="{{.PrimaryColor}}"/>
|
<input type="color" name="PrimaryColor" id="PrimaryColor" value="{{.PrimaryColor}}"/>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Editor{{end}}
|
{{define "title"}}Editor{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Editor{{end}}
|
{{define "title"}}Editor{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{{define "title"}}Editor{{end}}
|
{{define "title"}}Editor{{end}}
|
||||||
|
{{define "head"}}{{end}}
|
||||||
|
|
||||||
{{define "main"}}
|
{{define "main"}}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ func (*MockEntryMetaData) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseFormData implements model.EntryMetaData.
|
// ParseFormData implements model.EntryMetaData.
|
||||||
func (*MockEntryMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
func (*MockEntryMetaData) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) error {
|
||||||
panic("unimplemented")
|
panic("unimplemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,48 +1,29 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
"strings"
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
|
||||||
"owl-blogs/render"
|
|
||||||
|
|
||||||
vocab "github.com/go-ap/activitypub"
|
vocab "github.com/go-ap/activitypub"
|
||||||
|
"github.com/go-ap/jsonld"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/adaptor"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ACT_PUB_CONF_NAME = "activity_pub"
|
|
||||||
|
|
||||||
type ActivityPubServer struct {
|
type ActivityPubServer struct {
|
||||||
configRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
entryService *app.EntryService
|
apService *app.ActivityPubService
|
||||||
}
|
entryService *app.EntryService
|
||||||
|
|
||||||
type ActivityPubConfig struct {
|
|
||||||
PreferredUsername string
|
|
||||||
PublicKeyPem string
|
|
||||||
PrivateKeyPem string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form implements app.AppConfig.
|
|
||||||
func (cfg *ActivityPubConfig) Form(binSvc model.BinaryStorageInterface) string {
|
|
||||||
f, _ := render.RenderTemplateToString("forms/ActivityPubConfig", cfg)
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseFormData implements app.AppConfig.
|
|
||||||
func (*ActivityPubConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (app.AppConfig, error) {
|
|
||||||
return &ActivityPubConfig{
|
|
||||||
PreferredUsername: data.FormValue("PreferredUsername"),
|
|
||||||
PublicKeyPem: data.FormValue("PublicKeyPem"),
|
|
||||||
PrivateKeyPem: data.FormValue("PrivateKeyPem"),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebfingerResponse struct {
|
type WebfingerResponse struct {
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
|
Aliases []string `json:"aliases"`
|
||||||
Links []WebfingerLink `json:"links"`
|
Links []WebfingerLink `json:"links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,27 +33,38 @@ type WebfingerLink struct {
|
||||||
Href string `json:"href"`
|
Href string `json:"href"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewActivityPubServer(configRepo repository.ConfigRepository, entryService *app.EntryService) *ActivityPubServer {
|
func NewActivityPubServer(siteConfigService *app.SiteConfigService, entryService *app.EntryService, apService *app.ActivityPubService) *ActivityPubServer {
|
||||||
return &ActivityPubServer{
|
return &ActivityPubServer{
|
||||||
configRepo: configRepo,
|
siteConfigService: siteConfigService,
|
||||||
entryService: entryService,
|
entryService: entryService,
|
||||||
|
apService: apService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, _ := s.siteConfigService.GetSiteConfig()
|
||||||
apConfig := ActivityPubConfig{}
|
apConfig, _ := s.apService.GetApConfig()
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
domain, err := url.Parse(siteConfig.FullUrl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := ctx.Query("resource", "")
|
||||||
|
blogSubject := "acct:" + apConfig.PreferredUsername + "@" + domain.Host
|
||||||
|
slog.Info("webfinger request", "for", subject, "required", blogSubject)
|
||||||
|
if subject != blogSubject {
|
||||||
|
return ctx.Status(404).JSON(nil)
|
||||||
|
}
|
||||||
|
|
||||||
webfinger := WebfingerResponse{
|
webfinger := WebfingerResponse{
|
||||||
Subject: ctx.Query("resource"),
|
Subject: subject,
|
||||||
|
|
||||||
Links: []WebfingerLink{
|
Links: []WebfingerLink{
|
||||||
{
|
{
|
||||||
Rel: "self",
|
Rel: "self",
|
||||||
Type: "application/activity+json",
|
Type: "application/activity+json",
|
||||||
Href: siteConfig.FullUrl + "/activitypub/actor",
|
Href: s.apService.ActorUrl(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -82,28 +74,40 @@ func (s *ActivityPubServer) HandleWebfinger(ctx *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ActivityPubServer) Router(router fiber.Router) {
|
func (s *ActivityPubServer) Router(router fiber.Router) {
|
||||||
router.Get("/actor", s.HandleActor)
|
|
||||||
router.Get("/outbox", s.HandleOutbox)
|
router.Get("/outbox", s.HandleOutbox)
|
||||||
|
router.Post("/inbox", s.HandleInbox)
|
||||||
|
router.Get("/followers", s.HandleFollowers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
|
func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
|
||||||
siteConfig := model.SiteConfig{}
|
accepts := (strings.Contains(string(ctx.Request().Header.Peek("Accept")), "application/activity+json") ||
|
||||||
apConfig := ActivityPubConfig{}
|
strings.Contains(string(ctx.Request().Header.Peek("Accept")), "application/ld+json"))
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
req_content := (strings.Contains(string(ctx.Request().Header.Peek("Content-Type")), "application/activity+json") ||
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
strings.Contains(string(ctx.Request().Header.Peek("Content-Type")), "application/ld+json"))
|
||||||
|
if !accepts && !req_content {
|
||||||
|
return ctx.Next()
|
||||||
|
}
|
||||||
|
apConfig, _ := s.apService.GetApConfig()
|
||||||
|
|
||||||
actor := vocab.PersonNew(vocab.IRI(siteConfig.FullUrl + "/activitypub/actor"))
|
actor := vocab.PersonNew(vocab.IRI(s.apService.ActorUrl()))
|
||||||
actor.PreferredUsername = vocab.NaturalLanguageValues{{Value: vocab.Content(apConfig.PreferredUsername)}}
|
actor.PreferredUsername = vocab.NaturalLanguageValues{{Value: vocab.Content(apConfig.PreferredUsername)}}
|
||||||
actor.Inbox = vocab.IRI(siteConfig.FullUrl + "/activitypub/inbox")
|
actor.Inbox = vocab.IRI(s.apService.InboxUrl())
|
||||||
actor.Outbox = vocab.IRI(siteConfig.FullUrl + "/activitypub/outbox")
|
actor.Outbox = vocab.IRI(s.apService.OutboxUrl())
|
||||||
actor.Followers = vocab.IRI(siteConfig.FullUrl + "/activitypub/followers")
|
actor.Followers = vocab.IRI(s.apService.FollowersUrl())
|
||||||
actor.PublicKey = vocab.PublicKey{
|
actor.PublicKey = vocab.PublicKey{
|
||||||
ID: vocab.ID(siteConfig.FullUrl + "/activitypub/actor#main-key"),
|
ID: vocab.IRI(s.apService.MainKeyUri()),
|
||||||
Owner: vocab.IRI(siteConfig.FullUrl + "/activitypub/actor"),
|
Owner: vocab.IRI(s.apService.ActorUrl()),
|
||||||
PublicKeyPem: apConfig.PublicKeyPem,
|
PublicKeyPem: apConfig.PublicKeyPem,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := actor.MarshalJSON()
|
actor.Name = vocab.NaturalLanguageValues{{Value: vocab.Content(s.apService.ActorName())}}
|
||||||
|
actor.Icon = s.apService.ActorIcon()
|
||||||
|
actor.Summary = vocab.NaturalLanguageValues{{Value: vocab.Content(s.apService.ActorSummary())}}
|
||||||
|
|
||||||
|
data, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(vocab.ActivityBaseURI),
|
||||||
|
jsonld.IRI(vocab.SecurityContextURI),
|
||||||
|
).Marshal(actor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -112,10 +116,8 @@ func (s *ActivityPubServer) HandleActor(ctx *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
|
func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, _ := s.siteConfigService.GetSiteConfig()
|
||||||
apConfig := ActivityPubConfig{}
|
// apConfig, _ := s.apService.GetApConfig()
|
||||||
s.configRepo.Get(ACT_PUB_CONF_NAME, &apConfig)
|
|
||||||
s.configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
entries, err := s.entryService.FindAllByType(nil, true, false)
|
entries, err := s.entryService.FindAllByType(nil, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -134,7 +136,7 @@ func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
outbox := vocab.OrderedCollectionNew(vocab.IRI(siteConfig.FullUrl + "/activitypub/outbox"))
|
outbox := vocab.OrderedCollectionNew(vocab.IRI(s.apService.OutboxUrl()))
|
||||||
outbox.TotalItems = uint(len(items))
|
outbox.TotalItems = uint(len(items))
|
||||||
outbox.OrderedItems = items
|
outbox.OrderedItems = items
|
||||||
|
|
||||||
|
@ -144,5 +146,158 @@ func (s *ActivityPubServer) HandleOutbox(ctx *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
ctx.Set("Content-Type", "application/activity+json")
|
ctx.Set("Content-Type", "application/activity+json")
|
||||||
return ctx.Send(data)
|
return ctx.Send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) processFollow(r *http.Request, act *vocab.Activity) error {
|
||||||
|
follower := act.Actor.GetID().String()
|
||||||
|
err := s.apService.VerifySignature(r, follower)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("wrong signature", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.apService.AddFollower(follower)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.apService.Accept(act)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) processUndo(r *http.Request, act *vocab.Activity) error {
|
||||||
|
sender := act.Actor.GetID().String()
|
||||||
|
err := s.apService.VerifySignature(r, sender)
|
||||||
|
|
||||||
|
return vocab.OnObject(act.Object, func(o *vocab.Object) error {
|
||||||
|
if o.Type == vocab.FollowType {
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("wrong signature", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.apService.RemoveFollower(sender)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go s.apService.Accept(act)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if o.Type == vocab.LikeType {
|
||||||
|
return s.apService.RemoveLike(o.ID.String())
|
||||||
|
}
|
||||||
|
if o.Type == vocab.AnnounceType {
|
||||||
|
return s.apService.RemoveRepost(o.ID.String())
|
||||||
|
}
|
||||||
|
slog.Warn("unsupporeted object type for undo", "object", o)
|
||||||
|
return errors.New("unsupporeted object type")
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) processLike(r *http.Request, act *vocab.Activity) error {
|
||||||
|
sender := act.Actor.GetID().String()
|
||||||
|
liked := act.Object.GetID().String()
|
||||||
|
err := s.apService.VerifySignature(r, sender)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("wrong signature", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.apService.AddLike(sender, liked, act.ID.String())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error saving like", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.apService.Accept(act)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) processAnnounce(r *http.Request, act *vocab.Activity) error {
|
||||||
|
sender := act.Actor.GetID().String()
|
||||||
|
liked := act.Object.GetID().String()
|
||||||
|
err := s.apService.VerifySignature(r, sender)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("wrong signature", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.apService.AddRepost(sender, liked, act.ID.String())
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("error saving like", "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.apService.Accept(act)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) processDelete(r *http.Request, act *vocab.Activity) error {
|
||||||
|
return vocab.OnObject(act.Object, func(o *vocab.Object) error {
|
||||||
|
slog.Warn("Not processing delete", "action", act, "object", o)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) HandleInbox(ctx *fiber.Ctx) error {
|
||||||
|
body := ctx.Request().Body()
|
||||||
|
data, err := vocab.UnmarshalJSON(body)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to parse request body", "body", body, "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = vocab.OnActivity(data, func(act *vocab.Activity) error {
|
||||||
|
slog.Info("activity retrieved", "activity", act, "type", act.Type)
|
||||||
|
|
||||||
|
r, err := adaptor.ConvertRequest(ctx, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if act.Type == vocab.FollowType {
|
||||||
|
return s.processFollow(r, act)
|
||||||
|
}
|
||||||
|
if act.Type == vocab.UndoType {
|
||||||
|
return s.processUndo(r, act)
|
||||||
|
}
|
||||||
|
if act.Type == vocab.DeleteType {
|
||||||
|
return s.processDelete(r, act)
|
||||||
|
}
|
||||||
|
if act.Type == vocab.LikeType {
|
||||||
|
return s.processLike(r, act)
|
||||||
|
}
|
||||||
|
if act.Type == vocab.AnnounceType {
|
||||||
|
return s.processAnnounce(r, act)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Warn("Unsupported action", "body", body)
|
||||||
|
|
||||||
|
return errors.New("only follow and undo actions supported")
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ActivityPubServer) HandleFollowers(ctx *fiber.Ctx) error {
|
||||||
|
fs, err := s.apService.AllFollowers()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := vocab.Collection{}
|
||||||
|
for _, f := range fs {
|
||||||
|
followers.Append(vocab.IRI(f))
|
||||||
|
}
|
||||||
|
followers.TotalItems = uint(len(fs))
|
||||||
|
followers.ID = vocab.IRI(s.apService.FollowersUrl())
|
||||||
|
data, err := jsonld.WithContext(
|
||||||
|
jsonld.IRI(vocab.ActivityBaseURI),
|
||||||
|
).Marshal(followers)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx.Set("Content-Type", "application/activity+json")
|
||||||
|
return ctx.Send(data)
|
||||||
|
}
|
||||||
|
|
|
@ -36,7 +36,6 @@ func NewAdminHandler(
|
||||||
func (h *adminHandler) Handle(c *fiber.Ctx) error {
|
func (h *adminHandler) Handle(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := getSiteConfig(h.configRepo)
|
|
||||||
configs := h.configRegister.Configs()
|
configs := h.configRegister.Configs()
|
||||||
|
|
||||||
types := h.typeRegistry.Types()
|
types := h.typeRegistry.Types()
|
||||||
|
@ -53,8 +52,7 @@ func (h *adminHandler) Handle(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(
|
return render.RenderTemplateWithBase(
|
||||||
c, siteConfig,
|
c, "views/admin", &adminContet{
|
||||||
"views/admin", &adminContet{
|
|
||||||
Configs: configs,
|
Configs: configs,
|
||||||
Types: typeNames,
|
Types: typeNames,
|
||||||
},
|
},
|
||||||
|
@ -73,14 +71,13 @@ func (h *adminHandler) HandleConfigGet(c *fiber.Ctx) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
siteConfig := getSiteConfig(h.configRepo)
|
|
||||||
|
|
||||||
htmlForm := config.Form(h.binSvc)
|
htmlForm := config.Form(h.binSvc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(c, siteConfig, "views/admin_config", htmlForm)
|
return render.RenderTemplateWithBase(c, "views/admin_config", htmlForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *adminHandler) HandleConfigPost(c *fiber.Ctx) error {
|
func (h *adminHandler) HandleConfigPost(c *fiber.Ctx) error {
|
||||||
|
@ -92,12 +89,12 @@ func (h *adminHandler) HandleConfigPost(c *fiber.Ctx) error {
|
||||||
return c.SendStatus(404)
|
return c.SendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig, err := config.ParseFormData(c, h.binSvc)
|
err := config.ParseFormData(c, h.binSvc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
h.configRepo.Update(configName, newConfig)
|
h.configRepo.Update(configName, config)
|
||||||
|
|
||||||
return c.Redirect("")
|
return c.Redirect("")
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/render"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
@ -18,6 +19,28 @@ func NewAdminInteractionHandler(configRepo repository.ConfigRepository, interact
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *AdminInteractionHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
|
filter := c.Query("filter", "")
|
||||||
|
|
||||||
|
interactions, err := h.interactionRepo.ListAllInteractions()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pageData := paginate(c, interactions, 50)
|
||||||
|
|
||||||
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
return render.RenderTemplateWithBase(c, "views/interaction_manager", fiber.Map{
|
||||||
|
"Interactions": pageData.items,
|
||||||
|
"Page": pageData.page,
|
||||||
|
"NextPage": pageData.page + 1,
|
||||||
|
"PrevPage": pageData.page - 1,
|
||||||
|
"FirstPage": pageData.page == 1,
|
||||||
|
"LastPage": pageData.lastPage,
|
||||||
|
"Filter": filter,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (h *AdminInteractionHandler) HandleDelete(c *fiber.Ctx) error {
|
func (h *AdminInteractionHandler) HandleDelete(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
|
|
84
web/app.go
84
web/app.go
|
@ -8,7 +8,6 @@ import (
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app/repository"
|
||||||
"owl-blogs/config"
|
"owl-blogs/config"
|
||||||
"owl-blogs/domain/model"
|
|
||||||
"owl-blogs/web/middleware"
|
"owl-blogs/web/middleware"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
@ -34,31 +33,36 @@ func NewWebApp(
|
||||||
authorService *app.AuthorService,
|
authorService *app.AuthorService,
|
||||||
configRepo repository.ConfigRepository,
|
configRepo repository.ConfigRepository,
|
||||||
configRegister *app.ConfigRegister,
|
configRegister *app.ConfigRegister,
|
||||||
|
siteConfigService *app.SiteConfigService,
|
||||||
webmentionService *app.WebmentionService,
|
webmentionService *app.WebmentionService,
|
||||||
interactionRepo repository.InteractionRepository,
|
interactionRepo repository.InteractionRepository,
|
||||||
|
apService *app.ActivityPubService,
|
||||||
) *WebApp {
|
) *WebApp {
|
||||||
app := fiber.New()
|
fiberApp := fiber.New(fiber.Config{
|
||||||
app.Use(middleware.NewUserMiddleware(authorService).Handle)
|
BodyLimit: 50 * 1024 * 1024, // 50MB in bytes
|
||||||
|
DisableStartupMessage: true,
|
||||||
|
})
|
||||||
|
fiberApp.Use(middleware.NewUserMiddleware(authorService).Handle)
|
||||||
|
|
||||||
indexHandler := NewIndexHandler(entryService, configRepo)
|
indexHandler := NewIndexHandler(entryService, siteConfigService)
|
||||||
listHandler := NewListHandler(entryService, configRepo)
|
listHandler := NewListHandler(entryService, siteConfigService)
|
||||||
entryHandler := NewEntryHandler(entryService, typeRegistry, authorService, configRepo, interactionRepo)
|
entryHandler := NewEntryHandler(entryService, typeRegistry, authorService, configRepo, interactionRepo)
|
||||||
mediaHandler := NewMediaHandler(binService)
|
mediaHandler := NewMediaHandler(binService)
|
||||||
rssHandler := NewRSSHandler(entryService, configRepo)
|
rssHandler := NewRSSHandler(entryService, siteConfigService)
|
||||||
loginHandler := NewLoginHandler(authorService, configRepo)
|
loginHandler := NewLoginHandler(authorService, configRepo)
|
||||||
editorHandler := NewEditorHandler(entryService, typeRegistry, binService, configRepo)
|
editorHandler := NewEditorHandler(entryService, typeRegistry, binService, configRepo)
|
||||||
webmentionHandler := NewWebmentionHandler(webmentionService, configRepo)
|
webmentionHandler := NewWebmentionHandler(webmentionService, configRepo)
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
app.Get("/auth/login", loginHandler.HandleGet)
|
fiberApp.Get("/auth/login", loginHandler.HandleGet)
|
||||||
app.Post("/auth/login", loginHandler.HandlePost)
|
fiberApp.Post("/auth/login", loginHandler.HandlePost)
|
||||||
|
|
||||||
// admin
|
// admin
|
||||||
adminHandler := NewAdminHandler(configRepo, configRegister, typeRegistry)
|
adminHandler := NewAdminHandler(configRepo, configRegister, typeRegistry)
|
||||||
draftHandler := NewDraftHandler(entryService, configRepo)
|
draftHandler := NewDraftHandler(entryService, siteConfigService)
|
||||||
binaryManageHandler := NewBinaryManageHandler(configRepo, binService)
|
binaryManageHandler := NewBinaryManageHandler(configRepo, binService)
|
||||||
adminInteractionHandler := NewAdminInteractionHandler(configRepo, interactionRepo)
|
adminInteractionHandler := NewAdminInteractionHandler(configRepo, interactionRepo)
|
||||||
admin := app.Group("/admin")
|
admin := fiberApp.Group("/admin")
|
||||||
admin.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
admin.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
||||||
admin.Get("/", adminHandler.Handle)
|
admin.Get("/", adminHandler.Handle)
|
||||||
admin.Get("/drafts/", draftHandler.Handle)
|
admin.Get("/drafts/", draftHandler.Handle)
|
||||||
|
@ -68,9 +72,13 @@ func NewWebApp(
|
||||||
admin.Post("/binaries/new/", binaryManageHandler.HandleUpload)
|
admin.Post("/binaries/new/", binaryManageHandler.HandleUpload)
|
||||||
admin.Post("/binaries/delete", binaryManageHandler.HandleDelete)
|
admin.Post("/binaries/delete", binaryManageHandler.HandleDelete)
|
||||||
admin.Post("/interactions/:id/delete/", adminInteractionHandler.HandleDelete)
|
admin.Post("/interactions/:id/delete/", adminInteractionHandler.HandleDelete)
|
||||||
|
admin.Get("/interactions/", adminInteractionHandler.HandleGet)
|
||||||
|
|
||||||
|
adminApi := admin.Group("/api")
|
||||||
|
adminApi.Post("/binaries", binaryManageHandler.HandleUploadApi)
|
||||||
|
|
||||||
// Editor
|
// Editor
|
||||||
editor := app.Group("/editor")
|
editor := fiberApp.Group("/editor")
|
||||||
editor.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
editor.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
||||||
editor.Get("/new/:editor/", editorHandler.HandleGetNew)
|
editor.Get("/new/:editor/", editorHandler.HandleGetNew)
|
||||||
editor.Post("/new/:editor/", editorHandler.HandlePostNew)
|
editor.Post("/new/:editor/", editorHandler.HandlePostNew)
|
||||||
|
@ -80,74 +88,62 @@ func NewWebApp(
|
||||||
editor.Post("/unpublish/:id/", editorHandler.HandlePostUnpublish)
|
editor.Post("/unpublish/:id/", editorHandler.HandlePostUnpublish)
|
||||||
|
|
||||||
// SiteConfig
|
// SiteConfig
|
||||||
siteConfig := app.Group("/site-config")
|
siteConfig := fiberApp.Group("/site-config")
|
||||||
siteConfig.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
siteConfig.Use(middleware.NewAuthMiddleware(authorService).Handle)
|
||||||
|
|
||||||
siteConfigHandler := NewSiteConfigHandler(configRepo)
|
siteConfigHandler := NewSiteConfigHandler(siteConfigService)
|
||||||
siteConfig.Get("/", siteConfigHandler.HandleGet)
|
siteConfig.Get("/", siteConfigHandler.HandleGet)
|
||||||
siteConfig.Post("/", siteConfigHandler.HandlePost)
|
siteConfig.Post("/", siteConfigHandler.HandlePost)
|
||||||
|
|
||||||
siteConfigMeHandler := NewSiteConfigMeHandler(configRepo)
|
siteConfigMeHandler := NewSiteConfigMeHandler(siteConfigService)
|
||||||
siteConfig.Get("/me", siteConfigMeHandler.HandleGet)
|
siteConfig.Get("/me", siteConfigMeHandler.HandleGet)
|
||||||
siteConfig.Post("/me/create/", siteConfigMeHandler.HandleCreate)
|
siteConfig.Post("/me/create/", siteConfigMeHandler.HandleCreate)
|
||||||
siteConfig.Post("/me/delete/", siteConfigMeHandler.HandleDelete)
|
siteConfig.Post("/me/delete/", siteConfigMeHandler.HandleDelete)
|
||||||
|
|
||||||
siteConfigListHandler := NewSiteConfigListHandler(configRepo, typeRegistry)
|
siteConfigListHandler := NewSiteConfigListHandler(siteConfigService, typeRegistry)
|
||||||
siteConfig.Get("/lists", siteConfigListHandler.HandleGet)
|
siteConfig.Get("/lists", siteConfigListHandler.HandleGet)
|
||||||
siteConfig.Post("/lists/create/", siteConfigListHandler.HandleCreate)
|
siteConfig.Post("/lists/create/", siteConfigListHandler.HandleCreate)
|
||||||
siteConfig.Post("/lists/delete/", siteConfigListHandler.HandleDelete)
|
siteConfig.Post("/lists/delete/", siteConfigListHandler.HandleDelete)
|
||||||
|
|
||||||
siteConfigMenusHandler := NewSiteConfigMenusHandler(configRepo)
|
siteConfigMenusHandler := NewSiteConfigMenusHandler(siteConfigService)
|
||||||
siteConfig.Get("/menus", siteConfigMenusHandler.HandleGet)
|
siteConfig.Get("/menus", siteConfigMenusHandler.HandleGet)
|
||||||
siteConfig.Post("/menus/create/", siteConfigMenusHandler.HandleCreate)
|
siteConfig.Post("/menus/create/", siteConfigMenusHandler.HandleCreate)
|
||||||
siteConfig.Post("/menus/delete/", siteConfigMenusHandler.HandleDelete)
|
siteConfig.Post("/menus/delete/", siteConfigMenusHandler.HandleDelete)
|
||||||
|
|
||||||
// app.Static("/static/*filepath", http.Dir(repo.StaticDir()))
|
activityPubServer := NewActivityPubServer(siteConfigService, entryService, apService)
|
||||||
app.Use("/static", filesystem.New(filesystem.Config{
|
configRegister.Register(config.ACT_PUB_CONF_NAME, &app.ActivityPubConfig{})
|
||||||
|
|
||||||
|
fiberApp.Use("/static", filesystem.New(filesystem.Config{
|
||||||
Root: http.FS(embedDirStatic),
|
Root: http.FS(embedDirStatic),
|
||||||
PathPrefix: "static",
|
PathPrefix: "static",
|
||||||
Browse: false,
|
Browse: false,
|
||||||
}))
|
}))
|
||||||
app.Get("/", indexHandler.Handle)
|
fiberApp.Get("/", activityPubServer.HandleActor, indexHandler.Handle)
|
||||||
app.Get("/lists/:list/", listHandler.Handle)
|
fiberApp.Get("/lists/:list/", listHandler.Handle)
|
||||||
// Media
|
// Media
|
||||||
app.Get("/media/+", mediaHandler.Handle)
|
fiberApp.Get("/media/+", mediaHandler.Handle)
|
||||||
// RSS
|
// RSS
|
||||||
app.Get("/index.xml", rssHandler.Handle)
|
fiberApp.Get("/index.xml", rssHandler.Handle)
|
||||||
// Posts
|
// Posts
|
||||||
app.Get("/posts/:post/", entryHandler.Handle)
|
fiberApp.Get("/posts/:post/", entryHandler.Handle)
|
||||||
// Webmention
|
// Webmention
|
||||||
app.Post("/webmention/", webmentionHandler.Handle)
|
fiberApp.Post("/webmention/", webmentionHandler.Handle)
|
||||||
// robots.txt
|
// robots.txt
|
||||||
app.Get("/robots.txt", func(c *fiber.Ctx) error {
|
fiberApp.Get("/robots.txt", func(c *fiber.Ctx) error {
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, _ := siteConfigService.GetSiteConfig()
|
||||||
configRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
sitemapUrl, _ := url.JoinPath(siteConfig.FullUrl, "/sitemap.xml")
|
sitemapUrl, _ := url.JoinPath(siteConfig.FullUrl, "/sitemap.xml")
|
||||||
c.Set("Content-Type", "text/plain")
|
c.Set("Content-Type", "text/plain")
|
||||||
return c.SendString(fmt.Sprintf("User-agent: GPTBot\nDisallow: /\n\nUser-agent: *\nAllow: /\n\nSitemap: %s\n", sitemapUrl))
|
return c.SendString(fmt.Sprintf("User-agent: GPTBot\nDisallow: /\n\nUser-agent: *\nAllow: /\n\nSitemap: %s\n", sitemapUrl))
|
||||||
})
|
})
|
||||||
// sitemap.xml
|
// sitemap.xml
|
||||||
app.Get("/sitemap.xml", NewSiteMapHandler(entryService, configRepo).Handle)
|
fiberApp.Get("/sitemap.xml", NewSiteMapHandler(entryService, siteConfigService).Handle)
|
||||||
|
|
||||||
// ActivityPub
|
// ActivityPub
|
||||||
activityPubServer := NewActivityPubServer(configRepo, entryService)
|
fiberApp.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
|
||||||
configRegister.Register(ACT_PUB_CONF_NAME, &ActivityPubConfig{})
|
fiberApp.Route("/activitypub", activityPubServer.Router)
|
||||||
app.Get("/.well-known/webfinger", activityPubServer.HandleWebfinger)
|
|
||||||
app.Route("/activitypub", activityPubServer.Router)
|
|
||||||
|
|
||||||
// Webmention
|
|
||||||
// app.Post("/webmention/", userWebmentionHandler(repo))
|
|
||||||
// Micropub
|
|
||||||
// app.Post("/micropub/", userMicropubHandler(repo))
|
|
||||||
// IndieAuth
|
|
||||||
// app.Get("/auth/", userAuthHandler(repo))
|
|
||||||
// app.Post("/auth/", userAuthProfileHandler(repo))
|
|
||||||
// app.Post("/auth/verify/", userAuthVerifyHandler(repo))
|
|
||||||
// app.Post("/auth/token/", userAuthTokenHandler(repo))
|
|
||||||
// app.Get("/.well-known/oauth-authorization-server", userAuthMetadataHandler(repo))
|
|
||||||
// app.NotFound = http.HandlerFunc(notFoundHandler(repo))
|
|
||||||
return &WebApp{
|
return &WebApp{
|
||||||
FiberApp: app,
|
FiberApp: fiberApp,
|
||||||
EntryService: entryService,
|
EntryService: entryService,
|
||||||
Registry: typeRegistry,
|
Registry: typeRegistry,
|
||||||
BinaryService: binService,
|
BinaryService: binService,
|
||||||
|
|
|
@ -3,6 +3,7 @@ package web
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app/repository"
|
||||||
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -23,9 +24,9 @@ func NewBinaryManageHandler(configRepo repository.ConfigRepository, service *app
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BinaryManageHandler) Handle(c *fiber.Ctx) error {
|
func (h *BinaryManageHandler) Handle(c *fiber.Ctx) error {
|
||||||
siteConfig := getSiteConfig(h.configRepo)
|
filter := c.Query("filter", "")
|
||||||
|
|
||||||
allIds, err := h.service.ListIds()
|
allIds, err := h.service.ListIds(filter)
|
||||||
sort.Slice(allIds, func(i, j int) bool {
|
sort.Slice(allIds, func(i, j int) bool {
|
||||||
return strings.ToLower(allIds[i]) < strings.ToLower(allIds[j])
|
return strings.ToLower(allIds[i]) < strings.ToLower(allIds[j])
|
||||||
})
|
})
|
||||||
|
@ -35,42 +36,60 @@ func (h *BinaryManageHandler) Handle(c *fiber.Ctx) error {
|
||||||
pageData := paginate(c, allIds, 50)
|
pageData := paginate(c, allIds, 50)
|
||||||
|
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
return render.RenderTemplateWithBase(c, siteConfig, "views/binary_manager", fiber.Map{
|
return render.RenderTemplateWithBase(c, "views/binary_manager", fiber.Map{
|
||||||
"Binaries": pageData.items,
|
"Binaries": pageData.items,
|
||||||
"Page": pageData.page,
|
"Page": pageData.page,
|
||||||
"NextPage": pageData.page + 1,
|
"NextPage": pageData.page + 1,
|
||||||
"PrevPage": pageData.page - 1,
|
"PrevPage": pageData.page - 1,
|
||||||
"FirstPage": pageData.page == 1,
|
"FirstPage": pageData.page == 1,
|
||||||
"LastPage": pageData.lastPage,
|
"LastPage": pageData.lastPage,
|
||||||
|
"Filter": filter,
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BinaryManageHandler) HandleUpload(c *fiber.Ctx) error {
|
func (h *BinaryManageHandler) saveFileUpload(c *fiber.Ctx) (*model.BinaryFile, error) {
|
||||||
file, err := c.FormFile("file")
|
file, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
reader, err := file.Open()
|
reader, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
content := make([]byte, file.Size)
|
content := make([]byte, file.Size)
|
||||||
_, err = reader.Read(content)
|
_, err = reader.Read(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
binary, err := h.service.Create(file.Filename, content)
|
binary, err := h.service.Create(file.Filename, content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return binary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BinaryManageHandler) HandleUpload(c *fiber.Ctx) error {
|
||||||
|
binary, err := h.saveFileUpload(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Redirect("/media/" + binary.Id)
|
return c.Redirect("/media/" + binary.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *BinaryManageHandler) HandleUploadApi(c *fiber.Ctx) error {
|
||||||
|
binary, err := h.saveFileUpload(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(map[string]string{
|
||||||
|
"location": "/media/" + binary.Id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *BinaryManageHandler) HandleDelete(c *fiber.Ctx) error {
|
func (h *BinaryManageHandler) HandleDelete(c *fiber.Ctx) error {
|
||||||
id := c.FormValue("file")
|
id := c.FormValue("file")
|
||||||
binary, err := h.service.FindById(id)
|
binary, err := h.service.FindById(id)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -12,17 +11,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type DraftHandler struct {
|
type DraftHandler struct {
|
||||||
configRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
entrySvc *app.EntryService
|
entrySvc *app.EntryService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDraftHandler(
|
func NewDraftHandler(
|
||||||
entryService *app.EntryService,
|
entryService *app.EntryService,
|
||||||
configRepo repository.ConfigRepository,
|
siteConfigService *app.SiteConfigService,
|
||||||
) *DraftHandler {
|
) *DraftHandler {
|
||||||
return &DraftHandler{
|
return &DraftHandler{
|
||||||
entrySvc: entryService,
|
entrySvc: entryService,
|
||||||
configRepo: configRepo,
|
siteConfigService: siteConfigService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +37,10 @@ type DraftRenderData struct {
|
||||||
func (h *DraftHandler) Handle(c *fiber.Ctx) error {
|
func (h *DraftHandler) Handle(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := getSiteConfig(h.configRepo)
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
entries, err := h.entrySvc.FindAllByType(&siteConfig.PrimaryListInclude, false, true)
|
entries, err := h.entrySvc.FindAllByType(&siteConfig.PrimaryListInclude, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -76,7 +78,7 @@ func (h *DraftHandler) Handle(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(c, siteConfig, "views/draft_list", DraftRenderData{
|
return render.RenderTemplateWithBase(c, "views/draft_list", DraftRenderData{
|
||||||
Entries: entries,
|
Entries: entries,
|
||||||
Page: pageNum,
|
Page: pageNum,
|
||||||
NextPage: pageNum + 1,
|
NextPage: pageNum + 1,
|
||||||
|
|
|
@ -48,7 +48,7 @@ func (h *EditorHandler) HandleGetNew(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
htmlForm := entryType.MetaData().Form(h.binSvc)
|
htmlForm := entryType.MetaData().Form(h.binSvc)
|
||||||
return render.RenderTemplateWithBase(c, getSiteConfig(h.configRepo), "views/editor", htmlForm)
|
return render.RenderTemplateWithBase(c, "views/editor", htmlForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *EditorHandler) HandlePostNew(c *fiber.Ctx) error {
|
func (h *EditorHandler) HandlePostNew(c *fiber.Ctx) error {
|
||||||
|
@ -59,7 +59,8 @@ func (h *EditorHandler) HandlePostNew(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
entryMeta, err := entry.MetaData().ParseFormData(c, h.binSvc)
|
entryMeta := entry.MetaData()
|
||||||
|
err = entryMeta.ParseFormData(c, h.binSvc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -93,7 +94,7 @@ func (h *EditorHandler) HandleGetEdit(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlForm := entry.MetaData().Form(h.binSvc)
|
htmlForm := entry.MetaData().Form(h.binSvc)
|
||||||
return render.RenderTemplateWithBase(c, getSiteConfig(h.configRepo), "views/editor", htmlForm)
|
return render.RenderTemplateWithBase(c, "views/editor", htmlForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *EditorHandler) HandlePostEdit(c *fiber.Ctx) error {
|
func (h *EditorHandler) HandlePostEdit(c *fiber.Ctx) error {
|
||||||
|
@ -106,7 +107,8 @@ func (h *EditorHandler) HandlePostEdit(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get form data
|
// get form data
|
||||||
meta, err := entry.MetaData().ParseFormData(c, h.binSvc)
|
meta := entry.MetaData()
|
||||||
|
err = meta.ParseFormData(c, h.binSvc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,6 @@ func (h *EntryHandler) Handle(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(
|
return render.RenderTemplateWithBase(
|
||||||
c,
|
c,
|
||||||
getSiteConfig(h.configRepo),
|
|
||||||
"views/entry",
|
"views/entry",
|
||||||
entryData{
|
entryData{
|
||||||
Entry: entry,
|
Entry: entry,
|
||||||
|
|
|
@ -2,7 +2,6 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -12,17 +11,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type IndexHandler struct {
|
type IndexHandler struct {
|
||||||
configRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
entrySvc *app.EntryService
|
entrySvc *app.EntryService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndexHandler(
|
func NewIndexHandler(
|
||||||
entryService *app.EntryService,
|
entryService *app.EntryService,
|
||||||
configRepo repository.ConfigRepository,
|
siteConfigService *app.SiteConfigService,
|
||||||
) *IndexHandler {
|
) *IndexHandler {
|
||||||
return &IndexHandler{
|
return &IndexHandler{
|
||||||
entrySvc: entryService,
|
entrySvc: entryService,
|
||||||
configRepo: configRepo,
|
siteConfigService: siteConfigService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +37,10 @@ type indexRenderData struct {
|
||||||
func (h *IndexHandler) Handle(c *fiber.Ctx) error {
|
func (h *IndexHandler) Handle(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := getSiteConfig(h.configRepo)
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
entries, err := h.entrySvc.FindAllByType(&siteConfig.PrimaryListInclude, true, false)
|
entries, err := h.entrySvc.FindAllByType(&siteConfig.PrimaryListInclude, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -76,7 +78,7 @@ func (h *IndexHandler) Handle(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(c, siteConfig, "views/index", indexRenderData{
|
return render.RenderTemplateWithBase(c, "views/index", indexRenderData{
|
||||||
Entries: entries,
|
Entries: entries,
|
||||||
Page: pageNum,
|
Page: pageNum,
|
||||||
NextPage: pageNum + 1,
|
NextPage: pageNum + 1,
|
||||||
|
|
|
@ -2,7 +2,6 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -12,17 +11,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListHandler struct {
|
type ListHandler struct {
|
||||||
configRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
entrySvc *app.EntryService
|
entrySvc *app.EntryService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListHandler(
|
func NewListHandler(
|
||||||
entryService *app.EntryService,
|
entryService *app.EntryService,
|
||||||
configRepo repository.ConfigRepository,
|
siteConfigService *app.SiteConfigService,
|
||||||
) *ListHandler {
|
) *ListHandler {
|
||||||
return &ListHandler{
|
return &ListHandler{
|
||||||
entrySvc: entryService,
|
entrySvc: entryService,
|
||||||
configRepo: configRepo,
|
siteConfigService: siteConfigService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +38,10 @@ type listRenderData struct {
|
||||||
func (h *ListHandler) Handle(c *fiber.Ctx) error {
|
func (h *ListHandler) Handle(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := getSiteConfig(h.configRepo)
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
listId := c.Params("list")
|
listId := c.Params("list")
|
||||||
list := model.EntryList{}
|
list := model.EntryList{}
|
||||||
for _, l := range siteConfig.Lists {
|
for _, l := range siteConfig.Lists {
|
||||||
|
@ -87,7 +89,7 @@ func (h *ListHandler) Handle(c *fiber.Ctx) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(c, siteConfig, "views/list", listRenderData{
|
return render.RenderTemplateWithBase(c, "views/list", listRenderData{
|
||||||
List: list,
|
List: list,
|
||||||
Entries: entries,
|
Entries: entries,
|
||||||
Page: pageNum,
|
Page: pageNum,
|
||||||
|
|
|
@ -26,7 +26,7 @@ func NewLoginHandler(
|
||||||
|
|
||||||
func (h *LoginHandler) HandleGet(c *fiber.Ctx) error {
|
func (h *LoginHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
return render.RenderTemplateWithBase(c, getSiteConfig(h.configRepo), "views/login", nil)
|
return render.RenderTemplateWithBase(c, "views/login", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *LoginHandler) HandlePost(c *fiber.Ctx) error {
|
func (h *LoginHandler) HandlePost(c *fiber.Ctx) error {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"net/url"
|
"net/url"
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
@ -104,18 +103,21 @@ func RenderRSSFeed(config model.SiteConfig, entries []model.Entry) (string, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
type RSSHandler struct {
|
type RSSHandler struct {
|
||||||
configRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
entrySvc *app.EntryService
|
entrySvc *app.EntryService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRSSHandler(entryService *app.EntryService, configRepo repository.ConfigRepository) *RSSHandler {
|
func NewRSSHandler(entryService *app.EntryService, siteConfigService *app.SiteConfigService) *RSSHandler {
|
||||||
return &RSSHandler{entrySvc: entryService, configRepo: configRepo}
|
return &RSSHandler{entrySvc: entryService, siteConfigService: siteConfigService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RSSHandler) Handle(c *fiber.Ctx) error {
|
func (h *RSSHandler) Handle(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationXML)
|
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationXML)
|
||||||
|
|
||||||
siteConfig := getSiteConfig(h.configRepo)
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
entries, err := h.entrySvc.FindAllByType(&siteConfig.PrimaryListInclude, true, false)
|
entries, err := h.entrySvc.FindAllByType(&siteConfig.PrimaryListInclude, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,49 +1,43 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app"
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SiteConfigHandler struct {
|
type SiteConfigHandler struct {
|
||||||
siteConfigRepo repository.ConfigRepository
|
svc *app.SiteConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSiteConfigHandler(siteConfigRepo repository.ConfigRepository) *SiteConfigHandler {
|
func NewSiteConfigHandler(svc *app.SiteConfigService) *SiteConfigHandler {
|
||||||
return &SiteConfigHandler{
|
return &SiteConfigHandler{
|
||||||
siteConfigRepo: siteConfigRepo,
|
svc: svc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SiteConfigHandler) HandleGet(c *fiber.Ctx) error {
|
func (h *SiteConfigHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.svc.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(c, getSiteConfig(h.siteConfigRepo), "views/site_config", siteConfig)
|
return render.RenderTemplateWithBase(c, "views/site_config", siteConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SiteConfigHandler) HandlePost(c *fiber.Ctx) error {
|
func (h *SiteConfigHandler) HandlePost(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.svc.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
siteConfig.Title = c.FormValue("Title")
|
siteConfig.Title = c.FormValue("Title")
|
||||||
siteConfig.SubTitle = c.FormValue("SubTitle")
|
siteConfig.SubTitle = c.FormValue("SubTitle")
|
||||||
siteConfig.HeaderColor = c.FormValue("HeaderColor")
|
|
||||||
siteConfig.PrimaryColor = c.FormValue("PrimaryColor")
|
siteConfig.PrimaryColor = c.FormValue("PrimaryColor")
|
||||||
siteConfig.AuthorName = c.FormValue("AuthorName")
|
siteConfig.AuthorName = c.FormValue("AuthorName")
|
||||||
siteConfig.AvatarUrl = c.FormValue("AvatarUrl")
|
siteConfig.AvatarUrl = c.FormValue("AvatarUrl")
|
||||||
|
@ -51,7 +45,7 @@ func (h *SiteConfigHandler) HandlePost(c *fiber.Ctx) error {
|
||||||
siteConfig.HtmlHeadExtra = c.FormValue("HtmlHeadExtra")
|
siteConfig.HtmlHeadExtra = c.FormValue("HtmlHeadExtra")
|
||||||
siteConfig.FooterExtra = c.FormValue("FooterExtra")
|
siteConfig.FooterExtra = c.FormValue("FooterExtra")
|
||||||
|
|
||||||
err = h.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
err = h.svc.UpdateSiteConfig(siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -12,8 +10,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SiteConfigListHandler struct {
|
type SiteConfigListHandler struct {
|
||||||
siteConfigRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
typeRegistry *app.EntryTypeRegistry
|
typeRegistry *app.EntryTypeRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
type siteConfigListTemplateData struct {
|
type siteConfigListTemplateData struct {
|
||||||
|
@ -22,21 +20,19 @@ type siteConfigListTemplateData struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSiteConfigListHandler(
|
func NewSiteConfigListHandler(
|
||||||
siteConfigRepo repository.ConfigRepository,
|
siteConfigService *app.SiteConfigService,
|
||||||
typeRegistry *app.EntryTypeRegistry,
|
typeRegistry *app.EntryTypeRegistry,
|
||||||
) *SiteConfigListHandler {
|
) *SiteConfigListHandler {
|
||||||
return &SiteConfigListHandler{
|
return &SiteConfigListHandler{
|
||||||
siteConfigRepo: siteConfigRepo,
|
siteConfigService: siteConfigService,
|
||||||
typeRegistry: typeRegistry,
|
typeRegistry: typeRegistry,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SiteConfigListHandler) HandleGet(c *fiber.Ctx) error {
|
func (h *SiteConfigListHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -51,7 +47,7 @@ func (h *SiteConfigListHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(
|
return render.RenderTemplateWithBase(
|
||||||
c, getSiteConfig(h.siteConfigRepo), "views/site_config_list", siteConfigListTemplateData{
|
c, "views/site_config_list", siteConfigListTemplateData{
|
||||||
Lists: siteConfig.Lists,
|
Lists: siteConfig.Lists,
|
||||||
Types: types,
|
Types: types,
|
||||||
})
|
})
|
||||||
|
@ -60,8 +56,7 @@ func (h *SiteConfigListHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
func (h *SiteConfigListHandler) HandleCreate(c *fiber.Ctx) error {
|
func (h *SiteConfigListHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -79,7 +74,7 @@ func (h *SiteConfigListHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
ListType: c.FormValue("ListType"),
|
ListType: c.FormValue("ListType"),
|
||||||
})
|
})
|
||||||
|
|
||||||
err = h.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
err = h.siteConfigService.UpdateSiteConfig(siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -90,8 +85,7 @@ func (h *SiteConfigListHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
func (h *SiteConfigListHandler) HandleDelete(c *fiber.Ctx) error {
|
func (h *SiteConfigListHandler) HandleDelete(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -104,7 +98,7 @@ func (h *SiteConfigListHandler) HandleDelete(c *fiber.Ctx) error {
|
||||||
|
|
||||||
siteConfig.Lists = append(siteConfig.Lists[:id], siteConfig.Lists[id+1:]...)
|
siteConfig.Lists = append(siteConfig.Lists[:id], siteConfig.Lists[id+1:]...)
|
||||||
|
|
||||||
err = h.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
err = h.siteConfigService.UpdateSiteConfig(siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app"
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -11,34 +10,31 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SiteConfigMeHandler struct {
|
type SiteConfigMeHandler struct {
|
||||||
siteConfigRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSiteConfigMeHandler(siteConfigRepo repository.ConfigRepository) *SiteConfigMeHandler {
|
func NewSiteConfigMeHandler(siteConfigService *app.SiteConfigService) *SiteConfigMeHandler {
|
||||||
return &SiteConfigMeHandler{
|
return &SiteConfigMeHandler{
|
||||||
siteConfigRepo: siteConfigRepo,
|
siteConfigService: siteConfigService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SiteConfigMeHandler) HandleGet(c *fiber.Ctx) error {
|
func (h *SiteConfigMeHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(
|
return render.RenderTemplateWithBase(
|
||||||
c, getSiteConfig(h.siteConfigRepo), "views/site_config_me", siteConfig.Me)
|
c, "views/site_config_me", siteConfig.Me)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SiteConfigMeHandler) HandleCreate(c *fiber.Ctx) error {
|
func (h *SiteConfigMeHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -49,7 +45,7 @@ func (h *SiteConfigMeHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
Url: c.FormValue("Url"),
|
Url: c.FormValue("Url"),
|
||||||
})
|
})
|
||||||
|
|
||||||
err = h.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
err = h.siteConfigService.UpdateSiteConfig(siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -60,8 +56,7 @@ func (h *SiteConfigMeHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
func (h *SiteConfigMeHandler) HandleDelete(c *fiber.Ctx) error {
|
func (h *SiteConfigMeHandler) HandleDelete(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -73,7 +68,7 @@ func (h *SiteConfigMeHandler) HandleDelete(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
siteConfig.Me = append(siteConfig.Me[:idx], siteConfig.Me[idx+1:]...)
|
siteConfig.Me = append(siteConfig.Me[:idx], siteConfig.Me[idx+1:]...)
|
||||||
|
|
||||||
err = h.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
err = h.siteConfigService.UpdateSiteConfig(siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app/repository"
|
"owl-blogs/app"
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -11,7 +10,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SiteConfigMenusHandler struct {
|
type SiteConfigMenusHandler struct {
|
||||||
siteConfigRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
type siteConfigMenusTemplateData struct {
|
type siteConfigMenusTemplateData struct {
|
||||||
|
@ -19,24 +18,23 @@ type siteConfigMenusTemplateData struct {
|
||||||
FooterMenu []model.MenuItem
|
FooterMenu []model.MenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSiteConfigMenusHandler(siteConfigRepo repository.ConfigRepository) *SiteConfigMenusHandler {
|
func NewSiteConfigMenusHandler(siteConfigService *app.SiteConfigService) *SiteConfigMenusHandler {
|
||||||
return &SiteConfigMenusHandler{
|
return &SiteConfigMenusHandler{
|
||||||
siteConfigRepo: siteConfigRepo,
|
siteConfigService: siteConfigService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SiteConfigMenusHandler) HandleGet(c *fiber.Ctx) error {
|
func (h *SiteConfigMenusHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return render.RenderTemplateWithBase(
|
return render.RenderTemplateWithBase(
|
||||||
c, getSiteConfig(h.siteConfigRepo), "views/site_config_menus", siteConfigMenusTemplateData{
|
c, "views/site_config_menus", siteConfigMenusTemplateData{
|
||||||
HeaderMenu: siteConfig.HeaderMenu,
|
HeaderMenu: siteConfig.HeaderMenu,
|
||||||
FooterMenu: siteConfig.FooterMenu,
|
FooterMenu: siteConfig.FooterMenu,
|
||||||
})
|
})
|
||||||
|
@ -45,8 +43,7 @@ func (h *SiteConfigMenusHandler) HandleGet(c *fiber.Ctx) error {
|
||||||
func (h *SiteConfigMenusHandler) HandleCreate(c *fiber.Ctx) error {
|
func (h *SiteConfigMenusHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -65,7 +62,7 @@ func (h *SiteConfigMenusHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
siteConfig.FooterMenu = append(siteConfig.FooterMenu, menuItem)
|
siteConfig.FooterMenu = append(siteConfig.FooterMenu, menuItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
err = h.siteConfigService.UpdateSiteConfig(siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -76,8 +73,7 @@ func (h *SiteConfigMenusHandler) HandleCreate(c *fiber.Ctx) error {
|
||||||
func (h *SiteConfigMenusHandler) HandleDelete(c *fiber.Ctx) error {
|
func (h *SiteConfigMenusHandler) HandleDelete(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
c.Set(fiber.HeaderContentType, fiber.MIMETextHTML)
|
||||||
|
|
||||||
siteConfig := model.SiteConfig{}
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
err := h.siteConfigRepo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -95,7 +91,7 @@ func (h *SiteConfigMenusHandler) HandleDelete(c *fiber.Ctx) error {
|
||||||
siteConfig.FooterMenu = append(siteConfig.FooterMenu[:idx], siteConfig.FooterMenu[idx+1:]...)
|
siteConfig.FooterMenu = append(siteConfig.FooterMenu[:idx], siteConfig.FooterMenu[idx+1:]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.siteConfigRepo.Update(config.SITE_CONFIG, siteConfig)
|
err = h.siteConfigService.UpdateSiteConfig(siteConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,13 @@ import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"net/url"
|
"net/url"
|
||||||
"owl-blogs/app"
|
"owl-blogs/app"
|
||||||
"owl-blogs/app/repository"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SiteMapHandler struct {
|
type SiteMapHandler struct {
|
||||||
entryService *app.EntryService
|
entryService *app.EntryService
|
||||||
configRepo repository.ConfigRepository
|
siteConfigService *app.SiteConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
type Sitemap struct {
|
type Sitemap struct {
|
||||||
|
@ -25,15 +24,18 @@ type SitemapUrl struct {
|
||||||
Loc string `xml:"loc"`
|
Loc string `xml:"loc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSiteMapHandler(entryService *app.EntryService, configRepo repository.ConfigRepository) *SiteMapHandler {
|
func NewSiteMapHandler(entryService *app.EntryService, siteConfigService *app.SiteConfigService) *SiteMapHandler {
|
||||||
return &SiteMapHandler{entryService: entryService, configRepo: configRepo}
|
return &SiteMapHandler{entryService: entryService, siteConfigService: siteConfigService}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle handles GET /sitemap.xml
|
// Handle handles GET /sitemap.xml
|
||||||
func (h *SiteMapHandler) Handle(c *fiber.Ctx) error {
|
func (h *SiteMapHandler) Handle(c *fiber.Ctx) error {
|
||||||
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationXML)
|
c.Set(fiber.HeaderContentType, fiber.MIMEApplicationXML)
|
||||||
|
|
||||||
siteConfig := getSiteConfig(h.configRepo)
|
siteConfig, err := h.siteConfigService.GetSiteConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
entries, err := h.entryService.FindAllByType(nil, true, false)
|
entries, err := h.entryService.FindAllByType(nil, true, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/**
|
||||||
|
* Add "drop to upload" to a text area
|
||||||
|
* @param {string} id id of a textarea
|
||||||
|
*/
|
||||||
|
function addFileDrop(id) {
|
||||||
|
// deactivate file drop on body
|
||||||
|
// this prevents accidentally opening the file instead uploading
|
||||||
|
document.body.ondrop = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
document.body.ondragover = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// get field
|
||||||
|
const textArea = document.getElementById(id)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a single file and add markdown to textarea
|
||||||
|
* @param {File} file file object to upload
|
||||||
|
*/
|
||||||
|
function processFile(file) {
|
||||||
|
console.log(`name = ${file.name}`);
|
||||||
|
console.log(`size = ${file.size}`);
|
||||||
|
console.log(`type = ${file.type}`);
|
||||||
|
console.log(file)
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append("file", file)
|
||||||
|
|
||||||
|
textArea.classList.add("drop-file-process")
|
||||||
|
fetch(
|
||||||
|
"/admin/api/binaries",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: formData
|
||||||
|
}
|
||||||
|
).then((resp) => {
|
||||||
|
return resp.json()
|
||||||
|
}).then(data => {
|
||||||
|
if (file.type.split("/")[0] == "image") {
|
||||||
|
textArea.value += `\n![](${data.location})`
|
||||||
|
} else {
|
||||||
|
textArea.value += `\n[${file.name}](${data.location})`
|
||||||
|
}
|
||||||
|
textArea.classList.remove("drop-file-process")
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
textArea.classList.add("drop-file-error")
|
||||||
|
setTimeout(() => {
|
||||||
|
textArea.classList.remove("drop-file-error")
|
||||||
|
}, 2000)
|
||||||
|
}).finally(() => {
|
||||||
|
textArea.classList.remove("drop-file-process")
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropHandler(ev) {
|
||||||
|
textArea.classList.remove("drop-file")
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
if (ev.dataTransfer.items) {
|
||||||
|
// Use DataTransferItemList interface to access the file(s)
|
||||||
|
[...ev.dataTransfer.items].forEach((item, i) => {
|
||||||
|
// If dropped items aren't files, reject them
|
||||||
|
if (item.kind === "file") {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
processFile(file)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Use DataTransfer interface to access the file(s)
|
||||||
|
[...ev.dataTransfer.files].forEach((file, i) => {
|
||||||
|
processFile(file)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragOverHandler(ev) {
|
||||||
|
textArea.classList.add("drop-file")
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragLeaveHandler(ev) {
|
||||||
|
textArea.classList.remove("drop-file")
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
textArea.ondrop = dropHandler;
|
||||||
|
textArea.ondragover = dragOverHandler;
|
||||||
|
textArea.ondragleave = dragLeaveHandler;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,486 @@
|
||||||
|
:root {
|
||||||
|
/* font sizes (fs) */
|
||||||
|
/*
|
||||||
|
0 base
|
||||||
|
lX large
|
||||||
|
sX small
|
||||||
|
*/
|
||||||
|
--fs-scale: 1.125;
|
||||||
|
--fs0: 1rem;
|
||||||
|
|
||||||
|
--fsl1: calc(var(--fs0) * var(--fs-scale));
|
||||||
|
--fsl2: calc(var(--fsl1) * var(--fs-scale));
|
||||||
|
--fsl3: calc(var(--fsl2) * var(--fs-scale));
|
||||||
|
--fsl4: calc(var(--fsl3) * var(--fs-scale));
|
||||||
|
|
||||||
|
--fss1: calc(var(--fs0) / var(--fs-scale));
|
||||||
|
--fss2: calc(var(--fss1) / var(--fs-scale));
|
||||||
|
--fss3: calc(var(--fss2) / var(--fs-scale));
|
||||||
|
--fss4: calc(var(--fss3) / var(--fs-scale));
|
||||||
|
|
||||||
|
/* font weight */
|
||||||
|
--fw: 400;
|
||||||
|
--fwb: 700;
|
||||||
|
--fwl: 100;
|
||||||
|
|
||||||
|
/* font color */
|
||||||
|
/* fonts */
|
||||||
|
--font: Arial, Helvetica, sans-serif;
|
||||||
|
--font-h: 'Courier New', Courier, monospace;
|
||||||
|
--font-code: 'Courier New', Courier, monospace;
|
||||||
|
|
||||||
|
|
||||||
|
/* spacings */
|
||||||
|
--spacing-scale: 1.5;
|
||||||
|
--max-spacing: 4rem;
|
||||||
|
--s5: var(--max-spacing);
|
||||||
|
--s4: calc(var(--s5) / var(--spacing-scale));
|
||||||
|
--s3: calc(var(--s4) / var(--spacing-scale));
|
||||||
|
--s2: calc(var(--s3) / var(--spacing-scale));
|
||||||
|
--s1: calc(var(--s2) / var(--spacing-scale));
|
||||||
|
--s0: calc(var(--s1) / var(--spacing-scale));
|
||||||
|
|
||||||
|
/* content-width */
|
||||||
|
--cw: 620px;
|
||||||
|
|
||||||
|
/* colors */
|
||||||
|
--text: hsl(0, 0%, 17%);
|
||||||
|
|
||||||
|
--primary: hsl(200, 25%, 50%);
|
||||||
|
--primary-l1: color-mix(in srgb, var(--primary), #fff 20%);
|
||||||
|
--primary-l2: color-mix(in srgb, var(--primary), #fff 40%);
|
||||||
|
--primary-l3: color-mix(in srgb, var(--primary), #fff 60%);
|
||||||
|
--primary-l4: color-mix(in srgb, var(--primary), #fff 80%);
|
||||||
|
--primary-d1: color-mix(in srgb, var(--primary), #000 20%);
|
||||||
|
--primary-d2: color-mix(in srgb, var(--primary), #000 40%);
|
||||||
|
--primary-d3: color-mix(in srgb, var(--primary), #000 60%);
|
||||||
|
--primary-d4: color-mix(in srgb, var(--primary), #000 80%);
|
||||||
|
|
||||||
|
--secondary: color-mix(in hsl longer hue, var(--primary), var(--primary) 50%);
|
||||||
|
--secondary-l1: color-mix(in srgb, var(--secondary), #fff 20%);
|
||||||
|
--secondary-l2: color-mix(in srgb, var(--secondary), #fff 40%);
|
||||||
|
--secondary-l3: color-mix(in srgb, var(--secondary), #fff 60%);
|
||||||
|
--secondary-l4: color-mix(in srgb, var(--secondary), #fff 80%);
|
||||||
|
--secondary-d1: color-mix(in srgb, var(--secondary), #000 20%);
|
||||||
|
--secondary-d2: color-mix(in srgb, var(--secondary), #000 40%);
|
||||||
|
--secondary-d3: color-mix(in srgb, var(--secondary), #000 60%);
|
||||||
|
--secondary-d4: color-mix(in srgb, var(--secondary), #000 80%);
|
||||||
|
|
||||||
|
--text-primary: color-mix(in srgb, var(--primary), #fff 90%);
|
||||||
|
--text-secondary: color-mix(in srgb, var(--secondary), #fff 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling of main page elements */
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
main {
|
||||||
|
max-width: 100%;
|
||||||
|
padding-left: var(--s1);
|
||||||
|
padding-right: var(--s1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 600px) {
|
||||||
|
main {
|
||||||
|
max-width: var(--cw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
/* global properties*/
|
||||||
|
font-family: var(--font);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
font-family: var(--font-h);
|
||||||
|
margin-top: var(--s4);
|
||||||
|
margin-bottom: var(--s0);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: var(--fsl4);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--fsl3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: var(--fsl2);
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: var(--fsl1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: var(--fs0);
|
||||||
|
font-weight: var(--fwb);
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: var(--fsl4);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
color: var(--primary);
|
||||||
|
border-style: dashed;
|
||||||
|
border-bottom: none;
|
||||||
|
border-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: var(--font-code);
|
||||||
|
background-color: var(--primary-l4);
|
||||||
|
padding: var(--s0);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
main img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* inline elements */
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--primary-d2);
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: 1px dashed;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
background-color: var(--primary-d3);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 0 var(--s0);
|
||||||
|
border-radius: var(--s0);
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background-color: var(--secondary-l3);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: var(--font-code);
|
||||||
|
background-color: var(--primary-l4);
|
||||||
|
padding: 0 var(--s0);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* lists */
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding-left: var(--s3);
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-bottom: var(--s0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
list-style: square;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quote */
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: var(--s4);
|
||||||
|
border-left: solid var(--primary) 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote>footer {
|
||||||
|
width: initial;
|
||||||
|
margin: initial;
|
||||||
|
background-color: initial;
|
||||||
|
color: initial;
|
||||||
|
padding-bottom: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table */
|
||||||
|
|
||||||
|
:where(table) {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
text-indent: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
padding: var(--s0);
|
||||||
|
border-bottom: solid var(--primary) 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(2n) {
|
||||||
|
background: var(--primary-l4);
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--s0);
|
||||||
|
border-bottom: solid var(--primary-l3) 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buttons */
|
||||||
|
a[role='button'] {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[role='button']:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input[type='button'],
|
||||||
|
input[type='submit'],
|
||||||
|
a[role='button'] {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: var(--s1);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--s0);
|
||||||
|
font-size: var(--fs0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover,
|
||||||
|
input[type='button']:hover,
|
||||||
|
input[type='submit']:hover,
|
||||||
|
a[role='button']:hover {
|
||||||
|
background-color: var(--primary-d1);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled,
|
||||||
|
input[type='button']:disabled,
|
||||||
|
input[type='submit']:disabled,
|
||||||
|
a[role='button']:disabled {
|
||||||
|
background-color: var(--primary-l2);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=checkbox]:checked,
|
||||||
|
input[type=radio]:checked,
|
||||||
|
input[type=range] {
|
||||||
|
accent-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* forms */
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
padding-bottom: var(--s0);
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-bottom: var(--s3);
|
||||||
|
padding: var(--s1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: solid var(--primary-l2) 1px;
|
||||||
|
border-radius: var(--s0);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='color'] {
|
||||||
|
padding: 0;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
label>input {
|
||||||
|
display: inherit;
|
||||||
|
width: initial;
|
||||||
|
margin-left: var(--s1);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label>input[type='color'] {
|
||||||
|
min-width: 170px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* button classes */
|
||||||
|
|
||||||
|
button.secondary,
|
||||||
|
input[type='button'].secondary,
|
||||||
|
input[type='submit'].secondary {
|
||||||
|
background-color: var(--secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary:hover,
|
||||||
|
input[type='button'].secondary:hover,
|
||||||
|
input[type='submit'].secondary:hover {
|
||||||
|
background-color: var(--secondary-d1);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.secondary:disabled,
|
||||||
|
input[type='button'].secondary:disabled,
|
||||||
|
input[type='submit'].secondary:disabled {
|
||||||
|
background-color: var(--secondary-l2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Header specific styling */
|
||||||
|
|
||||||
|
header {
|
||||||
|
width: 100vw;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: var(--s4);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
header img {
|
||||||
|
max-height: var(--s5);
|
||||||
|
margin-left: var(--s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
header>hgroup {
|
||||||
|
flex: 1;
|
||||||
|
min-width: calc(var(--cw) / 2);
|
||||||
|
margin-right: var(--s5);
|
||||||
|
margin-left: var(--s1);
|
||||||
|
}
|
||||||
|
|
||||||
|
header>hgroup>h1 {
|
||||||
|
margin-top: var(--s0);
|
||||||
|
margin-bottom: var(--s0);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
header>hgroup>h1>a {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
header>hgroup>h1>a:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
header>hgroup>p {
|
||||||
|
font-size: var(--fsl1);
|
||||||
|
margin-top: var(--s0);
|
||||||
|
margin-bottom: var(--s0);
|
||||||
|
font-family: var(--font);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
header>nav>ul {
|
||||||
|
display: flex;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: var(--s1);
|
||||||
|
padding-bottom: var(--s1);
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
header>nav>ul>li {
|
||||||
|
float: left;
|
||||||
|
margin-right: var(--s1);
|
||||||
|
padding: var(--s1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
header>nav>ul>li>a {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
header>nav>ul>li>a:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer specific styling */
|
||||||
|
|
||||||
|
footer {
|
||||||
|
width: 100vw;
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding-bottom: var(--s5);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer>div {
|
||||||
|
max-width: var(--cw);
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer>nav {
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer>nav>ul {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: var(--s1);
|
||||||
|
padding-bottom: var(--s1);
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer>nav>ul>li {
|
||||||
|
float: left;
|
||||||
|
margin-right: var(--s1);
|
||||||
|
padding: var(--s1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
list-style-type: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,33 @@
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid > * {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.row {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.action-tile-list {
|
.action-tile-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -5,20 +35,71 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-tile {
|
.action-tile {
|
||||||
border: 1px solid var(--background-dark);
|
border: 1px solid var(--primary-l1);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
flex: 1 1 0px;
|
flex: 1 1 0px;
|
||||||
margin: 6px;
|
margin: 6px;
|
||||||
min-width: 20%;
|
min-width: 20%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: var(--background);
|
background: var(--primary-l4);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-tile:hover {
|
.action-tile:hover {
|
||||||
background: var(--background-light);
|
background: var(--primary-l3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger {
|
.danger {
|
||||||
background-color: var(--del-color) !important;
|
background-color: rgb(136, 57, 53) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drop-file {
|
||||||
|
background: #a2dbff;
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 4px;
|
||||||
|
border-color: #5391ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes border-pulsate {
|
||||||
|
0% {
|
||||||
|
border-color: #5391ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
border-color: rgba(0, 255, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
border-color: #5391ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-file-process {
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 4px;
|
||||||
|
border-color: #5391ff;
|
||||||
|
animation: border-pulsate 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes border-pulsate-error {
|
||||||
|
0% {
|
||||||
|
border-color: #ff3e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
border-color: rgba(0, 255, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
border-color: #ff3e3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-file-error {
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 4px;
|
||||||
|
border-color: #ff3e3e;
|
||||||
|
animation: border-pulsate-error 1s 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
12
web/utils.go
12
web/utils.go
|
@ -1,23 +1,11 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/app/repository"
|
|
||||||
"owl-blogs/config"
|
|
||||||
"owl-blogs/domain/model"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getSiteConfig(repo repository.ConfigRepository) model.SiteConfig {
|
|
||||||
siteConfig := model.SiteConfig{}
|
|
||||||
err := repo.Get(config.SITE_CONFIG, &siteConfig)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return siteConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type paginationData[T any] struct {
|
type paginationData[T any] struct {
|
||||||
items []T
|
items []T
|
||||||
page uint
|
page uint
|
||||||
|
|
Loading…
Reference in New Issue