WIP rebuilding incoming webmention from v1 code

This commit is contained in:
Niko Abeler 2023-08-08 21:32:24 +02:00
parent b1c46a86aa
commit 6ab9af2d53
10 changed files with 249 additions and 4 deletions

13
app/owlhttp/interface.go Normal file
View File

@ -0,0 +1,13 @@
package owlhttp
import (
"io"
"net/http"
"net/url"
)
type HttpClient interface {
Get(url string) (resp *http.Response, err error)
Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
PostForm(url string, data url.Values) (resp *http.Response, err error)
}

View File

@ -2,6 +2,7 @@ package app
import (
"math/rand"
"strings"
)
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
@ -13,3 +14,12 @@ func RandStringRunes(n int) string {
}
return string(b)
}
func UrlToEntryId(url string) string {
parts := strings.Split(url, "/")
if parts[len(parts)-1] == "" {
return parts[len(parts)-2]
} else {
return parts[len(parts)-1]
}
}

View File

@ -1,18 +1,166 @@
package app
import "owl-blogs/app/repository"
import (
"bytes"
"errors"
"io"
"net/http"
"owl-blogs/app/owlhttp"
"owl-blogs/app/repository"
"owl-blogs/interactions"
"strings"
"time"
"golang.org/x/net/html"
)
type WebmentionService struct {
InteractionRepository repository.InteractionRepository
EntryRepository repository.EntryRepository
Http owlhttp.HttpClient
}
type ParsedHEntry struct {
Title string
}
func NewWebmentionService(
interactionRepository repository.InteractionRepository,
entryRepository repository.EntryRepository,
http owlhttp.HttpClient,
) *WebmentionService {
return &WebmentionService{
InteractionRepository: interactionRepository,
EntryRepository: entryRepository,
Http: http,
}
}
func readResponseBody(resp *http.Response) (string, error) {
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(bodyBytes), nil
}
func collectText(n *html.Node, buf *bytes.Buffer) {
if n.Type == html.TextNode {
buf.WriteString(n.Data)
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
collectText(c, buf)
}
}
func (WebmentionService) ParseHEntry(resp *http.Response) (ParsedHEntry, error) {
htmlStr, err := readResponseBody(resp)
if err != nil {
return ParsedHEntry{}, err
}
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
return ParsedHEntry{}, err
}
var interpretHFeed func(*html.Node, *ParsedHEntry, bool) (ParsedHEntry, error)
interpretHFeed = func(n *html.Node, curr *ParsedHEntry, parent bool) (ParsedHEntry, error) {
attrs := n.Attr
for _, attr := range attrs {
if attr.Key == "class" && strings.Contains(attr.Val, "p-name") {
buf := &bytes.Buffer{}
collectText(n, buf)
curr.Title = buf.String()
return *curr, nil
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
interpretHFeed(c, curr, false)
}
return *curr, nil
}
var findHFeed func(*html.Node) (ParsedHEntry, error)
findHFeed = func(n *html.Node) (ParsedHEntry, error) {
attrs := n.Attr
for _, attr := range attrs {
if attr.Key == "class" && strings.Contains(attr.Val, "h-entry") {
return interpretHFeed(n, &ParsedHEntry{}, true)
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
entry, err := findHFeed(c)
if err == nil {
return entry, nil
}
}
return ParsedHEntry{}, errors.New("no h-entry found")
}
return findHFeed(doc)
}
func (s *WebmentionService) GetExistingWebmention(entryId string, source string, target string) (*interactions.Webmention, error) {
inters, err := s.InteractionRepository.FindAll(entryId)
if err != nil {
return nil, err
}
for _, interaction := range inters {
if webm, ok := interaction.(*interactions.Webmention); ok {
m := webm.MetaData().(interactions.WebmentionInteractionMetaData)
if m.Source == source && m.Target == target {
return webm, nil
}
}
}
return nil, nil
}
func (s *WebmentionService) ProcessWebmention(source string, target string) error {
resp, err := s.Http.Get(source)
if err != nil {
return err
}
hEntry, err := s.ParseHEntry(resp)
if err != nil {
return err
}
entryId := UrlToEntryId(target)
_, err = s.EntryRepository.FindById(entryId)
if err != nil {
return err
}
webmention, err := s.GetExistingWebmention(entryId, source, target)
if err != nil {
return err
}
if webmention != nil {
data := interactions.WebmentionInteractionMetaData{
Source: source,
Target: target,
Title: hEntry.Title,
}
webmention.SetMetaData(data)
webmention.SetEntryID(entryId)
webmention.SetCreatedAt(time.Now())
err = s.InteractionRepository.Update(webmention)
return err
} else {
webmention = &interactions.Webmention{}
data := interactions.WebmentionInteractionMetaData{
Source: source,
Target: target,
Title: hEntry.Title,
}
webmention.SetMetaData(data)
webmention.SetEntryID(entryId)
webmention.SetCreatedAt(time.Now())
err = s.InteractionRepository.Create(webmention)
return err
}
}

View File

@ -49,12 +49,15 @@ func App(db infra.Database) *web.WebApp {
siteConfigRepo := infra.NewConfigRepo(db)
interactionRepo := infra.NewInteractionRepo(db, interactionRegister)
// Create External Services
httpClient := &infra.OwlHttpClient{}
// Create Services
entryService := app.NewEntryService(entryRepo)
binaryService := app.NewBinaryFileService(binRepo)
authorService := app.NewAuthorService(authorRepo, siteConfigRepo)
webmentionService := app.NewWebmentionService(
interactionRepo, entryRepo,
interactionRepo, entryRepo, httpClient,
)
// Create WebApp

5
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
github.com/yuin/goldmark v1.5.4
golang.org/x/crypto v0.11.0
golang.org/x/crypto v0.12.0
gopkg.in/yaml.v2 v2.4.0
)
@ -37,6 +37,7 @@ require (
github.com/valyala/fasthttp v1.47.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

6
go.sum
View File

@ -76,6 +76,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
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=
@ -85,6 +87,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
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=
@ -101,6 +105,8 @@ 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.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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=

5
infra/http.go Normal file
View File

@ -0,0 +1,5 @@
package infra
import "net/http"
type OwlHttpClient = http.Client

View File

@ -10,6 +10,7 @@ type Webmention struct {
type WebmentionInteractionMetaData struct {
Source string
Target string
Title string
}
func (i *Webmention) Content() model.InteractionContent {

View File

@ -46,6 +46,7 @@ func NewWebApp(
rssHandler := NewRSSHandler(entryService, configRepo)
loginHandler := NewLoginHandler(authorService, configRepo)
editorHandler := NewEditorHandler(entryService, typeRegistry, binService, configRepo)
webmentionHandler := NewWebmentionHandler(webmentionService, configRepo)
// Login
app.Get("/auth/login", loginHandler.HandleGet)
@ -112,6 +113,8 @@ func NewWebApp(
app.Get("/index.xml", rssHandler.Handle)
// Posts
app.Get("/posts/:post/", entryHandler.Handle)
// Webmention
app.Post("/webmention/", webmentionHandler.Handle)
// robots.txt
app.Get("/robots.txt", func(c *fiber.Ctx) error {
siteConfig := model.SiteConfig{}

55
web/webmention_handler.go Normal file
View File

@ -0,0 +1,55 @@
package web
import (
"owl-blogs/app"
"owl-blogs/app/repository"
"github.com/gofiber/fiber/v2"
)
type WebmentionHandler struct {
configRepo repository.ConfigRepository
webmentionService *app.WebmentionService
}
func NewWebmentionHandler(
webmentionService *app.WebmentionService,
configRepo repository.ConfigRepository,
) *WebmentionHandler {
return &WebmentionHandler{
webmentionService: webmentionService,
configRepo: configRepo,
}
}
func (h *WebmentionHandler) Handle(c *fiber.Ctx) error {
target := c.FormValue("target")
source := c.FormValue("source")
if target == "" {
return c.Status(400).SendString("target is required")
}
if source == "" {
return c.Status(400).SendString("source is required")
}
if len(target) < 7 || (target[:7] != "http://" && target[:8] != "https://") {
return c.Status(400).SendString("target must be a valid URL")
}
if len(source) < 7 || (source[:7] != "http://" && source[:8] != "https://") {
return c.Status(400).SendString("source must be a valid URL")
}
if source == target {
return c.Status(400).SendString("source and target must be different")
}
err := h.webmentionService.ProcessWebmention(source, target)
if err != nil {
return err
}
return c.SendString("ok")
}