package public import ( "context" "fmt" "strconv" "strings" "time" "velox-bot/internal/commands/schedule/shared" "velox-bot/internal/db/repos/schedulerepo" "velox-bot/internal/db/services" "github.com/bwmarrin/discordgo" ) var Schedule = &discordgo.ApplicationCommand{ Name: "schedule", Description: "Schedule 1:1 sessions", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "create", Description: "Create a new session with a user", }, { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "list", Description: "List your upcoming sessions", }, { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "reschedule", Description: "Reschedule an existing session", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionInteger, Name: "id", Description: "ID of the session (from /schedule list)", Required: true, }, { Type: discordgo.ApplicationCommandOptionString, Name: "datetime", Description: "New datetime (UTC), format: 2006-01-02 15:04", Required: true, }, }, }, }, } func ScheduleHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { if !shared.RequireGuild(s, i) || !shared.RequireScheduleService(s, i) { return } data := i.ApplicationCommandData() if len(data.Options) == 0 { handleList(s, i) return } switch data.Options[0].Name { case "create": handleCreate(s, i) case "list": handleList(s, i) case "reschedule": handleReschedule(s, i) default: shared.RespondEphemeral(s, i, "Unknown subcommand.") } } func handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) { if _, ok := shared.ParseGuildID(s, i); !ok { return } if i.Member == nil || i.Member.User == nil { shared.RespondEphemeral(s, i, "Missing member info.") return } s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ Content: "Who do you want to schedule a session with?", Flags: discordgo.MessageFlagsEphemeral, Components: []discordgo.MessageComponent{ discordgo.ActionsRow{ Components: []discordgo.MessageComponent{ discordgo.SelectMenu{ MenuType: discordgo.UserSelectMenu, CustomID: createUserSelectCustomID, Placeholder: "Select a user", }, }, }, }, }, }) } func handleList(s *discordgo.Session, i *discordgo.InteractionCreate) { 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 } userID, err := strconv.ParseInt(i.Member.User.ID, 10, 64) if err != nil { shared.RespondEphemeral(s, i, "Invalid user ID.") return } const limit = 10 items, err := services.Global.Schedule.ListForUser(context.Background(), guildID, userID, limit) if err != nil { shared.RespondEphemeral(s, i, "Failed to load your sessions.") return } if len(items) == 0 { shared.RespondEphemeral(s, i, "You have no upcoming sessions.") return } var b strings.Builder for _, sch := range items { role := "Requester" otherID := sch.InviteeID if sch.InviteeID == userID { role = "Invitee" otherID = sch.RequesterID } status := string(sch.Status) whenStr := sch.ScheduledAt.UTC().Format("2006-01-02 15:04") line := fmt.Sprintf("ID `%d` • %s with <@%d> at %s UTC (%s)\n", sch.ID, role, otherID, whenStr, status) if sch.Description != "" { line += " " + sch.Description + "\n" } b.WriteString(line) } shared.RespondEphemeral(s, i, b.String()) } func handleReschedule(s *discordgo.Session, i *discordgo.InteractionCreate) { 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 ( idVal int64 whenStr string ) for _, o := range opt.Options { switch o.Name { case "id": idVal = o.IntValue() case "datetime": whenStr = o.StringValue() } } if idVal <= 0 { shared.RespondEphemeral(s, i, "Invalid session ID.") return } whenStr = strings.TrimSpace(whenStr) if whenStr == "" { shared.RespondEphemeral(s, i, "Datetime cannot be empty.") return } userID, err := strconv.ParseInt(i.Member.User.ID, 10, 64) if err != nil { shared.RespondEphemeral(s, i, "Invalid user ID.") return } ctx := context.Background() sch, err := services.Global.Schedule.GetByID(ctx, idVal) if err != nil || sch == nil { shared.RespondEphemeral(s, i, "Session not found.") return } if sch.GuildID != guildID { shared.RespondEphemeral(s, i, "This session belongs to another server.") return } if userID != sch.RequesterID && userID != sch.InviteeID { shared.RespondEphemeral(s, i, "You are not part of this session.") return } if sch.Status != schedulerepo.StatusPending && sch.Status != schedulerepo.StatusAccepted { shared.RespondEphemeral(s, i, "This session can't be rescheduled right now.") return } // Use rescheduler's timezone for parsing. loc, zone, _, err := services.Global.UserSettings.GetTimezone(ctx, userID) if err != nil { loc = time.UTC zone = "UTC" } // Expect explicit layout "2006-01-02 15:04" in user's timezone. when, err := shared.ParseScheduleDatetime(whenStr, loc) if err != nil { shared.RespondEphemeral(s, i, err.Error()+" (use `2006-01-02 15:04` in your timezone: "+zone+")") return } when = when.In(time.UTC) if err := services.Global.Schedule.ProposeReschedule(ctx, sch.ID, when, userID); err != nil { shared.RespondEphemeral(s, i, "Failed to propose reschedule.") return } whenFmt := when.Format("2006-01-02 15:04") otherID := sch.InviteeID if userID == sch.InviteeID { otherID = sch.RequesterID } dmCh, err := s.UserChannelCreate(strconv.FormatInt(otherID, 10)) if err != nil { shared.RespondEphemeral(s, i, "Proposal saved, but failed to DM the other participant (are DMs disabled?).") return } embed := &discordgo.MessageEmbed{ Title: "Reschedule proposed", Description: fmt.Sprintf("<@%s> has proposed a new time for your session.", i.Member.User.ID), Color: 0x4caf50, } // Show both UTC and the other participant's local time if available. locOther, zoneOther, _, errTZ := services.Global.UserSettings.GetTimezone(ctx, otherID) if errTZ != nil { locOther = time.UTC zoneOther = "UTC" } localOtherStr := when.In(locOther).Format("2006-01-02 15:04") embed.Fields = []*discordgo.MessageEmbedField{ { Name: "When (UTC)", Value: whenFmt, }, { Name: "When (" + zoneOther + ")", Value: localOtherStr, }, } if sch.Description != "" { embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ Name: "Description", Value: sch.Description, }) } _, err = s.ChannelMessageSendComplex(dmCh.ID, &discordgo.MessageSend{ Content: "A new time has been proposed for your session:", Embed: embed, Components: rescheduleProposalButtons(sch.ID), }) if err != nil { shared.RespondEphemeral(s, i, "Proposal saved, but failed to DM the other participant.") return } shared.RespondEphemeral(s, i, "Proposed new time sent to <@"+strconv.FormatInt(otherID, 10)+">.") }