From 0a63dd8fe4f1378ddb1b7f4a9eb8e20c64122ca2 Mon Sep 17 00:00:00 2001 From: Eccentric Date: Sat, 10 Feb 2024 01:55:56 +0000 Subject: [PATCH] Improve ban commands --- aid/syncer.go | 9 ++ aid/time.go | 174 ++++++++++++++++++++++++++++++++++++- discord/admin.go | 185 ++++++++++++++++++++++++++-------------- discord/handlers.go | 121 +++++++++++++++++++------- handlers/lightswitch.go | 11 +-- person/permissions.go | 10 ++- person/person.go | 92 +++++++++++++++----- storage/postgres.go | 8 ++ storage/storage.go | 11 +++ storage/tables.go | 8 +- 10 files changed, 499 insertions(+), 130 deletions(-) diff --git a/aid/syncer.go b/aid/syncer.go index 913fc1a..cbaf56a 100644 --- a/aid/syncer.go +++ b/aid/syncer.go @@ -55,4 +55,13 @@ func (s *GenericSyncMap[T]) ChangeKey(oldKey, newKey string) { s.Set(newKey, v) s.Delete(oldKey) +} + +func (s *GenericSyncMap[T]) Len() int { + count := 0 + s.m.Range(func(_, _ interface{}) bool { + count++ + return true + }) + return count } \ No newline at end of file diff --git a/aid/time.go b/aid/time.go index c1bc8c4..cd66d36 100644 --- a/aid/time.go +++ b/aid/time.go @@ -1,6 +1,9 @@ package aid -import "time" +import ( + "errors" + "time" +) func TimeStartOfDay() string { return time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Now().Location()).Format("2006-01-02T15:04:05.999Z") @@ -12,4 +15,173 @@ func TimeEndOfDay() string { func TimeEndOfWeekString() string { return time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 23, 59, 59, 999999999, time.Now().Location()).AddDate(0, 0, 7).Format("2006-01-02T15:04:05.999Z") +} + +// everything below is taken from the golang standard library I just added the extra units to the map + +var unitMap = map[string]uint64{ + "ns": uint64(time.Nanosecond), + "us": uint64(time.Microsecond), + "µs": uint64(time.Microsecond), // U+00B5 = micro symbol + "μs": uint64(time.Microsecond), // U+03BC = Greek letter mu + "ms": uint64(time.Millisecond), + "s": uint64(time.Second), + "m": uint64(time.Minute), + "h": uint64(time.Hour), + "d": uint64(time.Hour * 24), + "w": uint64(time.Hour * 24 * 7), + "y": uint64(time.Hour * 24 * 365), // no leap year + "c": uint64(time.Hour * 24 * 365 * 100), // no leap year +} + +// ParseDuration parses a duration string. +// A duration string is a possibly signed sequence of +// decimal numbers, each with optional fraction and a unit suffix, +// such as "300ms", "-1.5h" or "2h45m". +// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". + +func leadingFraction(s string) (x uint64, scale float64, rem string) { + i := 0 + scale = 1 + overflow := false + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if overflow { + continue + } + if x > (1<<63-1)/10 { + // It's possible for overflow to give a positive number, so take care. + overflow = true + continue + } + y := x*10 + uint64(c) - '0' + if y > 1<<63 { + overflow = true + continue + } + x = y + scale *= 10 + } + return x, scale, s[i:] +} + +func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) { + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c < '0' || c > '9' { + break + } + if x > 1<<63/10 { + // overflow + return 0, rem, errors.New("time: invalid duration ") + } + x = x*10 + uint64(c) - '0' + if x > 1<<63 { + // overflow + return 0, rem, errors.New("time: invalid duration ") + } + } + return x, s[i:], nil +} + +func ParseDuration(s string) (time.Duration, error) { + // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ + var d uint64 + neg := false + + // Consume [-+]? + if s != "" { + c := s[0] + if c == '-' || c == '+' { + neg = c == '-' + s = s[1:] + } + } + // Special case: if all that is left is "0", this is zero. + if s == "0" { + return 0, nil + } + if s == "" { + return 0, errors.New("time: invalid duration") + } + for s != "" { + var ( + v, f uint64 // integers before, after decimal point + scale float64 = 1 // value = v + f/scale + ) + + var err error + + // The next character must be [0-9.] + if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { + return 0, errors.New("time: invalid duration") + } + // Consume [0-9]* + pl := len(s) + v, s, err = leadingInt(s) + if err != nil { + return 0, errors.New("time: invalid duration") + } + pre := pl != len(s) // whether we consumed anything before a period + + // Consume (\.[0-9]*)? + post := false + if s != "" && s[0] == '.' { + s = s[1:] + pl := len(s) + f, scale, s = leadingFraction(s) + post = pl != len(s) + } + if !pre && !post { + // no digits (e.g. ".s" or "-.s") + return 0, errors.New("time: invalid duration") + } + + // Consume unit. + i := 0 + for ; i < len(s); i++ { + c := s[i] + if c == '.' || '0' <= c && c <= '9' { + break + } + } + if i == 0 { + return 0, errors.New("time: missing unit in duration") + } + u := s[:i] + s = s[i:] + unit, ok := unitMap[u] + if !ok { + return 0, errors.New("time: unknown unit in duration") + } + if v > 1<<63/unit { + // overflow + return 0, errors.New("time: invalid duration ") + } + v *= unit + if f > 0 { + // float64 is needed to be nanosecond accurate for fractions of hours. + // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) + v += uint64(float64(f) * (float64(unit) / scale)) + if v > 1<<63 { + // overflow + return 0, errors.New("time: invalid duration") + } + } + d += v + if d > 1<<63 { + return 0, errors.New("time: invalid duration") + } + } + if neg { + return -time.Duration(d), nil + } + if d > 1<<63-1 { + return 0, errors.New("time: invalid duration") + } + return time.Duration(d), nil } \ No newline at end of file diff --git a/discord/admin.go b/discord/admin.go index 956baa5..a657be5 100644 --- a/discord/admin.go +++ b/discord/admin.go @@ -1,6 +1,7 @@ package discord import ( + "fmt" "strings" "github.com/bwmarrin/discordgo" @@ -107,64 +108,117 @@ func whoHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { }) } -func banHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { +func bansHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { looker := person.FindByDiscord(i.Member.User.ID) if looker == nil { s.InteractionRespond(i.Interaction, &ErrorNoPermission) return } - if !looker.HasPermission(person.PermissionBan) { + if !looker.HasPermission(person.PermissionBansControl) { s.InteractionRespond(i.Interaction, &ErrorNoPermission) return } - player := getPersonFromOptions(i.ApplicationCommandData().Options, s) + if len(i.ApplicationCommandData().Options) <= 0 { + s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) + return + } + + subCommand := i.ApplicationCommandData().Options[0] + if len(subCommand.Options) <= 0 { + s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) + return + } + + player := getPersonFromOptions(subCommand.Options, s) if player == nil { s.InteractionRespond(i.Interaction, &ErrorInvalidDisplayOrDiscord) return } - player.Ban() + lookup := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate, looker *person.Person, player *person.Person, options []*discordgo.ApplicationCommandInteractionDataOption){ + "add": addBanHandler, + "clear": clearBansHandler, + "list": listBansHandler, + } + + if handler, ok := lookup[subCommand.Name]; ok { + handler(s, i, looker, player, subCommand.Options) + return + } + + s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) +} + +func addBanHandler(s *discordgo.Session, i *discordgo.InteractionCreate, looker *person.Person, player *person.Person, options []*discordgo.ApplicationCommandInteractionDataOption) { + reason := options[0].StringValue() + if reason == "" { + s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) + return + } + + var expiry string + for _, option := range options { + if option.Name == "expires" { + expiry = option.StringValue() + break + } + } + + aid.Print(expiry) + + player.AddBan(reason, looker.ID, expiry) s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: player.DisplayName + " has been banned.", + Content: player.DisplayName + " has been banned for `" + reason + "`.", }, }) } -func unbanHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { - looker := person.FindByDiscord(i.Member.User.ID) - if looker == nil { - s.InteractionRespond(i.Interaction, &ErrorNoPermission) - return - } - - if !looker.HasPermission(person.PermissionBan) { - s.InteractionRespond(i.Interaction, &ErrorNoPermission) - return - } - - player := getPersonFromOptions(i.ApplicationCommandData().Options, s) - if player == nil { - s.InteractionRespond(i.Interaction, &ErrorInvalidDisplayOrDiscord) - return - } - - player.Unban() +func clearBansHandler(s *discordgo.Session, i *discordgo.InteractionCreate, looker *person.Person, player *person.Person, options []*discordgo.ApplicationCommandInteractionDataOption) { + player.ClearBans() s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, Data: &discordgo.InteractionResponseData{ - Content: player.DisplayName + " has been unbanned.", + Content: player.DisplayName + " has had all bans cleared.", }, }) } -func giveItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { +func listBansHandler(s *discordgo.Session, i *discordgo.InteractionCreate, looker *person.Person, player *person.Person, options []*discordgo.ApplicationCommandInteractionDataOption) { + embed := NewEmbedBuilder(). + SetTitle("Ban History"). + SetColor(0x2b2d31) + + player.BanHistory.Range(func(key string, ban *storage.DB_BanStatus) bool { + banIssuer := person.Find(ban.IssuedBy) + if banIssuer == nil { + banIssuer = &person.Person{Discord: &storage.DB_DiscordPerson{ID: "0"}} + } + + embed.AddField(ban.Reason, "Banned by <@"+banIssuer.Discord.ID+"> on ", false) + return true + }) + + if player.BanHistory.Len() <= 0 { + embed.SetDescription("No bans found.") + } + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Embeds: []*discordgo.MessageEmbed{embed.Build()}, + Flags: discordgo.MessageFlagsEphemeral, + }, + }) +} + +func itemsHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { looker := person.FindByDiscord(i.Member.User.ID) if looker == nil { - s.InteractionRespond(i.Interaction, &ErrorNoAccount) + s.InteractionRespond(i.Interaction, &ErrorNoPermission) return } @@ -173,25 +227,56 @@ func giveItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { return } - player := getPersonFromOptions(i.ApplicationCommandData().Options, s) + if len(i.ApplicationCommandData().Options) <= 0 { + s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) + return + } + + subCommand := i.ApplicationCommandData().Options[0] + if len(subCommand.Options) <= 0 { + s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) + return + } + + player := getPersonFromOptions(subCommand.Options, s) if player == nil { s.InteractionRespond(i.Interaction, &ErrorInvalidDisplayOrDiscord) return } - item := i.ApplicationCommandData().Options[0].StringValue() + lookup := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate, looker *person.Person, player *person.Person, options []*discordgo.ApplicationCommandInteractionDataOption){ + "add": addItemHandler, + "remove": removeItemHandler, + "fill": fillItemsHandler, + } + + if handler, ok := lookup[subCommand.Name]; ok { + handler(s, i, looker, player, subCommand.Options) + return + } + + s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) +} + +func addItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate, looker *person.Person, player *person.Person, options []*discordgo.ApplicationCommandInteractionDataOption) { + if !looker.HasPermission(person.PermissionItemControl) { + s.InteractionRespond(i.Interaction, &ErrorNoPermission) + return + } + + item := options[0].StringValue() if item == "" { s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) return } - qty := i.ApplicationCommandData().Options[1].IntValue() + qty := options[1].IntValue() if qty <= 0 { s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) return } - profile := i.ApplicationCommandData().Options[2].StringValue() + profile := options[2].StringValue() if profile == "" { s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) return @@ -222,37 +307,25 @@ func giveItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { }) } -func takeItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { - looker := person.FindByDiscord(i.Member.User.ID) - if looker == nil { - s.InteractionRespond(i.Interaction, &ErrorNoAccount) - return - } - +func removeItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate, looker *person.Person, player *person.Person, options []*discordgo.ApplicationCommandInteractionDataOption) { if !looker.HasPermission(person.PermissionItemControl) { s.InteractionRespond(i.Interaction, &ErrorNoPermission) return } - player := getPersonFromOptions(i.ApplicationCommandData().Options, s) - if player == nil { - s.InteractionRespond(i.Interaction, &ErrorInvalidDisplayOrDiscord) - return - } - - item := i.ApplicationCommandData().Options[0].StringValue() + item := options[0].StringValue() if item == "" { s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) return } - qty := i.ApplicationCommandData().Options[1].IntValue() + qty := options[1].IntValue() if qty <= 0 { s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) return } - profile := i.ApplicationCommandData().Options[2].StringValue() + profile := options[2].StringValue() if profile == "" { s.InteractionRespond(i.Interaction, &ErrorInvalidArguments) return @@ -273,7 +346,6 @@ func takeItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { default: foundItem.Quantity -= int(qty) foundItem.Save() - if foundItem.Quantity <= 0 { player.GetProfileFromType(profile).Items.DeleteItem(foundItem.ID) remove = true @@ -294,31 +366,18 @@ func takeItemHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { }) } -func giveEverythingHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { - looker := person.FindByDiscord(i.Member.User.ID) - if looker == nil { - s.InteractionRespond(i.Interaction, &ErrorNoPermission) - return - } - +func fillItemsHandler(s *discordgo.Session, i *discordgo.InteractionCreate, looker *person.Person, player *person.Person, options []*discordgo.ApplicationCommandInteractionDataOption) { if !looker.HasPermission(person.PermissionItemControl) || !looker.HasPermission(person.PermissionLockerControl) { s.InteractionRespond(i.Interaction, &ErrorNoPermission) return } - player := getPersonFromOptions(i.ApplicationCommandData().Options, s) - if player == nil { - s.InteractionRespond(i.Interaction, &ErrorInvalidDisplayOrDiscord) - return - } - s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, }) fortnite.GiveEverything(player) - - str := player.DisplayName + "has been granted everything." + str := player.DisplayName + "has been granted all items." s.InteractionResponseEdit(i.Interaction, &discordgo.WebhookEdit{ Content: &str, }) diff --git a/discord/handlers.go b/discord/handlers.go index 641e3bd..02e810c 100644 --- a/discord/handlers.go +++ b/discord/handlers.go @@ -48,7 +48,7 @@ func addCommands() { addCommand(&DiscordCommand{ Command: &discordgo.ApplicationCommand{ - Name: "me", + Name: "account", Description: "Lookup your own information.", }, Handler: meHandler, @@ -89,23 +89,49 @@ func addCommands() { AdminOnly: true, }) - addCommand(&DiscordCommand{ - Command: &discordgo.ApplicationCommand{ - Name: "ban", - Description: "Ban a player from using the bot.", + bansOptions := append([]*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "reason", + Description: "The reason for the ban.", + Required: true, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "expires", + Description: "The time the ban expires. (e.g. 1y, 1w, 1d, 1h, 1m, 1s) (default: 1w)", + Required: false, + }, + }, personOptions...) + + bansSubCommands := []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "add", + Description: "Ban a player from using this service.", + Options: bansOptions, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "clear", + Description: "Clear all bans from a player.", Options: personOptions, }, - Handler: banHandler, - AdminOnly: true, - }) + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "list", + Description: "List all previous bans from a player.", + Options: personOptions, + }, + } addCommand(&DiscordCommand{ Command: &discordgo.ApplicationCommand{ - Name: "unban", - Description: "Unban a player from using the bot.", - Options: personOptions, + Name: "bans", + Description: "Perform an action on a player's bans.", + Options: bansSubCommands, }, - Handler: unbanHandler, + Handler: bansHandler, AdminOnly: true, }) @@ -125,38 +151,71 @@ func addCommands() { { Type: discordgo.ApplicationCommandOptionString, Name: "profile", - Description: "common_core, athena, common_public, profile0, collections, creative", + Description: "The profile to give/take the item from.", Required: true, + Choices: []*discordgo.ApplicationCommandOptionChoice{ + { + Name: "Athena", + Value: "athena", + }, + { + Name: "Common Core", + Value: "common_core", + }, + { + Name: "Profile0", + Value: "profile0", + }, + { + Name: "Creative", + Value: "creative", + }, + { + Name: "Collections", + Value: "collections", + }, + { + Name: "Common Public", + Value: "common_public", + }, + }, }, }, personOptions...) - addCommand(&DiscordCommand{ - Command: &discordgo.ApplicationCommand{ - Name: "give", + grantSubCommands := []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "add", Description: "Grant a player an item in the game.", Options: grantOptions, }, - Handler: giveItemHandler, - AdminOnly: true, - }) - - addCommand(&DiscordCommand{ - Command: &discordgo.ApplicationCommand{ - Name: "take", + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "remove", Description: "Take an item from a player in the game.", Options: grantOptions, }, - Handler: takeItemHandler, - AdminOnly: true, - }) + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "fill", + Description: "Grant a player all items in the game.", + Options: personOptions, + }, + { + Type: discordgo.ApplicationCommandOptionSubCommand, + Name: "clear", + Description: "Reset their locker to default.", + Options: personOptions, + }, + } addCommand(&DiscordCommand{ Command: &discordgo.ApplicationCommand{ - Name: "everything", - Description: "Give a player full locker", - Options: personOptions, + Name: "items", + Description: "Perform an action on a player's items.", + Options: grantSubCommands, }, - Handler: giveEverythingHandler, + Handler: itemsHandler, AdminOnly: true, }) @@ -215,7 +274,7 @@ func addCommands() { { Type: discordgo.ApplicationCommandOptionSubCommand, Name: "remove", - Description: "Rake a permission from a player.", + Description: "Remove a permission from a player.", Options: permissionOptions, }, } diff --git a/handlers/lightswitch.go b/handlers/lightswitch.go index e4966b1..2d94343 100644 --- a/handlers/lightswitch.go +++ b/handlers/lightswitch.go @@ -14,14 +14,7 @@ import ( func GetLightswitchBulkStatus(c *fiber.Ctx) error { person := c.Locals("person").(*person.Person) - isBanned := false - for _, ban := range person.BanHistory { - expres := time.Unix(ban.Expiry, 0) - if time.Now().Before(expres) { - isBanned = true - break - } - } + ban := person.GetLatestActiveBan() return c.Status(fiber.StatusOK).JSON([]aid.JSON{{ "serviceInstanceId": "fortnite", @@ -29,7 +22,7 @@ func GetLightswitchBulkStatus(c *fiber.Ctx) error { "message": "fortnite is up.", "maintenanceUri": nil, "allowedActions": []string{"PLAY","DOWNLOAD"}, - "banned": isBanned, + "banned": ban != nil && time.Now().Before(ban.Expiry), "launcherInfoDTO": aid.JSON{ "appName":"Fortnite", "namespace":"fn", diff --git a/person/permissions.go b/person/permissions.go index c986906..1e49261 100644 --- a/person/permissions.go +++ b/person/permissions.go @@ -4,18 +4,22 @@ type Permission int64 // DO NOT MOVE THE ORDER OF THESE PERMISSIONS AS THEY ARE USED IN THE DATABASE const ( + // random utility permissions PermissionLookup Permission = 1 << iota - PermissionBan PermissionInformation + + // control permissions + PermissionBansControl PermissionItemControl PermissionLockerControl PermissionPermissionControl + // user roles, not really permissions but implemented as such PermissionOwner PermissionDonator // special permissions - PermissionAll = PermissionLookup | PermissionBan | PermissionInformation | PermissionItemControl | PermissionLockerControl | PermissionPermissionControl + PermissionAll = PermissionLookup | PermissionBansControl | PermissionInformation | PermissionItemControl | PermissionLockerControl | PermissionPermissionControl PermissionAllWithRoles = PermissionAll | PermissionOwner | PermissionDonator ) @@ -36,7 +40,7 @@ func (p Permission) GetName() string { return "Lookup" } - if p&PermissionBan != 0 { + if p&PermissionBansControl != 0 { return "Ban" } diff --git a/person/person.go b/person/person.go index dbb3eaa..075a51b 100644 --- a/person/person.go +++ b/person/person.go @@ -20,7 +20,7 @@ type Person struct { CollectionsProfile *Profile CreativeProfile *Profile Discord *storage.DB_DiscordPerson - BanHistory []storage.DB_BanStatus + BanHistory aid.GenericSyncMap[storage.DB_BanStatus] Relationships aid.GenericSyncMap[Relationship] } @@ -188,7 +188,7 @@ func findHelper(databasePerson *storage.DB_Person, shallow bool, save bool) *Per ID: databasePerson.ID, DisplayName: databasePerson.DisplayName, Permissions: Permission(databasePerson.Permissions), - BanHistory: databasePerson.BanHistory, + BanHistory: aid.GenericSyncMap[storage.DB_BanStatus]{}, AthenaProfile: athenaProfile, CommonCoreProfile: commonCoreProfile, CommonPublicProfile: commonPublicProfile, @@ -200,6 +200,10 @@ func findHelper(databasePerson *storage.DB_Person, shallow bool, save bool) *Per Relationships: aid.GenericSyncMap[Relationship]{}, } + for _, ban := range databasePerson.BanHistory { + person.BanHistory.Set(ban.ID, &ban) + } + if !shallow { person.LoadRelationships() } @@ -262,24 +266,53 @@ func (p *Person) SaveShallow() { storage.Repo.SavePerson(dbPerson) } -func (p *Person) Ban() { - p.BanHistory = append(p.BanHistory, storage.DB_BanStatus{ - ID: uuid.New().String(), - PersonID: p.ID, - IssuedBy: "system", - Reason: "Banned by system", - Expiry: time.Now().AddDate(0, 0, 7).Unix(), - }) +func (p *Person) AddBan(reason string, issuedBy string, expiry ...string) { + t := time.Now().AddDate(0, 0, 7) - p.SaveShallow() -} - -func (p *Person) Unban() { - for _, ban := range p.BanHistory { - ban.Expiry = time.Now().Unix() + if len(expiry) > 0 && expiry[0] != "" { + parsed, err := aid.ParseDuration(expiry[0]) + if err == nil { + t = time.Now().Add(parsed) + aid.Print("Parsed duration for ban expiry:", t.Format("2006-01-02T15:04:05.999Z")) + } else { + aid.Print("Failed to parse duration for ban expiry:", err) + } } - p.SaveShallow() + ban := &storage.DB_BanStatus{ + ID: uuid.New().String(), + PersonID: p.ID, + IssuedBy: issuedBy, + Reason: reason, + Expiry: t, + } + + p.BanHistory.Set(ban.ID, ban) + storage.Repo.SaveBanStatus(ban) +} + +func (p *Person) ClearBans() { + p.BanHistory.Range(func(key string, ban *storage.DB_BanStatus) bool { + ban.Expiry = time.Now() + storage.Repo.SaveBanStatus(ban) + return true + }) +} + +func (p *Person) GetLatestActiveBan() *storage.DB_BanStatus { + var latestBan *storage.DB_BanStatus + p.BanHistory.Range(func(key string, ban *storage.DB_BanStatus) bool { + if latestBan == nil || ban.Expiry.After(latestBan.Expiry) { + latestBan = ban + } + return true + }) + + if latestBan != nil && latestBan.Expiry.Before(time.Now()) { + return nil + } + + return latestBan } func (p *Person) AddPermission(permission Permission) { @@ -305,7 +338,7 @@ func (p *Person) ToDatabase() *storage.DB_Person { ID: p.ID, DisplayName: p.DisplayName, Permissions: int64(p.Permissions), - BanHistory: p.BanHistory, + BanHistory: []storage.DB_BanStatus{}, RefundTickets: p.RefundTickets, Profiles: []storage.DB_Profile{}, Stats: []storage.DB_SeasonStat{}, @@ -325,6 +358,11 @@ func (p *Person) ToDatabase() *storage.DB_Person { "creative": p.CreativeProfile, } + p.BanHistory.Range(func(key string, ban *storage.DB_BanStatus) bool { + dbPerson.BanHistory = append(dbPerson.BanHistory, *ban) + return true + }) + for profileType, profile := range profilesToConvert { dbProfile := storage.DB_Profile{ ID: profile.ID, @@ -374,7 +412,7 @@ func (p *Person) ToDatabaseShallow() *storage.DB_Person { ID: p.ID, DisplayName: p.DisplayName, Permissions: int64(p.Permissions), - BanHistory: p.BanHistory, + BanHistory: []storage.DB_BanStatus{}, RefundTickets: p.RefundTickets, Profiles: []storage.DB_Profile{}, Stats: []storage.DB_SeasonStat{}, @@ -385,11 +423,16 @@ func (p *Person) ToDatabaseShallow() *storage.DB_Person { dbPerson.Discord = *p.Discord } + p.BanHistory.Range(func(key string, ban *storage.DB_BanStatus) bool { + dbPerson.BanHistory = append(dbPerson.BanHistory, *ban) + return true + }) + return &dbPerson } func (p *Person) Snapshot() *PersonSnapshot { - return &PersonSnapshot{ + snapshot := &PersonSnapshot{ ID: p.ID, DisplayName: p.DisplayName, Permissions: int64(p.Permissions), @@ -399,9 +442,16 @@ func (p *Person) Snapshot() *PersonSnapshot { Profile0Profile: *p.Profile0Profile.Snapshot(), CollectionsProfile: *p.CollectionsProfile.Snapshot(), CreativeProfile: *p.CreativeProfile.Snapshot(), - BanHistory: p.BanHistory, + BanHistory: []storage.DB_BanStatus{}, Discord: *p.Discord, } + + p.BanHistory.Range(func(key string, ban *storage.DB_BanStatus) bool { + snapshot.BanHistory = append(snapshot.BanHistory, *ban) + return true + }) + + return snapshot } func (p *Person) Delete() { diff --git a/storage/postgres.go b/storage/postgres.go index 1752ca9..5db1243 100644 --- a/storage/postgres.go +++ b/storage/postgres.go @@ -250,4 +250,12 @@ func (s *PostgresStorage) SaveDiscordPerson(discordPerson *DB_DiscordPerson) { func (s *PostgresStorage) DeleteDiscordPerson(discordPersonId string) { s.Postgres.Delete(&DB_DiscordPerson{}, "id = ?", discordPersonId) +} + +func (s *PostgresStorage) SaveBanStatus(banStatus *DB_BanStatus) { + s.Postgres.Save(banStatus) +} + +func (s *PostgresStorage) DeleteBanStatus(banStatusId string) { + s.Postgres.Delete(&DB_BanStatus{}, "id = ?", banStatusId) } \ No newline at end of file diff --git a/storage/storage.go b/storage/storage.go index 34af125..2dae24d 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -54,6 +54,9 @@ type Storage interface { SaveDiscordPerson(person *DB_DiscordPerson) DeleteDiscordPerson(personId string) + + SaveBanStatus(ban *DB_BanStatus) + DeleteBanStatus(banId string) } type Repository struct { @@ -225,4 +228,12 @@ func (r *Repository) SaveDiscordPerson(person *DB_DiscordPerson) { func (r *Repository) DeleteDiscordPerson(personId string) { r.Storage.DeleteDiscordPerson(personId) +} + +func (r *Repository) SaveBanStatus(ban *DB_BanStatus) { + r.Storage.SaveBanStatus(ban) +} + +func (r *Repository) DeleteBanStatus(banId string) { + r.Storage.DeleteBanStatus(banId) } \ No newline at end of file diff --git a/storage/tables.go b/storage/tables.go index 9277a7b..6bdd066 100644 --- a/storage/tables.go +++ b/storage/tables.go @@ -1,6 +1,10 @@ package storage -import "github.com/lib/pq" +import ( + "time" + + "github.com/lib/pq" +) type Tabler interface { TableName() string @@ -210,7 +214,7 @@ type DB_BanStatus struct { ID string `gorm:"primary_key"` PersonID string `gorm:"index"` IssuedBy string - Expiry int64 + Expiry time.Time Reason string }