300 lines
7.9 KiB
Go
300 lines
7.9 KiB
Go
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",
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "add-helper",
|
|
Description: "Add a helper to an existing project",
|
|
},
|
|
{
|
|
Type: discordgo.ApplicationCommandOptionSubCommand,
|
|
Name: "close",
|
|
Description: "Close (deactivate) a project",
|
|
},
|
|
},
|
|
}
|
|
|
|
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)
|
|
case "close":
|
|
handleClose(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) + ">"
|
|
}
|
|
|
|
// handleCreate just opens the modal - the actual creation happens on
|
|
// submit, in HandleModalSubmit (components.go).
|
|
func handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !shared.RequireManageGuild(s, i) {
|
|
return
|
|
}
|
|
|
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseModal,
|
|
Data: &discordgo.InteractionResponseData{
|
|
CustomID: "projects:create_modal",
|
|
Title: "Create a project",
|
|
Components: []discordgo.MessageComponent{
|
|
discordgo.ActionsRow{
|
|
Components: []discordgo.MessageComponent{
|
|
discordgo.TextInput{
|
|
CustomID: "name",
|
|
Label: "Project name",
|
|
Style: discordgo.TextInputShort,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
discordgo.ActionsRow{
|
|
Components: []discordgo.MessageComponent{
|
|
discordgo.TextInput{
|
|
CustomID: "description",
|
|
Label: "Description (optional)",
|
|
Style: discordgo.TextInputParagraph,
|
|
Required: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// handleAddHelper collects the user via Discord's native user-option (a
|
|
// modal can't hold one of these), then responds with a select menu of the
|
|
// guild's active projects instead of making the caller remember/type a
|
|
// numeric project ID - the actual add-helper call happens on selection, in
|
|
// HandleComponent (components.go).
|
|
// handleAddHelper is the first of two dropdown steps: pick a project here,
|
|
// then HandleComponent (components.go) follows up with a native Discord
|
|
// user-select to pick who helps, and performs the actual AddHelper call
|
|
// once both are known.
|
|
func handleAddHelper(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !shared.RequireManageGuild(s, i) {
|
|
return
|
|
}
|
|
|
|
guildID, ok := shared.ParseGuildID(s, i)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
options, err := activeProjectOptions(context.Background(), guildID)
|
|
if err != nil {
|
|
shared.RespondEphemeral(s, i, "Failed to load projects.")
|
|
return
|
|
}
|
|
if len(options) == 0 {
|
|
shared.RespondEphemeral(s, i, "There are no active projects to add a helper to.")
|
|
return
|
|
}
|
|
|
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{
|
|
Content: "Which project should get a new helper?",
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
Components: []discordgo.MessageComponent{
|
|
discordgo.ActionsRow{
|
|
Components: []discordgo.MessageComponent{
|
|
discordgo.SelectMenu{
|
|
CustomID: "projects:add_helper_project_select",
|
|
Placeholder: "Select a project",
|
|
Options: options,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// handleClose responds with a select menu of the guild's active projects -
|
|
// same reasoning as handleAddHelper, picking from a live list beats typing
|
|
// an ID. The actual deactivation happens on selection, in HandleComponent.
|
|
func handleClose(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
if !shared.RequireManageGuild(s, i) {
|
|
return
|
|
}
|
|
|
|
guildID, ok := shared.ParseGuildID(s, i)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
options, err := activeProjectOptions(context.Background(), guildID)
|
|
if err != nil {
|
|
shared.RespondEphemeral(s, i, "Failed to load projects.")
|
|
return
|
|
}
|
|
if len(options) == 0 {
|
|
shared.RespondEphemeral(s, i, "There are no active projects to close.")
|
|
return
|
|
}
|
|
|
|
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
Data: &discordgo.InteractionResponseData{
|
|
Content: "Which project should be closed?",
|
|
Flags: discordgo.MessageFlagsEphemeral,
|
|
Components: []discordgo.MessageComponent{
|
|
discordgo.ActionsRow{
|
|
Components: []discordgo.MessageComponent{
|
|
discordgo.SelectMenu{
|
|
CustomID: "projects:close_select",
|
|
Placeholder: "Select a project",
|
|
Options: options,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// activeProjectOptions builds select-menu options for the guild's active
|
|
// projects, truncating name/description to Discord's option limits
|
|
// (label: 100 chars, description: 100 chars).
|
|
func activeProjectOptions(ctx context.Context, guildID int64) ([]discordgo.SelectMenuOption, error) {
|
|
const limit = 25 // Discord's own cap on select menu options
|
|
items, err := services.Global.Projects.ListActive(ctx, guildID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
options := make([]discordgo.SelectMenuOption, 0, len(items))
|
|
for _, p := range items {
|
|
if p == nil {
|
|
continue
|
|
}
|
|
name := p.Name
|
|
if name == "" {
|
|
name = "Unnamed project"
|
|
}
|
|
options = append(options, discordgo.SelectMenuOption{
|
|
Label: truncate(name, 100),
|
|
Value: strconv.FormatInt(p.ID, 10),
|
|
Description: truncate(p.Description, 100),
|
|
})
|
|
}
|
|
return options, nil
|
|
}
|
|
|
|
func truncate(s string, max int) string {
|
|
if len(s) <= max {
|
|
return s
|
|
}
|
|
return s[:max]
|
|
}
|