feat(dashboard): add opengraph/oembed, Docker build
This commit is contained in:
parent
d956bd4577
commit
15d48db6f3
@ -11,6 +11,7 @@
|
|||||||
!.git
|
!.git
|
||||||
!proto
|
!proto
|
||||||
!scripts/run-clustered.sh
|
!scripts/run-clustered.sh
|
||||||
|
!dashboard
|
||||||
|
|
||||||
# Re-exclude host build artifact directories
|
# Re-exclude host build artifact directories
|
||||||
**/bin
|
**/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;
|
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
|
public struct FrontersReturnNew
|
||||||
|
@ -97,6 +97,21 @@ public class GroupControllerV2: PKControllerBase
|
|||||||
return Ok(group.ToJson(ContextFor(group), system.Hid));
|
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}")]
|
[HttpPatch("groups/{groupRef}")]
|
||||||
public async Task<IActionResult> DoGroupPatch(string groupRef, [FromBody] JObject data)
|
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));
|
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}")]
|
[HttpPatch("members/{memberRef}")]
|
||||||
public async Task<IActionResult> DoMemberPatch(string memberRef, [FromBody] JObject data)
|
public async Task<IActionResult> DoMemberPatch(string memberRef, [FromBody] JObject data)
|
||||||
{
|
{
|
||||||
|
@ -20,6 +20,16 @@ public class SystemControllerV2: PKControllerBase
|
|||||||
return Ok(system.ToJson(ContextFor(system)));
|
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}")]
|
[HttpPatch("{systemRef}")]
|
||||||
public async Task<IActionResult> DoSystemPatch(string systemRef, [FromBody] JObject data)
|
public async Task<IActionResult> DoSystemPatch(string systemRef, [FromBody] JObject data)
|
||||||
{
|
{
|
||||||
|
3
dashboard/.gitignore
vendored
3
dashboard/.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
dist/
|
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" />
|
<link rel="icon" href="./myriad.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PluralKit | home</title>
|
<title>PluralKit | home</title>
|
||||||
|
<!-- extra data -->
|
||||||
<link rel="stylesheet" href="/styles/generic.scss" />
|
<link rel="stylesheet" href="/styles/generic.scss" />
|
||||||
<link rel="stylesheet" href="/styles/dark.scss" />
|
<link rel="stylesheet" href="/styles/dark.scss" />
|
||||||
<link rel="stylesheet" href="/styles/light.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