feat(dashboard): add opengraph/oembed, Docker build
This commit is contained in:
parent
d956bd4577
commit
15d48db6f3
@ -11,6 +11,7 @@
|
||||
!.git
|
||||
!proto
|
||||
!scripts/run-clustered.sh
|
||||
!dashboard
|
||||
|
||||
# Re-exclude host build artifact directories
|
||||
**/bin
|
||||
|
34
.github/workflows/dashboard
vendored
Normal file
34
.github/workflows/dashboard
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: Build dashboard Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'dashboard/'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
if: github.repository == 'xSke/PluralKit'
|
||||
steps:
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.CR_PAT }}
|
||||
- uses: actions/checkout@v2
|
||||
- run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV
|
||||
- uses: docker/build-push-action@v2
|
||||
with:
|
||||
# https://github.com/docker/build-push-action/issues/378
|
||||
context: .
|
||||
file: Dockerfile.dashboard
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/pluralkit/dashboard:${{ env.BRANCH_NAME }}
|
||||
ghcr.io/pluralkit/dashboard:${{ github.sha }}
|
||||
ghcr.io/pluralkit/dashboard:latest
|
||||
cache-from: type=registry,ref=ghcr.io/pluralkit/dashboard:${{ env.BRANCH_NAME }}
|
||||
cache-to: type=inline
|
17
Dockerfile.dashboard
Normal file
17
Dockerfile.dashboard
Normal file
@ -0,0 +1,17 @@
|
||||
FROM alpine:latest as builder
|
||||
|
||||
RUN apk add nodejs-current yarn go git
|
||||
|
||||
COPY dashboard/ /build
|
||||
WORKDIR /build
|
||||
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn build
|
||||
|
||||
RUN go build -ldflags "-X dashboard/main.version=$(git rev-parse HEAD)"
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
COPY --from=builder /build/dashboard /bin/dashboard
|
||||
|
||||
ENTRYPOINT /bin/dashboard
|
@ -25,6 +25,18 @@ public static class APIJsonExt
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
public static JObject EmbedJson(string title, string type)
|
||||
{
|
||||
var o = new JObject();
|
||||
|
||||
o.Add("type", "rich");
|
||||
o.Add("provider_name", "PluralKit " + type);
|
||||
o.Add("provider_url", "https://pluralkit.me");
|
||||
o.Add("title", title);
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
||||
|
||||
public struct FrontersReturnNew
|
||||
|
@ -97,6 +97,21 @@ public class GroupControllerV2: PKControllerBase
|
||||
return Ok(group.ToJson(ContextFor(group), system.Hid));
|
||||
}
|
||||
|
||||
[HttpGet("groups/{groupRef}/oembed.json")]
|
||||
public async Task<IActionResult> GroupEmbed(string groupRef)
|
||||
{
|
||||
var group = await ResolveGroup(groupRef);
|
||||
if (group == null)
|
||||
throw Errors.GroupNotFound;
|
||||
var system = await _repo.GetSystem(group.System);
|
||||
|
||||
var name = group.NameFor(LookupContext.ByNonOwner);
|
||||
if (system.Name != null)
|
||||
name += $" ({system.Name})";
|
||||
|
||||
return Ok(APIJsonExt.EmbedJson(name, "Group"));
|
||||
}
|
||||
|
||||
[HttpPatch("groups/{groupRef}")]
|
||||
public async Task<IActionResult> DoGroupPatch(string groupRef, [FromBody] JObject data)
|
||||
{
|
||||
|
@ -79,6 +79,21 @@ public class MemberControllerV2: PKControllerBase
|
||||
return Ok(member.ToJson(ContextFor(member), systemStr: system.Hid));
|
||||
}
|
||||
|
||||
[HttpGet("members/{memberRef}/oembed.json")]
|
||||
public async Task<IActionResult> MemberEmbed(string memberRef)
|
||||
{
|
||||
var member = await ResolveMember(memberRef);
|
||||
if (member == null)
|
||||
throw Errors.MemberNotFound;
|
||||
var system = await _repo.GetSystem(member.System);
|
||||
|
||||
var name = member.NameFor(LookupContext.ByNonOwner);
|
||||
if (system.Name != null)
|
||||
name += $" ({system.Name})";
|
||||
|
||||
return Ok(APIJsonExt.EmbedJson(name, "Member"));
|
||||
}
|
||||
|
||||
[HttpPatch("members/{memberRef}")]
|
||||
public async Task<IActionResult> DoMemberPatch(string memberRef, [FromBody] JObject data)
|
||||
{
|
||||
|
@ -20,6 +20,16 @@ public class SystemControllerV2: PKControllerBase
|
||||
return Ok(system.ToJson(ContextFor(system)));
|
||||
}
|
||||
|
||||
[HttpGet("{systemRef}/oembed.json")]
|
||||
public async Task<IActionResult> SystemEmbed(string systemRef)
|
||||
{
|
||||
var system = await ResolveSystem(systemRef);
|
||||
if (system == null)
|
||||
throw Errors.SystemNotFound;
|
||||
|
||||
return Ok(APIJsonExt.EmbedJson(system.Name ?? $"System with ID `{system.Hid}`", "System"));
|
||||
}
|
||||
|
||||
[HttpPatch("{systemRef}")]
|
||||
public async Task<IActionResult> DoSystemPatch(string systemRef, [FromBody] JObject data)
|
||||
{
|
||||
|
3
dashboard/.gitignore
vendored
3
dashboard/.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
node_modules/
|
||||
dashboard
|
5
dashboard/go.mod
Normal file
5
dashboard/go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module dashboard
|
||||
|
||||
go 1.18
|
||||
|
||||
require github.com/go-chi/chi v1.5.4 // indirect
|
2
dashboard/go.sum
Normal file
2
dashboard/go.sum
Normal file
@ -0,0 +1,2 @@
|
||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
@ -5,6 +5,7 @@
|
||||
<link rel="icon" href="./myriad.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PluralKit | home</title>
|
||||
<!-- extra data -->
|
||||
<link rel="stylesheet" href="/styles/generic.scss" />
|
||||
<link rel="stylesheet" href="/styles/dark.scss" />
|
||||
<link rel="stylesheet" href="/styles/light.scss" />
|
||||
|
139
dashboard/main.go
Normal file
139
dashboard/main.go
Normal file
@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
var fs embed.FS
|
||||
|
||||
type entity struct {
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
IconURL *string `json:"icon_url"`
|
||||
Description *string `json:"description"`
|
||||
Color *string `json:"color"`
|
||||
}
|
||||
|
||||
var baseURL = "https://api.pluralkit.me/v2"
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.Header().Set("X-PluralKit-Version", version)
|
||||
next.ServeHTTP(rw, r)
|
||||
})
|
||||
})
|
||||
|
||||
r.NotFound(notFoundHandler)
|
||||
|
||||
r.Get("/profile/{type}/{id}", func(rw http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if a := recover(); a != nil {
|
||||
notFoundHandler(rw, r)
|
||||
return
|
||||
}
|
||||
}()
|
||||
createEmbed(rw, r)
|
||||
})
|
||||
|
||||
http.ListenAndServe(":8080", r)
|
||||
}
|
||||
|
||||
func notFoundHandler(rw http.ResponseWriter, r *http.Request) {
|
||||
var data []byte
|
||||
var err error
|
||||
|
||||
// lol
|
||||
if strings.HasSuffix(r.URL.Path, ".js") {
|
||||
data, err = fs.ReadFile("dist" + r.URL.Path)
|
||||
rw.Header().Add("content-type", "application/javascript")
|
||||
} else if strings.HasSuffix(r.URL.Path, ".css") {
|
||||
data, err = fs.ReadFile("dist" + r.URL.Path)
|
||||
rw.Header().Add("content-type", "text/css")
|
||||
} else if strings.HasSuffix(r.URL.Path, ".map") {
|
||||
data, err = fs.ReadFile("dist" + r.URL.Path)
|
||||
} else {
|
||||
data, err = fs.ReadFile("dist/index.html")
|
||||
rw.Header().Add("content-type", "text/html")
|
||||
data = []byte(strings.Replace(string(data), `<!-- extra data -->`, "", 1))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rw.Write(data)
|
||||
}
|
||||
|
||||
// explanation for createEmbed:
|
||||
// we don't care about errors, we just want to return a HTML page as soon as possible
|
||||
// `panic(nil)` is caught by upstream, which then returns the raw HTML page
|
||||
|
||||
func createEmbed(rw http.ResponseWriter, r *http.Request) {
|
||||
entityType := chi.URLParam(r, "type")
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var path string
|
||||
|
||||
switch entityType {
|
||||
case "s":
|
||||
path = "/systems/" + id
|
||||
case "m":
|
||||
path = "/members/" + id
|
||||
case "g":
|
||||
path = "/groups/" + id
|
||||
default:
|
||||
panic(nil)
|
||||
}
|
||||
|
||||
res, err := http.Get(baseURL + path)
|
||||
if err != nil {
|
||||
panic(nil)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
panic(nil)
|
||||
}
|
||||
|
||||
var data entity
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
panic(nil)
|
||||
}
|
||||
|
||||
text := fmt.Sprintf(`<link type="application/json+oembed" href="%s/%s/oembed.json" />%s`, baseURL, path, "\n")
|
||||
|
||||
if data.AvatarURL != nil {
|
||||
text += fmt.Sprintf(`<meta content='%s' property='og:image'>%s`, *data.AvatarURL, "\n")
|
||||
} else if data.IconURL != nil {
|
||||
text += fmt.Sprintf(`<meta content='%s' property='og:image'>%s`, *data.IconURL, "\n")
|
||||
}
|
||||
|
||||
if data.Description != nil {
|
||||
text += fmt.Sprintf(`<meta content="%s" property="og:description">%s`, *data.Description, "\n")
|
||||
}
|
||||
|
||||
if data.Color != nil {
|
||||
text += fmt.Sprintf(`<meta name="theme-color" content="#%s">%s`, *data.Color, "\n")
|
||||
}
|
||||
|
||||
html, err := fs.ReadFile("dist/index.html")
|
||||
if err != nil {
|
||||
panic(nil)
|
||||
}
|
||||
html = []byte(strings.Replace(string(html), `<!-- extra data -->`, text, 1))
|
||||
|
||||
rw.Header().Add("content-type", "text/html")
|
||||
rw.Write(html)
|
||||
}
|
Loading…
Reference in New Issue
Block a user