diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7b035e0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Dusty.P (https://github.com/dustinpianalto) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..65627a2 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +## Disgoman +Disgoman is a command handler for DiscordGo, inspired by [Anpan](https://github.com/MikeModder/anpan) + +## Status +[![Go Report Card](https://goreportcard.com/badge/github.com/MikeModder/disgoman)](https://goreportcard.com/report/github.com/MikeModder/disgoman) + +## \ No newline at end of file diff --git a/djpianalto.com/disgoman/command-manager.go b/djpianalto.com/disgoman/command-manager.go new file mode 100644 index 0000000..e8836e8 --- /dev/null +++ b/djpianalto.com/disgoman/command-manager.go @@ -0,0 +1,183 @@ +package disgoman + +/* command-manager.go + * The main command manager code + * + * Disgoman (c) 2020 Dusty.P/dustinpianalto + */ + +import ( + "errors" + "fmt" + "github.com/bwmarrin/discordgo" + "github.com/kballard/go-shellquote" + "log" + "strings" +) + +func (c *CommandManager) AddCommand(name string, aliases []string, desc string, ownerOnly, hidden bool, perms Permission, command CommandInvokeFunc) error { + if _, ok := c.Commands[name]; ok { + return errors.New(fmt.Sprintf("A command named %v already exists", name)) + } + for _, alias := range aliases { + if _, ok := c.Aliases[alias]; ok { + return errors.New(fmt.Sprintf("An alias named %v already exists", alias)) + } + } + c.Commands[name] = &Command{ + Name: name, + Description: desc, + OwnerOnly: ownerOnly, + Hidden: hidden, + RequiredPermissions: perms, + Invoke: command, + } + if len(aliases) > 0 { + for _, alias := range aliases { + c.Aliases[alias] = name + } + } + return nil +} + +func (c *CommandManager) RemoveAliasesFor(name string) { + for alias, cmd := range c.Aliases { + if cmd == name { + delete(c.Aliases, alias) + } + } +} + +func (c *CommandManager) RemoveCommand(name string) error { + if _, ok := c.Commands[name]; ok { + delete(c.Commands, name) + c.RemoveAliasesFor(name) + return nil + } + return errors.New(fmt.Sprintf("%v not in commands", name)) +} + +func (c *CommandManager) RemoveAlias(alias string) error { + if _, ok := c.Aliases[alias]; ok { + delete(c.Aliases, alias) + return nil + } + return errors.New(fmt.Sprintf("%v not in aliases", alias)) +} + +func (c *CommandManager) IsOwner(id string) bool { + for _, o := range c.Owners { + if o == id { + return true + } + } + return false +} + +func (c *CommandManager) OnMessage(session *discordgo.Session, m *discordgo.MessageCreate) { + if m.Author.Bot && c.IgnoreBots { + return // If the author is a bot and ignore bots is set then just exit + } + + content := m.Content + + prefixes := c.Prefixes(m.GuildID) + var prefix string + for _, prefix = range prefixes { + if strings.HasPrefix(content, prefix) { + break + } + } + if prefix == "" { + return // If we didn't find a valid prefix then exit + } + + // If we found our prefix then remove it and split the command into pieces + cmd, err := shellquote.Split(strings.TrimPrefix(content, prefix)) + if err != nil { + log.Fatal(err) + return + } + + var command *Command + invoked := cmd[0] + if n, ok := c.Aliases[invoked]; ok { + if cmnd, ok := c.Commands[n]; ok { + command = cmnd + } else { + log.Fatal("Alias Not Found in Commands") + return + } + } else if cmnd, ok := c.Commands[invoked]; ok { + command = cmnd + } else { + log.Fatal("Command Not Found") + return + } + + channel, err := session.Channel(m.ChannelID) + if err != nil { + log.Fatal("Couldn't retrieve Channel.") + return + } + + if !CheckPermissions(session, *m.Member, *channel, command.RequiredPermissions) { + embed := &discordgo.MessageEmbed{ + Title: "Insufficient Permissions", + Description: "You don't have the correct permissions to run this command.", + Color: 0xFF0000, + } + if !command.Hidden { + session.ChannelMessageSendEmbed(m.ChannelID, embed) + } + return + } + + me, err := session.GuildMember(m.GuildID, session.State.User.ID) + if err != nil { + log.Fatal(err) + return + } + + if !CheckPermissions(session, *me, *channel, command.RequiredPermissions) { + embed := &discordgo.MessageEmbed{ + Title: "Insufficient Permissions", + Description: "I don't have the correct permissions to run this command.", + Color: 0xFF0000, + } + if !command.Hidden { + session.ChannelMessageSendEmbed(m.ChannelID, embed) + } + return + } + + if command.OwnerOnly && !c.IsOwner(m.Author.ID) { + embed := &discordgo.MessageEmbed{ + Title: "You can't run that command!", + Description: "Sorry, only the bot owner(s) can run that command!", + Color: 0xff0000, + } + + if !command.Hidden { + session.ChannelMessageSendEmbed(m.ChannelID, embed) + } + return + } + + guild, _ := session.Guild(m.GuildID) + + context := Context{ + Session: session, + Channel: channel, + Message: m.Message, + User: m.Author, + Guild: guild, + Member: m.Member, + } + + err = command.Invoke(context, cmd[1:]) + if err != nil && c.OnErrorFunc != nil { + c.OnErrorFunc(context, cmd[0], err) + } + +} diff --git a/djpianalto.com/disgoman/context.go b/djpianalto.com/disgoman/context.go new file mode 100644 index 0000000..ab7218f --- /dev/null +++ b/djpianalto.com/disgoman/context.go @@ -0,0 +1,29 @@ +package disgoman + +import ( + "github.com/bwmarrin/discordgo" + "io" +) + +/* context.go: + * Utility functions for command context + * + * Disgoman (c) 2020 Dusty.P/dustinpianalto + */ + +// Send message to originating channel +func (c *Context) Send(message string) (*discordgo.Message, error) { + return c.Session.ChannelMessageSend(c.Channel.ID, message) +} + +// Send embed to originating channel +func (c *Context) SendEmbed(embed *discordgo.MessageEmbed) (*discordgo.Message, error) { + return c.Session.ChannelMessageSendEmbed(c.Channel.ID, embed) +} + +// Send embed to originating channel +func (c *Context) SendFile(filename string, file io.Reader) (*discordgo.Message, error) { + return c.Session.ChannelFileSend(c.Channel.ID, filename, file) +} + +// TODO Combine these to all use ChannelMessageSendComplex diff --git a/djpianalto.com/disgoman/disgoman.go b/djpianalto.com/disgoman/disgoman.go index 3fea4cf..45e3293 100644 --- a/djpianalto.com/disgoman/disgoman.go +++ b/djpianalto.com/disgoman/disgoman.go @@ -1 +1,37 @@ package disgoman + +/* Package Disgoman: + * Command Handler for DisgordGo. + * Inspired by: + * - Anpan (https://github.com/MikeModder/anpan) + * + * Disgoman (c) 2020 Dusty.P/dustinpianalto + */ + +func GetCommandManager(prefixes PrefixesFunc, owners []string, ignoreBots, checkPerms bool) CommandManager { + return CommandManager{ + Prefixes: prefixes, + Owners: owners, + StatusManager: GetDefaultStatusManager(), + Commands: make(map[string]*Command), + Aliases: make(map[string]string), + IgnoreBots: ignoreBots, + CheckPermissions: checkPerms, + } +} + +func GetStatusManager(values []string, interval string) StatusManager { + return StatusManager{ + Values: values, + Interval: interval, + } +} + +func GetDefaultStatusManager() StatusManager { + return GetStatusManager( + []string{ + "Golang!", + "DiscordGo!", + "Disgoman!", + }, "10s") +} diff --git a/djpianalto.com/disgoman/status-manager.go b/djpianalto.com/disgoman/status-manager.go new file mode 100644 index 0000000..70e1267 --- /dev/null +++ b/djpianalto.com/disgoman/status-manager.go @@ -0,0 +1,61 @@ +package disgoman + +import ( + "github.com/bwmarrin/discordgo" + "log" + "math/rand" + "time" +) + +/* status-manager.go: + * Built in status manager which cycles through a list of status at a specified interval + * + * Disgoman (c) 2020 Dusty.P/dustinpianalto + */ + +// Add a status to the manager +func (s *StatusManager) AddStatus(status string) { + s.Values = append(s.Values, status) +} + +// Remove a status from the manager +func (s *StatusManager) RemoveStatus(status string) []string { + for i, v := range s.Values { + if v == status { + s.Values = append(s.Values[:i], s.Values[i+1:]...) + break + } + } + return s.Values +} + +// Sets interval to new value +func (s *StatusManager) SetInterval(interval string) { + s.Interval = interval +} + +func (s *StatusManager) UpdateStatus(session *discordgo.Session) error { + i := rand.Intn(len(s.Values)) + err := session.UpdateStatus(0, s.Values[i]) + return err +} + +func (s *StatusManager) OnReady(session *discordgo.Session, _ *discordgo.Ready) { + interval, err := time.ParseDuration(s.Interval) + if err != nil { + return + } + + err = s.UpdateStatus(session) + if err != nil { + log.Fatal(err) + } + + ticker := time.NewTicker(interval) + for range ticker.C { + err = s.UpdateStatus(session) + if err != nil { + log.Fatal(err) + } + } +} diff --git a/djpianalto.com/disgoman/structs.go b/djpianalto.com/disgoman/structs.go new file mode 100644 index 0000000..76e806d --- /dev/null +++ b/djpianalto.com/disgoman/structs.go @@ -0,0 +1,43 @@ +package disgoman + +/* structs.go + * Contains structs used in Disgoman + * + * Disgoman (c) 2020 Dusty.P/dustinpianalto + */ + +import "github.com/bwmarrin/discordgo" + +type CommandManager struct { + Prefixes PrefixesFunc + Owners []string + StatusManager StatusManager + OnErrorFunc OnErrorFunc + Commands map[string]*Command + Aliases map[string]string + IgnoreBots bool + CheckPermissions bool +} + +type StatusManager struct { + Values []string + Interval string +} + +type Command struct { + Name string + Description string + OwnerOnly bool + Hidden bool + RequiredPermissions Permission + Invoke CommandInvokeFunc +} + +type Context struct { + Session *discordgo.Session + Channel *discordgo.Channel + Message *discordgo.Message + User *discordgo.User + Guild *discordgo.Guild + Member *discordgo.Member +} diff --git a/djpianalto.com/disgoman/types.go b/djpianalto.com/disgoman/types.go new file mode 100644 index 0000000..a1d4d1d --- /dev/null +++ b/djpianalto.com/disgoman/types.go @@ -0,0 +1,54 @@ +package disgoman + +/* types.go + * Defines function types and other types + * + * Disgoman (c) 2020 Dusty.P/dustinpianalto + */ + +// Function to invoke for command +type CommandInvokeFunc func(Context, []string) error + +// Function to get prefixes +type PrefixesFunc func(string) []string + +// Function to run on command error +type OnErrorFunc func(Context, string, error) + +type Permission int + +// Defining permissions based on the Discord API +const ( + PermissionAdministrator Permission = 8 + PermissionViewAuditLog Permission = 128 + PermissionViewServerInsights Permission = 524288 + PermissionManageServer Permission = 32 + PermissionManageRoles Permission = 268435456 + PermissionManageChannels Permission = 16 + PermissionKickMembers Permission = 2 + PermissionBanMembers Permission = 4 + PermissionCreateInstantInvite Permission = 1 + PermissionChangeNickname Permission = 67108864 + PermissionManageNicknames Permission = 134217728 + PermissionManageEmojis Permission = 1073741824 + PermissionManageWebhooks Permission = 536870912 + PermissionViewChannels Permission = 1024 + + PermissionMessagesSend Permission = 2048 + PermissionMessagesSendTTS Permission = 4096 + PermissionMessagesManage Permission = 8192 + PermissionMessagesEmbedLinks Permission = 16384 + PermissionMessagesAttachFiles Permission = 32768 + PermissionMessagesReadHistory Permission = 65536 + PermissionMessagesMentionEveryone Permission = 131072 + PermissionMessagesUseExternalEmojis Permission = 262144 + PermissionMessagesAddReactions Permission = 64 + + PermissionVoiceConnect Permission = 1048576 + PermissionVoiceSpeak Permission = 2097152 + PermissionVoiceMuteMembers Permission = 4194304 + PermissionVoiceDeafenMembers Permission = 8388608 + PermissionVoiceUseMembers Permission = 16777216 + PermissionVoiceUseActivity Permission = 33554432 + PermissionVoicePrioritySpeaker Permission = 256 +) diff --git a/djpianalto.com/disgoman/utils.go b/djpianalto.com/disgoman/utils.go new file mode 100644 index 0000000..d841a21 --- /dev/null +++ b/djpianalto.com/disgoman/utils.go @@ -0,0 +1,53 @@ +package disgoman + +/* helpers.go: + * Utility functions to make my life easier. + * + * Disgoman (c) 2020 Dusty.P/dustinpianalto + */ + +import "github.com/bwmarrin/discordgo" + +// Checks the channel and guild permissions to see if the member has the needed permissions +func CheckPermissions(session *discordgo.Session, member discordgo.Member, channel discordgo.Channel, perms Permission) bool { + if perms == 0 { + return true // If no permissions are required then just return true + } + + for _, overwrite := range channel.PermissionOverwrites { + if overwrite.ID == member.User.ID { + if overwrite.Allow&int(perms) != 0 { + return true // If the channel has an overwrite for the user then true + } else if overwrite.Deny&int(perms) != 0 { + return false // If there is an explicit deny then false + } + } + } + + for _, roleID := range member.Roles { + role, err := session.State.Role(member.GuildID, roleID) + if err != nil { + return false // There is something wrong with the role, default to false + } + + for _, overwrite := range channel.PermissionOverwrites { + if overwrite.ID == roleID { + if overwrite.Allow&int(perms) != 0 { + return true // If the channel has an overwrite for the role then true + } else if overwrite.Deny&int(perms) != 0 { + return false // If there is an explicit deny then false + } + } + } + + if role.Permissions&int(PermissionAdministrator) != 0 { + return true // If they are an administrator then they automatically have all permissions + } + + if role.Permissions&int(perms) != 0 { + return true // The role has the required permissions + } + } + + return false // Default to false +} diff --git a/go.mod b/go.mod index 004d923..ac4dbc8 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module djpianalto.com/disgoman -go 1.14 \ No newline at end of file +go 1.14 + +require ( + github.com/bwmarrin/discordgo v0.20.2 + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 +)