interactions yay!!
This commit is contained in:
parent
b9a05bedf9
commit
f60033a3e4
30 changed files with 716 additions and 44 deletions
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
pkgs ? import <nixpkgs> {},
|
||||
vendorHash ? "sha256-dnYDdGjQHLcHD78EFoUqMcACXUuKBSK3FWHalWjCd9c=",
|
||||
vendorHash ? "sha256-OxPbXF7ouilQUCp8I76gqoq3t9PPYrUQukgxliACqlI=",
|
||||
}:
|
||||
rec {
|
||||
default = roleypoly;
|
||||
|
|
8
discord/api.go
Normal file
8
discord/api.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package discord
|
||||
|
||||
type IDiscordClient interface {
|
||||
}
|
||||
|
||||
func Get(url string) {
|
||||
|
||||
}
|
11
discord/guild.go
Normal file
11
discord/guild.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package discord
|
||||
|
||||
import "context"
|
||||
|
||||
type IGuild interface {
|
||||
GetMember(ctx context.Context, memberID string) (IMember, error)
|
||||
}
|
||||
|
||||
type Guild struct {
|
||||
ID string
|
||||
}
|
21
discord/guild_mock.go
Normal file
21
discord/guild_mock.go
Normal file
|
@ -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)
|
||||
}
|
14
discord/guildservice.go
Normal file
14
discord/guildservice.go
Normal file
|
@ -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
|
||||
}
|
21
discord/guildservice_mock.go
Normal file
21
discord/guildservice_mock.go
Normal file
|
@ -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)
|
||||
}
|
20
discord/member.go
Normal file
20
discord/member.go
Normal file
|
@ -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
|
||||
}
|
17
discord/member_mock.go
Normal file
17
discord/member_mock.go
Normal file
|
@ -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)
|
||||
}
|
1
go.mod
1
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
|
||||
|
|
2
go.sum
2
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=
|
||||
|
|
5
interactions/cmd_helloworld.go
Normal file
5
interactions/cmd_helloworld.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package interactions
|
||||
|
||||
func (i *Interactions) CmdHelloWorld(ix Interaction) (InteractionResponse, error) {
|
||||
return String("Hello world!", true)
|
||||
}
|
16
interactions/cmd_helloworld_test.go
Normal file
16
interactions/cmd_helloworld_test.go
Normal file
|
@ -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)
|
||||
}
|
74
interactions/interactions.go
Normal file
74
interactions/interactions.go
Normal file
|
@ -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)
|
||||
}
|
85
interactions/interactions_test.go
Normal file
85
interactions/interactions_test.go
Normal file
|
@ -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)
|
||||
}
|
24
interactions/responses.go
Normal file
24
interactions/responses.go
Normal file
|
@ -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
|
||||
}
|
38
interactions/router.go
Normal file
38
interactions/router.go
Normal file
|
@ -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)
|
||||
}
|
16
interactions/router_test.go
Normal file
16
interactions/router_test.go
Normal file
|
@ -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)
|
||||
}
|
50
interactions/types.go
Normal file
50
interactions/types.go
Normal file
|
@ -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{}
|
93
interactions/utils_test.go
Normal file
93
interactions/utils_test.go
Normal file
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
40
interactions/verify.go
Normal file
40
interactions/verify.go
Normal file
|
@ -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
|
||||
}
|
46
interactions/verify_test.go
Normal file
46
interactions/verify_test.go
Normal file
|
@ -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")
|
||||
}
|
1
justfile
1
justfile
|
@ -15,6 +15,7 @@ fmt:
|
|||
go fmt ./...
|
||||
|
||||
tidy:
|
||||
go clean -modcache
|
||||
go mod tidy
|
||||
|
||||
update-vendor-hash:
|
||||
|
|
46
main.go
46
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
|
||||
}
|
||||
|
|
51
roleypoly/fiber.go
Normal file
51
roleypoly/fiber.go
Normal file
|
@ -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
|
||||
// }
|
6
templates/embed.go
Normal file
6
templates/embed.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package templates
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *.html */*.html
|
||||
var FS embed.FS
|
1
templates/picker/main.html
Normal file
1
templates/picker/main.html
Normal file
|
@ -0,0 +1 @@
|
|||
<h1>picker!</h1>
|
14
testing/testing.go
Normal file
14
testing/testing.go
Normal file
|
@ -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{})
|
||||
}
|
22
types/api_error.go
Normal file
22
types/api_error.go
Normal file
|
@ -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)
|
||||
}
|
8
types/member.go
Normal file
8
types/member.go
Normal file
|
@ -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"`
|
||||
}
|
7
types/user.go
Normal file
7
types/user.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package types
|
||||
|
||||
type DiscordUser struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
Loading…
Add table
Reference in a new issue