From 607d7e121cf3e497cf2c1dd4b2f092e49c64df3e Mon Sep 17 00:00:00 2001 From: noe Date: Wed, 26 Mar 2025 16:36:00 -0700 Subject: [PATCH] slash roleypoly yay --- .env.example | 4 +- discord/clientmock/discord_client_mock.go | 21 +++++++ discord/discord_client.go | 5 ++ discord/guild_test.go | 3 +- discord/guildservice.go | 5 -- discord/guildservice_test.go | 3 +- discord/utils_test.go | 16 ------ interactions/cmd_roleypoly.go | 52 +++++++++++++++++ interactions/cmd_roleypoly_test.go | 66 +++++++++++++++++++++ interactions/interactions.go | 56 ++++++++++++++---- interactions/interactions_test.go | 2 +- interactions/types.go | 70 +++++++++++++++++++++-- interactions/utils_test.go | 22 ++++--- main.go | 26 ++++----- roleypoly/fiber.go | 4 +- types/discord.go | 7 +++ types/user.go | 7 ++- utils/const.go | 5 ++ utils/emojis.go | 61 ++++++++++++++++++++ utils/env.go | 12 ++++ utils/strings.go | 8 +++ utils/strings_test.go | 5 ++ 22 files changed, 394 insertions(+), 66 deletions(-) delete mode 100644 discord/utils_test.go create mode 100644 interactions/cmd_roleypoly.go create mode 100644 interactions/cmd_roleypoly_test.go create mode 100644 types/discord.go create mode 100644 utils/const.go create mode 100644 utils/emojis.go create mode 100644 utils/env.go diff --git a/.env.example b/.env.example index 9681e43..5d4cde3 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,6 @@ DISCORD_PUBLIC_KEY=cc DISCORD_BOT_TOKEN=dd LISTEN_ADDR=:8169 -STORAGE_DIR=./.storage \ No newline at end of file +STORAGE_DIR=./.storage + +PUBLIC_BASE_URL=http://localhost:8169 \ No newline at end of file diff --git a/discord/clientmock/discord_client_mock.go b/discord/clientmock/discord_client_mock.go index c35a22e..c0a640d 100644 --- a/discord/clientmock/discord_client_mock.go +++ b/discord/clientmock/discord_client_mock.go @@ -13,6 +13,23 @@ import ( type DiscordClientMock struct { mock.Mock + + BotToken string + ClientID string + ClientSecret string +} + +func NewDiscordClientMock() *DiscordClientMock { + dcm := &DiscordClientMock{ + BotToken: "bot-token", + ClientID: "client-id", + ClientSecret: "client-secret", + } + + dcm.On("BotAuth", mock.AnythingOfType("*http.Request")) + dcm.On("GetClientID").Return(dcm.ClientID) + + return dcm } func (c *DiscordClientMock) Do(req *http.Request) (*http.Response, error) { @@ -40,3 +57,7 @@ func (c *DiscordClientMock) MockResponse(method, path string, statusCode int, da return req.Method == method && pathMatcher.MatchString(req.URL.Path) })).Return(r, nil) } + +func (c *DiscordClientMock) GetClientID() string { + return c.Called().String(0) +} diff --git a/discord/discord_client.go b/discord/discord_client.go index e5b446f..d3f832f 100644 --- a/discord/discord_client.go +++ b/discord/discord_client.go @@ -14,6 +14,7 @@ const DiscordBaseUrl = "https://discord.com/api/v10" type IDiscordClient interface { Do(req *http.Request) (*http.Response, error) BotAuth(req *http.Request) + GetClientID() string } type DiscordClient struct { @@ -64,3 +65,7 @@ func OutputResponse(resp *http.Response, dst any) error { // TODO: more checks? return json.NewDecoder(resp.Body).Decode(dst) } + +func (d *DiscordClient) GetClientID() string { + return d.ClientID +} diff --git a/discord/guild_test.go b/discord/guild_test.go index fe7eade..e33fb9d 100644 --- a/discord/guild_test.go +++ b/discord/guild_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" "git.sapphic.engineer/roleypoly/v4/discord" + "git.sapphic.engineer/roleypoly/v4/discord/clientmock" "git.sapphic.engineer/roleypoly/v4/types/fixtures" "git.sapphic.engineer/roleypoly/v4/utils" ) @@ -17,7 +18,7 @@ var ( ) func TestGetMember(t *testing.T) { - dc := defaultMocks(t) + dc := clientmock.NewDiscordClientMock() g := discord.Guild{ Client: dc, DiscordGuild: fixtures.Guild, diff --git a/discord/guildservice.go b/discord/guildservice.go index a118074..f19fce1 100644 --- a/discord/guildservice.go +++ b/discord/guildservice.go @@ -6,11 +6,6 @@ import ( "git.sapphic.engineer/roleypoly/v4/utils" ) -type IGuildService interface { - Client() IDiscordClient - GetGuild(guildID string) (IGuild, error) -} - type GuildService struct { client IDiscordClient } diff --git a/discord/guildservice_test.go b/discord/guildservice_test.go index d742987..78dc66f 100644 --- a/discord/guildservice_test.go +++ b/discord/guildservice_test.go @@ -4,6 +4,7 @@ import ( "testing" "git.sapphic.engineer/roleypoly/v4/discord" + "git.sapphic.engineer/roleypoly/v4/discord/clientmock" "git.sapphic.engineer/roleypoly/v4/types/fixtures" "git.sapphic.engineer/roleypoly/v4/utils" "github.com/stretchr/testify/assert" @@ -16,7 +17,7 @@ var ( ) func TestGetGuild(t *testing.T) { - dc := defaultMocks(t) + dc := clientmock.NewDiscordClientMock() gs := discord.NewGuildService(dc) dc.MockResponse("GET", utils.J("guilds", fixtures.Guild.ID), 200, fixtureGuild) diff --git a/discord/utils_test.go b/discord/utils_test.go deleted file mode 100644 index 87dadf2..0000000 --- a/discord/utils_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package discord_test - -import ( - "testing" - - "git.sapphic.engineer/roleypoly/v4/discord/clientmock" - "github.com/stretchr/testify/mock" -) - -func defaultMocks(t *testing.T) *clientmock.DiscordClientMock { - dc := &clientmock.DiscordClientMock{} - - dc.On("BotAuth", mock.AnythingOfType("*http.Request")) - - return dc -} diff --git a/interactions/cmd_roleypoly.go b/interactions/cmd_roleypoly.go new file mode 100644 index 0000000..5ee2efb --- /dev/null +++ b/interactions/cmd_roleypoly.go @@ -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 +} diff --git a/interactions/cmd_roleypoly_test.go b/interactions/cmd_roleypoly_test.go new file mode 100644 index 0000000..869a369 --- /dev/null +++ b/interactions/cmd_roleypoly_test.go @@ -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": "", + "Pick a role": "", + "Remove a 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) +} diff --git a/interactions/interactions.go b/interactions/interactions.go index 0c43209..0a80b2b 100644 --- a/interactions/interactions.go +++ b/interactions/interactions.go @@ -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("", command.ID, command.Name) + } + + return i.commandsMentions +} diff --git a/interactions/interactions_test.go b/interactions/interactions_test.go index 624eaf1..1d2f1bf 100644 --- a/interactions/interactions_test.go +++ b/interactions/interactions_test.go @@ -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") } diff --git a/interactions/types.go b/interactions/types.go index aa57c9e..082dbf4 100644 --- a/interactions/types.go +++ b/interactions/types.go @@ -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"` +} diff --git a/interactions/utils_test.go b/interactions/utils_test.go index dbd8e32..4a5e03e 100644 --- a/interactions/utils_test.go +++ b/interactions/utils_test.go @@ -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, }, diff --git a/main.go b/main.go index 096ac8f..a467e20 100644 --- a/main.go +++ b/main.go @@ -2,27 +2,27 @@ package main // import "git.sapphic.engineer/roleypoly/v4" import ( "log" - "os" "git.sapphic.engineer/roleypoly/v4/discord" "git.sapphic.engineer/roleypoly/v4/roleypoly" + "git.sapphic.engineer/roleypoly/v4/utils" ) func main() { app := roleypoly.CreateFiberApp() - clientID := os.Getenv("DISCORD_CLIENT_ID") - clientSecret := os.Getenv("DISCORD_CLIENT_SECRET") - botToken := os.Getenv("DISCORD_BOT_TOKEN") - dc := discord.NewDiscordClient(clientID, clientSecret, botToken) + dc := discord.NewDiscordClient( + utils.DiscordClientID, + utils.DiscordClientSecret, + utils.DiscordBotToken, + ) - publicKey := os.Getenv("DISCORD_PUBLIC_KEY") - roleypoly.SetupControllers(app, dc, publicKey) + roleypoly.SetupControllers( + app, + dc, + utils.DiscordPublicKey, + utils.PublicBaseURL, + ) - listenAddr := os.Getenv("LISTEN_ADDR") - if listenAddr == "" { - listenAddr = ":8169" - } - - log.Fatal(app.Listen(listenAddr)) + log.Fatal(app.Listen(utils.ListenAddr)) } diff --git a/roleypoly/fiber.go b/roleypoly/fiber.go index 2dd3f82..b02463a 100644 --- a/roleypoly/fiber.go +++ b/roleypoly/fiber.go @@ -29,14 +29,14 @@ func CreateFiberApp() *fiber.App { return app } -func SetupControllers(app *fiber.App, dc *discord.DiscordClient, publicKey string) { +func SetupControllers(app *fiber.App, dc *discord.DiscordClient, publicKey string, publicBaseURL string) { gs := discord.NewGuildService(dc) (&testing.TestingController{ Guilds: gs, }).Routes(app.Group("/testing")) - interactions.NewInteractions(publicKey, gs).Routes(app.Group("/interactions")) + interactions.NewInteractions(publicKey, publicBaseURL, gs).Routes(app.Group("/interactions")) } // func getStorageDirectory() string { diff --git a/types/discord.go b/types/discord.go new file mode 100644 index 0000000..96b51ab --- /dev/null +++ b/types/discord.go @@ -0,0 +1,7 @@ +package types + +type Emoji struct { + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + Animated bool `json:"animated,omitempty"` +} diff --git a/types/user.go b/types/user.go index b2f5753..6eea972 100644 --- a/types/user.go +++ b/types/user.go @@ -1,7 +1,8 @@ package types type DiscordUser struct { - ID string `json:"id"` - Username string `json:"username"` - Avatar string `json:"avatar"` + ID string `json:"id"` + Username string `json:"username"` + Avatar string `json:"avatar"` + GlobalName string `json:"globalName"` } diff --git a/utils/const.go b/utils/const.go new file mode 100644 index 0000000..61e78c3 --- /dev/null +++ b/utils/const.go @@ -0,0 +1,5 @@ +package utils + +const ( + EmbedColor = 0x453e3d +) diff --git a/utils/emojis.go b/utils/emojis.go new file mode 100644 index 0000000..3429cc4 --- /dev/null +++ b/utils/emojis.go @@ -0,0 +1,61 @@ +package utils + +import ( + "math/rand/v2" + + "git.sapphic.engineer/roleypoly/v4/types" +) + +var ( + AllEmojis = []types.Emoji{ + // Repeated for probability reasons + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext", ID: "785201823945850942"}, + {Name: "roleypolynext_420", ID: "785201824101564466"}, + {Name: "roleypolynext_ace", ID: "785201823497715723"}, + {Name: "roleypolynext_ace", ID: "785201823497715723"}, + {Name: "roleypolynext_ace", ID: "785201823497715723"}, + {Name: "roleypolynext_ace", ID: "785201823497715723"}, + {Name: "roleypolynext_ace", ID: "785201823497715723"}, + {Name: "roleypolynext_ace", ID: "785201823497715723"}, + {Name: "roleypolynext_anim", ID: "785203654125682698", Animated: true}, + {Name: "roleypolynext_bi", ID: "785201823584616500"}, + {Name: "roleypolynext_bi", ID: "785201823584616500"}, + {Name: "roleypolynext_bi", ID: "785201823584616500"}, + {Name: "roleypolynext_bi", ID: "785201823584616500"}, + {Name: "roleypolynext_bi", ID: "785201823584616500"}, + {Name: "roleypolynext_bi", ID: "785201823584616500"}, + {Name: "roleypolynext_lesbian", ID: "785201823627476993"}, + {Name: "roleypolynext_lesbian", ID: "785201823627476993"}, + {Name: "roleypolynext_lesbian", ID: "785201823627476993"}, + {Name: "roleypolynext_lesbian", ID: "785201823627476993"}, + {Name: "roleypolynext_lesbian", ID: "785201823627476993"}, + {Name: "roleypolynext_lny", ID: "785201824092651560"}, + {Name: "roleypolynext_newyear", ID: "785201824277200956"}, + {Name: "roleypolynext_pride", ID: "785201823501385749"}, + {Name: "roleypolynext_pride", ID: "785201823501385749"}, + {Name: "roleypolynext_pride", ID: "785201823501385749"}, + {Name: "roleypolynext_pride", ID: "785201823501385749"}, + {Name: "roleypolynext_pride", ID: "785201823501385749"}, + {Name: "roleypolynext_spectrum", ID: "785201823526682666"}, + {Name: "roleypolynext_trans", ID: "785201823967215617"}, + {Name: "roleypolynext_trans", ID: "785201823967215617"}, + {Name: "roleypolynext_trans", ID: "785201823967215617"}, + {Name: "roleypolynext_trans", ID: "785201823967215617"}, + {Name: "roleypolynext_trans", ID: "785201823967215617"}, + } +) + +func GetRandomEmoji() types.Emoji { + idx := rand.UintN(uint(len(AllEmojis))) + return AllEmojis[idx] +} diff --git a/utils/env.go b/utils/env.go new file mode 100644 index 0000000..7a3294c --- /dev/null +++ b/utils/env.go @@ -0,0 +1,12 @@ +package utils + +import "os" + +var ( + DiscordBotToken = os.Getenv("DISCORD_BOT_TOKEN") + DiscordClientID = os.Getenv("DISCORD_CLIENT_ID") + DiscordClientSecret = os.Getenv("DISCORD_CLIENT_SECRET") + DiscordPublicKey = os.Getenv("DISCORD_PUBLIC_KEY") + ListenAddr = Or(os.Getenv("LISTEN_ADDR"), ":8169") + PublicBaseURL = os.Getenv("PUBLIC_BASE_URL") +) diff --git a/utils/strings.go b/utils/strings.go index 5a24214..edbe3ed 100644 --- a/utils/strings.go +++ b/utils/strings.go @@ -12,3 +12,11 @@ func HeadTitle(text string) string { func J(parts ...string) string { return "/" + strings.Join(parts, "/") } + +func Or(input, fallback string) string { + if input == "" { + return fallback + } + + return input +} diff --git a/utils/strings_test.go b/utils/strings_test.go index 6b73c58..ad79673 100644 --- a/utils/strings_test.go +++ b/utils/strings_test.go @@ -15,3 +15,8 @@ func TestHeadTitle(t *testing.T) { func TestJ(t *testing.T) { assert.Equal(t, "/a/b/c", utils.J("a", "b", "c")) } + +func TestOr(t *testing.T) { + assert.Equal(t, "a", utils.Or("a", "b")) + assert.Equal(t, "b", utils.Or("", "b")) +}