diff --git a/cmd/main.go b/cmd/main.go index 39f3438..b4dbc0d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -112,6 +112,8 @@ func main() { log.Info().Msg("Nickname history enabled, adding listeners...") s.AddHandler(history.OnGuildMemberUpdate) } + s.AddHandler(history.OnGuildMemeberJoin) + s.AddHandler(history.OnGuildMemberLeave) // Register available slash commands log.Info().Msg("Adding commands...") diff --git a/internal/history/member.go b/internal/history/member.go index c1bd4ea..d68b344 100644 --- a/internal/history/member.go +++ b/internal/history/member.go @@ -4,6 +4,7 @@ import ( "context" "math280h/wisp/db" "math280h/wisp/internal/shared" + "time" "github.com/bwmarrin/discordgo" "github.com/rs/zerolog/log" @@ -20,8 +21,8 @@ func updateNickIfChanged(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { newNick := m.Nick - if userObj.Nickname != newNick { - log.Debug().Msg("Nickname changed" + newNick) + if m.Nick != "" && userObj.Nickname != newNick { + log.Debug().Msg("Nickname changed: " + newNick) if newNick == "" { newNick = m.Member.User.Username } @@ -63,3 +64,55 @@ func updateNickIfChanged(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { func OnGuildMemberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { updateNickIfChanged(s, m) } + +func OnGuildMemeberJoin(s *discordgo.Session, m *discordgo.GuildMemberAdd) { + _, err := shared.DBClient.User.UpsertOne( + db.User.UserID.Equals(m.Member.User.ID), + ).Create( + db.User.UserID.Set(m.Member.User.ID), + db.User.Nickname.Set(m.Member.User.Username), + db.User.LastJoin.Set(time.Now().Format(time.RFC3339)), + ).Update( + db.User.LastJoin.Set(time.Now().Format(time.RFC3339)), + db.User.Nickname.Set(m.Member.User.Username), + ).Exec(context.Background()) + if err != nil { + log.Error().Err(err).Msg("Failed to create user") + return + } + + embed := discordgo.MessageEmbed{ + Title: "Member Joined", + Description: m.Member.User.Mention() + " (" + m.Member.User.String() + ")", + Color: shared.Green, + } + + _, channelErr := s.ChannelMessageSendEmbed(*shared.HistoryChannel, &embed) + if channelErr != nil { + log.Error().Err(channelErr).Msg("Failed to send message") + } +} + +func OnGuildMemberLeave(s *discordgo.Session, m *discordgo.GuildMemberRemove) { + _, err := shared.DBClient.User.FindUnique( + db.User.UserID.Equals(m.Member.User.ID), + ).Update( + db.User.LastLeave.Set(time.Now().Format(time.RFC3339)), + db.User.LeaveCount.Increment(1), + ).Exec(context.Background()) + if err != nil { + log.Error().Err(err).Msg("Failed to update user") + return + } + + embed := discordgo.MessageEmbed{ + Title: "Member Left", + Description: m.Member.User.Mention() + " (" + m.Member.User.String() + ")", + Color: shared.Red, + } + + _, channelErr := s.ChannelMessageSendEmbed(*shared.HistoryChannel, &embed) + if channelErr != nil { + log.Error().Err(channelErr).Msg("Failed to send message") + } +} diff --git a/internal/moderation/info.go b/internal/moderation/info.go index 057fdce..aeeaf62 100644 --- a/internal/moderation/info.go +++ b/internal/moderation/info.go @@ -75,6 +75,17 @@ func GenerateOverviewEmbed(user db.UserModel, userDiscordID string, reports int, } } + // Ensure last leave is not empty, if it is set to "Never" + usrLastLeave, lastLeaveOk := user.LastLeave() + if !lastLeaveOk { + usrLastLeave = "Never" + } + + usrLastJoin, lastJoinOk := user.LastJoin() + if !lastJoinOk { + usrLastJoin = "Never" + } + embed := discordgo.MessageEmbed{ Color: shared.DarkBlue, Fields: []*discordgo.MessageEmbedField{ @@ -103,6 +114,26 @@ func GenerateOverviewEmbed(user db.UserModel, userDiscordID string, reports int, Value: strconv.Itoa(reports), Inline: true, }, + { + Name: "__Leave History__", + Value: "", + Inline: false, + }, + { + Name: "Last Join", + Value: shared.StringWithTzToDiscordTimestamp(usrLastJoin), + Inline: true, + }, + { + Name: "Last Leave", + Value: shared.StringWithTzToDiscordTimestamp(usrLastLeave), + Inline: true, + }, + { + Name: "Leave Count", + Value: strconv.Itoa(user.LeaveCount), + Inline: true, + }, { Name: "__Most Recent Infraction__", Value: "", diff --git a/internal/shared/utils.go b/internal/shared/utils.go index d69a6f6..6a8c726 100644 --- a/internal/shared/utils.go +++ b/internal/shared/utils.go @@ -19,3 +19,16 @@ func StringTimeToDiscordTimestamp(t string) string { unixTimestamp := parsedTime.Unix() return "" } + +func StringWithTzToDiscordTimestamp(t string) string { + if t == "Never" { + return "Never" + } + + parsedTime, err := time.Parse("2006-01-02T15:04:05-07:00", t) + if err != nil { + log.Error().Err(err).Msg("Failed to parse time") + } + unixTimestamp := parsedTime.Unix() + return "" +} diff --git a/migrations/20250106051446_add_leave_join_tracking/migration.sql b/migrations/20250106051446_add_leave_join_tracking/migration.sql new file mode 100644 index 0000000..8f885a6 --- /dev/null +++ b/migrations/20250106051446_add_leave_join_tracking/migration.sql @@ -0,0 +1,83 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_infractions" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "user_id" INTEGER NOT NULL, + "reason" TEXT NOT NULL, + "type" TEXT NOT NULL, + "points" INTEGER NOT NULL, + "moderator_id" TEXT NOT NULL, + "moderator_username" TEXT NOT NULL, + "created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "infractions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_infractions" ("created_at", "id", "moderator_id", "moderator_username", "points", "reason", "type", "user_id") SELECT "created_at", "id", "moderator_id", "moderator_username", "points", "reason", "type", "user_id" FROM "infractions"; +DROP TABLE "infractions"; +ALTER TABLE "new_infractions" RENAME TO "infractions"; +CREATE TABLE "new_messages" ( + "id" TEXT NOT NULL PRIMARY KEY, + "content" TEXT NOT NULL, + "author_id" INTEGER NOT NULL, + "channel_id" TEXT NOT NULL, + "created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "messages_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_messages" ("author_id", "channel_id", "content", "created_at", "id", "updated_at") SELECT "author_id", "channel_id", "content", "created_at", "id", "updated_at" FROM "messages"; +DROP TABLE "messages"; +ALTER TABLE "new_messages" RENAME TO "messages"; +CREATE UNIQUE INDEX "messages_id_key" ON "messages"("id"); +CREATE TABLE "new_reports" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "channel_id" TEXT NOT NULL, + "channel_name" TEXT NOT NULL, + "user_id" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "reports_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_reports" ("channel_id", "channel_name", "created_at", "id", "status", "updated_at", "user_id") SELECT "channel_id", "channel_name", "created_at", "id", "status", "updated_at", "user_id" FROM "reports"; +DROP TABLE "reports"; +ALTER TABLE "new_reports" RENAME TO "reports"; +CREATE TABLE "new_suggestion_votes" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "suggestion_id" INTEGER NOT NULL, + "user_id" INTEGER NOT NULL, + "sentiment" TEXT NOT NULL DEFAULT 'vote_up', + "created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "suggestion_votes_suggestion_id_fkey" FOREIGN KEY ("suggestion_id") REFERENCES "suggestions" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "suggestion_votes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_suggestion_votes" ("created_at", "id", "sentiment", "suggestion_id", "user_id") SELECT "created_at", "id", "sentiment", "suggestion_id", "user_id" FROM "suggestion_votes"; +DROP TABLE "suggestion_votes"; +ALTER TABLE "new_suggestion_votes" RENAME TO "suggestion_votes"; +CREATE TABLE "new_suggestions" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "suggestion" TEXT NOT NULL, + "user_id" INTEGER NOT NULL, + "embed_id" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "suggestions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_suggestions" ("created_at", "embed_id", "id", "status", "suggestion", "user_id") SELECT "created_at", "embed_id", "id", "status", "suggestion", "user_id" FROM "suggestions"; +DROP TABLE "suggestions"; +ALTER TABLE "new_suggestions" RENAME TO "suggestions"; +CREATE TABLE "new_users" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "user_id" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "points" INTEGER NOT NULL DEFAULT 0, + "last_join" TEXT, + "last_leave" TEXT, + "leave_count" INTEGER NOT NULL DEFAULT 0, + "created_at" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP +); +INSERT INTO "new_users" ("created_at", "id", "nickname", "points", "user_id") SELECT "created_at", "id", "nickname", "points", "user_id" FROM "users"; +DROP TABLE "users"; +ALTER TABLE "new_users" RENAME TO "users"; +CREATE UNIQUE INDEX "users_user_id_key" ON "users"("user_id"); +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/schema.prisma b/schema.prisma index 08ca18a..eff9556 100644 --- a/schema.prisma +++ b/schema.prisma @@ -13,6 +13,10 @@ model User { nickname String points Int @default(0) + lastJoin String? @map("last_join") + lastLeave String? @map("last_leave") + leaveCount Int @default(0) @map("leave_count") + reports Report[] infractions Infraction[] suggestions Suggestion[]