2021-09-26 22:34:38 +00:00
|
|
|
package discord
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
|
2022-02-12 07:00:45 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/hmndata"
|
|
|
|
|
2021-09-26 22:34:38 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/db"
|
|
|
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
|
|
|
"git.handmade.network/hmn/hmn/src/logging"
|
|
|
|
"git.handmade.network/hmn/hmn/src/models"
|
|
|
|
)
|
|
|
|
|
2021-09-27 01:30:09 +00:00
|
|
|
// Slash command names and options
|
|
|
|
const SlashCommandProfile = "profile"
|
|
|
|
const ProfileOptionUser = "user"
|
2021-09-26 22:34:38 +00:00
|
|
|
|
2021-09-27 01:30:09 +00:00
|
|
|
// User command names
|
|
|
|
const UserCommandProfile = "HMN Profile"
|
2021-09-26 22:34:38 +00:00
|
|
|
|
|
|
|
func (bot *botInstance) createApplicationCommands(ctx context.Context) {
|
|
|
|
doOrWarn := func(err error) {
|
|
|
|
if err == nil {
|
|
|
|
logging.ExtractLogger(ctx).Info().Msg("Created Discord application command")
|
|
|
|
} else {
|
|
|
|
logging.ExtractLogger(ctx).Warn().Err(err).Msg("Failed to create Discord application command")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
doOrWarn(CreateGuildApplicationCommand(ctx, CreateGuildApplicationCommandRequest{
|
2021-09-27 01:30:09 +00:00
|
|
|
Type: ApplicationCommandTypeChatInput,
|
|
|
|
Name: SlashCommandProfile,
|
2021-09-26 22:34:38 +00:00
|
|
|
Description: "Get a link to a user's Handmade Network profile",
|
|
|
|
Options: []ApplicationCommandOption{
|
|
|
|
{
|
|
|
|
Type: ApplicationCommandOptionTypeUser,
|
2021-09-27 01:30:09 +00:00
|
|
|
Name: ProfileOptionUser,
|
2021-09-26 22:34:38 +00:00
|
|
|
Description: "The Discord user to look up on Handmade Network",
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
},
|
2021-09-27 01:30:09 +00:00
|
|
|
}))
|
|
|
|
doOrWarn(CreateGuildApplicationCommand(ctx, CreateGuildApplicationCommandRequest{
|
|
|
|
Type: ApplicationCommandTypeUser,
|
|
|
|
Name: UserCommandProfile,
|
2021-09-26 22:34:38 +00:00
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (bot *botInstance) doInteraction(ctx context.Context, i *Interaction) {
|
|
|
|
defer func() {
|
|
|
|
if recovered := recover(); recovered != nil {
|
|
|
|
logger := logging.ExtractLogger(ctx).Error()
|
|
|
|
if err, ok := recovered.(error); ok {
|
|
|
|
logger = logger.Err(err)
|
|
|
|
} else {
|
|
|
|
logger = logger.Interface("recovered", recovered)
|
|
|
|
}
|
|
|
|
logger.Msg("panic when handling Discord interaction")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
switch i.Data.Name {
|
2021-09-27 01:30:09 +00:00
|
|
|
case SlashCommandProfile:
|
|
|
|
userOpt := mustGetInteractionOption(i.Data.Options, ProfileOptionUser)
|
|
|
|
userID := userOpt.Value.(string)
|
|
|
|
bot.handleProfileCommand(ctx, i, userID)
|
|
|
|
case UserCommandProfile:
|
|
|
|
bot.handleProfileCommand(ctx, i, i.Data.TargetID)
|
2021-09-26 22:34:38 +00:00
|
|
|
default:
|
|
|
|
logging.ExtractLogger(ctx).Warn().Str("name", i.Data.Name).Msg("didn't recognize Discord interaction name")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-27 01:30:09 +00:00
|
|
|
func (bot *botInstance) handleProfileCommand(ctx context.Context, i *Interaction, userID string) {
|
2021-09-26 22:34:38 +00:00
|
|
|
member := i.Data.Resolved.Members[userID]
|
|
|
|
|
2021-09-27 01:30:09 +00:00
|
|
|
if member.User.IsBot {
|
2021-09-26 22:34:38 +00:00
|
|
|
err := CreateInteractionResponse(ctx, i.ID, i.Token, InteractionResponse{
|
|
|
|
Type: InteractionCallbackTypeChannelMessageWithSource,
|
|
|
|
Data: &InteractionCallbackData{
|
2021-09-27 01:30:09 +00:00
|
|
|
Content: "<a:confusedparrot:865957487026765864>",
|
2021-09-27 04:04:53 +00:00
|
|
|
Flags: FlagEphemeral,
|
2021-09-26 22:34:38 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to send profile response")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
type profileResult struct {
|
|
|
|
HMNUser models.User `db:"auth_user"`
|
|
|
|
}
|
|
|
|
ires, err := db.QueryOne(ctx, bot.dbConn, profileResult{},
|
|
|
|
`
|
|
|
|
SELECT $columns
|
|
|
|
FROM
|
|
|
|
handmade_discorduser AS duser
|
|
|
|
JOIN auth_user ON duser.hmn_user_id = auth_user.id
|
2021-12-29 14:38:23 +00:00
|
|
|
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
2021-09-26 22:34:38 +00:00
|
|
|
WHERE
|
|
|
|
duser.userid = $1
|
|
|
|
`,
|
|
|
|
userID,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
if errors.Is(err, db.NotFound) {
|
|
|
|
err = CreateInteractionResponse(ctx, i.ID, i.Token, InteractionResponse{
|
|
|
|
Type: InteractionCallbackTypeChannelMessageWithSource,
|
|
|
|
Data: &InteractionCallbackData{
|
2021-09-27 01:30:09 +00:00
|
|
|
Content: fmt.Sprintf("<@%s> hasn't linked a Handmade Network profile.", member.User.ID),
|
2021-09-27 04:04:53 +00:00
|
|
|
Flags: FlagEphemeral,
|
2021-09-26 22:34:38 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to send profile response")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to look up user profile")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
res := ires.(*profileResult)
|
|
|
|
|
2022-02-12 07:00:45 +00:00
|
|
|
projectsAndStuff, err := hmndata.FetchProjects(ctx, bot.dbConn, nil, hmndata.ProjectsQuery{
|
|
|
|
OwnerIDs: []int{res.HMNUser.ID},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to fetch user projects")
|
|
|
|
}
|
|
|
|
|
2021-09-26 22:34:38 +00:00
|
|
|
url := hmnurl.BuildUserProfile(res.HMNUser.Username)
|
2022-02-12 07:00:45 +00:00
|
|
|
msg := fmt.Sprintf("<@%s>'s profile can be viewed at %s.", member.User.ID, url)
|
|
|
|
if len(projectsAndStuff) > 0 {
|
|
|
|
projectNoun := "projects"
|
|
|
|
if len(projectsAndStuff) == 1 {
|
|
|
|
projectNoun = "project"
|
|
|
|
}
|
|
|
|
msg += fmt.Sprintf(" They have %d %s:\n", len(projectsAndStuff), projectNoun)
|
|
|
|
|
|
|
|
for _, p := range projectsAndStuff {
|
|
|
|
msg += fmt.Sprintf("- %s: %s\n", p.Project.Name, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-26 22:34:38 +00:00
|
|
|
err = CreateInteractionResponse(ctx, i.ID, i.Token, InteractionResponse{
|
|
|
|
Type: InteractionCallbackTypeChannelMessageWithSource,
|
|
|
|
Data: &InteractionCallbackData{
|
2022-02-12 07:00:45 +00:00
|
|
|
Content: msg,
|
2021-09-27 04:04:53 +00:00
|
|
|
Flags: FlagEphemeral,
|
2021-09-26 22:34:38 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
logging.ExtractLogger(ctx).Error().Err(err).Msg("failed to send profile response")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getInteractionOption(opts []ApplicationCommandInteractionDataOption, name string) (ApplicationCommandInteractionDataOption, bool) {
|
|
|
|
for _, opt := range opts {
|
|
|
|
if opt.Name == name {
|
|
|
|
return opt, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ApplicationCommandInteractionDataOption{}, false
|
|
|
|
}
|
|
|
|
|
|
|
|
func mustGetInteractionOption(opts []ApplicationCommandInteractionDataOption, name string) ApplicationCommandInteractionDataOption {
|
|
|
|
opt, ok := getInteractionOption(opts, name)
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Errorf("failed to get interaction option with name '%s'", name))
|
|
|
|
}
|
|
|
|
return opt
|
|
|
|
}
|