290 lines
7.4 KiB
Go
290 lines
7.4 KiB
Go
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)+">.")
|
|
}
|