-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
240 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters