feat(dashboard): add opengraph/oembed, Docker build

This commit is contained in:
spiral 2022-06-03 01:01:54 -04:00
parent d956bd4577
commit 15d48db6f3
No known key found for this signature in database
GPG Key ID: 244A11E4B0BCF40E
12 changed files with 253 additions and 1 deletions

View File

@ -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
View 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
View 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

View File

@ -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

View File

@ -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)
{ {

View File

@ -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)
{ {

View File

@ -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)
{ {

View File

@ -1,2 +1,3 @@
dist/ dist/
node_modules/ node_modules/
dashboard

5
dashboard/go.mod Normal file
View 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
View 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=

View File

@ -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
View 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)
}