diff --git a/.golangci.yml b/.golangci.yml index 1d27543..0dec532 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -16,6 +16,9 @@ run: # This file contains only configs which differ from defaults. # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml linters-settings: + goconst: + min-occurrences: 5 + cyclop: # The maximal code complexity to report. # Default: 10 diff --git a/internal/moderation/buttons.go b/internal/moderation/buttons.go index 90ea603..5e9409b 100644 --- a/internal/moderation/buttons.go +++ b/internal/moderation/buttons.go @@ -27,67 +27,70 @@ func InfoButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { // Get the action var action = customIDSplit[0] + // Get the userID var userID = customIDSplit[1] + // Get user from the user ID + discordUser, err := s.User(userID) + if err != nil { + shared.SimpleEphemeralInteractionResponse("Failed to get discord user", s, i.Interaction) + log.Error().Err(err).Msg("Failed to get discord user") + return + } + // Get the embedID var embedID = customIDSplit[2] // Get the channelID var channelID = customIDSplit[3] - // TODO: Handle if a user has never been seen before - userObj, err := shared.DBClient.User.FindUnique( - db.User.UserID.Equals(userID), - ).Exec(context.Background()) + userObj, err := shared.GetUserIfExists(&discordgo.User{ + ID: userID, + Username: discordUser.Username, + }) if err != nil { + shared.SimpleEphemeralInteractionResponse("Failed to get or create user", s, i.Interaction) log.Error().Err(err).Msg("Failed to get user") return } reportCount := getUserReportCount(userObj.ID) - // Get discord user - discordUser, err := s.User(userID) - if err != nil { - log.Error().Err(err).Msg("Failed to get user") - return - } - log.Debug().Msg("Got user info button event") - var embed discordgo.MessageEmbed + var embed = discordgo.MessageEmbed{ + Title: "Infractions", + Color: shared.DarkBlue, + Fields: []*discordgo.MessageEmbedField{ + { + Name: "User (Tag)", + Value: "", + Inline: true, + }, + { + Name: "Points", + Value: strconv.Itoa(userObj.Points), + Inline: true, + }, + { + Name: "Reports", + Value: strconv.Itoa(reportCount), + Inline: true, + }, + }, + // Set the image as the users avatar + Thumbnail: &discordgo.MessageEmbedThumbnail{ + URL: discordUser.AvatarURL("256x256"), + }, + } + switch action { case "overview": embed = GenerateOverviewEmbed(*userObj, userID, reportCount, discordUser.AvatarURL("256x256")) case "infractions": - embed = discordgo.MessageEmbed{ - Title: "Infractions", - Color: shared.DarkBlue, - Fields: []*discordgo.MessageEmbedField{ - { - Name: "User (Tag)", - Value: "", - Inline: true, - }, - { - Name: "Points", - Value: strconv.Itoa(userObj.Points), - Inline: true, - }, - { - Name: "Reports", - Value: strconv.Itoa(reportCount), - Inline: true, - }, - { - Name: "__Infractions__", - Value: "", - }, - }, - // Set the image as the users avatar - Thumbnail: &discordgo.MessageEmbedThumbnail{ - URL: discordUser.AvatarURL("256x256"), - }, - } + embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ + Name: "__Infractions__", + Value: "", + }) // Get all infractions infractions, infractionErr := shared.DBClient.Infraction.FindMany( @@ -96,6 +99,7 @@ func InfoButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { db.Infraction.CreatedAt.Order(db.SortOrderDesc), ).Exec(context.Background()) if infractionErr != nil { + shared.SimpleEphemeralInteractionResponse("Failed to get infractions", s, i.Interaction) log.Error().Err(err).Msg("Failed to get infractions") return } @@ -112,7 +116,33 @@ func InfoButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { }) } case "notes": - // Show the notes + embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ + Name: "__Notes__", + Value: "", + }) + + // Get all notes + notes, noteErr := shared.DBClient.Note.FindMany( + db.Note.UserID.Equals(userObj.ID), + ).OrderBy( + db.Note.CreatedAt.Order(db.SortOrderDesc), + ).Exec(context.Background()) + if noteErr != nil { + shared.SimpleEphemeralInteractionResponse("Failed to get notes", s, i.Interaction) + log.Error().Err(err).Msg("Failed to get notes") + return + } + + for _, note := range notes { + dateWithoutTime := strings.Split(note.CreatedAt, "T")[0] + + embed.Fields = append(embed.Fields, &discordgo.MessageEmbedField{ + Name: "ID:: " + strconv.Itoa(note.ID) + " :: Staff ::" + note.ModeratorUsername, + Value: "Date: **" + dateWithoutTime + "** (" + shared.StringTimeToDiscordTimestamp(note.CreatedAt) + ")\n" + + "Note: " + note.Content, + Inline: false, + }) + } case "messages": // Show the messages case "leaves": @@ -127,7 +157,9 @@ func InfoButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { Embeds: &[]*discordgo.MessageEmbed{&embed}, }) if err != nil { + shared.SimpleEphemeralInteractionResponse("Failed to edit message", s, i.Interaction) log.Error().Err(err).Msg("Failed to edit message") + return } // Respond to the interaction @@ -136,5 +168,6 @@ func InfoButtons(s *discordgo.Session, i *discordgo.InteractionCreate) { }) if err != nil { log.Error().Err(err).Msg("Failed to respond to interaction") + return } } diff --git a/internal/moderation/commands.go b/internal/moderation/commands.go index a9cfec3..51e67e2 100644 --- a/internal/moderation/commands.go +++ b/internal/moderation/commands.go @@ -7,6 +7,25 @@ import ( ) var Commands = []*discordgo.ApplicationCommand{ //nolint:gochecknoglobals // This is a list of commands for Discord + { + Name: "note", + Description: "Add a note to a user", + DefaultMemberPermissions: shared.Int64Ptr(discordgo.PermissionKickMembers), + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionUser, + Name: "user", + Description: "The user to add a note to", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "text", + Description: "The text of the note", + Required: true, + }, + }, + }, { Name: "warn", Description: "Warn the user", @@ -115,6 +134,7 @@ var CommandHandlers = map[string]func( //nolint:gochecknoglobals // This is a ma s *discordgo.Session, i *discordgo.InteractionCreate, ){ + "note": NoteCommand, "warn": WarnCommand, "strike": StrikeCommand, "info": InfoCommand, diff --git a/internal/moderation/info.go b/internal/moderation/info.go index aeeaf62..a3ce0f7 100644 --- a/internal/moderation/info.go +++ b/internal/moderation/info.go @@ -33,14 +33,14 @@ func GenerateInfoButtons(channelID string, embedID string, userID string) []disc Name: "⚠️", }, }, - // discordgo.Button{ - // Label: "Notes", - // Style: discordgo.PrimaryButton, - // CustomID: "notes" + suffix, - // Emoji: &discordgo.ComponentEmoji{ - // Name: "📝", - // }, - // }, + discordgo.Button{ + Label: "Notes", + Style: discordgo.PrimaryButton, + CustomID: "notes" + suffix, + Emoji: &discordgo.ComponentEmoji{ + Name: "📝", + }, + }, // discordgo.Button{ // Label: "Messages", // Style: discordgo.PrimaryButton, @@ -175,17 +175,12 @@ func InfoCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { db.User.UserID.Equals(user.ID), ).Exec(context.Background()) if err != nil { - log.Error().Err(err).Msg("Failed to get user") - err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Failed to get user info", - Flags: discordgo.MessageFlagsEphemeral, - }, - }) - if err != nil { - log.Error().Err(err).Msg("Failed to send fail interaction response for info") + if errors.Is(err, db.ErrNotFound) { + shared.SimpleEphemeralInteractionResponse("User not found", s, i.Interaction) + return } + shared.SimpleEphemeralInteractionResponse("Failed to get user", s, i.Interaction) + log.Error().Err(err).Msg("Failed to get user") return } @@ -202,17 +197,8 @@ func InfoCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { Embed: &embed, }) if err != nil { + shared.SimpleEphemeralInteractionResponse("Failed to send info message", s, i.Interaction) log.Error().Err(err).Msg("Failed to send info message") - err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "Failed to get user info", - Flags: discordgo.MessageFlagsEphemeral, - }, - }) - if err != nil { - log.Error().Err(err).Msg("Failed to send fail interaction response for info") - } return } @@ -223,17 +209,10 @@ func InfoCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { Components: &buttons, }) if err != nil { + shared.SimpleEphemeralInteractionResponse("Failed to add buttons to info message", s, i.Interaction) log.Error().Err(err).Msg("Failed to add buttons to info message") + return } - err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ - Type: discordgo.InteractionResponseChannelMessageWithSource, - Data: &discordgo.InteractionResponseData{ - Content: "User information available below", - Flags: 64, - }, - }) - if err != nil { - log.Error().Err(err).Msg("Failed to send interaction response for info") - } + shared.SimpleEphemeralInteractionResponse("User info generated below", s, i.Interaction) } diff --git a/internal/moderation/note.go b/internal/moderation/note.go new file mode 100644 index 0000000..d78b4a3 --- /dev/null +++ b/internal/moderation/note.go @@ -0,0 +1,52 @@ +package moderation + +import ( + "context" + "errors" + "math280h/wisp/db" + "math280h/wisp/internal/shared" + + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog/log" +) + +func NoteCommand(s *discordgo.Session, i *discordgo.InteractionCreate) { + userID := i.ApplicationCommandData().Options[0].UserValue(s).ID + text := i.ApplicationCommandData().Options[1].StringValue() + + // Get user from the user ID + user, err := s.User(userID) + if err != nil { + shared.SimpleEphemeralInteractionResponse("Failed to get discord user", s, i.Interaction) + log.Error().Err(err).Msg("Failed to get discord user") + return + } + + userObj, userErr := shared.GetUserIfExists(user) + if userErr != nil { + if errors.Is(userErr, db.ErrNotFound) { + shared.SimpleEphemeralInteractionResponse("User not found", s, i.Interaction) + return + } + shared.SimpleEphemeralInteractionResponse("Failed to get or create user", s, i.Interaction) + log.Error().Err(userErr).Msg("Failed to get or create user") + return + } + + _, noteErr := shared.DBClient.Note.CreateOne( + db.Note.User.Link( + db.User.UserID.Equals(userObj.UserID), + ), + db.Note.Content.Set(text), + db.Note.ModeratorID.Set(i.Member.User.ID), + db.Note.ModeratorUsername.Set(i.Member.User.Username), + ).Exec(context.Background()) + if noteErr != nil { + shared.SimpleEphemeralInteractionResponse("Failed to create note", s, i.Interaction) + log.Error().Err(noteErr).Msg("Failed to create note") + return + } + + // Respond to the command + shared.SimpleEphemeralInteractionResponse("Note added to user", s, i.Interaction) +} diff --git a/internal/shared/utils.go b/internal/shared/utils.go index 6a8c726..c522d8f 100644 --- a/internal/shared/utils.go +++ b/internal/shared/utils.go @@ -4,6 +4,7 @@ import ( "strconv" "time" + "github.com/bwmarrin/discordgo" "github.com/rs/zerolog/log" ) @@ -32,3 +33,20 @@ func StringWithTzToDiscordTimestamp(t string) string { unixTimestamp := parsedTime.Unix() return "" } + +func SimpleEphemeralInteractionResponse( + content string, + session *discordgo.Session, + interaction *discordgo.Interaction, +) { + err := session.InteractionRespond(interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: content, + Flags: 64, + }, + }) + if err != nil { + log.Error().Err(err).Msg("Failed to respond to interaction") + } +} diff --git a/schema.prisma b/schema.prisma index eff9556..9f9ae62 100644 --- a/schema.prisma +++ b/schema.prisma @@ -9,34 +9,35 @@ generator db { model User { id Int @id @default(autoincrement()) - userID String @unique @map("user_id") + userID String @unique @map("user_id") nickname String points Int @default(0) - lastJoin String? @map("last_join") - lastLeave String? @map("last_leave") - leaveCount Int @default(0) @map("leave_count") + lastJoin String? @map("last_join") + lastLeave String? @map("last_leave") + leaveCount Int @default(0) @map("leave_count") - reports Report[] - infractions Infraction[] - suggestions Suggestion[] + reports Report[] + infractions Infraction[] + suggestions Suggestion[] suggestionVotes SuggestionVote[] - messages Message[] - + messages Message[] + notes Note[] + created_at String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") @@map("users") } model Report { - id Int @id @default(autoincrement()) - channelID String @map("channel_id") - channelName String @map("channel_name") + id Int @id @default(autoincrement()) + channelID String @map("channel_id") + channelName String @map("channel_name") - user User @relation(fields: [userID], references: [id]) - userID Int @map("user_id") + user User @relation(fields: [userID], references: [id]) + userID Int @map("user_id") - status String @default("pending") + status String @default("pending") createdAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") updatedAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("updated_at") @@ -44,37 +45,52 @@ model Report { @@map("reports") } - model Infraction { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) - user User @relation(fields: [userID], references: [id]) - userID Int @map("user_id") + user User @relation(fields: [userID], references: [id]) + userID Int @map("user_id") - reason String - type String - points Int + reason String + type String + points Int - moderatorID String @map("moderator_id") - moderatorUsername String @map("moderator_username") + moderatorID String @map("moderator_id") + moderatorUsername String @map("moderator_username") - createdAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") + createdAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") @@map("infractions") } +model Note { + id Int @id @default(autoincrement()) + + user User @relation(fields: [userID], references: [id]) + userID Int @map("user_id") + + content String + + moderatorID String @map("moderator_id") + moderatorUsername String @map("moderator_username") + + createdAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") + + @@map("notes") +} + model Suggestion { id Int @id @default(autoincrement()) suggestion String - user User @relation(fields: [userID], references: [id]) - userID Int @map("user_id") + user User @relation(fields: [userID], references: [id]) + userID Int @map("user_id") - embedID String? @map("embed_id") + embedID String? @map("embed_id") - status String @default("pending") + status String @default("pending") - votes SuggestionVote[] + votes SuggestionVote[] createdAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") @@ -82,30 +98,30 @@ model Suggestion { } model SuggestionVote { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) - suggestion Suggestion @relation(fields: [suggestionID], references: [id]) - suggestionID Int @map("suggestion_id") + suggestion Suggestion @relation(fields: [suggestionID], references: [id]) + suggestionID Int @map("suggestion_id") - user User @relation(fields: [userID], references: [id]) - userID Int @map("user_id") + user User @relation(fields: [userID], references: [id]) + userID Int @map("user_id") - sentiment String @default("vote_up") + sentiment String @default("vote_up") - createdAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") + createdAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") @@map("suggestion_votes") } model Message { - id String @id @unique + id String @id @unique + + content String - content String + author User @relation(fields: [authorID], references: [id]) + authorID Int @map("author_id") - author User @relation(fields: [authorID], references: [id]) - authorID Int @map("author_id") - - channelID String @map("channel_id") + channelID String @map("channel_id") createdAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("created_at") updatedAt String @default(dbgenerated("CURRENT_TIMESTAMP")) @map("updated_at")