Skip to content

Commit

Permalink
Enable OEmbed in custom Templates
Browse files Browse the repository at this point in the history
  • Loading branch information
benpate committed Nov 8, 2024
1 parent fb4f8af commit 7a499a9
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 9 deletions.
185 changes: 177 additions & 8 deletions handler/oembed.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,191 @@
package handler

import (
"github.com/EmissarySocial/emissary/server"
"net/url"
"regexp"
"strings"

"github.com/EmissarySocial/emissary/build"
"github.com/EmissarySocial/emissary/domain"
"github.com/EmissarySocial/emissary/model"
"github.com/benpate/derp"
"github.com/labstack/echo/v4"
"github.com/benpate/rosetta/convert"
"github.com/benpate/rosetta/mapof"
"github.com/benpate/steranko"
)

// GetOEmbed will provide an OEmbed service to be used exclusively by websites on this domain.
func GetOEmbed(factoryManager *server.Factory) echo.HandlerFunc {
func GetOEmbed(ctx *steranko.Context, factory *domain.Factory) error {

const location = "handler.GetOEmbed"

// Verify that the URL is valid
token := ctx.QueryParam("url")
format := ctx.QueryParam("format")

parsedToken, err := url.Parse(token)

if err != nil {
return derp.Wrap(err, location, "Invalid URL")
}

// Verify that the URL is on this domain
if parsedToken.Host != factory.Hostname() {
return derp.NewNotFoundError(location, "Invalid URL", "URL does not match domain")
}

// Load the OEmbed result
result, err := getOEmbed_record(ctx, factory, parsedToken.Path)

if err != nil {
return derp.Wrap(err, location, "Error loading OEmbed record")
}

// Return the result in the requested format
if format == "xml" {
return ctx.XML(200, result)
}

return ctx.JSON(200, result)
}

func getOEmbed_record(ctx *steranko.Context, factory *domain.Factory, path string) (mapof.Any, error) {

// Parse the path as either a Stream or a User
path = strings.TrimPrefix(path, "/")

// If the path begins with "@", then it is a User
if strings.HasPrefix(path, "@") {
path = strings.TrimPrefix(path, "@")
return getOEmbed_User(factory, path)
}

// Otherwise, the path is for a Stream
return getOEmbed_Stream(ctx, factory, path)
}

func getOEmbed_Stream(ctx *steranko.Context, factory *domain.Factory, token string) (mapof.Any, error) {

const location = "handler.getOEmbed_Stream"

// Load the Stream
streamService := factory.Stream()
stream := model.NewStream()

if err := streamService.LoadByToken(token, &stream); err != nil {
return mapof.Any{}, derp.Wrap(err, location, "Error loading stream from database")
}

// Export the stream as an OEmbed object
// Export the user as an OEmbed object
// Get the domain
domain := factory.Domain().Get()

// Export the user as an OEmbed object
result := mapof.Any{
"version": "1.0",
"type": "link",
"title": stream.Label,
"cache_age": 86400, // cache for 24 hours
"provider_name": domain.Label,
"provider_url": domain.Host(),
}

if iconURL := stream.IconURL; iconURL != "" {
result["thumbnail_url"] = iconURL + ".webp?height=300&width=300"
result["thumbnail_height"] = 300
result["thumbnail_width"] = 300
}

// Special case for Templates that define HTML content of OEmbed
templateService := factory.Template()
if template, err := templateService.Load(stream.TemplateID); err == nil {

if htmlTemplate := template.GetOEmbed(); htmlTemplate != nil {

if builder, err := build.NewStream(factory, ctx.Request(), ctx.Response(), template, &stream, "view"); err == nil {

html := executeTemplate(htmlTemplate, builder)

if html != "" {

// Enable this line for nice-ish debugging
// return nil, ctx.HTML(200, html)

result["html"] = html
result["type"] = "rich"

return func(ctx echo.Context) error {
height, width := getOEmbed_heightAndWidth(html)

factory, err := factoryManager.ByContext(ctx)
if height > 0 {
result["height"] = height
}

if err != nil {
return derp.Wrap(err, "handlers.GetOEmbed", "Can't get domain")
if width > 0 {
result["width"] = width
}
}
}
}
}

return result, nil
}

func getOEmbed_User(factory *domain.Factory, token string) (mapof.Any, error) {

const location = "handler.getOEmbed_User"

// Load the User
userService := factory.User()
user := model.NewUser()

if err := userService.LoadByToken(token, &user); err != nil {
return mapof.Any{}, derp.Wrap(err, location, "Error loading user from database")
}

// Get the domain
domain := factory.Domain().Get()

// Export the user as an OEmbed object
result := mapof.Any{
"version": "1.0",
"type": "link",
"title": "@" + domain.Hostname + "@" + user.Username,
"cache_age": 86400, // cache for 24 hours
"provider_name": domain.Label,
"provider_url": domain.Host(),
}

return ctx.JSON(200, factory.Hostname())
if iconURL := user.ActivityPubIconURL(); iconURL != "" {
result["thumbnail_url"] = iconURL + ".webp?height=300&width=300"
result["thumbnail_height"] = 300
result["thumbnail_width"] = 300
}

return result, nil
}

func getOEmbed_heightAndWidth(html string) (int, int) {

var height int
var width int

// Find height
findHeight := regexp.MustCompile(`height:\s*(\d+)px;`)
heightStrings := findHeight.FindStringSubmatch(html)

if len(heightStrings) == 2 {
height = convert.Int(heightStrings[1])
}

// Find width
findWidth := regexp.MustCompile(`width:\s*(\d+)px;`)
widthStrings := findWidth.FindStringSubmatch(html)

if len(widthStrings) == 2 {
width = convert.Int(widthStrings[1])
}

return height, width
}
36 changes: 36 additions & 0 deletions handler/oembed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package handler

import (
"regexp"
"testing"

"github.com/benpate/rosetta/convert"
"github.com/stretchr/testify/require"
)

func TestOEmbed(t *testing.T) {

var height int
var width int

html := `<html><div style="max-height:100px; max-width:200px;">Here's some stuff</div></html>`
findWidth := regexp.MustCompile(`max-width:\s*(\d+)px;`)
findHeight := regexp.MustCompile(`max-height:\s*(\d+)px;`)

heightStrings := findHeight.FindStringSubmatch(html)
widthStrings := findWidth.FindStringSubmatch(html)

if len(heightStrings) == 2 {
height = convert.Int(heightStrings[1])
}

t.Log(height)
require.Equal(t, 100, height)

if len(widthStrings) == 2 {
width = convert.Int(widthStrings[1])
}

t.Log(width)
require.Equal(t, 200, width)
}
10 changes: 10 additions & 0 deletions handler/utilities.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package handler

import (
"bytes"
"html/template"
"net/http"
"net/url"

Expand Down Expand Up @@ -128,3 +130,11 @@ func inlineError(ctx echo.Context, errorMessage string) error {

return ctx.String(http.StatusOK, errorMessage)
}

func executeTemplate(template *template.Template, data any) string {
var buffer bytes.Buffer
if err := template.Execute(&buffer, data); err != nil {
return ""
}
return buffer.String()
}
1 change: 1 addition & 0 deletions model/domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (domain *Domain) HasRegistrationForm() bool {
return domain.RegistrationID != ""
}

// Host returns a usable URL for this domain, including the HTTP(S) protocol and hostname
func (domain *Domain) Host() string {
return domainlib.Protocol(domain.Hostname) + domain.Hostname
}
Expand Down
15 changes: 15 additions & 0 deletions model/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,18 @@ func (template *Template) Inherit(parent *Template) {
}
}
}

/******************************************
* OEmbed Methods
******************************************/

// HasOEmbed returns TRUE if this Template has an OEmbed template
func (template *Template) HasOEmbed() bool {
return template.HTMLTemplate.Lookup("oembed") != nil
}

// GetOEmbed returns the OEmbed template for this Template
// If no OEmbed template is found, then nil is returned
func (template *Template) GetOEmbed() *template.Template {
return template.HTMLTemplate.Lookup("oembed")
}
2 changes: 1 addition & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func makeStandardRoutes(factory *server.Factory, e *echo.Echo) {
e.GET("/.well-known/host-meta", handler.GetHostMeta(factory))
e.GET("/.well-known/host-meta.json", handler.GetHostMetaJSON(factory))
e.GET("/.well-known/nodeinfo", handler.GetNodeInfo(factory))
e.GET("/.well-known/oembed", handler.GetOEmbed(factory))
e.GET("/.well-known/oembed", handler.WithFactory(factory, handler.GetOEmbed))
e.GET("/.well-known/webfinger", handler.GetWebfinger(factory))
e.GET("/nodeinfo/2.0", handler.GetNodeInfo20(factory))
e.GET("/nodeinfo/2.1", handler.GetNodeInfo21(factory))
Expand Down

0 comments on commit 7a499a9

Please sign in to comment.