🚧 Reorganize some packages

This commit is contained in:
Maxim Lebedev 2024-02-15 11:44:45 +06:00
parent a509c797ba
commit b1beb17e0e
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
14 changed files with 216 additions and 239 deletions

View File

@ -0,0 +1,72 @@
package http
import (
"encoding/json"
"net/http"
"time"
"github.com/go-ap/activitypub"
"source.toby3d.me/toby3d/home/internal/common"
"source.toby3d.me/toby3d/home/internal/domain"
)
type (
Handler struct{}
/* TODO(toby3d)
Profile struct {
*activitypub.Person
// Mastodon related
// Pinned posts. See [Featured collection].
//
// [Featured collection]: https://docs.joinmastodon.org/spec/activitypub/#featured
Featured []interface{} `json:"featured"`
// Required for Move activity.
AlsoKnownAs []string `json:"alsoKnownAs"`
// Will be shown as a locked account.
ManuallyApprovesFollowers bool `json:"manuallyApprovesFollowers"`
// Will be shown in the profile directory.
// See [Discoverability flag].
//
// [Discoverability flag]: https://docs.joinmastodon.org/spec/activitypub/#discoverable
Discoverable bool `json:"discoverable"`
}
*/
)
func NewHandler() *Handler {
return &Handler{}
}
func (Handler) HandleProfile(site *domain.Site) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
langRef := activitypub.LangRef(site.DefaultLanguage.Lang())
person := activitypub.PersonNew(activitypub.IRI(site.BaseURL.String()))
person.URL = person.ID
person.Name.Add(activitypub.LangRefValueNew(langRef, "Maxim Lebedev"))
person.Summary.Add(activitypub.LangRefValueNew(langRef, "Creative dude from russia"))
person.PreferredUsername.Add(activitypub.LangRefValueNew(langRef, "toby3d"))
person.Published = time.Date(2009, time.February, 0, 0, 0, 0, 0, time.UTC)
w.Header().Set(common.HeaderContentType, common.MIMEApplicationActivityJSONCharsetUTF8)
_ = json.NewEncoder(w).Encode(person)
})
}
func (Handler) HandleEntry(site *domain.Site, entry *domain.Entry) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
resp := activitypub.ObjectNew(activitypub.NoteType)
resp.ID = activitypub.ID(site.BaseURL.JoinPath(entry.File.Path()).String())
resp.Content.Add(activitypub.LangRefValueNew(activitypub.LangRef(entry.Language.Lang()),
string(entry.Content)))
w.Header().Set(common.HeaderContentType, common.MIMEApplicationActivityJSONCharsetUTF8)
_ = json.NewEncoder(w).Encode(resp)
})
}

View File

@ -12,9 +12,9 @@ import (
"strings"
"time"
activitypubhttpdelivery "source.toby3d.me/toby3d/home/internal/activitypub/delivery/http"
"source.toby3d.me/toby3d/home/internal/common"
"source.toby3d.me/toby3d/home/internal/domain"
entryhttpdelivery "source.toby3d.me/toby3d/home/internal/entry/delivery/http"
pagefsrepo "source.toby3d.me/toby3d/home/internal/entry/repository/fs"
pageucase "source.toby3d.me/toby3d/home/internal/entry/usecase"
"source.toby3d.me/toby3d/home/internal/middleware"
@ -28,6 +28,7 @@ import (
statichttpdelivery "source.toby3d.me/toby3d/home/internal/static/delivery/http"
staticfsrepo "source.toby3d.me/toby3d/home/internal/static/repository/fs"
staticucase "source.toby3d.me/toby3d/home/internal/static/usecase"
themehttpdelivery "source.toby3d.me/toby3d/home/internal/theme/delivery/http"
themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs"
themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase"
"source.toby3d.me/toby3d/home/internal/urlutil"
@ -64,7 +65,8 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
resourceHandler := resourcehttpdelivery.NewHandler(resourcer, contentDir)
staticHandler := statichttpdelivery.NewHandler(staticer)
siteHandler := sitehttpdelivery.NewHandler(siter)
entryHandler := entryhttpdelivery.NewHandler(themer)
themeHandler := themehttpdelivery.NewHandler(themer)
activityPubHadnler := activitypubhttpdelivery.NewHandler()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// NOTE(toby3d): any file in $HOME_STATIC_DIR is public and
// unprotected by design, so it's safe to search it first before
@ -97,10 +99,11 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return
}
mediaType, _, _ := mime.ParseMediaType(r.Header.Get(common.HeaderAccept))
if handler != nil {
mediaType, _, _ := mime.ParseMediaType(r.Header.Get(common.HeaderAccept))
if strings.EqualFold(mediaType, common.MIMEApplicationLdJSON) {
entryHandler.ActivityPubHandler.HandleProfile(site).ServeHTTP(w, r)
activityPubHadnler.HandleProfile(site).ServeHTTP(w, r)
} else {
handler.ServeHTTP(w, r)
}
@ -125,7 +128,12 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
return
}
entryHandler.Handle(site, entry).ServeHTTP(w, r)
switch strings.ToLower(mediaType) {
default:
themeHandler.Handle(site, entry).ServeHTTP(w, r)
case common.MIMEApplicationLdJSON:
activityPubHadnler.HandleEntry(site, entry).ServeHTTP(w, r)
}
})
chain := middleware.Chain{
// middleware.LogFmt(),

View File

@ -1,140 +0,0 @@
package http
import (
"encoding/json"
"mime"
"net/http"
"strings"
"time"
"github.com/go-ap/activitypub"
"source.toby3d.me/toby3d/home/internal/common"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/theme"
)
type (
Handler struct {
*ActivityPubHandler
*TemplateHandler
}
ActivityPubHandler struct{}
TemplateHandler struct {
themes theme.UseCase
}
/* TODO(toby3d)
Profile struct {
*activitypub.Person
// Mastodon related
// Pinned posts. See [Featured collection].
//
// [Featured collection]: https://docs.joinmastodon.org/spec/activitypub/#featured
Featured []interface{} `json:"featured"`
// Required for Move activity.
AlsoKnownAs []string `json:"alsoKnownAs"`
// Will be shown as a locked account.
ManuallyApprovesFollowers bool `json:"manuallyApprovesFollowers"`
// Will be shown in the profile directory.
// See [Discoverability flag].
//
// [Discoverability flag]: https://docs.joinmastodon.org/spec/activitypub/#discoverable
Discoverable bool `json:"discoverable"`
}
*/
)
func NewHandler(themes theme.UseCase) *Handler {
return &Handler{
ActivityPubHandler: NewActivityPubHandler(),
TemplateHandler: NewTemplateHandler(themes),
}
}
func (h *Handler) Handle(site *domain.Site, entry *domain.Entry) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mediaType, _, _ := mime.ParseMediaType(r.Header.Get(common.HeaderAccept))
switch strings.ToLower(mediaType) {
default:
h.TemplateHandler.Handle(site, entry).ServeHTTP(w, r)
case common.MIMEApplicationLdJSON:
h.ActivityPubHandler.Handle(site, entry).ServeHTTP(w, r)
}
})
}
func NewTemplateHandler(themes theme.UseCase) *TemplateHandler {
return &TemplateHandler{themes: themes}
}
func (h *TemplateHandler) Handle(site *domain.Site, entry *domain.Entry) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO(toby3d): handle home page.
// TODO(toby3d): handle sections.
// TODO(toby3d): handle errors.
// NOTE(toby3d): wrap founded entry into theme template and
// answer to client.
contentLanguage := make([]string, len(entry.Translations))
for i := range entry.Translations {
contentLanguage[i] = entry.Translations[i].Language.Code()
}
w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", "))
template, err := h.themes.Do(r.Context(), site, entry)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
if err = template(w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
}
func NewActivityPubHandler() *ActivityPubHandler {
return &ActivityPubHandler{}
}
func (ActivityPubHandler) HandleProfile(site *domain.Site) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(common.HeaderContentType, common.MIMEApplicationActivityJSONCharsetUTF8)
encoder := json.NewEncoder(w)
langRef := activitypub.LangRef(site.DefaultLanguage.Lang())
person := activitypub.PersonNew(activitypub.IRI(site.BaseURL.String()))
person.URL = person.ID
person.Name.Add(activitypub.LangRefValueNew(langRef, "Maxim Lebedev"))
person.Summary.Add(activitypub.LangRefValueNew(langRef, "Creative dude from russia"))
person.PreferredUsername.Add(activitypub.LangRefValueNew(langRef, "toby3d"))
person.Published = time.Date(2009, time.February, 0, 0, 0, 0, 0, time.UTC)
_ = encoder.Encode(person)
})
}
func (ActivityPubHandler) Handle(site *domain.Site, entry *domain.Entry) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(common.HeaderContentType, common.MIMEApplicationActivityJSONCharsetUTF8)
encoder := json.NewEncoder(w)
resp := activitypub.ObjectNew(activitypub.NoteType)
resp.ID = activitypub.ID(site.BaseURL.JoinPath(entry.File.Path()).String())
resp.Content.Add(activitypub.LangRefValueNew(activitypub.NilLangRef, string(entry.Content)))
_ = encoder.Encode(resp)
})
}

View File

@ -1,42 +0,0 @@
package http_test
import (
"net/http"
"net/http/httptest"
"testing"
"source.toby3d.me/toby3d/home/internal/common"
"source.toby3d.me/toby3d/home/internal/domain"
delivery "source.toby3d.me/toby3d/home/internal/entry/delivery/http"
entryucase "source.toby3d.me/toby3d/home/internal/entry/usecase"
siteucase "source.toby3d.me/toby3d/home/internal/site/usecase"
"source.toby3d.me/toby3d/home/internal/testutil"
)
func TestHandler_ServeHTTP(t *testing.T) {
t.Parallel()
testEntry := domain.TestEntry(t)
for name, path := range map[string]string{
"person": "/",
"note": testEntry.File.Path(),
} {
name, path := name, path
t.Run(name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(http.MethodGet, "https://example.com/"+path, nil)
req.Header.Set(common.HeaderAccept, common.MIMEApplicationLdJSONProfile)
w := httptest.NewRecorder()
delivery.NewHandler(
siteucase.NewStubSiteUseCase(domain.TestSite(t), nil),
entryucase.NewStubEntryUseCase(domain.TestEntry(t), nil),
).ServeHTTP(w, req)
testutil.GoldenEqual(t, w.Result().Body)
})
}
}

View File

@ -1 +0,0 @@
{"id":"https://example.com/en/sample-page","type":"Note","content":"Hello, world!"}

View File

@ -1 +0,0 @@
{"id":"https://example.com/","type":"Person","name":"Maxim Lebedev","summary":"Creative dude from russia","url":"https://example.com/","published":"2009-01-31T00:00:00Z","preferredUsername":"toby3d"}

View File

@ -0,0 +1,73 @@
package memory
import (
"context"
"errors"
"fmt"
"io/fs"
"sync"
"testing/fstest"
"github.com/adrg/frontmatter"
"gopkg.in/yaml.v2"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/entry"
)
type (
Page struct {
Title string `yaml:"title"`
Description string `yaml:"description"`
Params map[string]any `yaml:",inline"`
Content []byte `yaml:"-"`
}
memoryEntryRepository struct {
mutex *sync.RWMutex
files fstest.MapFS
}
)
var FrontMatterFormats = []*frontmatter.Format{
frontmatter.NewFormat(`---`, `---`, yaml.Unmarshal),
}
func (repo *memoryEntryRepository) Get(_ context.Context, lang domain.Language, p string) (*domain.Entry, error) {
f, err := repo.files.Open(p)
if err != nil {
return nil, fmt.Errorf("cannot get entry from memory: %w", err)
}
defer f.Close()
data := &Page{
Params: make(map[string]any),
}
if data.Content, err = frontmatter.Parse(f, data, FrontMatterFormats...); err != nil {
return nil, fmt.Errorf("cannot parse entry content as FrontMatter: %w", err)
}
return &domain.Entry{
File: domain.NewPath(p),
Language: lang,
Title: data.Title,
Content: data.Content,
Description: data.Description,
Params: data.Params,
Resources: make([]*domain.Resource, 0),
Translations: make([]*domain.Entry, 0),
}, nil
}
func (repo *memoryEntryRepository) Stat(_ context.Context, l domain.Language, p string) (bool, error) {
_, err := fs.Stat(repo.files, p)
return errors.Is(err, fs.ErrExist), nil
}
func NewMemoryEntryRepository(files fstest.MapFS) entry.Repository {
return &memoryEntryRepository{
mutex: new(sync.RWMutex),
files: files,
}
}

View File

@ -1,41 +0,0 @@
package memory
import (
"context"
"errors"
"fmt"
"io/fs"
"sync"
"testing/fstest"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/entry"
)
type memoryEntryRepository struct {
mutex *sync.RWMutex
files fstest.MapFS
}
func (repo *memoryEntryRepository) Get(_ context.Context, _ domain.Language, p string) (*domain.Page, error) {
f, err := repo.files.Open(p)
if err != nil {
return nil, fmt.Errorf("cannot get entry from memory: %w", err)
}
defer f.Close()
return nil, nil
}
func (repo *memoryEntryRepository) Stat(_ context.Context, l domain.Language, p string) (bool, error) {
_, err := fs.Stat(repo.files, p)
return errors.Is(err, fs.ErrExist), nil
}
func NewMemoryEntryRepository(files fstest.MapFS) entry.Repository {
return &memoryEntryRepository{
mutex: new(sync.RWMutex),
files: files,
}
}

View File

@ -15,7 +15,7 @@ func TestDo(t *testing.T) {
site := domain.TestSite(t)
actual, err := usecase.NewServerUseCase().Do(context.Background(), *site)
actual, err := usecase.NewServerUseCase().Do(context.Background(), site)
if err != nil {
t.Fatal(err)
}

View File

@ -0,0 +1,49 @@
package http
import (
"net/http"
"strings"
"source.toby3d.me/toby3d/home/internal/common"
"source.toby3d.me/toby3d/home/internal/domain"
"source.toby3d.me/toby3d/home/internal/theme"
)
type Handler struct {
themes theme.UseCase
}
func NewHandler(themes theme.UseCase) *Handler {
return &Handler{
themes: themes,
}
}
func (h *Handler) Handle(site *domain.Site, entry *domain.Entry) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO(toby3d): handle home page.
// TODO(toby3d): handle sections.
// TODO(toby3d): handle errors.
// NOTE(toby3d): wrap founded entry into theme template and
// answer to client.
contentLanguage := make([]string, len(entry.Translations))
for i := range entry.Translations {
contentLanguage[i] = entry.Translations[i].Language.Code()
}
w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", "))
template, err := h.themes.Do(r.Context(), site, entry)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
if err = template(w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
}

View File

@ -43,7 +43,7 @@ func NewBaseOf(site *domain.Site) BaseOf {
{% endfunc %}
{% func (b BaseOf) Dir() %}
{%s b.site.Language.Dir() %}
{%s b.site.Language.Dir().String() %}
{% endfunc %}
{% func Template(p Pager) %}

View File

@ -206,7 +206,7 @@ func (b BaseOf) Lang() string {
//line web/template/baseof.qtpl:45
func (b BaseOf) StreamDir(qw422016 *qt422016.Writer) {
//line web/template/baseof.qtpl:46
qw422016.E().S(b.site.Language.Dir())
qw422016.E().S(b.site.Language.Dir().String())
//line web/template/baseof.qtpl:47
}

View File

@ -5,10 +5,10 @@
{% code
type Page struct {
BaseOf
page *domain.Page
page *domain.Entry
}
func NewPage(base BaseOf, page *domain.Page) Page {
func NewPage(base BaseOf, page *domain.Entry) Page {
return Page{
BaseOf: base,
page: page,
@ -35,7 +35,7 @@ func NewPage(base BaseOf, page *domain.Page) Page {
{% func (p Page) Dir() %}
{% if p.page.Language != domain.LanguageUnd %}
{%s p.page.Language.Dir() %}
{%s p.page.Language.Dir().String() %}
{% else %}
{%= p.BaseOf.Lang() %}
{% endif %}

View File

@ -25,10 +25,10 @@ var (
//line web/template/page.qtpl:6
type Page struct {
BaseOf
page *domain.Page
page *domain.Entry
}
func NewPage(base BaseOf, page *domain.Page) Page {
func NewPage(base BaseOf, page *domain.Entry) Page {
return Page{
BaseOf: base,
page: page,
@ -122,7 +122,7 @@ func (p Page) StreamDir(qw422016 *qt422016.Writer) {
//line web/template/page.qtpl:37
if p.page.Language != domain.LanguageUnd {
//line web/template/page.qtpl:38
qw422016.E().S(p.page.Language.Dir())
qw422016.E().S(p.page.Language.Dir().String())
//line web/template/page.qtpl:39
} else {
//line web/template/page.qtpl:40