feat: leveling system refractor

This commit is contained in:
2026-03-17 15:54:17 +00:00
parent 8ba26f8bb9
commit 0f49c79567
18 changed files with 668 additions and 579 deletions
+12 -12
View File
@@ -9,21 +9,21 @@ import (
) )
var AllCommands = []*discordgo.ApplicationCommand{ var AllCommands = []*discordgo.ApplicationCommand{
fun.PingCommand, fun.Ping,
help.HelpCommand, help.Help,
cmdlevel.LevelConfigCommand, cmdlevel.Slvl,
cmdlevel.RankCommand, cmdlevel.Rank,
cmdlevel.LeaderboardCommand, cmdlevel.Leaderboard,
cmdlevel.RewardsCommand, cmdlevel.Rewards,
} }
var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){ var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"ping": fun.HandlePing, "ping": fun.PingHandler,
"help": help.HandleHelp, "help": help.HelpHandler,
"slvl": cmdlevel.HandleLevelConfig, "slvl": cmdlevel.SlvlHandler,
"rank": cmdlevel.HandleRank, "rank": cmdlevel.RankHandler,
"leaderboard": cmdlevel.HandleLeaderboard, "leaderboard": cmdlevel.LeaderboardHandler,
"rewards": cmdlevel.HandleRewards, "rewards": cmdlevel.RewardsHandler,
} }
func HandleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) { func HandleInteraction(s *discordgo.Session, i *discordgo.InteractionCreate) {
+2 -2
View File
@@ -2,12 +2,12 @@ package fun
import "github.com/bwmarrin/discordgo" import "github.com/bwmarrin/discordgo"
var PingCommand = &discordgo.ApplicationCommand{ var Ping = &discordgo.ApplicationCommand{
Name: "ping", Name: "ping",
Description: "Ping the bot", Description: "Ping the bot",
} }
func HandlePing(s *discordgo.Session, i *discordgo.InteractionCreate) { func PingHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
+2 -2
View File
@@ -6,12 +6,12 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
var HelpCommand = &discordgo.ApplicationCommand{ var Help = &discordgo.ApplicationCommand{
Name: "help", Name: "help",
Description: "Get help with the bot", Description: "Get help with the bot",
} }
func HandleHelp(s *discordgo.Session, i *discordgo.InteractionCreate) { func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
+41
View File
@@ -0,0 +1,41 @@
package config
import (
"velox-bot/internal/commands/level/shared"
"github.com/bwmarrin/discordgo"
)
var Slvl = &discordgo.ApplicationCommand{
Name: "slvl",
Description: "Leveling System",
Options: []*discordgo.ApplicationCommandOption{
slvlToggleOption(),
slvlSetLevelOption(),
slvlSetChannelOption(),
slvlSetMessageOption(),
slvlSetRewardOption(),
},
}
func SlvlHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
data := i.ApplicationCommandData()
if len(data.Options) == 0 {
return
}
switch data.Options[0].Name {
case "toggle":
slvlToggleHandler(s, i)
case "set":
slvlSetLevelHandler(s, i)
case "set-channel":
slvlSetChannelHandler(s, i)
case "set-message":
slvlSetMessageHandler(s, i)
case "set-reward":
slvlSetRewardHandler(s, i)
default:
shared.RespondEphemeral(s, i, "Invalid subcommand.")
}
}
@@ -0,0 +1,68 @@
package config
import (
"context"
"strconv"
"velox-bot/internal/commands/level/shared"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
func slvlSetChannelOption() *discordgo.ApplicationCommandOption {
return &discordgo.ApplicationCommandOption{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set-channel",
Description: "Set the level up channel",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "Channel for level-up messages",
Required: true,
},
},
}
}
func slvlSetChannelHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireManageGuild(s, i) || !shared.RequireLevelSystemEnabled(s, i) {
return
}
if !shared.RequireLevelSettingsService(s, i) {
return
}
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
optMap := shared.SubcommandOptionMap(i)
channelOpt, ok := optMap["channel"]
if !ok {
shared.RespondEphemeral(s, i, "Missing channel option.")
return
}
ch := channelOpt.ChannelValue(s)
if ch == nil {
shared.RespondEphemeral(s, i, "Invalid channel.")
return
}
channelID, err := strconv.ParseInt(ch.ID, 10, 64)
if err != nil {
shared.RespondEphemeral(s, i, "Invalid channel ID.")
return
}
if err := services.Global.LevelSettings.SetLevelUpChannel(context.Background(), guildID, channelID); err != nil {
shared.RespondEphemeral(s, i, "Failed to set level up channel.")
return
}
shared.RespondEphemeral(s, i, "Level up channel set to "+ch.Mention()+".")
}
@@ -0,0 +1,84 @@
package config
import (
"context"
"strconv"
"velox-bot/internal/commands/level/shared"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
func slvlSetLevelOption() *discordgo.ApplicationCommandOption {
return &discordgo.ApplicationCommandOption{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set",
Description: "Set the level of a user",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "The user to set the level of",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "level",
Description: "The level to set the user to",
Required: true,
},
},
}
}
func slvlSetLevelHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireManageGuild(s, i) || !shared.RequireLevelSystemEnabled(s, i) {
return
}
if services.Global == nil || services.Global.Level == nil {
shared.RespondEphemeral(s, i, "Leveling service is not available.")
return
}
optMap := shared.SubcommandOptionMap(i)
userOpt, ok := optMap["user"]
if !ok {
shared.RespondEphemeral(s, i, "Missing user option.")
return
}
levelOpt, ok := optMap["level"]
if !ok {
shared.RespondEphemeral(s, i, "Missing level option.")
return
}
targetUser := userOpt.UserValue(s)
if targetUser == nil {
shared.RespondEphemeral(s, i, "Invalid user.")
return
}
if targetUser.Bot {
shared.RespondEphemeral(s, i, "You can't set a bot's level.")
return
}
newLevel := int(levelOpt.IntValue())
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
userID, err := strconv.ParseInt(targetUser.ID, 10, 64)
if err != nil {
shared.RespondEphemeral(s, i, "Invalid user ID.")
return
}
if err := services.Global.Level.SetLevel(context.Background(), guildID, userID, newLevel); err != nil {
shared.RespondEphemeral(s, i, "Failed to set level.")
return
}
shared.RespondEphemeral(s, i, "Level set to "+strconv.Itoa(newLevel)+" for "+targetUser.Mention()+".")
}
@@ -0,0 +1,60 @@
package config
import (
"context"
"velox-bot/internal/commands/level/shared"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
func slvlSetMessageOption() *discordgo.ApplicationCommandOption {
return &discordgo.ApplicationCommandOption{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set-message",
Description: "Set the level up message",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message",
Description: "Level-up message. Variables: {user}, {level}",
Required: true,
},
},
}
}
func slvlSetMessageHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireManageGuild(s, i) || !shared.RequireLevelSystemEnabled(s, i) {
return
}
if !shared.RequireLevelSettingsService(s, i) {
return
}
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
optMap := shared.SubcommandOptionMap(i)
messageOpt, ok := optMap["message"]
if !ok {
shared.RespondEphemeral(s, i, "Missing message option.")
return
}
message := messageOpt.StringValue()
if message == "" {
message = "GG {user}, you reached level {level}!"
}
if err := services.Global.LevelSettings.SetLevelUpMessage(context.Background(), guildID, message); err != nil {
shared.RespondEphemeral(s, i, "Failed to set level up message.")
return
}
shared.RespondEphemeral(s, i, "Level up message set.")
}
@@ -0,0 +1,79 @@
package config
import (
"context"
"strconv"
"velox-bot/internal/commands/level/shared"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
func slvlSetRewardOption() *discordgo.ApplicationCommandOption {
return &discordgo.ApplicationCommandOption{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set-reward",
Description: "Set role reward for a level",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "level",
Description: "Level to reward",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionRole,
Name: "role",
Description: "Role to grant",
Required: true,
},
},
}
}
func slvlSetRewardHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireManageGuild(s, i) || !shared.RequireLevelSystemEnabled(s, i) {
return
}
if !shared.RequireLevelSettingsService(s, i) {
return
}
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
optMap := shared.SubcommandOptionMap(i)
levelOpt, ok := optMap["level"]
if !ok {
shared.RespondEphemeral(s, i, "Missing level option.")
return
}
roleOpt, ok := optMap["role"]
if !ok {
shared.RespondEphemeral(s, i, "Missing role option.")
return
}
level := int(levelOpt.IntValue())
role := roleOpt.RoleValue(s, i.GuildID)
if role == nil {
shared.RespondEphemeral(s, i, "Invalid role.")
return
}
roleID, err := strconv.ParseInt(role.ID, 10, 64)
if err != nil {
shared.RespondEphemeral(s, i, "Invalid role ID.")
return
}
if err := services.Global.LevelSettings.SetRoleRewardForLevel(context.Background(), guildID, level, roleID); err != nil {
shared.RespondEphemeral(s, i, "Failed to set reward.")
return
}
shared.RespondEphemeral(s, i, "Reward set: level "+strconv.Itoa(level)+" -> "+role.Mention())
}
+43
View File
@@ -0,0 +1,43 @@
package config
import (
"context"
"velox-bot/internal/commands/level/shared"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
func slvlToggleOption() *discordgo.ApplicationCommandOption {
return &discordgo.ApplicationCommandOption{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "toggle",
Description: "Toggle the leveling system",
}
}
func slvlToggleHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireManageGuild(s, i) {
return
}
if !shared.RequireLevelSettingsService(s, i) {
return
}
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
enabled, err := services.Global.LevelSettings.ToggleLevelSystem(context.Background(), guildID)
if err != nil {
shared.RespondEphemeral(s, i, "Failed to toggle leveling system.")
return
}
status := "disabled"
if enabled {
status = "enabled"
}
shared.RespondEphemeral(s, i, "Leveling system "+status+" for this server.")
}
-218
View File
@@ -1,218 +0,0 @@
package level
import (
"context"
"bytes"
"strconv"
"fmt"
"velox-bot/internal/db/services"
"velox-bot/internal/rankcard"
"github.com/bwmarrin/discordgo"
)
var RankCommand = &discordgo.ApplicationCommand{
Name: "rank",
Description: "Get your level card",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "The user to get the rank card of",
Required: false,
},
},
}
var LeaderboardCommand = &discordgo.ApplicationCommand{
Name: "leaderboard",
Description: "Top 10 levels in this server",
}
var RewardsCommand = &discordgo.ApplicationCommand{
Name: "rewards",
Description: "View level rewards in this server",
}
func HandleRank(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !requireLevelServices(s, i) {
return
}
if i.GuildID != "" && !requireLevelSystemEnabled(s, i) {
respondEphemeral(s, i, "Leveling system is disabled on this server.")
return
}
// Determine target user: option or caller.
target := i.User
if i.Member != nil && i.Member.User != nil {
target = i.Member.User
}
if len(i.ApplicationCommandData().Options) > 0 {
if u := i.ApplicationCommandData().Options[0].UserValue(s); u != nil {
target = u
}
}
if target == nil {
return
}
guildID, ok := parseGuildID(s, i)
if !ok {
return
}
userID, err := strconv.ParseInt(target.ID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid user ID.")
return
}
ctx := context.Background()
lvl, err := services.Global.Level.GetLevel(ctx, guildID, userID)
if err != nil {
respondEphemeral(s, i, "Failed to load level.")
return
}
percent := float64(lvl.XP) / 100.0
avatarURL := target.AvatarURL("256")
pngBytes, err := rankcard.RenderPNG(ctx, rankcard.Data{
Name: target.Username,
Level: lvl.Level,
XP: lvl.XP,
Percent: percent,
Avatar: avatarURL,
})
if err != nil {
respondEphemeral(s, i, "Failed to render rank card.")
return
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Files: []*discordgo.File{
{
Name: "levelcard.png",
Reader: bytes.NewReader(pngBytes),
},
},
},
})
}
func HandleLeaderboard(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !requireGuild(s, i) {
return
}
if !requireLevelServices(s, i) {
return
}
guildID, ok := parseGuildID(s, i)
if !ok {
return
}
ctx := context.Background()
if !requireLevelSystemEnabled(s, i) {
return
}
top, err := services.Global.Level.TopLevels(ctx, guildID, 10)
if err != nil {
respondEphemeral(s, i, "Failed to load leaderboard.")
return
}
if len(top) == 0 {
respondEphemeral(s, i, "No leaderboard data yet.")
return
}
fields := make([]*discordgo.MessageEmbedField, 0, len(top))
for idx, entry := range top {
display := fmt.Sprintf("<@%d>", entry.UserID)
if u, err := s.User(strconv.FormatInt(entry.UserID, 10)); err == nil && u != nil && u.Username != "" {
display = u.Username
}
fields = append(fields, &discordgo.MessageEmbedField{
Name: fmt.Sprintf("%d. %s", idx+1, display),
Value: fmt.Sprintf("Level: **%d** | XP: **%d**", entry.Level, entry.XP),
Inline: false,
})
}
embed := &discordgo.MessageEmbed{
Title: "Leaderboard",
Description: fmt.Sprintf("These are the top %d users in the server", len(top)),
Fields: fields,
Color: 0xF59E0B,
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{embed},
},
})
}
func HandleRewards(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !requireGuild(s, i) {
return
}
if !requireLevelSettingsService(s, i) {
return
}
guildID, ok := parseGuildID(s, i)
if !ok {
return
}
ctx := context.Background()
if !requireLevelSystemEnabled(s, i) {
return
}
rewards, err := services.Global.LevelSettings.ListRoleRewards(ctx, guildID, 25)
if err != nil {
respondEphemeral(s, i, "Failed to load rewards.")
return
}
if len(rewards) == 0 {
respondEphemeral(s, i, "No rewards configured yet.")
return
}
fields := make([]*discordgo.MessageEmbedField, 0, len(rewards))
for _, r := range rewards {
fields = append(fields, &discordgo.MessageEmbedField{
Name: fmt.Sprintf("Level %d", r.Level),
Value: fmt.Sprintf("<@&%d>", r.RoleID),
Inline: true,
})
}
embed := &discordgo.MessageEmbed{
Title: "Level Rewards",
Description: "Roles granted when you reach a level.",
Fields: fields,
Color: 0xF59E0B,
}
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{embed},
},
})
}
-326
View File
@@ -1,326 +0,0 @@
package level
import (
"context"
"strconv"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
var LevelConfigCommand = &discordgo.ApplicationCommand{
Name: "slvl",
Description: "Leveling System",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "toggle",
Description: "Toggle the leveling system",
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set",
Description: "Set the level of a user",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "The user to set the level of",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "level",
Description: "The level to set the user to",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set-channel",
Description: "Set the level up channel",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionChannel,
Name: "channel",
Description: "The channel to set the level up channel to. If not provided, the current channel will be used.",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set-message",
Description: "Set the level up message",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "message",
Description: "Level-up message. Variables: {user}, {level}",
Required: true,
},
},
},
{
Type: discordgo.ApplicationCommandOptionSubCommand,
Name: "set-reward",
Description: "Set role reward for a level",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionInteger,
Name: "level",
Description: "Level to reward",
Required: true,
},
{
Type: discordgo.ApplicationCommandOptionRole,
Name: "role",
Description: "Role to grant",
Required: true,
},
},
},
},
}
func HandleLevelConfig(s *discordgo.Session, i *discordgo.InteractionCreate) {
data := i.ApplicationCommandData()
subCommand := data.Options[0].Name
switch subCommand {
case "toggle":
HandleLevelToggle(s, i)
case "set":
HandleLevelSet(s, i)
case "set-channel":
HandleLevelSetChannel(s, i)
case "set-message":
HandleLevelSetMessage(s, i)
case "set-reward":
HandleLevelSetReward(s, i)
default:
respondEphemeral(s, i, "Invalid subcommand.")
}
}
func HandleLevelToggle(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !requireManageGuild(s, i) {
return
}
if !requireLevelSettingsService(s, i) {
return
}
ctx := context.Background()
guildID, ok := parseGuildID(s, i)
if !ok {
return
}
enabled, err := services.Global.LevelSettings.ToggleLevelSystem(ctx, guildID)
if err != nil {
respondEphemeral(s, i, "Failed to toggle leveling system.")
return
}
status := "disabled"
if enabled {
status = "enabled"
}
respondEphemeral(s, i, "Leveling system "+status+" for this server.")
}
func HandleLevelSet(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !requireManageGuild(s, i) || !requireLevelSystemEnabled(s, i) {
return
}
if services.Global == nil || services.Global.Level == nil {
respondEphemeral(s, i, "Leveling service is not available.")
return
}
optMap := subcommandOptionMap(i)
userOpt, ok := optMap["user"]
if !ok {
respondEphemeral(s, i, "Missing user option.")
return
}
levelOpt, ok := optMap["level"]
if !ok {
respondEphemeral(s, i, "Missing level option.")
return
}
targetUser := userOpt.UserValue(s)
if targetUser == nil {
respondEphemeral(s, i, "Invalid user.")
return
}
// prevent setting bots (including ourselves)
if targetUser.Bot {
respondEphemeral(s, i, "You can't set a bot's level.")
return
}
newLevel := int(levelOpt.IntValue())
ctx := context.Background()
guildID, ok := parseGuildID(s, i)
if !ok {
return
}
userID, err := strconv.ParseInt(targetUser.ID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid user ID.")
return
}
err = services.Global.Level.SetLevel(ctx, guildID, userID, newLevel)
if err != nil {
respondEphemeral(s, i, "Failed to set level.")
return
}
respondEphemeral(s, i, "Level set to "+strconv.Itoa(newLevel)+" for "+targetUser.Mention()+".")
}
func HandleLevelSetChannel(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !requireManageGuild(s, i) || !requireLevelSystemEnabled(s, i) {
return
}
if !requireLevelSettingsService(s, i) {
return
}
guildID, ok := parseGuildID(s, i)
if !ok {
return
}
optMap := subcommandOptionMap(i)
channelOpt, ok := optMap["channel"]
if !ok {
respondEphemeral(s, i, "Missing channel option.")
return
}
channelID := channelOpt.ChannelValue(s)
if channelID == nil {
respondEphemeral(s, i, "Invalid channel.")
return
}
ctx := context.Background()
channelIDInt, err := strconv.ParseInt(channelID.ID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid channel ID.")
return
}
err = services.Global.LevelSettings.SetLevelUpChannel(ctx, guildID, channelIDInt)
if err != nil {
respondEphemeral(s, i, "Failed to set level up channel.")
return
}
respondEphemeral(s, i, "Level up channel set to "+channelID.Mention()+".")
}
func HandleLevelSetMessage(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !requireManageGuild(s, i) || !requireLevelSystemEnabled(s, i) {
return
}
if !requireLevelSettingsService(s, i) {
return
}
guildID, ok := parseGuildID(s, i)
if !ok {
return
}
optMap := subcommandOptionMap(i)
messageOpt, ok := optMap["message"]
if !ok {
respondEphemeral(s, i, "Missing message option.")
return
}
message := messageOpt.StringValue()
if message == "" {
message = "GG {user}, you reached level {level}!"
}
ctx := context.Background()
err := services.Global.LevelSettings.SetLevelUpMessage(ctx, guildID, message)
if err != nil {
respondEphemeral(s, i, "Failed to set level up message.")
return
}
respondEphemeral(s, i, "Level up message set.")
}
func HandleLevelSetReward(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !requireManageGuild(s, i) || !requireLevelSystemEnabled(s, i) {
return
}
if !requireLevelSettingsService(s, i) {
return
}
guildID, ok := parseGuildID(s, i)
if !ok {
return
}
optMap := subcommandOptionMap(i)
levelOpt, ok := optMap["level"]
if !ok {
respondEphemeral(s, i, "Missing level option.")
return
}
roleOpt, ok := optMap["role"]
if !ok {
respondEphemeral(s, i, "Missing role option.")
return
}
level := int(levelOpt.IntValue())
role := roleOpt.RoleValue(s, i.GuildID)
if role == nil {
respondEphemeral(s, i, "Invalid role.")
return
}
roleID, err := strconv.ParseInt(role.ID, 10, 64)
if err != nil {
respondEphemeral(s, i, "Invalid role ID.")
return
}
ctx := context.Background()
if err := services.Global.LevelSettings.SetRoleRewardForLevel(ctx, guildID, level, roleID); err != nil {
respondEphemeral(s, i, "Failed to set reward.")
return
}
respondEphemeral(s, i, "Reward set: level "+strconv.Itoa(level)+" -> "+role.Mention())
}
@@ -0,0 +1,73 @@
package public
import (
"context"
"fmt"
"strconv"
"velox-bot/internal/commands/level/shared"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
var Leaderboard = &discordgo.ApplicationCommand{
Name: "leaderboard",
Description: "Top 10 levels in this server",
}
func LeaderboardHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireGuild(s, i) {
return
}
if !shared.RequireLevelServices(s, i) {
return
}
if !shared.RequireLevelSystemEnabled(s, i) {
return
}
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
ctx := context.Background()
top, err := services.Global.Level.TopLevels(ctx, guildID, 10)
if err != nil {
shared.RespondEphemeral(s, i, "Failed to load leaderboard.")
return
}
if len(top) == 0 {
shared.RespondEphemeral(s, i, "No leaderboard data yet.")
return
}
fields := make([]*discordgo.MessageEmbedField, 0, len(top))
for idx, entry := range top {
display := fmt.Sprintf("<@%d>", entry.UserID)
if u, err := s.User(strconv.FormatInt(entry.UserID, 10)); err == nil && u != nil && u.Username != "" {
display = u.Username
}
fields = append(fields, &discordgo.MessageEmbedField{
Name: fmt.Sprintf("%d. %s", idx+1, display),
Value: fmt.Sprintf("Level: **%d** | XP: **%d**", entry.Level, entry.XP),
Inline: false,
})
}
embed := &discordgo.MessageEmbed{
Title: "Leaderboard",
Description: fmt.Sprintf("These are the top %d users in the server", len(top)),
Fields: fields,
Color: 0xF59E0B,
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{embed},
},
})
}
+94
View File
@@ -0,0 +1,94 @@
package public
import (
"bytes"
"context"
"strconv"
"velox-bot/internal/commands/level/shared"
"velox-bot/internal/db/services"
"velox-bot/internal/rankcard"
"github.com/bwmarrin/discordgo"
)
var Rank = &discordgo.ApplicationCommand{
Name: "rank",
Description: "Get your level card",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionUser,
Name: "user",
Description: "The user to get the rank card of",
Required: false,
},
},
}
func RankHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireLevelServices(s, i) {
return
}
if i.GuildID != "" && !shared.RequireLevelSystemEnabled(s, i) {
return
}
// Determine target user: option or caller.
target := i.User
if i.Member != nil && i.Member.User != nil {
target = i.Member.User
}
if len(i.ApplicationCommandData().Options) > 0 {
if u := i.ApplicationCommandData().Options[0].UserValue(s); u != nil {
target = u
}
}
if target == nil {
return
}
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
userID, err := strconv.ParseInt(target.ID, 10, 64)
if err != nil {
shared.RespondEphemeral(s, i, "Invalid user ID.")
return
}
ctx := context.Background()
lvl, err := services.Global.Level.GetLevel(ctx, guildID, userID)
if err != nil {
shared.RespondEphemeral(s, i, "Failed to load level.")
return
}
percent := float64(lvl.XP) / 100.0
avatarURL := target.AvatarURL("256")
pngBytes, err := rankcard.RenderPNG(ctx, rankcard.Data{
Name: target.Username,
Level: lvl.Level,
XP: lvl.XP,
Percent: percent,
Avatar: avatarURL,
})
if err != nil {
shared.RespondEphemeral(s, i, "Failed to render rank card.")
return
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Files: []*discordgo.File{
{
Name: "levelcard.png",
Reader: bytes.NewReader(pngBytes),
},
},
},
})
}
+68
View File
@@ -0,0 +1,68 @@
package public
import (
"context"
"fmt"
"velox-bot/internal/commands/level/shared"
"velox-bot/internal/db/services"
"github.com/bwmarrin/discordgo"
)
var Rewards = &discordgo.ApplicationCommand{
Name: "rewards",
Description: "View level rewards in this server",
}
func RewardsHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if !shared.RequireGuild(s, i) {
return
}
if !shared.RequireLevelSettingsService(s, i) {
return
}
if !shared.RequireLevelSystemEnabled(s, i) {
return
}
guildID, ok := shared.ParseGuildID(s, i)
if !ok {
return
}
ctx := context.Background()
rewards, err := services.Global.LevelSettings.ListRoleRewards(ctx, guildID, 25)
if err != nil {
shared.RespondEphemeral(s, i, "Failed to load rewards.")
return
}
if len(rewards) == 0 {
shared.RespondEphemeral(s, i, "No rewards configured yet.")
return
}
fields := make([]*discordgo.MessageEmbedField, 0, len(rewards))
for _, r := range rewards {
fields = append(fields, &discordgo.MessageEmbedField{
Name: fmt.Sprintf("Level %d", r.Level),
Value: fmt.Sprintf("<@&%d>", r.RoleID),
Inline: true,
})
}
embed := &discordgo.MessageEmbed{
Title: "Level Rewards",
Description: "Roles granted when you reach a level.",
Fields: fields,
Color: 0xF59E0B,
}
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{embed},
},
})
}
+23
View File
@@ -0,0 +1,23 @@
package level
import (
"velox-bot/internal/commands/level/config"
"velox-bot/internal/commands/level/public"
"github.com/bwmarrin/discordgo"
)
// Commands
var (
Rank *discordgo.ApplicationCommand = public.Rank
Leaderboard *discordgo.ApplicationCommand = public.Leaderboard
Rewards *discordgo.ApplicationCommand = public.Rewards
Slvl *discordgo.ApplicationCommand = config.Slvl
)
// Handlers
func RankHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.RankHandler(s, i) }
func LeaderboardHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.LeaderboardHandler(s, i) }
func RewardsHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { public.RewardsHandler(s, i) }
func SlvlHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { config.SlvlHandler(s, i) }
@@ -1,4 +1,4 @@
package level package shared
import ( import (
"context" "context"
@@ -9,7 +9,7 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
func respondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) { func RespondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) {
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
@@ -19,68 +19,68 @@ func respondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg
}) })
} }
func requireGuild(s *discordgo.Session, i *discordgo.InteractionCreate) bool { func RequireGuild(s *discordgo.Session, i *discordgo.InteractionCreate) bool {
if i.GuildID == "" { if i.GuildID == "" {
respondEphemeral(s, i, "This command can only be used in a server.") RespondEphemeral(s, i, "This command can only be used in a server.")
return false return false
} }
return true return true
} }
func requireManageGuild(s *discordgo.Session, i *discordgo.InteractionCreate) bool { func RequireManageGuild(s *discordgo.Session, i *discordgo.InteractionCreate) bool {
if i.Member == nil || (i.Member.Permissions&discordgo.PermissionManageGuild) == 0 { if i.Member == nil || (i.Member.Permissions&discordgo.PermissionManageGuild) == 0 {
respondEphemeral(s, i, "You don't have permission to manage leveling settings.") RespondEphemeral(s, i, "You don't have permission to manage leveling settings.")
return false return false
} }
return true return true
} }
func requireLevelServices(s *discordgo.Session, i *discordgo.InteractionCreate) bool { func RequireLevelServices(s *discordgo.Session, i *discordgo.InteractionCreate) bool {
if services.Global == nil || services.Global.Level == nil || services.Global.LevelSettings == nil { if services.Global == nil || services.Global.Level == nil || services.Global.LevelSettings == nil {
respondEphemeral(s, i, "Leveling service is not available.") RespondEphemeral(s, i, "Leveling service is not available.")
return false return false
} }
return true return true
} }
func requireLevelSettingsService(s *discordgo.Session, i *discordgo.InteractionCreate) bool { func RequireLevelSettingsService(s *discordgo.Session, i *discordgo.InteractionCreate) bool {
if services.Global == nil || services.Global.LevelSettings == nil { if services.Global == nil || services.Global.LevelSettings == nil {
respondEphemeral(s, i, "Leveling service is not available.") RespondEphemeral(s, i, "Leveling service is not available.")
return false return false
} }
return true return true
} }
func parseGuildID(s *discordgo.Session, i *discordgo.InteractionCreate) (int64, bool) { func ParseGuildID(s *discordgo.Session, i *discordgo.InteractionCreate) (int64, bool) {
guildID, err := strconv.ParseInt(i.GuildID, 10, 64) guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
if err != nil { if err != nil {
respondEphemeral(s, i, "Invalid guild ID.") RespondEphemeral(s, i, "Invalid guild ID.")
return 0, false return 0, false
} }
return guildID, true return guildID, true
} }
func requireLevelSystemEnabled(s *discordgo.Session, i *discordgo.InteractionCreate) bool { func RequireLevelSystemEnabled(s *discordgo.Session, i *discordgo.InteractionCreate) bool {
if !requireLevelSettingsService(s, i) { if !RequireLevelSettingsService(s, i) {
return false return false
} }
guildID, ok := parseGuildID(s, i) guildID, ok := ParseGuildID(s, i)
if !ok { if !ok {
return false return false
} }
enabled, err := services.Global.LevelSettings.IsLevelSystemEnabled(context.Background(), guildID) enabled, err := services.Global.LevelSettings.IsLevelSystemEnabled(context.Background(), guildID)
if err != nil { if err != nil {
respondEphemeral(s, i, "Failed to load leveling settings.") RespondEphemeral(s, i, "Failed to load leveling settings.")
return false return false
} }
if !enabled { if !enabled {
respondEphemeral(s, i, "Leveling system is disabled on this server. Use /slvl toggle to enable it.") RespondEphemeral(s, i, "Leveling system is disabled on this server. Use /slvl toggle to enable it.")
return false return false
} }
return true return true
} }
func subcommandOptionMap(i *discordgo.InteractionCreate) map[string]*discordgo.ApplicationCommandInteractionDataOption { func SubcommandOptionMap(i *discordgo.InteractionCreate) map[string]*discordgo.ApplicationCommandInteractionDataOption {
data := i.ApplicationCommandData() data := i.ApplicationCommandData()
if len(data.Options) == 0 { if len(data.Options) == 0 {
return map[string]*discordgo.ApplicationCommandInteractionDataOption{} return map[string]*discordgo.ApplicationCommandInteractionDataOption{}
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1