From f60033a3e46309998bdbe6bf1daf93ab5cf142e1 Mon Sep 17 00:00:00 2001 From: noe Date: Tue, 25 Mar 2025 21:26:24 -0700 Subject: [PATCH] interactions yay!! --- default.nix | 2 +- discord/api.go | 8 +++ discord/guild.go | 11 ++++ discord/guild_mock.go | 21 +++++++ discord/guildservice.go | 14 +++++ discord/guildservice_mock.go | 21 +++++++ discord/member.go | 20 +++++++ discord/member_mock.go | 17 ++++++ go.mod | 1 + go.sum | 2 + interactions/cmd_helloworld.go | 5 ++ interactions/cmd_helloworld_test.go | 16 +++++ interactions/interactions.go | 74 +++++++++++++++++++++++ interactions/interactions_test.go | 85 ++++++++++++++++++++++++++ interactions/responses.go | 24 ++++++++ interactions/router.go | 38 ++++++++++++ interactions/router_test.go | 16 +++++ interactions/types.go | 50 ++++++++++++++++ interactions/utils_test.go | 93 +++++++++++++++++++++++++++++ interactions/verify.go | 40 +++++++++++++ interactions/verify_test.go | 46 ++++++++++++++ justfile | 1 + main.go | 46 +------------- roleypoly/fiber.go | 51 ++++++++++++++++ templates/embed.go | 6 ++ templates/picker/main.html | 1 + testing/testing.go | 14 +++++ types/api_error.go | 22 +++++++ types/member.go | 8 +++ types/user.go | 7 +++ 30 files changed, 716 insertions(+), 44 deletions(-) create mode 100644 discord/api.go create mode 100644 discord/guild.go create mode 100644 discord/guild_mock.go create mode 100644 discord/guildservice.go create mode 100644 discord/guildservice_mock.go create mode 100644 discord/member.go create mode 100644 discord/member_mock.go create mode 100644 interactions/cmd_helloworld.go create mode 100644 interactions/cmd_helloworld_test.go create mode 100644 interactions/interactions.go create mode 100644 interactions/interactions_test.go create mode 100644 interactions/responses.go create mode 100644 interactions/router.go create mode 100644 interactions/router_test.go create mode 100644 interactions/types.go create mode 100644 interactions/utils_test.go create mode 100644 interactions/verify.go create mode 100644 interactions/verify_test.go create mode 100644 roleypoly/fiber.go create mode 100644 templates/embed.go create mode 100644 templates/picker/main.html create mode 100644 testing/testing.go create mode 100644 types/api_error.go create mode 100644 types/member.go create mode 100644 types/user.go diff --git a/default.nix b/default.nix index 46ed616..3d47038 100644 --- a/default.nix +++ b/default.nix @@ -1,6 +1,6 @@ { pkgs ? import {}, - vendorHash ? "sha256-dnYDdGjQHLcHD78EFoUqMcACXUuKBSK3FWHalWjCd9c=", + vendorHash ? "sha256-OxPbXF7ouilQUCp8I76gqoq3t9PPYrUQukgxliACqlI=", }: rec { default = roleypoly; diff --git a/discord/api.go b/discord/api.go new file mode 100644 index 0000000..29d6895 --- /dev/null +++ b/discord/api.go @@ -0,0 +1,8 @@ +package discord + +type IDiscordClient interface { +} + +func Get(url string) { + +} diff --git a/discord/guild.go b/discord/guild.go new file mode 100644 index 0000000..205a4d2 --- /dev/null +++ b/discord/guild.go @@ -0,0 +1,11 @@ +package discord + +import "context" + +type IGuild interface { + GetMember(ctx context.Context, memberID string) (IMember, error) +} + +type Guild struct { + ID string +} diff --git a/discord/guild_mock.go b/discord/guild_mock.go new file mode 100644 index 0000000..4a9fb9a --- /dev/null +++ b/discord/guild_mock.go @@ -0,0 +1,21 @@ +package discord + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +type GuildMock struct { + mock.Mock +} + +func (g *GuildMock) GetMember(ctx context.Context, memberID string) (IMember, error) { + args := g.Called(ctx, memberID) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(IMember), args.Error(1) +} diff --git a/discord/guildservice.go b/discord/guildservice.go new file mode 100644 index 0000000..3067d7b --- /dev/null +++ b/discord/guildservice.go @@ -0,0 +1,14 @@ +package discord + +import "context" + +type IGuildService interface { + GetGuild(ctx context.Context, guildID string) (IGuild, error) +} + +type GuildService struct { +} + +func (gs *GuildService) GetGuild(ctx context.Context, guildID string) (IGuild, error) { + return nil, nil +} diff --git a/discord/guildservice_mock.go b/discord/guildservice_mock.go new file mode 100644 index 0000000..36c67b0 --- /dev/null +++ b/discord/guildservice_mock.go @@ -0,0 +1,21 @@ +package discord + +import ( + "context" + + "github.com/stretchr/testify/mock" +) + +type GuildServiceMock struct { + mock.Mock +} + +func (gs *GuildServiceMock) GetGuild(ctx context.Context, guildID string) (IGuild, error) { + args := gs.Called(ctx, guildID) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(IGuild), args.Error(1) +} diff --git a/discord/member.go b/discord/member.go new file mode 100644 index 0000000..d3849c4 --- /dev/null +++ b/discord/member.go @@ -0,0 +1,20 @@ +package discord + +import "git.sapphic.engineer/roleypoly/v4/types" + +type IMember interface { + GetRoles() error + PatchRoles() error +} + +type Member struct { + types.DiscordMember +} + +func (m *Member) GetRoles() error { + return nil +} + +func (m *Member) PatchRoles() error { + return nil +} diff --git a/discord/member_mock.go b/discord/member_mock.go new file mode 100644 index 0000000..5a68725 --- /dev/null +++ b/discord/member_mock.go @@ -0,0 +1,17 @@ +package discord + +import "github.com/stretchr/testify/mock" + +type MemberMock struct { + mock.Mock +} + +func (m *MemberMock) GetRoles() error { + args := m.Called() + return args.Error(0) +} + +func (m *MemberMock) PatchRoles() error { + args := m.Called() + return args.Error(0) +} diff --git a/go.mod b/go.mod index 5f6c00f..287f903 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tinylib/msgp v1.2.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.58.0 // indirect diff --git a/go.sum b/go.sum index c79b732..8543bfb 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= diff --git a/interactions/cmd_helloworld.go b/interactions/cmd_helloworld.go new file mode 100644 index 0000000..78064e5 --- /dev/null +++ b/interactions/cmd_helloworld.go @@ -0,0 +1,5 @@ +package interactions + +func (i *Interactions) CmdHelloWorld(ix Interaction) (InteractionResponse, error) { + return String("Hello world!", true) +} diff --git a/interactions/cmd_helloworld_test.go b/interactions/cmd_helloworld_test.go new file mode 100644 index 0000000..6af2065 --- /dev/null +++ b/interactions/cmd_helloworld_test.go @@ -0,0 +1,16 @@ +package interactions_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCmdHelloWorld(t *testing.T) { + i, _, _ := makeInteractions(t) + ix := mkInteraction("hello-world") + + ir, err := i.CmdHelloWorld(ix) + assert.Nil(t, err) + assert.Equal(t, "Hello world!", ir.Data.Content) +} diff --git a/interactions/interactions.go b/interactions/interactions.go new file mode 100644 index 0000000..7ec7dbb --- /dev/null +++ b/interactions/interactions.go @@ -0,0 +1,74 @@ +package interactions + +import ( + "crypto/ed25519" + "log" + "net/http" + "os" + + "github.com/gofiber/fiber/v3" + + "git.sapphic.engineer/roleypoly/v4/discord" + "git.sapphic.engineer/roleypoly/v4/types" +) + +type Interactions struct { + PublicKey ed25519.PublicKey + Guilds discord.IGuildService + Router *InteractionRouter + + // if interactions are able to be used + OK bool +} + +func NewInteractions(publicKey string, guildsService discord.IGuildService) *Interactions { + if publicKey == "" { + publicKey = os.Getenv("DISCORD_PUBLIC_KEY") + } + + return &Interactions{ + PublicKey: ed25519.PublicKey(publicKey), + Guilds: guildsService, + Router: NewInteractionRouter(), + OK: len(publicKey) != 0, + } +} + +func (i *Interactions) Routes(r fiber.Router) { + r.Post("/", i.PostHandler) + + i.Router.Add("hello-world", i.CmdHelloWorld) +} + +func (i *Interactions) PostHandler(c fiber.Ctx) error { + if !i.OK { + return types.NewAPIError(http.StatusUnauthorized, "interactions not enabled").Send(c) + } + + signature := c.Get("X-Signature-Ed25519") + timestamp := c.Get("X-Signature-Timestamp") + err := i.Verify(signature, timestamp, c.BodyRaw()) + if err != nil { + log.Println("interactions: verification failed, ", err) + return types.NewAPIError(http.StatusUnauthorized, "verification failed").Send(c) + } + + ix := Interaction{} + err = c.Bind().JSON(&ix) + if err != nil { + log.Println("interactions: json body parse failed, ", err) + return types.NewAPIError(http.StatusBadRequest, "bad request").Send(c) + } + + if ix.Type == TypePing { + return c.JSON(InteractionResponse{ + Type: ResponsePong, + }) + } + + if ix.Type == TypeApplicationCommand { + return i.Router.Handle(c, ix) + } + + return types.NewAPIError(http.StatusNotImplemented, "not implemented").Send(c) +} diff --git a/interactions/interactions_test.go b/interactions/interactions_test.go new file mode 100644 index 0000000..1d01916 --- /dev/null +++ b/interactions/interactions_test.go @@ -0,0 +1,85 @@ +package interactions_test + +import ( + "bytes" + "crypto/ed25519" + "encoding/hex" + "net/http" + "testing" + + "git.sapphic.engineer/roleypoly/v4/discord" + "git.sapphic.engineer/roleypoly/v4/interactions" + "git.sapphic.engineer/roleypoly/v4/roleypoly" + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/assert" +) + +func TestNewInteractions(t *testing.T) { + pub, _, _ := ed25519.GenerateKey(nil) + key := hex.EncodeToString(pub) + t.Setenv("DISCORD_PUBLIC_KEY", key) + + gsm := &discord.GuildServiceMock{} + + i := interactions.NewInteractions("", gsm) + assert.True(t, i.OK, "interactions OK") + assert.Equal(t, string(i.PublicKey), key, "public key fetched from environment") + + i = interactions.NewInteractions(key, gsm) + assert.True(t, i.OK, "interactions OK") + assert.Equal(t, string(i.PublicKey), key, "public key accepted from args") +} + +func TestPostHandler(t *testing.T) { + i, _, key := makeInteractions(t) + app := fiber.New() + i.Routes(app) + + body, err := json.Marshal(map[string]any{ + "type": 1, + }) + if err != nil { + t.Error(err) + return + } + + sig, ts := sign(t, key, body) + + req, _ := http.NewRequest("POST", "/", bytes.NewBuffer(body)) + req.Header.Add("X-Signature-Ed25519", sig) + req.Header.Add("X-Signature-Timestamp", ts) + + res, err := app.Test(req, fiber.TestConfig{}) + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) +} + +func TestPostHandlerSigFail(t *testing.T) { + i, _, _ := makeInteractions(t) + app := roleypoly.CreateFiberApp() + i.Routes(app) + + // TODO: make real interaction + body, err := json.Marshal(map[string]string{ + "temp": "temp", + }) + if err != nil { + t.Error(err) + return + } + + badPub, _, _ := ed25519.GenerateKey(nil) + + sig, ts := sign(t, ed25519.PrivateKey(hex.EncodeToString(badPub)), body) + + req, _ := http.NewRequest("POST", "/", bytes.NewBuffer(body)) + req.Header.Set("X-Signature-Ed25519", sig) + req.Header.Set("X-Signature-Timestamp", ts) + + res, err := app.Test(req, fiber.TestConfig{}) + + assert.Nil(t, err) + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) +} diff --git a/interactions/responses.go b/interactions/responses.go new file mode 100644 index 0000000..e9f3295 --- /dev/null +++ b/interactions/responses.go @@ -0,0 +1,24 @@ +package interactions + +import "github.com/gofiber/fiber/v3" + +func Deferred(c fiber.Ctx) (InteractionResponse, error) { + return InteractionResponse{ + Type: ResponseDeferredChannelMessage, + }, nil +} + +func String(msg string, ephemeral bool) (InteractionResponse, error) { + flags := 0 + if ephemeral { + flags = FlagEphemeral + } + + return InteractionResponse{ + Type: ResponseChannelMessage, + Data: InteractionResponseData{ + Content: msg, + Flags: flags, + }, + }, nil +} diff --git a/interactions/router.go b/interactions/router.go new file mode 100644 index 0000000..f2e7821 --- /dev/null +++ b/interactions/router.go @@ -0,0 +1,38 @@ +package interactions + +import ( + "net/http" + + "git.sapphic.engineer/roleypoly/v4/types" + "github.com/gofiber/fiber/v3" +) + +type InteractionHandler func(interaction Interaction) (InteractionResponse, error) + +type InteractionRouter struct { + Routes map[string]InteractionHandler +} + +func NewInteractionRouter() *InteractionRouter { + return &InteractionRouter{ + Routes: map[string]InteractionHandler{}, + } +} + +func (r *InteractionRouter) Add(name string, h InteractionHandler) { + r.Routes[name] = h +} + +func (r *InteractionRouter) Handle(c fiber.Ctx, interaction Interaction) error { + route, ok := r.Routes[interaction.Data.Name] + if !ok { + return types.NewAPIError(http.StatusNotFound, "command not found").Send(c) + } + + resp, err := route(interaction) + if err != nil { + return err + } + + return c.JSON(resp) +} diff --git a/interactions/router_test.go b/interactions/router_test.go new file mode 100644 index 0000000..f30cf15 --- /dev/null +++ b/interactions/router_test.go @@ -0,0 +1,16 @@ +package interactions_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRouter(t *testing.T) { + i, _, key := makeInteractions(t) + i.Router.Add("hello-world", cmdHelloWorld) + + r := sendInteraction(t, i, key, mkInteraction("hello-world")) + + assert.Equal(t, "Hello world!", r.Data.Content) +} diff --git a/interactions/types.go b/interactions/types.go new file mode 100644 index 0000000..092c489 --- /dev/null +++ b/interactions/types.go @@ -0,0 +1,50 @@ +package interactions + +import "git.sapphic.engineer/roleypoly/v4/types" + +type Interaction struct { + ID string `json:"id"` + Member types.DiscordMember `json:"member"` + Data InteractionData `json:"data"` + GuildID string `json:"guild_id"` + AppPermissions uint64 `json:"app_permissions"` + Type uint64 `json:"type"` +} + +const ( + TypePing uint64 = 1 + TypeApplicationCommand uint64 = 2 +) + +type InteractionData struct { + Name string `json:"name"` + Options []InteractionOption `json:"options"` +} + +type InteractionOption struct { + Name string `json:"name"` + Value string `json:"value"` +} + +const ( + ResponsePong = 1 + ResponseChannelMessage = 4 + ResponseDeferredChannelMessage = 5 +) + +type InteractionResponse struct { + Type int `json:"type"` + Data InteractionResponseData `json:"data"` +} + +const ( + FlagEphemeral = 1 << 6 +) + +type InteractionResponseData struct { + Content string + Embeds []Embed + Flags int +} + +type Embed struct{} diff --git a/interactions/utils_test.go b/interactions/utils_test.go new file mode 100644 index 0000000..1d926e2 --- /dev/null +++ b/interactions/utils_test.go @@ -0,0 +1,93 @@ +package interactions_test + +import ( + "bytes" + "crypto" + "crypto/ed25519" + "encoding/hex" + "net/http" + "testing" + "time" + + "git.sapphic.engineer/roleypoly/v4/discord" + "git.sapphic.engineer/roleypoly/v4/interactions" + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v3" + "github.com/stretchr/testify/assert" +) + +func makeInteractions(t *testing.T) (*interactions.Interactions, *discord.GuildServiceMock, ed25519.PrivateKey) { + pub, priv, err := ed25519.GenerateKey(nil) + if err != nil { + t.Errorf("makeInteractions: failed to generate key: %v", err) + } + + gs := &discord.GuildServiceMock{} + + i := &interactions.Interactions{ + PublicKey: pub, + Guilds: gs, + Router: interactions.NewInteractionRouter(), + OK: true, + } + + return i, gs, priv +} + +func cmdHelloWorld(ix interactions.Interaction) (interactions.InteractionResponse, error) { + return interactions.String("Hello world!", true) +} + +func sign(t *testing.T, key ed25519.PrivateKey, body []byte) (string, string) { + timestamp := time.Now().UTC().Format(time.RFC3339) + + payloadBuf := bytes.Buffer{} + payloadBuf.WriteString(timestamp) + payloadBuf.Write(body) + + signature, err := key.Sign(nil, payloadBuf.Bytes(), crypto.Hash(0)) + if err != nil { + t.Errorf("signing failed: %v", err) + } + + sigHex := hex.EncodeToString(signature) + + return sigHex, timestamp +} + +func sendInteraction(t *testing.T, i *interactions.Interactions, key ed25519.PrivateKey, ix interactions.Interaction) (ir interactions.InteractionResponse) { + app := fiber.New() + i.Routes(app) + + body, err := json.Marshal(ix) + if err != nil { + t.Error(err) + return + } + + sig, ts := sign(t, key, body) + + req, _ := http.NewRequest("POST", "/", bytes.NewBuffer(body)) + req.Header.Add("X-Signature-Ed25519", sig) + req.Header.Add("X-Signature-Timestamp", ts) + + resp, err := app.Test(req) + + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + err = json.NewDecoder(resp.Body).Decode(&ir) + if err != nil { + t.Error(err) + } + return +} + +func mkInteraction(name string) interactions.Interaction { + return interactions.Interaction{ + Type: interactions.TypeApplicationCommand, + Data: interactions.InteractionData{ + Name: name, + }, + } +} diff --git a/interactions/verify.go b/interactions/verify.go new file mode 100644 index 0000000..e284127 --- /dev/null +++ b/interactions/verify.go @@ -0,0 +1,40 @@ +package interactions + +import ( + "bytes" + "crypto/ed25519" + "encoding/hex" + "errors" + "fmt" +) + +var ( + ErrMissingHeader = errors.New("interactions: missing verification headers") + ErrSignatureSize = errors.New("interactions: signature length invalid") + ErrNotVerifed = errors.New("interactions: not verified") +) + +func (i *Interactions) Verify(signature, timestamp string, body []byte) error { + if signature == "" || timestamp == "" { + return ErrMissingHeader + } + + sig, err := hex.DecodeString(signature) + if err != nil { + return fmt.Errorf("interactions: signature failed to parse: %w", err) + } + + if len(sig) != ed25519.SignatureSize || sig[63]&224 != 0 { + return ErrSignatureSize + } + + buf := bytes.Buffer{} + buf.WriteString(timestamp) + buf.Write(body) + + if !ed25519.Verify(i.PublicKey, buf.Bytes(), sig) { + return ErrNotVerifed + } + + return nil +} diff --git a/interactions/verify_test.go b/interactions/verify_test.go new file mode 100644 index 0000000..940015b --- /dev/null +++ b/interactions/verify_test.go @@ -0,0 +1,46 @@ +package interactions_test + +import ( + "crypto/ed25519" + "encoding/hex" + "testing" + + "github.com/goccy/go-json" + "github.com/stretchr/testify/assert" +) + +func TestVerify(t *testing.T) { + i, _, key := makeInteractions(t) + + // TODO: make this into a real interaction + body, _ := json.Marshal(map[string]string{ + "test": "test", + }) + + sig, ts := sign(t, key, body) + + assert.NoError(t, i.Verify(sig, ts, body), "verification errored") +} + +func TestVerifyFailures(t *testing.T) { + i, _, key := makeInteractions(t) + badPub, _, _ := ed25519.GenerateKey(nil) + + // TODO: make this into a real interaction + body, _ := json.Marshal(map[string]string{ + "test": "test", + }) + + sig, ts := sign(t, key, body) + + assert.Error(t, i.Verify(sig, "", body), "erroneously allowed no timestamp") + assert.Error(t, i.Verify("", ts, body), "erroneously allowed no signature") + assert.Error(t, i.Verify(hex.EncodeToString(badPub), ts, body), "signature passed erroneously") + assert.Error(t, i.Verify(string(badPub), ts, body), "accepted an incorrectly encoded key") + assert.Error(t, i.Verify(sig+"aaaa", ts, body), "length of signature was not checked") + + body, _ = json.Marshal(map[string]string{ + "test": "test2", + }) + assert.Error(t, i.Verify(sig, ts, body), "verified against different data") +} diff --git a/justfile b/justfile index 9e2d1bf..1bea0d0 100644 --- a/justfile +++ b/justfile @@ -15,6 +15,7 @@ fmt: go fmt ./... tidy: + go clean -modcache go mod tidy update-vendor-hash: diff --git a/main.go b/main.go index 196ede1..234709c 100644 --- a/main.go +++ b/main.go @@ -1,36 +1,15 @@ package main // import "git.sapphic.engineer/roleypoly/v4" import ( - "embed" "log" - "net/http" "os" - "github.com/goccy/go-json" - "github.com/gofiber/fiber/v3" - "github.com/gofiber/template/html/v2" + "git.sapphic.engineer/roleypoly/v4/roleypoly" ) -//go:embed templates/* -var templatesfs embed.FS - func main() { - // storageDir := getStorageDirectory() - - viewEngine := html.NewFileSystem(http.FS(templatesfs), ".html") - - app := fiber.New(fiber.Config{ - JSONEncoder: json.Marshal, - JSONDecoder: json.Unmarshal, - Views: viewEngine, - ViewsLayout: "main", - }) - - // app.Use(session.New(session.Config{ - // Storage: pebble.New(pebble.Config{ - // Path: storageDir + "/sessions", - // }), - // })) + app := roleypoly.CreateFiberApp() + roleypoly.SetupRoutes(app) listenAddr := os.Getenv("LISTEN_ADDR") if listenAddr == "" { @@ -39,22 +18,3 @@ func main() { log.Fatal(app.Listen(listenAddr)) } - -func getStorageDirectory() string { - path := os.Getenv("STORAGE_DIR") - if path == "" { - path = "./.storage" - } - - _, err := os.Stat(path) - if err != nil { - if os.IsNotExist(err) { - err := os.MkdirAll(path, 0744) - if err != nil { - log.Fatalln("failed to create storage directory: ", err) - } - } - } - - return path -} diff --git a/roleypoly/fiber.go b/roleypoly/fiber.go new file mode 100644 index 0000000..9671e43 --- /dev/null +++ b/roleypoly/fiber.go @@ -0,0 +1,51 @@ +package roleypoly + +import ( + "net/http" + + "github.com/goccy/go-json" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/middleware/session" + "github.com/gofiber/template/html/v2" + + "git.sapphic.engineer/roleypoly/v4/templates" + "git.sapphic.engineer/roleypoly/v4/testing" +) + +func CreateFiberApp() *fiber.App { + viewEngine := html.NewFileSystem(http.FS(templates.FS), ".html") + + app := fiber.New(fiber.Config{ + JSONEncoder: json.Marshal, + JSONDecoder: json.Unmarshal, + Views: viewEngine, + ViewsLayout: "layouts/main", + }) + + app.Use(session.New(session.Config{})) + + return app +} + +func SetupRoutes(app *fiber.App) { + (&testing.TestingController{}).Routes(app.Group("/testing")) +} + +// func getStorageDirectory() string { +// path := os.Getenv("STORAGE_DIR") +// if path == "" { +// path = "./.storage" +// } + +// _, err := os.Stat(path) +// if err != nil { +// if os.IsNotExist(err) { +// err := os.MkdirAll(path, 0744) +// if err != nil { +// log.Fatalln("failed to create storage directory: ", err) +// } +// } +// } + +// return path +// } diff --git a/templates/embed.go b/templates/embed.go new file mode 100644 index 0000000..83c89b3 --- /dev/null +++ b/templates/embed.go @@ -0,0 +1,6 @@ +package templates + +import "embed" + +//go:embed *.html */*.html +var FS embed.FS diff --git a/templates/picker/main.html b/templates/picker/main.html new file mode 100644 index 0000000..a8a6eec --- /dev/null +++ b/templates/picker/main.html @@ -0,0 +1 @@ +

picker!

diff --git a/testing/testing.go b/testing/testing.go new file mode 100644 index 0000000..1df8b49 --- /dev/null +++ b/testing/testing.go @@ -0,0 +1,14 @@ +package testing + +import "github.com/gofiber/fiber/v3" + +type TestingController struct{} + +func (t *TestingController) Routes(r fiber.Router) { + r.Get("/picker/:version?", t.Picker) +} + +func (t *TestingController) Picker(c fiber.Ctx) error { + version := c.Params("version", "main") + return c.Render("picker/"+version, fiber.Map{}) +} diff --git a/types/api_error.go b/types/api_error.go new file mode 100644 index 0000000..f7efbad --- /dev/null +++ b/types/api_error.go @@ -0,0 +1,22 @@ +package types + +import "github.com/gofiber/fiber/v3" + +type APIError struct { + StatusCode int `json:"-"` + Message string `json:"message"` + Success bool `json:"success"` +} + +func NewAPIError(code int, message string) APIError { + return APIError{ + StatusCode: code, + Message: message, + Success: false, + } +} + +func (a APIError) Send(c fiber.Ctx) error { + c.JSON(a) + return c.SendStatus(a.StatusCode) +} diff --git a/types/member.go b/types/member.go new file mode 100644 index 0000000..3147eba --- /dev/null +++ b/types/member.go @@ -0,0 +1,8 @@ +package types + +type DiscordMember struct { + User DiscordUser `json:"user"` + Roles []string `json:"roles"` + Permissions uint64 `json:"permissions"` + Nick string `json:"nick"` +} diff --git a/types/user.go b/types/user.go new file mode 100644 index 0000000..b2f5753 --- /dev/null +++ b/types/user.go @@ -0,0 +1,7 @@ +package types + +type DiscordUser struct { + ID string `json:"id"` + Username string `json:"username"` + Avatar string `json:"avatar"` +}