diff --git a/EsefexApi/bot/commands/permission.go b/EsefexApi/bot/commands/permission.go index 361fa65..19b520b 100644 --- a/EsefexApi/bot/commands/permission.go +++ b/EsefexApi/bot/commands/permission.go @@ -3,6 +3,8 @@ package commands import ( "esefexapi/permissions" "esefexapi/types" + "esefexapi/util" + "esefexapi/util/refl" "fmt" "github.com/bwmarrin/discordgo" @@ -29,8 +31,7 @@ var PermissionCommand = &discordgo.ApplicationCommand{ Description: "The permission to set.", Type: discordgo.ApplicationCommandOptionString, Required: true, - // TODO: Dynamically get the choices from the permission type on startup using reflection. - // Choices: []*discordgo.ApplicationCommandOptionChoice{} + Choices: getPathOptions(), }, { Name: "value", @@ -70,8 +71,7 @@ var PermissionCommand = &discordgo.ApplicationCommand{ Description: "The permission to get.", Type: discordgo.ApplicationCommandOptionString, Required: true, - // TODO: Dynamically get the choices from the permission type on startup using reflection. - // Choices: []*discordgo.ApplicationCommandOptionChoice{} + Choices: getPathOptions(), }, }, }, @@ -101,6 +101,11 @@ var PermissionCommand = &discordgo.ApplicationCommand{ }, }, }, + { + Name: "list-all", + Description: "List all permissions for all users, channels and roles.", + Type: discordgo.ApplicationCommandOptionSubCommand, + }, }, } @@ -114,52 +119,272 @@ func (c *CommandHandlers) Permission(s *discordgo.Session, i *discordgo.Interact return c.PermissionClear(s, i) case "list": return c.PermissionList(s, i) + case "list-all": + return c.PermissionListAll(s, i) default: return nil, errors.Wrap(fmt.Errorf("Unknown subcommand %s", i.ApplicationCommandData().Options[0].Name), "Error handling user command") } } +// TODO: Fix the race condition that might occur here func (c *CommandHandlers) PermissionSet(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { - return nil, errors.Wrap(fmt.Errorf("Not implemented"), "Error handling user command PermissionSet") + id := fmt.Sprintf("%v", i.ApplicationCommandData().Options[0].Options[0].Value) + ty, err := extractTypeFromString(s, types.GuildID(i.GuildID), id) + if err != nil { + return nil, errors.Wrap(err, "Error extracting type from string") + } + + p, err := getPermissions(s, c.dbs, types.GuildID(i.GuildID), id) + if err != nil { + return nil, errors.Wrap(err, "Error getting permissions") + } + + ps := permissions.PSFromString(fmt.Sprintf("%v", i.ApplicationCommandData().Options[0].Options[2].Value)) + + ppath := fmt.Sprintf("%v", i.ApplicationCommandData().Options[0].Options[1].Value) + + err = refl.SetNestedFieldValue(&p, ppath, ps) + if err != nil { + return nil, errors.Wrap(err, "Error setting nested field value") + } + + switch ty.PermissionType { + case permissions.User: + err = c.dbs.PermissionDB.UpdateUser(types.UserID(ty.ID), p) + case permissions.Role: + err = c.dbs.PermissionDB.UpdateRole(types.RoleID(ty.ID), p) + case permissions.Channel: + err = c.dbs.PermissionDB.UpdateChannel(types.ChannelID(ty.ID), p) + } + + if err != nil { + return nil, errors.Wrap(err, "Error setting permissions") + } + + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf("Set %s for %s to %s", ppath, id, ps.String()), + }, + }, nil } -func (c *CommandHandlers) PermissionGet(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { - return nil, errors.Wrap(fmt.Errorf("Not implemented"), "Error handling user command PermissionGet") +// TODO: Better alignment for the list all command (maybe use a table?) +func (c *CommandHandlers) PermissionListAll(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { + resp := "Permissions for all users, channels and roles:\n" + + resp += "**Users**\n" + uids, err := c.dbs.PermissionDB.GetUsers() + if err != nil { + return nil, errors.Wrap(err, "Error getting users") + } + if len(uids) == 0 { + resp += "`No users found.`\n" + } + for _, uid := range uids { + p, err := getPermissions(s, c.dbs, types.GuildID(i.GuildID), uid.String()) + if err != nil { + return nil, errors.Wrap(err, "Error getting permissions") + } + + pstr, err := formatPermissionsCompact(p) + if err != nil { + return nil, errors.Wrap(err, "Error formatting permissions") + } + + uname, err := util.UserIDName(s, uid) + if err != nil { + return nil, errors.Wrap(err, "Error getting user") + } + + resp += fmt.Sprintf("%s: ", uname) + resp += pstr + resp += "\n" + } + + resp += "**Roles**\n" + rids, err := c.dbs.PermissionDB.GetRoles() + if err != nil { + return nil, errors.Wrap(err, "Error getting roles") + } + if len(rids) == 0 { + resp += "`No roles found.`\n" + } + for _, rid := range rids { + p, err := getPermissions(s, c.dbs, types.GuildID(i.GuildID), rid.String()) + if err != nil { + return nil, errors.Wrap(err, "Error getting permissions") + } + + pstr, err := formatPermissionsCompact(p) + if err != nil { + return nil, errors.Wrap(err, "Error formatting permissions") + } + + rmention, err := util.RoleIDName(s, types.GuildID(i.GuildID), rid) + if err != nil { + return nil, errors.Wrap(err, "Error getting role") + } + + resp += fmt.Sprintf("%s: ", rmention) + resp += pstr + resp += "\n" + } + + resp += "**Channels**\n" + cids, err := c.dbs.PermissionDB.GetChannels() + if err != nil { + return nil, errors.Wrap(err, "Error getting channels") + } + if len(cids) == 0 { + resp += "`No channels found.`\n" + } + for _, cid := range cids { + p, err := getPermissions(s, c.dbs, types.GuildID(i.GuildID), cid.String()) + if err != nil { + return nil, errors.Wrap(err, "Error getting permissions") + } + + pstr, err := formatPermissionsCompact(p) + if err != nil { + return nil, errors.Wrap(err, "Error formatting permissions") + } + + cmention, err := util.ChannelIDMention(s, types.GuildID(i.GuildID), cid) + if err != nil { + return nil, errors.Wrap(err, "Error getting channel") + } + + resp += fmt.Sprintf("%s: ", cmention) + resp += pstr + resp += "\n" + } + + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: resp, + }, + }, nil } -func (c *CommandHandlers) PermissionClear(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { - return nil, errors.Wrap(fmt.Errorf("Not implemented"), "Error handling user command PermissionClear") +func (c *CommandHandlers) PermissionGet(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { + id := fmt.Sprintf("%v", i.ApplicationCommandData().Options[0].Options[0].Value) + + p, err := getPermissions(s, c.dbs, types.GuildID(i.GuildID), id) + if err != nil { + return nil, errors.Wrap(err, "Error getting permissions") + } + + ppath := fmt.Sprintf("%v", i.ApplicationCommandData().Options[0].Options[1].Value) + + ps, err := getPermission(p, ppath) + if err != nil { + return nil, errors.Wrap(err, "Error getting permission") + } + + ty, err := extractTypeFromString(s, types.GuildID(i.GuildID), id) + if err != nil { + return nil, errors.Wrap(err, "Error extracting type from string") + } + + var name string + switch ty.PermissionType { + case permissions.User: + name, err = util.UserIDName(s, types.UserID(ty.ID)) + case permissions.Role: + name, err = util.RoleIDName(s, types.GuildID(i.GuildID), types.RoleID(ty.ID)) + case permissions.Channel: + name, err = util.ChannelIDMention(s, types.GuildID(i.GuildID), types.ChannelID(ty.ID)) + } + if err != nil { + return nil, errors.Wrap(err, "Error getting name") + } + + // TODO: add name display here + resp := fmt.Sprintf("%s for %s: %s", ppath, name, ps.String()) + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: resp, + }, + }, nil } -func (c *CommandHandlers) PermissionList(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { +// TODO: Fix this command (it is not clearing permissions) +func (c *CommandHandlers) PermissionClear(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { id := fmt.Sprintf("%v", i.ApplicationCommandData().Options[0].Options[0].Value) ty, err := extractTypeFromString(s, types.GuildID(i.GuildID), id) if err != nil { return nil, errors.Wrap(err, "Error extracting type from string") } - var p permissions.Permissions + var name string + switch ty.PermissionType { + case permissions.User: + name, err = util.UserIDName(s, types.UserID(ty.ID)) + case permissions.Role: + name, err = util.RoleIDName(s, types.GuildID(i.GuildID), types.RoleID(ty.ID)) + case permissions.Channel: + name, err = util.ChannelIDMention(s, types.GuildID(i.GuildID), types.ChannelID(ty.ID)) + } + if err != nil { + return nil, errors.Wrap(err, "Error getting name") + } switch ty.PermissionType { case permissions.User: - p, err = c.dbs.PremissionDB.GetUser(types.UserID(ty.ID)) + err = c.dbs.PermissionDB.UpdateUser(types.UserID(ty.ID), permissions.NewUnset()) case permissions.Role: - p, err = c.dbs.PremissionDB.GetRole(types.RoleID(ty.ID)) + err = c.dbs.PermissionDB.UpdateRole(types.RoleID(ty.ID), permissions.NewUnset()) case permissions.Channel: - p, err = c.dbs.PremissionDB.GetChannel(types.ChannelID(ty.ID)) + err = c.dbs.PermissionDB.UpdateChannel(types.ChannelID(ty.ID), permissions.NewUnset()) + } + if err != nil { + return nil, errors.Wrap(err, "Error clearing permissions") } + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Content: fmt.Sprintf("Cleared permissions for %s %s", ty.PermissionType, name), + }, + }, nil +} + +func (c *CommandHandlers) PermissionList(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { + id := fmt.Sprintf("%v", i.ApplicationCommandData().Options[0].Options[0].Value) + + p, err := getPermissions(s, c.dbs, types.GuildID(i.GuildID), id) if err != nil { return nil, errors.Wrap(err, "Error getting permissions") } - ps, err := formatPermissions(p) + pstr, err := formatPermissions(p) if err != nil { return nil, errors.Wrap(err, "Error formatting permissions") } - resp := fmt.Sprintf("Permissions for %s %s:\n", ty.PermissionType, ty.ID) - resp += ps + ty, err := extractTypeFromString(s, types.GuildID(i.GuildID), id) + if err != nil { + return nil, errors.Wrap(err, "Error extracting type from string") + } + + var name string + switch ty.PermissionType { + case permissions.User: + name, err = util.UserIDName(s, types.UserID(ty.ID)) + case permissions.Role: + name, err = util.RoleIDName(s, types.GuildID(i.GuildID), types.RoleID(ty.ID)) + case permissions.Channel: + name, err = util.ChannelIDMention(s, types.GuildID(i.GuildID), types.ChannelID(ty.ID)) + } + if err != nil { + return nil, errors.Wrap(err, "Error getting name") + } + + resp := fmt.Sprintf("Permissions for %s:\n", name) + resp += pstr return &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseChannelMessageWithSource, diff --git a/EsefexApi/bot/commands/sound.go b/EsefexApi/bot/commands/sound.go index b43d551..257b27c 100644 --- a/EsefexApi/bot/commands/sound.go +++ b/EsefexApi/bot/commands/sound.go @@ -8,7 +8,6 @@ import ( "log" "github.com/bwmarrin/discordgo" - "github.com/davecgh/go-spew/spew" "github.com/pkg/errors" ) @@ -80,11 +79,6 @@ func (c *CommandHandlers) Sound(s *discordgo.Session, i *discordgo.InteractionCr default: return nil, errors.Wrap(fmt.Errorf("Unknown subcommand %s", i.ApplicationCommandData().Options[0].Name), "Error handling user command") } - - log.Println("User command called with options:") - spew.Dump(i.ApplicationCommandData().Options) - - return nil, errors.Wrap(fmt.Errorf("Not implemented"), "Sound") } func (c *CommandHandlers) SoundUpload(s *discordgo.Session, i *discordgo.InteractionCreate) (*discordgo.InteractionResponse, error) { diff --git a/EsefexApi/bot/commands/util.go b/EsefexApi/bot/commands/util.go index 6bf7ed4..8d2712e 100644 --- a/EsefexApi/bot/commands/util.go +++ b/EsefexApi/bot/commands/util.go @@ -1,11 +1,12 @@ package commands import ( + "esefexapi/db" "esefexapi/permissions" "esefexapi/sounddb" "esefexapi/types" + "esefexapi/util/refl" "fmt" - "log" "regexp" "github.com/bwmarrin/discordgo" @@ -25,13 +26,12 @@ func fmtMetaList(metas []sounddb.SoundMeta) string { // checks if its a user, role or channel func extractTypeFromString(s *discordgo.Session, g types.GuildID, str string) (PermissionSet, error) { - regex, err := regexp.Compile(`^<(@|#|@&)(\d+)>$|^(@everyone)$|^(\d+)$`) + regex, err := regexp.Compile(`^<(@|#|@&)(\d+)>$|^(@?everyone)$|^(\d+)$`) if err != nil { return PermissionSet{}, errors.Wrap(err, "Error compiling regex") } matches := regex.FindStringSubmatch(str) - log.Printf("%d matches: %#v", len(matches), matches) if len(matches) != 5 { return PermissionSet{}, errors.Wrap(fmt.Errorf("Invalid id %s", str), "Error extracting type from string") @@ -55,7 +55,7 @@ func extractTypeFromString(s *discordgo.Session, g types.GuildID, str string) (P }, nil } - if matches[3] == "@everyone" { + if matches[3] != "" { return PermissionSet{ PermissionType: permissions.Role, ID: "everyone", @@ -116,28 +116,91 @@ func formatPermissions(p permissions.Permissions) (string, error) { resp := "**Sound**\n" resp += fmt.Sprintf("```%s\n", mdlang) - resp += fmt.Sprintf("Play: %s\n", p.Sound.Play.String()) - resp += fmt.Sprintf("Upload: %s\n", p.Sound.Upload.String()) - resp += fmt.Sprintf("Modify: %s\n", p.Sound.Modify.String()) - resp += fmt.Sprintf("Delete: %s\n", p.Sound.Delete.String()) + resp += fmt.Sprintf("Sound.Play: %s\n", p.Sound.Play.String()) + resp += fmt.Sprintf("Sound.Upload: %s\n", p.Sound.Upload.String()) + resp += fmt.Sprintf("Sound.Modify: %s\n", p.Sound.Modify.String()) + resp += fmt.Sprintf("Sound.Delete: %s\n", p.Sound.Delete.String()) resp += "```" resp += "\n**Bot**\n" resp += fmt.Sprintf("```%s\n", mdlang) - resp += fmt.Sprintf("Join: %s\n", p.Bot.Join.String()) - resp += fmt.Sprintf("Leave: %s\n", p.Bot.Leave.String()) + resp += fmt.Sprintf("Bot.Join: %s\n", p.Bot.Join.String()) + resp += fmt.Sprintf("Bot.Leave: %s\n", p.Bot.Leave.String()) resp += "```" resp += "\n**Guild**\n" resp += fmt.Sprintf("```%s\n", mdlang) - resp += fmt.Sprintf("ManageBot: %s\n", p.Guild.ManageBot.String()) - resp += fmt.Sprintf("ManageUser: %s\n", p.Guild.ManageUser.String()) + resp += fmt.Sprintf("Guild.ManageBot: %s\n", p.Guild.ManageBot.String()) + resp += fmt.Sprintf("Guild.ManageUser: %s\n", p.Guild.ManageUser.String()) resp += "```" return resp, nil } +func formatPermissionsCompact(p permissions.Permissions) (string, error) { + ppaths := refl.FindAllPaths(p) + + resp := "" + for _, ppath := range ppaths { + ps, err := refl.GetNestedFieldValue(p, ppath) + if err != nil { + return "", errors.Wrap(err, "Error getting permission") + } + resp += ps.(permissions.PermissionState).Emoji() + } + + return resp, nil +} + type PermissionSet struct { PermissionType permissions.PermissionType ID string } + +func getPermissions(s *discordgo.Session, dbs *db.Databases, g types.GuildID, id string) (permissions.Permissions, error) { + ty, err := extractTypeFromString(s, g, id) + if err != nil { + return permissions.Permissions{}, errors.Wrap(err, "Error extracting type from string") + } + + var p permissions.Permissions + + switch ty.PermissionType { + case permissions.User: + p, err = dbs.PermissionDB.GetUser(types.UserID(ty.ID)) + case permissions.Role: + p, err = dbs.PermissionDB.GetRole(types.RoleID(ty.ID)) + case permissions.Channel: + p, err = dbs.PermissionDB.GetChannel(types.ChannelID(ty.ID)) + } + + if err != nil { + return permissions.Permissions{}, errors.Wrap(err, "Error getting permissions") + } + + return p, nil +} + +func getPermission(p permissions.Permissions, key string) (permissions.PermissionState, error) { + v, err := refl.GetNestedFieldValue(p, key) + if err != nil { + return permissions.Unset, errors.Wrap(err, "Error getting nested field value") + } + + return v.(permissions.PermissionState), nil +} + +func getPathOptions() []*discordgo.ApplicationCommandOptionChoice { + util := refl.FindAllPaths(permissions.NewUnset()) + + var options []*discordgo.ApplicationCommandOptionChoice + + for _, u := range util { + options = append(options, &discordgo.ApplicationCommandOptionChoice{ + Name: u, + Value: u, + }) + } + + return options +} diff --git a/EsefexApi/db/db.go b/EsefexApi/db/db.go index 394c202..56197e6 100644 --- a/EsefexApi/db/db.go +++ b/EsefexApi/db/db.go @@ -11,5 +11,5 @@ type Databases struct { SoundDB sounddb.ISoundDB UserDB userdb.IUserDB LinkTokenStore linktokenstore.ILinkTokenStore - PremissionDB permissiondb.PermissionDB + PermissionDB permissiondb.PermissionDB } diff --git a/EsefexApi/main.go b/EsefexApi/main.go index e12ad6c..494599a 100644 --- a/EsefexApi/main.go +++ b/EsefexApi/main.go @@ -55,7 +55,7 @@ func main() { SoundDB: sdbc, UserDB: udb, LinkTokenStore: ldb, - PremissionDB: fpdb, + PermissionDB: fpdb, } botT := time.Duration(cfg.Bot.Timeout * float32(time.Minute)) diff --git a/EsefexApi/permissiondb/filepermisssiondb/filepermisssiondb.go b/EsefexApi/permissiondb/filepermisssiondb/filepermisssiondb.go index acee79c..6da8ba3 100644 --- a/EsefexApi/permissiondb/filepermisssiondb/filepermisssiondb.go +++ b/EsefexApi/permissiondb/filepermisssiondb/filepermisssiondb.go @@ -4,7 +4,6 @@ import ( "encoding/json" "esefexapi/permissiondb" "esefexapi/permissions" - "esefexapi/types" "os" "sync" @@ -29,60 +28,6 @@ func NewFilePermissionDB(path string) (*FilePermissionDB, error) { return fpdb, err } -// GetChannel implements permissiondb.PermissionDB. -func (f *FilePermissionDB) GetChannel(channelID types.ChannelID) (permissions.Permissions, error) { - f.rw.RLock() - defer f.rw.RUnlock() - - return f.stack.GetChannel(channelID), nil -} - -// GetRole implements permissiondb.PermissionDB. -func (f *FilePermissionDB) GetRole(roleID types.RoleID) (permissions.Permissions, error) { - f.rw.RLock() - defer f.rw.RUnlock() - - return f.stack.GetRole(roleID), nil -} - -// GetUser implements permissiondb.PermissionDB. -func (f *FilePermissionDB) GetUser(userID types.UserID) (permissions.Permissions, error) { - f.rw.RLock() - defer f.rw.RUnlock() - - return f.stack.GetUser(userID), nil -} - -// UpdateChannel implements permissiondb.PermissionDB. -func (f *FilePermissionDB) UpdateChannel(channelID types.ChannelID, p permissions.Permissions) error { - f.rw.Lock() - defer f.rw.Unlock() - - f.stack.UpdateChannel(channelID, p) - go f.save() - return nil -} - -// UpdateRole implements permissiondb.PermissionDB. -func (f *FilePermissionDB) UpdateRole(roleID types.RoleID, p permissions.Permissions) error { - f.rw.Lock() - defer f.rw.Unlock() - - f.stack.UpdateRole(roleID, p) - go f.save() - return nil -} - -// UpdateUser implements permissiondb.PermissionDB. -func (f *FilePermissionDB) UpdateUser(userID types.UserID, p permissions.Permissions) error { - f.rw.Lock() - defer f.rw.Unlock() - - f.stack.UpdateUser(userID, p) - go f.save() - return nil -} - // load loads the permission stack from the file. func (f *FilePermissionDB) load() error { file, err := os.Open(f.filePath) @@ -100,6 +45,7 @@ func (f *FilePermissionDB) load() error { } // save saves the permission stack to the file. +// TODO: Make this work func (f *FilePermissionDB) save() error { file, err := os.OpenFile(f.filePath, os.O_RDWR|os.O_CREATE, os.ModePerm) if err != nil { diff --git a/EsefexApi/permissiondb/filepermisssiondb/impl.go b/EsefexApi/permissiondb/filepermisssiondb/impl.go new file mode 100644 index 0000000..a31ce3d --- /dev/null +++ b/EsefexApi/permissiondb/filepermisssiondb/impl.go @@ -0,0 +1,96 @@ +package filepermisssiondb + +import ( + "esefexapi/permissions" + "esefexapi/types" +) + +// GetChannel implements permissiondb.PermissionDB. +func (f *FilePermissionDB) GetChannel(channelID types.ChannelID) (permissions.Permissions, error) { + f.rw.RLock() + defer f.rw.RUnlock() + + return f.stack.GetChannel(channelID), nil +} + +// GetRole implements permissiondb.PermissionDB. +func (f *FilePermissionDB) GetRole(roleID types.RoleID) (permissions.Permissions, error) { + f.rw.RLock() + defer f.rw.RUnlock() + + return f.stack.GetRole(roleID), nil +} + +// GetUser implements permissiondb.PermissionDB. +func (f *FilePermissionDB) GetUser(userID types.UserID) (permissions.Permissions, error) { + f.rw.RLock() + defer f.rw.RUnlock() + + return f.stack.GetUser(userID), nil +} + +// UpdateChannel implements permissiondb.PermissionDB. +func (f *FilePermissionDB) UpdateChannel(channelID types.ChannelID, p permissions.Permissions) error { + f.rw.Lock() + defer f.rw.Unlock() + + f.stack.UpdateChannel(channelID, p) + go f.save() + return nil +} + +// UpdateRole implements permissiondb.PermissionDB. +func (f *FilePermissionDB) UpdateRole(roleID types.RoleID, p permissions.Permissions) error { + f.rw.Lock() + defer f.rw.Unlock() + + f.stack.UpdateRole(roleID, p) + go f.save() + return nil +} + +// UpdateUser implements permissiondb.PermissionDB. +func (f *FilePermissionDB) UpdateUser(userID types.UserID, p permissions.Permissions) error { + f.rw.Lock() + defer f.rw.Unlock() + + f.stack.UpdateUser(userID, p) + go f.save() + return nil +} + +func (f *FilePermissionDB) GetUsers() ([]types.UserID, error) { + f.rw.RLock() + defer f.rw.RUnlock() + + var users []types.UserID + for k := range f.stack.User { + users = append(users, k) + } + + return users, nil +} + +func (f *FilePermissionDB) GetRoles() ([]types.RoleID, error) { + f.rw.RLock() + defer f.rw.RUnlock() + + var roles []types.RoleID + for k := range f.stack.Role { + roles = append(roles, k) + } + + return roles, nil +} + +func (f *FilePermissionDB) GetChannels() ([]types.ChannelID, error) { + f.rw.RLock() + defer f.rw.RUnlock() + + var channels []types.ChannelID + for k := range f.stack.Channel { + channels = append(channels, k) + } + + return channels, nil +} diff --git a/EsefexApi/permissiondb/permissiondb.go b/EsefexApi/permissiondb/permissiondb.go index 44edc17..70f4ffe 100644 --- a/EsefexApi/permissiondb/permissiondb.go +++ b/EsefexApi/permissiondb/permissiondb.go @@ -12,4 +12,7 @@ type PermissionDB interface { UpdateUser(userID types.UserID, p permissions.Permissions) error UpdateRole(roleID types.RoleID, p permissions.Permissions) error UpdateChannel(channelID types.ChannelID, p permissions.Permissions) error + GetUsers() ([]types.UserID, error) + GetRoles() ([]types.RoleID, error) + GetChannels() ([]types.ChannelID, error) } diff --git a/EsefexApi/permissions/permissions.go b/EsefexApi/permissions/permissions.go index 3e93026..d3714ad 100644 --- a/EsefexApi/permissions/permissions.go +++ b/EsefexApi/permissions/permissions.go @@ -11,34 +11,60 @@ const ( func (pt PermissionType) String() string { switch pt { case User: - return "user" + return "User" case Role: - return "role" + return "Role" case Channel: - return "channel" + return "Channel" default: - return "unknown" + return "Unknown" } } type PermissionState int const ( - Allow PermissionState = iota + Unset PermissionState = iota + Allow Deny - Unset ) +func PSFromString(str string) PermissionState { + switch str { + case "Allow": + return Allow + case "Deny": + return Deny + case "Unset": + return Unset + default: + return Unset + } +} + func (ps PermissionState) String() string { switch ps { case Allow: - return "allow" + return "Allow" + case Deny: + return "Deny" + case Unset: + return "Unset" + default: + return "Unknown" + } +} + +func (ps PermissionState) Emoji() string { + switch ps { + case Allow: + return "✅" case Deny: - return "deny" + return "❌" case Unset: - return "unset" + return " " default: - return "unknown" + return "❓" } } diff --git a/EsefexApi/permissions/stack.go b/EsefexApi/permissions/stack.go index d83ca1e..bed9ba7 100644 --- a/EsefexApi/permissions/stack.go +++ b/EsefexApi/permissions/stack.go @@ -71,6 +71,8 @@ func (ps *PermissionStack) UpdateUser(user types.UserID, p Permissions) { } ps.User[user] = ps.User[user].MergeParent(p) + + ps.clean() } func (ps *PermissionStack) UpdateRole(role types.RoleID, p Permissions) { @@ -79,6 +81,8 @@ func (ps *PermissionStack) UpdateRole(role types.RoleID, p Permissions) { } ps.Role[role] = ps.Role[role].MergeParent(p) + + ps.clean() } func (ps *PermissionStack) UpdateChannel(channel types.ChannelID, p Permissions) { @@ -87,6 +91,29 @@ func (ps *PermissionStack) UpdateChannel(channel types.ChannelID, p Permissions) } ps.Channel[channel] = ps.Channel[channel].MergeParent(p) + + ps.clean() +} + +// clean removes all permissions that are just unset. +func (ps *PermissionStack) clean() { + for user, p := range ps.User { + if p == NewUnset() { + delete(ps.User, user) + } + } + + for role, p := range ps.Role { + if p == NewUnset() { + delete(ps.Role, role) + } + } + + for channel, p := range ps.Channel { + if p == NewUnset() { + delete(ps.Channel, channel) + } + } } // Query returns the permission state for a given user, role, and channel by merging them together. diff --git a/EsefexApi/util/iddisplay.go b/EsefexApi/util/iddisplay.go new file mode 100644 index 0000000..4901d46 --- /dev/null +++ b/EsefexApi/util/iddisplay.go @@ -0,0 +1,92 @@ +package util + +import ( + "esefexapi/types" + + "github.com/bwmarrin/discordgo" + "github.com/pkg/errors" +) + +func UserIDName(ds *discordgo.Session, u types.UserID) (string, error) { + user, err := ds.User(u.String()) + if err != nil { + return "", errors.Wrap(err, "Error getting user") + } + return user.Username, nil +} + +func RoleIDName(ds *discordgo.Session, g types.GuildID, r types.RoleID) (string, error) { + if r == "everyone" { + return "everyone", nil + } + + roles, err := ds.GuildRoles(g.String()) + if err != nil { + return "", errors.Wrap(err, "Error getting roles") + } + + for _, role := range roles { + if role.ID == r.String() { + return role.Name, nil + } + } + + return "", errors.Wrap(err, "Error getting role") +} + +func ChannelIDName(ds *discordgo.Session, g types.GuildID, c types.ChannelID) (string, error) { + channels, err := ds.GuildChannels(g.String()) + if err != nil { + return "", errors.Wrap(err, "Error getting channels") + } + + for _, channel := range channels { + if channel.ID == c.String() { + return channel.Name, nil + } + } + + return "", errors.Wrap(err, "Error getting channel") +} + +func UserIDMention(ds *discordgo.Session, u types.UserID) (string, error) { + user, err := ds.User(u.String()) + if err != nil { + return "", errors.Wrap(err, "Error getting user") + } + return user.Mention(), nil +} + +func RoleIDMention(ds *discordgo.Session, g types.GuildID, r types.RoleID) (string, error) { + if r == "everyone" { + return "@everyone", nil + } + + roles, err := ds.GuildRoles(g.String()) + if err != nil { + return "", errors.Wrap(err, "Error getting roles") + } + + for _, role := range roles { + if role.ID == r.String() { + return role.Mention(), nil + } + } + + return "", errors.Wrap(err, "Error getting role") +} + +func ChannelIDMention(ds *discordgo.Session, g types.GuildID, c types.ChannelID) (string, error) { + channels, err := ds.GuildChannels(g.String()) + if err != nil { + return "", errors.Wrap(err, "Error getting channels") + } + + for _, channel := range channels { + if channel.ID == c.String() { + return channel.Mention(), nil + } + } + + return "", errors.Wrap(err, "Error getting channel") +} diff --git a/EsefexApi/util/refl/err.go b/EsefexApi/util/refl/err.go new file mode 100644 index 0000000..4bd68fb --- /dev/null +++ b/EsefexApi/util/refl/err.go @@ -0,0 +1,5 @@ +package refl + +import "fmt" + +var ErrFieldNotFound = fmt.Errorf("field not found") diff --git a/EsefexApi/util/refl/get.go b/EsefexApi/util/refl/get.go new file mode 100644 index 0000000..4c122eb --- /dev/null +++ b/EsefexApi/util/refl/get.go @@ -0,0 +1,49 @@ +package refl + +import ( + "fmt" + "reflect" + "strings" + + "github.com/pkg/errors" +) + +func GetNestedFieldValue(obj interface{}, path string) (interface{}, error) { + val := reflect.ValueOf(obj) + + // Navigate through the nested fields + for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("provided object is not a struct") + } + + // Split the path into individual field names + fieldNames := strings.Split(path, ".") + + // Call the recursive helper function + return GetNestedFieldValueRecursive(val, fieldNames) +} + +func GetNestedFieldValueRecursive(val reflect.Value, fieldNames []string) (interface{}, error) { + // Base case: no more field names to process + if len(fieldNames) == 0 { + return val.Interface(), nil + } + + // Iterate through the fields of the current struct + for i := 0; i < val.NumField(); i++ { + field := val.Type().Field(i) + + // Check if the field name matches the current path element + if field.Name == fieldNames[0] { + // Recursively call the function for the next level of nesting + return GetNestedFieldValueRecursive(val.Field(i), fieldNames[1:]) + } + } + + // If the field is not found, return an error + return nil, errors.Wrap(ErrFieldNotFound, fieldNames[0]) +} diff --git a/EsefexApi/util/refl/paths.go b/EsefexApi/util/refl/paths.go new file mode 100644 index 0000000..2cb7ddc --- /dev/null +++ b/EsefexApi/util/refl/paths.go @@ -0,0 +1,41 @@ +package refl + +import "reflect" + +func FindAllPaths(obj interface{}) []string { + paths := make([]string, 0) + FindAllPathsRecursive(reflect.ValueOf(obj), "", &paths) + return paths +} + +func FindAllPathsRecursive(val reflect.Value, currentPath string, paths *[]string) { + // Navigate through the nested fields + for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return + } + + // Iterate through the fields of the current struct + for i := 0; i < val.NumField(); i++ { + field := val.Type().Field(i) + + // Construct the path for the current field + fieldPath := currentPath + field.Name + + // Add a dot separator if not the first field in the path + if currentPath != "" { + fieldPath = currentPath + "." + field.Name + } + + // Recursively call the function for the next level of nesting + FindAllPathsRecursive(val.Field(i), fieldPath, paths) + + // Add the path to the result if the field is not a struct + if val.Field(i).Kind() != reflect.Struct { + *paths = append(*paths, fieldPath) + } + } +} diff --git a/EsefexApi/util/refl/set.go b/EsefexApi/util/refl/set.go new file mode 100644 index 0000000..91326d3 --- /dev/null +++ b/EsefexApi/util/refl/set.go @@ -0,0 +1,55 @@ +package refl + +import ( + "fmt" + "reflect" + "strings" + + "github.com/pkg/errors" +) + +func SetNestedFieldValue(obj interface{}, path string, value interface{}) error { + val := reflect.ValueOf(obj) + + // Navigate through the nested fields + for val.Kind() == reflect.Ptr || val.Kind() == reflect.Interface { + val = val.Elem() + } + + if val.Kind() != reflect.Struct { + return fmt.Errorf("provided object is not a struct") + } + + // Split the path into individual field names + fieldNames := strings.Split(path, ".") + + // Call the recursive helper function + return SetNestedFieldValueRecursive(val, fieldNames, value) +} + +func SetNestedFieldValueRecursive(val reflect.Value, fieldNames []string, newValue interface{}) error { + // Base case: no more field names to process + if len(fieldNames) == 0 { + // Set the value at the final field + valSettable := reflect.ValueOf(newValue) + if val.CanSet() && valSettable.Type().AssignableTo(val.Type()) { + val.Set(valSettable) + return nil + } + return fmt.Errorf("cannot set value at path") + } + + // Iterate through the fields of the current struct + for i := 0; i < val.NumField(); i++ { + field := val.Type().Field(i) + + // Check if the field name matches the current path element + if field.Name == fieldNames[0] { + // Recursively call the function for the next level of nesting + return SetNestedFieldValueRecursive(val.Field(i), fieldNames[1:], newValue) + } + } + + // If the field is not found, return an error + return errors.Wrap(ErrFieldNotFound, fieldNames[0]) +}