slash roleypoly yay

This commit is contained in:
41666 2025-03-26 16:36:00 -07:00
parent 02f5075d3b
commit 607d7e121c
22 changed files with 394 additions and 66 deletions

View file

@ -0,0 +1,52 @@
package interactions
import (
"fmt"
"net/url"
"git.sapphic.engineer/roleypoly/v4/utils"
)
func (i *Interactions) CmdRoleypoly(ix Interaction) (InteractionResponse, error) {
mentions := i.CommandsMap()
hostname := "roleypoly.com"
publicURL, _ := url.Parse(i.PublicBaseURL)
if publicURL != nil {
hostname = publicURL.Host
}
return InteractionResponse{
Type: ResponseChannelMessage,
Data: InteractionResponseData{
Flags: FlagEphemeral,
Embeds: []Embed{{
Color: utils.EmbedColor,
Title: fmt.Sprintf(":beginner: Hey there, %s", ix.GetName()),
Description: "Try these slash commands, or pick roles from a browser!",
Fields: []EmbedField{
{
Name: "See all the roles",
Value: utils.Or(mentions["pickable-roles"], "/pickable-roles"),
},
{
Name: "Pick a role",
Value: utils.Or(mentions["pick-role"], "/pick-role"),
},
{
Name: "Remove a role",
Value: utils.Or(mentions["remove-role"], "/remove-role"),
},
},
}},
Components: []ButtonComponent{
{
Type: 2, // Button
Style: 5, // Link
Label: fmt.Sprintf("Pick roles on %s", hostname),
Emoji: utils.GetRandomEmoji(),
URL: fmt.Sprintf("%s/s/%s", i.PublicBaseURL, ix.GuildID),
},
},
},
}, nil
}

View file

@ -0,0 +1,66 @@
package interactions_test
import (
"fmt"
"testing"
"git.sapphic.engineer/roleypoly/v4/interactions"
"github.com/stretchr/testify/assert"
)
func TestCmdRoleypoly(t *testing.T) {
i, dcm, _ := makeInteractions(t)
ix := mkInteraction("roleypoly")
commands := []interactions.InteractionCommand{
{ID: "1", Name: "pick-role"},
{ID: "2", Name: "pickable-roles"},
{ID: "3", Name: "remove-role"},
}
dcm.MockResponse("GET", "/applications/*/commands", 200, commands)
ir, err := i.CmdRoleypoly(ix)
assert.Nil(t, err)
embed := ir.Data.Embeds[0]
// test that the button is cool
button := ir.Data.Components[0]
assert.Equal(t, fmt.Sprintf("%s/s/guild-id", i.PublicBaseURL), button.URL)
// test the command mentions
tests := map[string]string{
"See all the roles": "</2:pickable-roles>",
"Pick a role": "</1:pick-role>",
"Remove a role": "</3:remove-role>",
}
for _, field := range embed.Fields {
assert.Equal(t, tests[field.Name], field.Value)
}
// test weird name cases
// not weird
ix.Member.Nick = "Doll"
ix.Member.User.GlobalName = "Dolly"
ix.Member.User.Username = "41666."
ir, err = i.CmdRoleypoly(ix)
assert.Nil(t, err)
assert.Equal(t, ":beginner: Hey there, Doll", ir.Data.Embeds[0].Title)
// no nick
ix.Member.Nick = ""
ix.Member.User.GlobalName = "Dolly"
ix.Member.User.Username = "41666."
ir, err = i.CmdRoleypoly(ix)
assert.Nil(t, err)
assert.Equal(t, ":beginner: Hey there, Dolly", ir.Data.Embeds[0].Title)
// no globalname
ix.Member.Nick = ""
ix.Member.User.GlobalName = ""
ix.Member.User.Username = "41666."
ir, err = i.CmdRoleypoly(ix)
assert.Nil(t, err)
assert.Equal(t, ":beginner: Hey there, 41666.", ir.Data.Embeds[0].Title)
}

View file

@ -2,6 +2,7 @@ package interactions
import (
"crypto/ed25519"
"fmt"
"log"
"net/http"
@ -9,23 +10,30 @@ import (
"git.sapphic.engineer/roleypoly/v4/discord"
"git.sapphic.engineer/roleypoly/v4/types"
"git.sapphic.engineer/roleypoly/v4/utils"
)
type Interactions struct {
PublicKey ed25519.PublicKey
Guilds *discord.GuildService
Router *InteractionRouter
PublicKey ed25519.PublicKey
PublicBaseURL string
Guilds *discord.GuildService
Router *InteractionRouter
// if interactions are able to be used
OK bool
commandsMap []InteractionCommand
commandsMentions map[string]string
}
func NewInteractions(publicKey string, guildsService *discord.GuildService) *Interactions {
func NewInteractions(publicKey string, publicBaseURL string, guildsService *discord.GuildService) *Interactions {
return &Interactions{
PublicKey: ed25519.PublicKey(publicKey),
Guilds: guildsService,
Router: NewInteractionRouter(),
OK: len(publicKey) != 0,
PublicKey: ed25519.PublicKey(publicKey),
PublicBaseURL: publicBaseURL,
Guilds: guildsService,
Router: NewInteractionRouter(),
OK: len(publicKey) != 0,
}
}
@ -68,6 +76,32 @@ func (i *Interactions) PostHandler(c fiber.Ctx) error {
return types.NewAPIError(http.StatusNotImplemented, "not implemented").Send(c)
}
// func (i *Interactions) Respond(ix Interaction, ir InteractionResponse) error {
// i.Guilds
// }
func (i *Interactions) CommandsMap() map[string]string {
if i.commandsMentions != nil {
return i.commandsMentions
}
dc := i.Guilds.Client()
req := discord.NewRequest("GET", utils.J("applications", i.Guilds.Client().GetClientID(), "commands"))
dc.BotAuth(req)
resp, err := dc.Do(req)
if err != nil {
log.Println("interactions: commands map fetch failed")
return nil
}
i.commandsMap = []InteractionCommand{}
err = discord.OutputResponse(resp, &i.commandsMap)
if err != nil {
log.Println("interactions: commands map parse failed")
return nil
}
i.commandsMentions = map[string]string{}
for _, command := range i.commandsMap {
i.commandsMentions[command.Name] = fmt.Sprintf("</%s:%s>", command.ID, command.Name)
}
return i.commandsMentions
}

View file

@ -21,7 +21,7 @@ func TestNewInteractions(t *testing.T) {
gs := &discord.GuildService{}
i := interactions.NewInteractions(key, gs)
i := interactions.NewInteractions(key, "http://roleypoly.local", gs)
assert.True(t, i.OK, "interactions OK")
assert.Equal(t, string(i.PublicKey), key, "public key accepted from args")
}

View file

@ -12,6 +12,18 @@ type Interaction struct {
Token string `json:"token"`
}
func (ix Interaction) GetName() string {
if ix.Member.Nick != "" {
return ix.Member.Nick
}
if ix.Member.User.GlobalName != "" {
return ix.Member.User.GlobalName
}
return ix.Member.User.Username
}
const (
TypePing uint64 = 1
TypeApplicationCommand uint64 = 2
@ -43,9 +55,59 @@ const (
)
type InteractionResponseData struct {
Content string
Embeds []Embed
Flags int
Content string `json:"content,omitempty"`
Components []ButtonComponent `json:"components,omitempty"`
Embeds []Embed `json:"embeds,omitempty"`
Flags int `json:"flags,omitempty"`
}
type Embed struct{}
type ButtonComponent struct {
Type int `json:"type,omitempty"`
Style int `json:"style,omitempty"`
Label string `json:"label,omitempty"`
Emoji types.Emoji `json:"emoji,omitempty"`
URL string `json:"url,omitempty"`
}
type InteractionCommand struct {
ID string `json:"id"`
Type int `json:"type"`
GuildID string `json:"guild_id,omitempty"`
Name string `json:"name"`
Description string `json:"description"`
Options InteractionCommandOption `json:"options"`
ChannelTypes []int `json:"channel_types,omitempty"`
}
type InteractionCommandOption struct {
Type int `json:"type"`
Name string `json:"name"`
Required bool `json:"required,omitempty"`
}
type Embed struct {
Author EmbedAuthor `json:"author"`
Color uint32 `json:"color"`
Description string `json:"description,omitempty"`
Fields []EmbedField `json:"fields"`
Footer EmbedFooter `json:"footer"`
Timestamp string `json:"timestamp,omitempty"`
Title string `json:"title"`
}
type EmbedField struct {
Name string `json:"name"`
Value string `json:"value"`
Inline bool `json:"inline,omitempty"`
}
type EmbedFooter struct {
Text string `json:"text"`
IconURL string `json:"icon_url,omitempty"`
ProxyIconURL string `json:"proxy_icon_url,omitempty"`
}
type EmbedAuthor struct {
Name string `json:"name"`
IconURL string `json:"icon_url"`
}

View file

@ -10,28 +10,32 @@ import (
"time"
"git.sapphic.engineer/roleypoly/v4/discord"
"git.sapphic.engineer/roleypoly/v4/discord/clientmock"
"git.sapphic.engineer/roleypoly/v4/interactions"
"git.sapphic.engineer/roleypoly/v4/types/fixtures"
"github.com/goccy/go-json"
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/assert"
)
func makeInteractions(t *testing.T) (*interactions.Interactions, *discord.GuildService, ed25519.PrivateKey) {
func makeInteractions(t *testing.T) (*interactions.Interactions, *clientmock.DiscordClientMock, ed25519.PrivateKey) {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
t.Errorf("makeInteractions: failed to generate key: %v", err)
}
gs := &discord.GuildService{}
dcm := clientmock.NewDiscordClientMock()
gs := discord.NewGuildService(dcm)
i := &interactions.Interactions{
PublicKey: pub,
Guilds: gs,
Router: interactions.NewInteractionRouter(),
OK: true,
PublicKey: pub,
PublicBaseURL: "http://roleypoly.local",
Guilds: gs,
Router: interactions.NewInteractionRouter(),
OK: true,
}
return i, gs, priv
return i, dcm, priv
}
func cmdHelloWorld(ix interactions.Interaction) (interactions.InteractionResponse, error) {
@ -85,7 +89,9 @@ func sendInteraction(t *testing.T, i *interactions.Interactions, key ed25519.Pri
func mkInteraction(name string) interactions.Interaction {
return interactions.Interaction{
Type: interactions.TypeApplicationCommand,
GuildID: "guild-id",
Type: interactions.TypeApplicationCommand,
Member: fixtures.Member,
Data: interactions.InteractionData{
Name: name,
},