feat: joke & dice command

This commit is contained in:
2026-03-17 16:34:12 +00:00
parent a664ae15fd
commit dee5008c60
9 changed files with 1045 additions and 8 deletions
+22
View File
@@ -0,0 +1,22 @@
package public
import (
"math/rand"
"velox-bot/internal/commands/fun/shared"
"github.com/bwmarrin/discordgo"
)
var Coinflip = &discordgo.ApplicationCommand{
Name: "coinflip",
Description: "Flip a coin",
}
func CoinflipHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
num := rand.Intn(2)
if num == 0 {
shared.Respond(s, i, "Heads!")
} else {
shared.Respond(s, i, "Tails!")
}
}
+62
View File
@@ -0,0 +1,62 @@
package public
import (
"strings"
"velox-bot/internal/commands/fun/shared"
"velox-bot/internal/diceexpr"
"github.com/bwmarrin/discordgo"
)
var Dice = &discordgo.ApplicationCommand{
Name: "dice",
Description: "Roll dice expressions (e.g. d20, 2d20+1, 2#d20+1)",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "expr",
Description: "Dice expression to roll",
Required: true,
},
},
Contexts: &[]discordgo.InteractionContextType{
discordgo.InteractionContextGuild,
discordgo.InteractionContextBotDM,
discordgo.InteractionContextPrivateChannel,
},
}
func DiceHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
expr := ""
if opts := i.ApplicationCommandData().Options; len(opts) > 0 {
expr = opts[0].StringValue()
}
expr = strings.TrimSpace(expr)
if expr == "" {
respondDiceFailure(s, i)
return
}
res, err := diceexpr.EvalMany(expr, diceexpr.DefaultLimits())
if err != nil {
respondDiceFailure(s, i)
return
}
out := diceexpr.FormatResult(res, diceexpr.DefaultFormatOptions())
if len(out) > 1900 {
// Keep within Discord message limits; trim safely.
out = out[:1900] + "…"
}
shared.Respond(s, i, out)
}
func respondDiceFailure(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.GuildID != "" {
shared.RespondEphemeral(s, i, "Failure!")
return
}
shared.Respond(s, i, "Failure!")
}
+153
View File
@@ -0,0 +1,153 @@
package public
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"velox-bot/internal/commands/fun/shared"
"github.com/bwmarrin/discordgo"
)
var Joke = &discordgo.ApplicationCommand{
Name: "joke",
Description: "Get a random joke",
Contexts: &[]discordgo.InteractionContextType{
discordgo.InteractionContextGuild,
discordgo.InteractionContextBotDM,
discordgo.InteractionContextPrivateChannel,
},
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "type",
Description: "The type of joke to get",
Required: false,
Choices: []*discordgo.ApplicationCommandOptionChoice{
{
Name: "Programming",
Value: "Programming",
},
{
Name: "Misc",
Value: "Miscellaneous",
},
{
Name: "Dark",
Value: "Dark",
},
{
Name: "Pun",
Value: "Pun",
},
{
Name: "Spooky",
Value: "Spooky",
},
{
Name: "Christmas",
Value: "Christmas",
},
},
},
},
}
func JokeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
jokeType := "Any"
if opts := i.ApplicationCommandData().Options; len(opts) > 0 {
if v := opts[0].StringValue(); v != "" {
jokeType = v
}
}
if !isValidJokeType(jokeType) {
jokeType = "Any"
}
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
defer cancel()
content, err := fetchJoke(ctx, jokeType)
if err != nil {
if i.GuildID != "" {
shared.RespondEphemeral(s, i, "Failed to get joke.")
} else {
shared.Respond(s, i, "Failed to get joke.")
}
return
}
shared.Respond(s, i, content)
}
func isValidJokeType(t string) bool {
switch t {
case "Any", "Programming", "Misc", "Dark", "Pun", "Spooky", "Christmas":
return true
default:
return false
}
}
type jokeAPIResponse struct {
Error bool `json:"error"`
Message string `json:"message"`
Type string `json:"type"`
Joke string `json:"joke"`
Setup string `json:"setup"`
Delivery string `json:"delivery"`
}
func fetchJoke(ctx context.Context, jokeType string) (string, error) {
const baseURL = "https://v2.jokeapi.dev/joke"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s", baseURL, jokeType), nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", fmt.Errorf("joke api status: %s", resp.Status)
}
var data jokeAPIResponse
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", err
}
if data.Error {
if data.Message != "" {
return "", errors.New(data.Message)
}
return "", errors.New("joke api returned error")
}
switch data.Type {
case "single":
if data.Joke == "" {
return "", errors.New("empty joke")
}
return data.Joke, nil
case "twopart":
if data.Setup == "" && data.Delivery == "" {
return "", errors.New("empty joke")
}
if data.Setup == "" {
return data.Delivery, nil
}
if data.Delivery == "" {
return data.Setup, nil
}
return data.Setup + "\n" + data.Delivery, nil
default:
return "", fmt.Errorf("unknown joke type: %q", data.Type)
}
}
@@ -1,4 +1,4 @@
package fun
package public
import "github.com/bwmarrin/discordgo"
+23
View File
@@ -0,0 +1,23 @@
package fun
import (
"velox-bot/internal/commands/fun/public"
"github.com/bwmarrin/discordgo"
)
// Commands
var (
Ping *discordgo.ApplicationCommand = public.Ping
Joke *discordgo.ApplicationCommand = public.Joke
Coinflip *discordgo.ApplicationCommand = public.Coinflip
Dice *discordgo.ApplicationCommand = public.Dice
)
// Handlers
func PingHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.PingHandler(s, i) }
func JokeHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.JokeHandler(s, i) }
func CoinflipHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
public.CoinflipHandler(s, i)
}
func DiceHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.DiceHandler(s, i) }
+24
View File
@@ -0,0 +1,24 @@
package shared
import (
"github.com/bwmarrin/discordgo"
)
func Respond(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: msg,
},
})
}
func RespondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: msg,
Flags: discordgo.MessageFlagsEphemeral,
},
})
}