feat: schedule rework
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"velox-bot/internal/commands/schedule/shared"
|
||||
"velox-bot/internal/db/repos/schedulerepo"
|
||||
"velox-bot/internal/db/services"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
@@ -21,26 +22,6 @@ var Schedule = &discordgo.ApplicationCommand{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "create",
|
||||
Description: "Create a new session with a user",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionUser,
|
||||
Name: "user",
|
||||
Description: "User to invite",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "datetime",
|
||||
Description: "When (UTC), format: 2006-01-02 15:04",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "description",
|
||||
Description: "What is this session about? (optional)",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
@@ -93,8 +74,7 @@ func ScheduleHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
}
|
||||
|
||||
func handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
guildID, ok := shared.ParseGuildID(s, i)
|
||||
if !ok {
|
||||
if _, ok := shared.ParseGuildID(s, i); !ok {
|
||||
return
|
||||
}
|
||||
if i.Member == nil || i.Member.User == nil {
|
||||
@@ -102,133 +82,24 @@ func handleCreate(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
return
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
if len(data.Options) == 0 {
|
||||
shared.RespondEphemeral(s, i, "Missing options.")
|
||||
return
|
||||
}
|
||||
opt := data.Options[0]
|
||||
|
||||
var (
|
||||
targetUser *discordgo.User
|
||||
whenStr string
|
||||
desc string
|
||||
)
|
||||
for _, o := range opt.Options {
|
||||
switch o.Name {
|
||||
case "user":
|
||||
targetUser = o.UserValue(s)
|
||||
case "datetime":
|
||||
whenStr = o.StringValue()
|
||||
case "description":
|
||||
desc = o.StringValue()
|
||||
}
|
||||
}
|
||||
|
||||
if targetUser == nil {
|
||||
shared.RespondEphemeral(s, i, "Invalid user.")
|
||||
return
|
||||
}
|
||||
if targetUser.ID == i.Member.User.ID {
|
||||
shared.RespondEphemeral(s, i, "You cannot schedule a session with yourself.")
|
||||
return
|
||||
}
|
||||
|
||||
requesterID, err := strconv.ParseInt(i.Member.User.ID, 10, 64)
|
||||
if err != nil {
|
||||
shared.RespondEphemeral(s, i, "Invalid user ID.")
|
||||
return
|
||||
}
|
||||
inviteeID, err := strconv.ParseInt(targetUser.ID, 10, 64)
|
||||
if err != nil {
|
||||
shared.RespondEphemeral(s, i, "Invalid target user ID.")
|
||||
return
|
||||
}
|
||||
|
||||
whenStr = strings.TrimSpace(whenStr)
|
||||
if whenStr == "" {
|
||||
shared.RespondEphemeral(s, i, "Datetime cannot be empty.")
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve requester's timezone and parse input using explicit layout in that zone.
|
||||
loc, zone, _, err := services.Global.UserSettings.GetTimezone(context.Background(), requesterID)
|
||||
if err != nil {
|
||||
loc = time.UTC
|
||||
zone = "UTC"
|
||||
}
|
||||
// Expect layout "2006-01-02 15:04" in the user's timezone.
|
||||
when, err := time.ParseInLocation("2006-01-02 15:04", whenStr, loc)
|
||||
if err != nil {
|
||||
shared.RespondEphemeral(s, i, "Invalid datetime. Use `2006-01-02 15:04` in your timezone ("+zone+").")
|
||||
return
|
||||
}
|
||||
when = when.In(time.UTC)
|
||||
|
||||
ctx := context.Background()
|
||||
scheduleID, err := services.Global.Schedule.CreateSchedule(ctx, guildID, requesterID, inviteeID, when, desc)
|
||||
if err != nil {
|
||||
shared.RespondEphemeral(s, i, "Failed to create schedule.")
|
||||
return
|
||||
}
|
||||
|
||||
// Send DM to invitee.
|
||||
dmCh, err := s.UserChannelCreate(targetUser.ID)
|
||||
if err != nil {
|
||||
shared.RespondEphemeral(s, i, "Failed to DM the invited user (are DMs disabled?).")
|
||||
return
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "New session request",
|
||||
Description: fmt.Sprintf("You have been invited to a session by <@%s>.", i.Member.User.ID),
|
||||
Color: 0x4caf50,
|
||||
}
|
||||
// Show both UTC and invitee-local time if available.
|
||||
utcStr := when.Format("2006-01-02 15:04")
|
||||
locInv, zoneInv, _, err := services.Global.UserSettings.GetTimezone(context.Background(), inviteeID)
|
||||
if err != nil {
|
||||
locInv = time.UTC
|
||||
zoneInv = "UTC"
|
||||
}
|
||||
localInvStr := when.In(locInv).Format("2006-01-02 15:04")
|
||||
embed.Fields = []*discordgo.MessageEmbedField{
|
||||
{
|
||||
Name: "When (UTC)",
|
||||
Value: utcStr,
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "When (" + zoneInv + ")",
|
||||
Value: localInvStr,
|
||||
},
|
||||
}
|
||||
if desc != "" {
|
||||
embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{
|
||||
Name: "Description",
|
||||
Value: desc,
|
||||
})
|
||||
}
|
||||
|
||||
msg, err := s.ChannelMessageSendComplex(dmCh.ID, &discordgo.MessageSend{
|
||||
Content: "React with ✅ to accept, ❌ to decline, or 🔁 to request rescheduling.",
|
||||
Embed: embed,
|
||||
})
|
||||
if err != nil {
|
||||
shared.RespondEphemeral(s, i, "Failed to DM the invited user.")
|
||||
return
|
||||
}
|
||||
|
||||
// Add reactions for interaction.
|
||||
for _, emoji := range []string{"✅", "❌", "🔁"} {
|
||||
_ = s.MessageReactionAdd(dmCh.ID, msg.ID, emoji)
|
||||
}
|
||||
|
||||
// Store linkage between message and schedule.
|
||||
msgID64, _ := strconv.ParseInt(msg.ID, 10, 64)
|
||||
chID64, _ := strconv.ParseInt(dmCh.ID, 10, 64)
|
||||
_ = services.Global.Schedule.AddMessage(ctx, scheduleID, msgID64, chID64, true)
|
||||
|
||||
shared.RespondEphemeral(s, i, "Session request created and sent to "+targetUser.Mention()+".")
|
||||
}
|
||||
|
||||
func handleList(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
@@ -338,6 +209,11 @@ func handleReschedule(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
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 {
|
||||
@@ -345,19 +221,18 @@ func handleReschedule(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
zone = "UTC"
|
||||
}
|
||||
// Expect explicit layout "2006-01-02 15:04" in user's timezone.
|
||||
when, err := time.ParseInLocation("2006-01-02 15:04", whenStr, loc)
|
||||
when, err := shared.ParseScheduleDatetime(whenStr, loc)
|
||||
if err != nil {
|
||||
shared.RespondEphemeral(s, i, "Invalid datetime. Use `2006-01-02 15:04` in your timezone ("+zone+").")
|
||||
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.Reschedule(ctx, sch.ID, when); err != nil {
|
||||
shared.RespondEphemeral(s, i, "Failed to reschedule session.")
|
||||
if err := services.Global.Schedule.ProposeReschedule(ctx, sch.ID, when, userID); err != nil {
|
||||
shared.RespondEphemeral(s, i, "Failed to propose reschedule.")
|
||||
return
|
||||
}
|
||||
|
||||
// Notify participants via DM and send new request DM to invitee.
|
||||
whenFmt := when.Format("2006-01-02 15:04")
|
||||
|
||||
otherID := sch.InviteeID
|
||||
@@ -365,75 +240,50 @@ func handleReschedule(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
otherID = sch.RequesterID
|
||||
}
|
||||
|
||||
// DM both about the change, including their local times if available.
|
||||
for _, uid := range []int64{sch.RequesterID, sch.InviteeID} {
|
||||
locU, zoneU, _, err := services.Global.UserSettings.GetTimezone(ctx, uid)
|
||||
if err != nil {
|
||||
locU = time.UTC
|
||||
zoneU = "UTC"
|
||||
}
|
||||
localU := when.In(locU).Format("2006-01-02 15:04")
|
||||
msg := fmt.Sprintf("Session `%d` has been rescheduled to %s UTC (%s %s) with <@%d>.", sch.ID, whenFmt, localU, zoneU, otherID)
|
||||
if sch.Description != "" {
|
||||
msg += "\nTopic: " + sch.Description
|
||||
}
|
||||
ch, err := s.UserChannelCreate(strconv.FormatInt(uid, 10))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = s.ChannelMessageSend(ch.ID, msg)
|
||||
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
|
||||
}
|
||||
|
||||
// Send new "request" DM to invitee with reactions again.
|
||||
inviteeStr := strconv.FormatInt(sch.InviteeID, 10)
|
||||
inviteeUser, err := s.User(inviteeStr)
|
||||
if err == nil && inviteeUser != nil {
|
||||
dmCh, err := s.UserChannelCreate(inviteeStr)
|
||||
if err == nil {
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Rescheduled session",
|
||||
Description: fmt.Sprintf("Session `%d` has been rescheduled by <@%s>.", sch.ID, i.Member.User.ID),
|
||||
Color: 0xffc107,
|
||||
}
|
||||
// Show both UTC and invitee-local time if available.
|
||||
locInv, zoneInv, _, errTZ := services.Global.UserSettings.GetTimezone(ctx, sch.InviteeID)
|
||||
if errTZ != nil {
|
||||
locInv = time.UTC
|
||||
zoneInv = "UTC"
|
||||
}
|
||||
localInvStr := when.In(locInv).Format("2006-01-02 15:04")
|
||||
embed.Fields = []*discordgo.MessageEmbedField{
|
||||
{
|
||||
Name: "When (UTC)",
|
||||
Value: whenFmt,
|
||||
},
|
||||
{
|
||||
Name: "When (" + zoneInv + ")",
|
||||
Value: localInvStr,
|
||||
},
|
||||
}
|
||||
if sch.Description != "" {
|
||||
embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{
|
||||
Name: "Description",
|
||||
Value: sch.Description,
|
||||
})
|
||||
}
|
||||
msgObj, err := s.ChannelMessageSendComplex(dmCh.ID, &discordgo.MessageSend{
|
||||
Content: "React with ✅ to accept, ❌ to decline, or 🔁 to request another reschedule.",
|
||||
Embed: embed,
|
||||
})
|
||||
if err == nil && msgObj != nil {
|
||||
for _, emoji := range []string{"✅", "❌", "🔁"} {
|
||||
_ = s.MessageReactionAdd(dmCh.ID, msgObj.ID, emoji)
|
||||
}
|
||||
msgID64, _ := strconv.ParseInt(msgObj.ID, 10, 64)
|
||||
chID64, _ := strconv.ParseInt(dmCh.ID, 10, 64)
|
||||
_ = services.Global.Schedule.AddMessage(ctx, sch.ID, msgID64, chID64, true)
|
||||
}
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
shared.RespondEphemeral(s, i, "Session rescheduled to "+whenFmt+" UTC.")
|
||||
_, 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)+">.")
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user