Skip to content

Commit

Permalink
multi-guild support (ongoing)
Browse files Browse the repository at this point in the history
  • Loading branch information
Depado committed Nov 20, 2020
1 parent 95ae550 commit ffdcf0c
Show file tree
Hide file tree
Showing 26 changed files with 672 additions and 188 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
fox
vendor/
dockervol/
*.db
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ conf.*yml
fox
dockervol/
*.priv
*.db
117 changes: 64 additions & 53 deletions acl/acl.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package acl

import (
"github.com/Depado/fox/cmd"
"fmt"

"github.com/Depado/fox/guild"
"github.com/Depado/fox/storage"
"github.com/bwmarrin/discordgo"
)

Expand All @@ -21,93 +24,101 @@ const (
)

type ACL struct {
AdminRoleID string
DJRoleID string
MusicChannelID string
storage *storage.StormDB
}

func NewACL(conf *cmd.Conf) *ACL {
func NewACL(s *storage.StormDB) *ACL {
return &ACL{
AdminRoleID: conf.Bot.Roles.Admin,
DJRoleID: conf.Bot.Roles.DJ,
MusicChannelID: conf.Bot.Channels.Text,
storage: s,
}
}

// Check will perform checks for the given RoleRestriction and
// ChannelRestriction.
func (a ACL) Check(r RoleRestriction, c ChannelRestriction, u *discordgo.Member, m *discordgo.Message) bool {
func (a ACL) Check(s *discordgo.Session, m *discordgo.Message, r RoleRestriction, c ChannelRestriction) (bool, error) {
var gs *guild.State
var err error

// Fetch guild state
if gs, err = a.storage.GetGuildState(m.GuildID); err != nil {
return false, fmt.Errorf("get guild state: %w", err)
}

// Check for user restriction
switch r {
case Admin:
if !a.IsAdmin(u) {
return false
}
return a.IsAdmin(s, m)
case Privileged:
if !a.IsPrivileged(u) {
return false
// If no privileged role is defined, automatically refuse unless admin
if gs.PrivilegedRole == "" {
return a.IsAdmin(s, m)
} else {
return a.IsPrivileged(s, m, gs)
}
}

// Check for channel restriction
if c == Music {
if !a.IsMusic(m) {
return false
// If no text channel defined, automatically approve
if gs.TextChannel == "" {
return true, nil
} else {
return m.ChannelID == gs.TextChannel, nil
}
}

return true
return true, nil
}

func RoleRestrictionString(r RoleRestriction) string {
var rr string

switch r {
case Admin:
rr = "🔐 Admin"
case Privileged:
rr = "🔒 Admin or DJ"
case Anyone:
rr = "🔓 No restriction"
}

return rr
// IsMusic will check if the provided message was sent to the music channel.
func (a ACL) IsMusic(m *discordgo.Message, gs *guild.State) bool {
return m.ChannelID == gs.TextChannel
}

func ChannelRestrictionString(c ChannelRestriction) string {
var cr string

switch c {
case Music:
cr = "🎶 Music text channel only"
case Anywhere:
cr = "🌍 No restriction"
// IsPrivileged will check if a member is either admin or DJ.
func (a ACL) IsPrivileged(s *discordgo.Session, m *discordgo.Message, gs *guild.State) (bool, error) {
adm, err := a.IsAdmin(s, m)
if err != nil {
return false, fmt.Errorf("check admin: %w", err)
}
if adm {
return true, nil
}

return cr
}

// IsMusic will check if the provided message was sent to the music channel.
func (a ACL) IsMusic(m *discordgo.Message) bool {
return m.ChannelID == a.MusicChannelID
if gs.PrivilegedRole != "" {
return a.HasRole(m.Member, gs.PrivilegedRole), nil
}
return false, nil
}

// IsPrivileged will check if a member is either admin or DJ.
func (a ACL) IsPrivileged(m *discordgo.Member) bool {
for _, r := range m.Roles {
if r == a.AdminRoleID || r == a.DJRoleID {
// HasRole will check if a guild member has the given role
func (a ACL) HasRole(u *discordgo.Member, r string) bool {
for _, ur := range u.Roles {
if ur == r {
return true
}
}
return false
}

// IsAdmin will check if a member has the admin role.
func (a ACL) IsAdmin(m *discordgo.Member) bool {
for _, r := range m.Roles {
if r == a.AdminRoleID {
return true
func (a ACL) IsAdmin(s *discordgo.Session, m *discordgo.Message) (bool, error) {
g, err := s.Guild(m.GuildID)
if err != nil {
return false, fmt.Errorf("get guild: %w", err)
}

// Always true for the guild owner
if m.Author.ID == g.OwnerID {
return true, nil
}

// For every non-managed admin role, check if user has this role
for _, r := range g.Roles {
if r.Permissions&discordgo.PermissionAdministrator != 0 && !r.Managed && a.HasRole(m.Member, r.ID) {
return true, nil
}
}
return false

return false, nil
}
29 changes: 29 additions & 0 deletions acl/display.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package acl

func RoleRestrictionString(r RoleRestriction) string {
var rr string

switch r {
case Admin:
rr = "🔐 Admin"
case Privileged:
rr = "🔒 Admin or DJ"
case Anyone:
rr = "🔓 No restriction"
}

return rr
}

func ChannelRestrictionString(c ChannelRestriction) string {
var cr string

switch c {
case Music:
cr = "🎶 Music text channel only"
case Anywhere:
cr = "🌍 No restriction"
}

return cr
}
43 changes: 38 additions & 5 deletions bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@ import (
"github.com/Depado/fox/acl"
"github.com/Depado/fox/cmd"
"github.com/Depado/fox/commands"
"github.com/Depado/fox/guild"
"github.com/Depado/fox/player"
"github.com/Depado/fox/storage"
"github.com/asdine/storm/v3"
"github.com/bwmarrin/discordgo"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
)

type Bot struct {
log *zerolog.Logger
log zerolog.Logger
session *discordgo.Session
allCommands []commands.Command
commands *CommandMap
conf *cmd.Conf
players *player.Players
storage *storage.StormDB
acl *acl.ACL
}

Expand All @@ -34,21 +40,48 @@ func (cm *CommandMap) Get(c string) (commands.Command, bool) {
return co, ok
}

func NewBot(s *discordgo.Session, l *zerolog.Logger, conf *cmd.Conf, a *acl.ACL, cmds []commands.Command) *Bot {
func NewBot(s *discordgo.Session, l *zerolog.Logger, c *cmd.Conf, cmds []commands.Command, p *player.Players, storage *storage.StormDB, a *acl.ACL) *Bot {
b := &Bot{
log: l,
conf: conf,
log: l.With().Str("component", "bot").Logger(),
conf: c,
session: s,
acl: a,
allCommands: cmds,
players: p,
commands: &CommandMap{m: make(map[string]commands.Command)},
storage: storage,
acl: a,
}

for _, cmd := range cmds {
b.AddCommand(cmd)
}

for _, g := range b.session.State.Guilds {
var err error
var gstate *guild.State

if gstate, err = b.storage.GetGuildState(g.ID); err != nil {
if err == storm.ErrNotFound {
if gstate, err = b.storage.NewGuildState(g.ID); err != nil {
b.log.Err(err).Msg("unable to instantiate new guild state")
continue
}
} else {
b.log.Err(err).Msg("unable to fetch guild state")
continue
}
}

if err := p.Create(s, c, l, g.ID, storage, gstate); err != nil {
l.Err(err).Msg("unable to handle guild create")
continue
}
l.Debug().Str("guild", g.ID).Str("name", g.Name).Msg("registered new player")
}

b.session.AddHandler(b.MessageCreatedHandler)
b.session.AddHandler(func(s *discordgo.Session, g *discordgo.GuildCreate) {})
b.session.AddHandler(func(s *discordgo.Session, vsu *discordgo.VoiceStateUpdate) {})

return b
}
Expand Down
12 changes: 5 additions & 7 deletions bot/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,14 @@ func (b *Bot) MessageCreatedHandler(s *discordgo.Session, m *discordgo.MessageCr
return
}

// Retrieve the guild member from the author of the message
member, err := s.GuildMember(m.GuildID, m.Author.ID)
// Check permissions
cr, rr := c.ACL()
ok, err := b.acl.Check(s, m.Message, rr, cr)
if err != nil {
b.log.Err(err).Msg("unable to get guild member from message author")
b.log.Err(err).Msg("unable to check acl")
return
}

// Check permissions
cr, rr := c.ACL()
if !b.acl.Check(rr, cr, member, m.Message) {
if !ok {
msg := fmt.Sprintf("You do not have permission to do that.\n%s\n%s", acl.RoleRestrictionString(rr), acl.ChannelRestrictionString(cr))
err := message.SendTimedReply(s, m.Message, "", msg, "", 5*time.Second)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion session/session.go → bot/session.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package session
package bot

import (
"github.com/Depado/fox/cmd"
Expand Down
17 changes: 2 additions & 15 deletions cmd/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,8 @@ type LogConf struct {
}

type BotConf struct {
Channels ChannelsConf `mapstructure:"channels"`
Roles RolesConf `mapstructure:"roles"`
Guild string `mapstrcture:"guild"`
Token string `mapstructure:"token"`
Prefix string `mapstructure:"prefix"`
}

type ChannelsConf struct {
Text string `mapstructure:"text"`
Voice string `mapstructure:"voice"`
}

type RolesConf struct {
Admin string `mapstructure:"admin"`
DJ string `mapstructure:"dj"`
Token string `mapstructure:"token"`
Prefix string `mapstructure:"prefix"`
}

type DatabaseConf struct {
Expand Down
7 changes: 1 addition & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,11 @@ func AddLoggerFlags(c *cobra.Command) {

func AddBotFlags(c *cobra.Command) {
c.PersistentFlags().String("bot.prefix", "!fox", "prefix to call the bot")
c.PersistentFlags().String("bot.guild", "", "guild (server) the bot is connected to")
c.PersistentFlags().String("bot.channels.text", "", "text channel ID where some commands can be issued")
c.PersistentFlags().String("bot.channels.voice", "", "voice channel ID the bot will stream to")
c.PersistentFlags().String("bot.roles.admin", "", "role ID for the admins")
c.PersistentFlags().String("bot.roles.dj", "", "role ID for the DJ role")
c.PersistentFlags().String("bot.token", "", "private bot token")
}

func AddDatabaseFlags(c *cobra.Command) {
c.PersistentFlags().String("database.path", "monit.db", "path to the database file to use")
c.PersistentFlags().String("database.path", "fox.db", "path to the database file to use")
}

// AddConfigurationFlag adds support to provide a configuration file on the
Expand Down
Loading

0 comments on commit ffdcf0c

Please sign in to comment.