package public import ( "context" "strconv" "strings" "velox-bot/internal/commands/projects/shared" "velox-bot/internal/db/services" "github.com/bwmarrin/discordgo" ) var Projects = &discordgo.ApplicationCommand{ Name: "projects", Description: "Manage and list ongoing projects", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "list", Description: "List active projects with creators and helpers", }, { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "create", Description: "Create a new active project", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, Name: "name", Description: "Project name", Required: true, }, { Type: discordgo.ApplicationCommandOptionString, Name: "description", Description: "Short description (optional)", Required: false, }, }, }, { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "add-helper", Description: "Add a helper to an existing project", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionInteger, Name: "project-id", Description: "ID of the project", Required: true, }, { Type: discordgo.ApplicationCommandOptionUser, Name: "user", Description: "User to add as helper", Required: true, }, }, }, }, } func ProjectsHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { if !shared.RequireGuild(s, i) || !shared.RequireProjectsService(s, i) { return } data := i.ApplicationCommandData() if len(data.Options) == 0 { handleList(s, i) return } switch data.Options[0].Name { case "list": handleList(s, i) case "create": handleCreate(s, i) case "add-helper": handleAddHelper(s, i) default: shared.RespondEphemeral(s, i, "Unknown subcommand.") } } func handleList(s *discordgo.Session, i *discordgo.InteractionCreate) { guildID, ok := shared.ParseGuildID(s, i) if !ok { return } const limit = 25 items, err := services.Global.Projects.ListActiveWithMembers(context.Background(), guildID, limit) if err != nil { shared.RespondEphemeral(s, i, "Failed to load projects.") return } if len(items) == 0 { shared.RespondEphemeral(s, i, "There are no active projects at the moment.") return } embed := &discordgo.MessageEmbed{ Title: "Active projects", Description: "", Color: 0x00bcd4, } for _, item := range items { if item == nil || item.Project == nil { continue } p := item.Project name := p.Name if name == "" { name = "Unnamed project" } creatorMention := mentionUser(item.Creator) var helpersMentions []string for _, h := range item.Helpers { helpersMentions = append(helpersMentions, mentionUser(h)) } helpersText := "None" if len(helpersMentions) > 0 { helpersText = strings.Join(helpersMentions, ", ") } desc := "**Creator:** " + creatorMention + "\n" + "**Helpers:** " + helpersText if p.Description != "" { desc += "\n" + p.Description } embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ Name: name, Value: desc, }) } _ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Embeds: []*discordgo.MessageEmbed{embed}, }, }) } func mentionUser(id int64) string { if id == 0 { return "Unknown" } return "<@" + strconv.FormatInt(id, 10) + ">" } func handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { if !shared.RequireManageGuild(s, i) { return } guildID, ok := shared.ParseGuildID(s, i) if !ok { return } if i.Member == nil || i.Member.User == nil { shared.RespondEphemeral(s, i, "Missing member info.") return } data := i.ApplicationCommandData() if len(data.Options) == 0 { shared.RespondEphemeral(s, i, "Missing options.") return } opt := data.Options[0] var name, description string for _, o := range opt.Options { switch o.Name { case "name": name = o.StringValue() case "description": description = o.StringValue() } } if strings.TrimSpace(name) == "" { shared.RespondEphemeral(s, i, "Project name cannot be empty.") return } creatorID, err := strconv.ParseInt(i.Member.User.ID, 10, 64) if err != nil { shared.RespondEphemeral(s, i, "Invalid user ID.") return } id, err := services.Global.Projects.CreateProject(context.Background(), guildID, creatorID, name, description) if err != nil { shared.RespondEphemeral(s, i, "Failed to create project.") return } shared.RespondEphemeral(s, i, "Created project **"+name+"** with ID `"+strconv.FormatInt(id, 10)+"`.") } func handleAddHelper(s *discordgo.Session, i *discordgo.InteractionCreate) { if !shared.RequireManageGuild(s, i) { return } guildID, ok := shared.ParseGuildID(s, i) if !ok { return } data := i.ApplicationCommandData() if len(data.Options) == 0 { shared.RespondEphemeral(s, i, "Missing options.") return } opt := data.Options[0] var projectID int64 var userID string for _, o := range opt.Options { switch o.Name { case "project-id": projectID = o.IntValue() case "user": if u := o.UserValue(s); u != nil { userID = u.ID } } } if projectID <= 0 { shared.RespondEphemeral(s, i, "Invalid project ID.") return } if userID == "" { shared.RespondEphemeral(s, i, "Invalid user.") return } exists, err := services.Global.Projects.ProjectExistsInGuild(context.Background(), guildID, projectID) if err != nil { shared.RespondEphemeral(s, i, "Failed to load project.") return } if !exists { shared.RespondEphemeral(s, i, "Project `"+strconv.FormatInt(projectID, 10)+"` not found in this server.") return } userID64, err := strconv.ParseInt(userID, 10, 64) if err != nil { shared.RespondEphemeral(s, i, "Invalid user ID.") return } if err := services.Global.Projects.AddHelper(context.Background(), projectID, userID64); err != nil { shared.RespondEphemeral(s, i, "Failed to add helper to project. ("+err.Error()+")") return } shared.RespondEphemeral(s, i, "Added <@"+userID+"> as helper to project `"+strconv.FormatInt(projectID, 10)+"`.") }