From ffdcf0c5e78da26febc83f35c301c8f21f2e6651 Mon Sep 17 00:00:00 2001 From: Paul Lhussiez Date: Thu, 19 Nov 2020 02:14:11 +0100 Subject: [PATCH] multi-guild support (ongoing) --- .dockerignore | 1 + .gitignore | 1 + acl/acl.go | 117 ++++++++++++++++++++---------------- acl/display.go | 29 +++++++++ bot/bot.go | 43 +++++++++++-- bot/handler.go | 12 ++-- {session => bot}/session.go | 2 +- cmd/conf.go | 17 +----- cmd/root.go | 7 +-- commands/add.go | 32 +++++++--- commands/command.go | 10 +-- commands/jam.go | 6 +- commands/player.go | 93 +++++++++++++++++++--------- commands/queue.go | 20 +++--- commands/remove.go | 16 +++-- commands/setup.go | 108 +++++++++++++++++++++++++++++++++ commands/stats.go | 14 +++-- commands/volume.go | 16 +++-- go.mod | 12 +++- go.sum | 69 +++++++++++++++++++-- guild/guild.go | 65 ++++++++++++++++++++ main.go | 7 +-- message/formatter.go | 13 ++++ player/display.go | 2 +- player/player.go | 95 ++++++++++++++++++++++------- storage/storage.go | 53 ++++++++++++++++ 26 files changed, 672 insertions(+), 188 deletions(-) create mode 100644 acl/display.go rename {session => bot}/session.go (96%) create mode 100644 commands/setup.go create mode 100644 guild/guild.go create mode 100644 storage/storage.go diff --git a/.dockerignore b/.dockerignore index a6a339e..bcd8068 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ fox vendor/ dockervol/ +*.db diff --git a/.gitignore b/.gitignore index 4952e3f..24f90c3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ conf.*yml fox dockervol/ *.priv +*.db diff --git a/acl/acl.go b/acl/acl.go index 761b0ad..11fe65e 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -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" ) @@ -21,81 +24,77 @@ 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 } } @@ -103,11 +102,23 @@ func (a ACL) IsPrivileged(m *discordgo.Member) bool { } // 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 } diff --git a/acl/display.go b/acl/display.go new file mode 100644 index 0000000..bec903e --- /dev/null +++ b/acl/display.go @@ -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 +} diff --git a/bot/bot.go b/bot/bot.go index 6d48e26..e1d1774 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -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 } @@ -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 } diff --git a/bot/handler.go b/bot/handler.go index 2925974..bc4bc5f 100644 --- a/bot/handler.go +++ b/bot/handler.go @@ -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 { diff --git a/session/session.go b/bot/session.go similarity index 96% rename from session/session.go rename to bot/session.go index ccba279..06f7d9a 100644 --- a/session/session.go +++ b/bot/session.go @@ -1,4 +1,4 @@ -package session +package bot import ( "github.com/Depado/fox/cmd" diff --git a/cmd/conf.go b/cmd/conf.go index e4b4cc9..416c59f 100644 --- a/cmd/conf.go +++ b/cmd/conf.go @@ -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 { diff --git a/cmd/root.go b/cmd/root.go index 5745b19..3f4811f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 diff --git a/commands/add.go b/commands/add.go index 492fb16..031abf6 100644 --- a/commands/add.go +++ b/commands/add.go @@ -29,9 +29,15 @@ func (c *add) Handler(s *discordgo.Session, m *discordgo.Message, args []string) return } + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + tr, e, err := c.sp.GetPlaylist(url, m) if err == nil { - c.Player.Queue.Append(tr...) + p.Queue.Append(tr...) e.Description = fmt.Sprintf("Added **%d** tracks to end of queue", len(tr)) if _, err = s.ChannelMessageSendEmbed(m.ChannelID, e); err != nil { c.log.Err(err).Msg("unable to send embed") @@ -41,7 +47,7 @@ func (c *add) Handler(s *discordgo.Session, m *discordgo.Message, args []string) t, e, err := c.sp.GetTrack(url, m) if err == nil { - c.Player.Queue.Append(t) + p.Queue.Append(t) e.Description = "Added one tracks to end of queue" if _, err = s.ChannelMessageSendEmbed(m.ChannelID, e); err != nil { c.log.Err(err).Msg("unable to send embed") @@ -54,7 +60,7 @@ func (c *add) Handler(s *discordgo.Session, m *discordgo.Message, args []string) } } -func NewAddCommand(p *player.Player, log *zerolog.Logger, sp *soundcloud.SoundCloudProvider) Command { +func NewAddCommand(p *player.Players, log *zerolog.Logger, sp *soundcloud.SoundCloudProvider) Command { cmd := "add" return &add{ sp: sp, @@ -78,8 +84,8 @@ func NewAddCommand(p *player.Player, log *zerolog.Logger, sp *soundcloud.SoundCl {Command: "a ", Explanation: "Add the track using the alias"}, }, }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } @@ -100,9 +106,15 @@ func (c *next) Handler(s *discordgo.Session, m *discordgo.Message, args []string return } + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + tr, e, err := c.sp.GetPlaylist(url, m) if err == nil { - c.Player.Queue.Prepend(tr...) + p.Queue.Prepend(tr...) e.Description = fmt.Sprintf("Added **%d** tracks to start of queue", len(tr)) if _, err = s.ChannelMessageSendEmbed(m.ChannelID, e); err != nil { c.log.Err(err).Msg("unable to send embed") @@ -112,7 +124,7 @@ func (c *next) Handler(s *discordgo.Session, m *discordgo.Message, args []string t, e, err := c.sp.GetTrack(url, m) if err == nil { - c.Player.Queue.Prepend(t) + p.Queue.Prepend(t) e.Description = "Added one tracks to start of queue" if _, err = s.ChannelMessageSendEmbed(m.ChannelID, e); err != nil { c.log.Err(err).Msg("unable to send embed") @@ -125,7 +137,7 @@ func (c *next) Handler(s *discordgo.Session, m *discordgo.Message, args []string } } -func NewNextCommand(p *player.Player, log *zerolog.Logger, sp *soundcloud.SoundCloudProvider) Command { +func NewNextCommand(p *player.Players, log *zerolog.Logger, sp *soundcloud.SoundCloudProvider) Command { cmd := "next" return &next{ sp: sp, @@ -149,8 +161,8 @@ func NewNextCommand(p *player.Player, log *zerolog.Logger, sp *soundcloud.SoundC {Command: "n ", Explanation: "Add the track using the alias"}, }, }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } diff --git a/commands/command.go b/commands/command.go index 55fc542..10001a8 100644 --- a/commands/command.go +++ b/commands/command.go @@ -9,9 +9,10 @@ import ( "github.com/Depado/fox/acl" "github.com/Depado/fox/player" "github.com/Depado/fox/soundcloud" + "github.com/Depado/fox/storage" ) -func InitializeAllCommands(p *player.Player, l *zerolog.Logger, sp *soundcloud.SoundCloudProvider) []Command { +func InitializeAllCommands(p *player.Players, l *zerolog.Logger, sp *soundcloud.SoundCloudProvider, st *storage.StormDB) []Command { return []Command{ NewPlayCommand(p, l), NewPauseCommand(p, l), @@ -25,6 +26,7 @@ func InitializeAllCommands(p *player.Player, l *zerolog.Logger, sp *soundcloud.S NewSkipCommand(p, l), NewRemoveCommand(p, l), NewStatsCommand(p, l), + NewSetupCommand(p, l, st), } } @@ -67,9 +69,9 @@ type BaseCommand struct { Options Options // Internal fields - Help Help - Player *player.Player - log zerolog.Logger + Help Help + Players *player.Players + log zerolog.Logger } func (c BaseCommand) ACL() (acl.ChannelRestriction, acl.RoleRestriction) { diff --git a/commands/jam.go b/commands/jam.go index 55b35ca..b98f583 100644 --- a/commands/jam.go +++ b/commands/jam.go @@ -37,7 +37,7 @@ func (c *jam) Handler(s *discordgo.Session, m *discordgo.Message, args []string) } } -func NewJamCommand(p *player.Player, log *zerolog.Logger) Command { +func NewJamCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "jam" return &jam{ BaseCommand{ @@ -60,8 +60,8 @@ func NewJamCommand(p *player.Player, log *zerolog.Logger) Command { {Command: "jam s", Explanation: "Small jam"}, }, }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } diff --git a/commands/player.go b/commands/player.go index cea7338..6510db6 100644 --- a/commands/player.go +++ b/commands/player.go @@ -16,36 +16,45 @@ type play struct { } func (c *play) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { - if c.Player.State.Playing && c.Player.State.Paused { - c.Player.Resume() + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + + if p.State.Playing && p.State.Paused { + p.Resume() msg := fmt.Sprintf("⏯ī¸ Resumed by <@%s>", m.Author.ID) if err := message.SendReply(s, m, "", msg, ""); err != nil { c.log.Err(err).Msg("unable to ") } return } - if c.Player.State.Playing { - if err := message.SendTimedReply(s, m, "", "Already playing", "", 5*time.Second); err != nil { - c.log.Err(err).Msg("unable to send timed reply") - } + if p.State.Playing { + message.SendShortTimedNotice(s, m, "Already playing", c.log) + return + } + + if p.Queue.Len() == 0 { + message.SendShortTimedNotice(s, m, "The queue is empty", c.log) return } msg := fmt.Sprintf("â–ļī¸ Started playing for <@%s>", m.Author.ID) if len(args) > 0 && args[0] == "ambient" { - if err := c.Player.SetVolumePercent(50); err != nil { + if err := p.SetVolumePercent(50); err != nil { c.log.Err(err).Msg("unable to set volume") } else { msg += " in ambient mode" } } - c.Player.Play() + p.Play() if err := message.SendReply(s, m, "", msg, ""); err != nil { c.log.Err(err).Msg("unable to send reply") } } -func NewPlayCommand(p *player.Player, log *zerolog.Logger) Command { +func NewPlayCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "play" return &play{ BaseCommand{ @@ -69,8 +78,8 @@ func NewPlayCommand(p *player.Player, log *zerolog.Logger) Command { {Command: "play ambient", Explanation: "Start playing in ambient mode"}, }, }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } @@ -80,21 +89,27 @@ type stop struct { } func (c *stop) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { - if !c.Player.State.Playing { + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + + if !p.State.Playing { if err := message.SendTimedReply(s, m, "", "Nothing to do", "", 5*time.Second); err != nil { c.log.Err(err).Msg("unable to send timed reply") } return } - c.Player.Stop() + p.Stop() msg := fmt.Sprintf("⏚ī¸ Stopped by <@%s>", m.Author.ID) if err := message.SendReply(s, m, "", msg, ""); err != nil { c.log.Err(err).Msg("unable to send reply") } } -func NewStopCommand(p *player.Player, log *zerolog.Logger) Command { +func NewStopCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "stop" return &stop{ BaseCommand{ @@ -111,8 +126,8 @@ func NewStopCommand(p *player.Player, log *zerolog.Logger) Command { Description: "This command will stop the player. If the player " + "is already stopped, this command has no effect.", }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } @@ -122,20 +137,26 @@ type pause struct { } func (c *pause) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { - if c.Player.State.Paused { + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + + if p.State.Paused { if err := message.SendTimedReply(s, m, "", "Already paused", "", 5*time.Second); err != nil { c.log.Err(err).Msg("unable to send timed reply") } return } - c.Player.Pause() + p.Pause() msg := fmt.Sprintf("⏸ī¸ Paused by <@%s>", m.Author.ID) if err := message.SendReply(s, m, "", msg, ""); err != nil { c.log.Err(err).Msg("unable to ") } } -func NewPauseCommand(p *player.Player, log *zerolog.Logger) Command { +func NewPauseCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "pause" return &pause{ BaseCommand{ @@ -153,8 +174,8 @@ func NewPauseCommand(p *player.Player, log *zerolog.Logger) Command { "track won't be skipped and will keep its current playback position. " + "If the player is already paused this command has no effect.", }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } @@ -164,14 +185,20 @@ type skip struct { } func (c *skip) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { - c.Player.Skip() + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + + p.Skip() msg := fmt.Sprintf("⏭ī¸ <@%s> skipped the currently playing track", m.Author.ID) if err := message.SendReply(s, m, "", msg, ""); err != nil { c.log.Err(err).Msg("unable to send reply") } } -func NewSkipCommand(p *player.Player, log *zerolog.Logger) Command { +func NewSkipCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "skip" return &skip{ BaseCommand{ @@ -187,8 +214,8 @@ func NewSkipCommand(p *player.Player, log *zerolog.Logger) Command { ShortDesc: "Skip the currently playing track", Description: "This command can be used to skip tracks at will.", }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } @@ -198,7 +225,13 @@ type np struct { } func (c *np) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { - if !c.Player.State.Playing { + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + + if !p.State.Playing { if err := message.SendTimedReply(s, m, "", "No track is currently playing", "", 5*time.Second); err != nil { c.log.Err(err).Msg("unable to send timed reply") } @@ -210,7 +243,7 @@ func (c *np) Handler(s *discordgo.Session, m *discordgo.Message, args []string) short = false } - e := c.Player.GenerateNowPlayingEmbed(short) + e := p.GenerateNowPlayingEmbed(short) if e == nil { if err := message.SendTimedReply(s, m, "", "No track is currently playing", "", 5*time.Second); err != nil { c.log.Err(err).Msg("unable to send timed reply") @@ -223,7 +256,7 @@ func (c *np) Handler(s *discordgo.Session, m *discordgo.Message, args []string) } } -func NewNowPlayingCommand(p *player.Player, log *zerolog.Logger) Command { +func NewNowPlayingCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "nowplaying" return &np{ BaseCommand{ @@ -247,8 +280,8 @@ func NewNowPlayingCommand(p *player.Player, log *zerolog.Logger) Command { {"np f", "Short notation"}, }, }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } diff --git a/commands/queue.go b/commands/queue.go index c55959c..7b00987 100644 --- a/commands/queue.go +++ b/commands/queue.go @@ -16,28 +16,34 @@ type queue struct { } func (c *queue) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + if len(args) > 0 && (args[0] == "shuffle" || args[0] == "s") { - if c.Player.Queue.Len() < 2 { + if p.Queue.Len() < 2 { err := message.SendTimedReply(s, m, "", "There is not enough tracks to shuffle", "", 5*time.Second) if err != nil { c.log.Err(err).Msg("unable to send embed") } return } - c.Player.Queue.Shuffle() - err := message.SendReply(s, m, "", fmt.Sprintf("🎲 Shuffled **%d** tracks for <@%s>", c.Player.Queue.Len(), m.Author.ID), "") + p.Queue.Shuffle() + err := message.SendReply(s, m, "", fmt.Sprintf("🎲 Shuffled **%d** tracks for <@%s>", p.Queue.Len(), m.Author.ID), "") if err != nil { c.log.Err(err).Msg("unable to send embed") } return } - if _, err := s.ChannelMessageSendEmbed(m.ChannelID, c.Player.Queue.GenerateQueueEmbed()); err != nil { + if _, err := s.ChannelMessageSendEmbed(m.ChannelID, p.Queue.GenerateQueueEmbed()); err != nil { c.log.Err(err).Msg("unable to send embed") } } -func NewQueueCommand(p *player.Player, log *zerolog.Logger) Command { +func NewQueueCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "queue" return &queue{ BaseCommand{ @@ -61,8 +67,8 @@ func NewQueueCommand(p *player.Player, log *zerolog.Logger) Command { {Command: "q", Explanation: "Display the queue with the alias"}, }, }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } diff --git a/commands/remove.go b/commands/remove.go index 8794d30..a7e9374 100644 --- a/commands/remove.go +++ b/commands/remove.go @@ -17,8 +17,14 @@ type remove struct { } func (c *remove) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + if len(args) > 0 && (args[0] == "all" || args[0] == "a" || args[0] == "-a") { - c.Player.Queue.Clear() + p.Queue.Clear() msg := fmt.Sprintf("🚮 The queue was reset by <@%s>", m.Author.ID) if err := message.SendReply(s, m, "", msg, ""); err != nil { c.log.Err(err).Msg("unable to send reply") @@ -32,14 +38,14 @@ func (c *remove) Handler(s *discordgo.Session, m *discordgo.Message, args []stri } return } - c.Player.Queue.RemoveN(n) + p.Queue.RemoveN(n) msg := fmt.Sprintf("🚮 The next %d tracks in queue were removed by <@%s>", n, m.Author.ID) if err := message.SendReply(s, m, "", msg, ""); err != nil { c.log.Err(err).Msg("unable to send reply") } } -func NewRemoveCommand(p *player.Player, log *zerolog.Logger) Command { +func NewRemoveCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "remove" return &remove{ BaseCommand{ @@ -62,8 +68,8 @@ func NewRemoveCommand(p *player.Player, log *zerolog.Logger) Command { {Command: "rm 10", Explanation: "Remove the next 10 tracks in queue"}, }, }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } diff --git a/commands/setup.go b/commands/setup.go new file mode 100644 index 0000000..4dc279c --- /dev/null +++ b/commands/setup.go @@ -0,0 +1,108 @@ +package commands + +import ( + "errors" + "fmt" + "strings" + + "github.com/Depado/fox/acl" + "github.com/Depado/fox/guild" + "github.com/Depado/fox/message" + "github.com/Depado/fox/player" + "github.com/Depado/fox/storage" + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog" +) + +type setup struct { + BaseCommand + Storage *storage.StormDB +} + +func (c *setup) handleVoiceChannel(s *discordgo.Session, m *discordgo.Message, gstate *guild.State, value string) { + if err := gstate.SetChannel(s, value, true); err != nil { + if errors.Is(err, guild.ChannelNotFoundError) { + message.SendShortTimedNotice(s, m, "I couldn't find any vocal channel named like this", c.log) + return + } + c.log.Err(err).Msg("unable to set voice channel") + return + } + message.SendShortTimedNotice(s, m, "Alright, I'll stream the music to this channel from now on", c.log) +} + +func (c *setup) handleTextChannel(s *discordgo.Session, m *discordgo.Message, gstate *guild.State, value string) { + if err := gstate.SetChannel(s, value, false); err != nil { + if errors.Is(err, guild.ChannelNotFoundError) { + message.SendShortTimedNotice(s, m, "I couldn't find any text channel named like this", c.log) + return + } + c.log.Err(err).Msg("unable to set voice channel") + return + } + message.SendShortTimedNotice(s, m, fmt.Sprintf("Noted, the music channel is now <#%s>", gstate.TextChannel), c.log) +} + +func (c *setup) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { + var err error + var gstate *guild.State + + if gstate, err = c.Storage.GetGuildState(m.GuildID); err != nil { + c.log.Err(err).Msg("unable to fetch guild state") + return + } + + if len(args) < 1 { + return + } + + setup := strings.Split(args[0], "=") + if len(setup) != 2 { + c.log.Error().Str("arg", args[0]).Msg("unable to parse param") + return + } + param, value := setup[0], strings.Trim(setup[1], `"`) + c.log.Debug().Str("param", param).Str("value", value).Msg("got that") + + switch param { + case "voice": + c.handleVoiceChannel(s, m, gstate, value) + case "text": + c.handleTextChannel(s, m, gstate, value) + default: + message.SendShortTimedNotice(s, m, "Unknwon parameter", c.log) + return + } + + if err := c.Storage.SaveGuildState(gstate); err != nil { + c.log.Err(err).Msg("unable to save guild state") + } +} + +func NewSetupCommand(p *player.Players, log *zerolog.Logger, storage *storage.StormDB) Command { + cmd := "setup" + return &setup{ + BaseCommand: BaseCommand{ + ChannelRestriction: acl.Anywhere, + RoleRestriction: acl.Admin, + Options: Options{ + ArgsRequired: false, + DeleteUserMessage: true, + }, + Long: cmd, + Help: Help{ + Usage: cmd, + ShortDesc: "Setup the bot", + Description: "This commands allows to setup the various bits of the bot.", + Examples: []Example{ + {Command: `setup voice="My Vocal Channel"`, Explanation: "Setup the vocal channel of the bot"}, + {Command: `setup text="fox-radio"`, Explanation: "Setup the text channel of the bot"}, + {Command: `setup djrole="DJ"`, Explanation: "Setup the privileged DJ role"}, + }, + }, + Players: p, + log: log.With().Str("command", cmd).Logger(), + }, + Storage: storage, + } +} diff --git a/commands/stats.go b/commands/stats.go index 11ba178..41a0d2b 100644 --- a/commands/stats.go +++ b/commands/stats.go @@ -16,7 +16,13 @@ type stats struct { } func (c *stats) Handler(s *discordgo.Session, m *discordgo.Message, args []string) { - st := c.Player.Stats() + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + + st := p.Stats() if st == nil { if err := message.SendTimedReply(s, m, "", "There is no encoding session", "", 5*time.Second); err != nil { c.log.Err(err).Msg("unable to send timed response") @@ -48,7 +54,7 @@ func (c *stats) Handler(s *discordgo.Session, m *discordgo.Message, args []strin }() } -func NewStatsCommand(p *player.Player, log *zerolog.Logger) Command { +func NewStatsCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "stats" return &stats{ BaseCommand{ @@ -66,8 +72,8 @@ func NewStatsCommand(p *player.Player, log *zerolog.Logger) Command { Description: "This command will display the encoding and " + "streaming stats if a stream is ongoing.", }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } diff --git a/commands/volume.go b/commands/volume.go index b75ab8f..1513e70 100644 --- a/commands/volume.go +++ b/commands/volume.go @@ -22,8 +22,14 @@ func (c *volume) Handler(s *discordgo.Session, m *discordgo.Message, args []stri var err error var emoji = "🔉" + p := c.Players.GetPlayer(m.GuildID) + if p == nil { + c.log.Error().Msg("no player associated to guild ID") + return + } + if len(args) < 1 { - v = c.Player.State.Volume * 100 / 256 + v = p.State.Volume * 100 / 256 if v > 100 { emoji = "🔊" } else if v < 100 { @@ -57,7 +63,7 @@ func (c *volume) Handler(s *discordgo.Session, m *discordgo.Message, args []stri } // Actually set the volume - if err := c.Player.SetVolumePercent(v); err != nil { + if err := p.SetVolumePercent(v); err != nil { c.log.Err(err).Msg("unable to set volume percentage") return } @@ -74,7 +80,7 @@ func (c *volume) Handler(s *discordgo.Session, m *discordgo.Message, args []stri } } -func NewVolumeCommand(p *player.Player, log *zerolog.Logger) Command { +func NewVolumeCommand(p *player.Players, log *zerolog.Logger) Command { cmd := "volume" return &volume{ BaseCommand{ @@ -100,8 +106,8 @@ func NewVolumeCommand(p *player.Player, log *zerolog.Logger) Command { {Command: "vol 50%", Explanation: "Sets the volume to half the normal volume using the alias"}, }, }, - Player: p, - log: log.With().Str("command", cmd).Logger(), + Players: p, + log: log.With().Str("command", cmd).Logger(), }, } } diff --git a/go.mod b/go.mod index 7bdcb43..4c177e0 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,16 @@ go 1.15 require ( github.com/Depado/soundcloud v0.1.1 + github.com/asdine/storm/v3 v3.2.1 github.com/bwmarrin/discordgo v0.22.0 github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/gin-gonic/gin v1.6.3 + github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/golang/protobuf v1.4.3 // indirect github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 github.com/jonas747/dca v0.0.0-20201113050843-65838623978b github.com/jonas747/ogg v0.0.0-20161220051205-b4f6f4cf3757 // indirect + github.com/json-iterator/go v1.1.10 // indirect github.com/magiconair/properties v1.8.4 // indirect github.com/mitchellh/mapstructure v1.3.3 // indirect github.com/pelletier/go-toml v1.8.1 // indirect @@ -19,15 +23,17 @@ require ( github.com/spf13/cobra v1.1.1 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.7.1 + github.com/ugorji/go v1.2.0 // indirect + go.etcd.io/bbolt v1.3.5 // indirect go.uber.org/fx v1.13.1 go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect + golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect - golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba // indirect + golang.org/x/sys v0.0.0-20201118182958-a01c418693c7 // indirect golang.org/x/text v0.3.4 // indirect golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + google.golang.org/protobuf v1.25.0 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/go.sum b/go.sum index 94bdbac..b28f500 100644 --- a/go.sum +++ b/go.sum @@ -13,20 +13,27 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= +github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Depado/soundcloud v0.1.1 h1:ZBzupSIVX2Cu8q810jvZG8Lz31vubrfoL/CesCm3XgQ= github.com/Depado/soundcloud v0.1.1/go.mod h1:I1Woxhnr/3k0taYYkALq6iX1omE95eF9831CYZbz5Ic= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM= +github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac= +github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM= github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -40,6 +47,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= @@ -61,6 +70,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87 github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -76,10 +87,24 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -130,6 +155,8 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -187,6 +214,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -237,11 +265,21 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.0 h1:6eXlzYLLwZwXroJx9NyqbYcbv/d93twiOzQLDewE6qM= +github.com/ugorji/go v1.2.0/go.mod h1:1ny++pKMXhLWrwWV5Nf+CbOuZJhMoaFD+0GMFfd8fEc= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.0 h1:As6RccOIlbm9wHuWYMlB30dErcI+4WiKWsYsmPkyrUw= +github.com/ugorji/go/codec v1.2.0/go.mod h1:dXvG35r7zTX6QImXOSFhGMmKtX+wJ7VTWzGvYQGIjBs= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -269,8 +307,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -308,6 +346,7 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= @@ -337,11 +376,14 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba h1:xmhUJGQGbxlod18iJGqVEp9cHIPLl7QiX2aA3to708s= -golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201118182958-a01c418693c7 h1:Z991aAXPjz0tLnj74pVXW3eWJ5lHMIBvbRfMq4M2jHA= +golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -360,6 +402,7 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -378,6 +421,7 @@ golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174 h1:0rx0F4EjJNbxTuzWe0KjKcI golang.org/x/tools v0.0.0-20201105001634-bc3cf281b174/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -389,6 +433,8 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -398,9 +444,23 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -423,5 +483,6 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/guild/guild.go b/guild/guild.go new file mode 100644 index 0000000..46b03f8 --- /dev/null +++ b/guild/guild.go @@ -0,0 +1,65 @@ +package guild + +import ( + "fmt" + + "github.com/bwmarrin/discordgo" +) + +var ( + ChannelNotFoundError = fmt.Errorf("no channel found") +) + +// State represents the guild state at a given point. +type State struct { + ID string + VoiceChannel string `json:"voice"` + TextChannel string `json:"text"` + QueueHistory int `json:"history"` + PrivilegedRole string `json:"privileged_role"` +} + +// New creates a new state for a given guild ID +func NewState(id string) *State { + return &State{ + ID: id, + } +} + +// SetVoiceChannel will cycle through the available channels to check if the +// vocal channel actually exists and set its ID in the state +func (st *State) SetChannel(s *discordgo.Session, value string, voice bool) error { + var dtype discordgo.ChannelType + + if st.ID == "" { + return fmt.Errorf("guild state with empty ID") + } + + chans, err := s.GuildChannels(st.ID) + if err != nil { + return err + } + if voice { + dtype = discordgo.ChannelTypeGuildVoice + } else { + dtype = discordgo.ChannelTypeGuildText + } + + var found bool + for _, c := range chans { + if (c.Name == value || c.ID == value) && c.Type == dtype { + if voice { + st.VoiceChannel = c.ID + } else { + st.TextChannel = c.ID + } + found = true + } + } + + if !found { + return ChannelNotFoundError + } + + return nil +} diff --git a/main.go b/main.go index 417c621..c6a3381 100644 --- a/main.go +++ b/main.go @@ -13,8 +13,8 @@ import ( "github.com/Depado/fox/cmd" "github.com/Depado/fox/commands" "github.com/Depado/fox/player" - "github.com/Depado/fox/session" sp "github.com/Depado/fox/soundcloud" + "github.com/Depado/fox/storage" ) // Build number and versions injected at compile time, set yours @@ -42,9 +42,8 @@ var versionCmd = &cobra.Command{ func run() { fx.New( fx.Provide( - cmd.NewConf, cmd.NewLogger, acl.NewACL, - session.NewDiscordSession, - player.NewPlayer, + cmd.NewConf, cmd.NewLogger, acl.NewACL, player.NewPlayers, storage.NewStormStorage, + bot.NewDiscordSession, soundcloud.NewAutoIDClient, sp.NewSoundCloudProvider, commands.InitializeAllCommands, bot.NewBot, diff --git a/message/formatter.go b/message/formatter.go index d7b7390..f357188 100644 --- a/message/formatter.go +++ b/message/formatter.go @@ -5,6 +5,7 @@ import ( "time" "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog" ) func base(title, body, footer string) *discordgo.MessageEmbed { @@ -37,3 +38,15 @@ func SendTimedReply(s *discordgo.Session, m *discordgo.Message, title, body, foo }() return nil } + +func SendTimedReplyLog(s *discordgo.Session, m *discordgo.Message, title, body, footer string, t time.Duration, l zerolog.Logger) { + if err := SendTimedReply(s, m, title, body, footer, t); err != nil { + l.Err(err).Msg("unable to send timed reply") + } +} + +func SendShortTimedNotice(s *discordgo.Session, m *discordgo.Message, body string, l zerolog.Logger) { + if err := SendTimedReply(s, m, "", body, "", 5*time.Second); err != nil { + l.Err(err).Msg("unable to send timed reply") + } +} diff --git a/player/display.go b/player/display.go index 384775f..2d8655e 100644 --- a/player/display.go +++ b/player/display.go @@ -74,7 +74,7 @@ func (p *Player) SendNotice(title, body, footer string) *discordgo.Message { Color: 0xff5500, } - m, err := p.session.ChannelMessageSendEmbed(p.conf.Bot.Channels.Text, e) + m, err := p.session.ChannelMessageSendEmbed(p.GuildState.TextChannel, e) if err != nil { p.log.Err(err).Msg("unable to send embed") } diff --git a/player/player.go b/player/player.go index 6464378..be5878d 100644 --- a/player/player.go +++ b/player/player.go @@ -1,22 +1,73 @@ package player import ( - "context" "fmt" "sync" "github.com/Depado/fox/cmd" + "github.com/Depado/fox/guild" + "github.com/Depado/fox/storage" "github.com/bwmarrin/discordgo" "github.com/jonas747/dca" "github.com/rs/zerolog" - "go.uber.org/fx" ) +type Players struct { + sync.RWMutex + Players map[string]*Player +} + +// GetPlayer will get the player associated with the guild ID if any +func (p *Players) GetPlayer(guildID string) *Player { + p.Lock() + defer p.Unlock() + + pl, ok := p.Players[guildID] + if !ok { + return nil + } + return pl +} + +// Create will create a new player and associate it with the guild +func (p *Players) Create(s *discordgo.Session, conf *cmd.Conf, log *zerolog.Logger, guild string, storage *storage.StormDB, gs *guild.State) error { + p.Lock() + defer p.Unlock() + + if _, ok := p.Players[guild]; ok { + return fmt.Errorf("guild already has an associated player") + } + play, err := NewPlayer(s, conf, log.With().Str("guild", guild).Logger(), guild, storage, gs) + if err != nil { + return fmt.Errorf("create player: %w", err) + } + p.Players[guild] = play + return nil +} + +// Kill will kill all the players +func (p *Players) Kill() { + p.Lock() + defer p.Unlock() + for _, pl := range p.Players { + pl.Kill() + } +} + +func NewPlayers() *Players { + return &Players{ + Players: make(map[string]*Player), + } +} + // Player is the struct in charge of connecting to voice channels and streaming // tracks to them. type Player struct { - Queue *Queue - State *State + Queue *Queue + State *State + Guild string + GuildState *guild.State + Storage *storage.StormDB log zerolog.Logger conf *cmd.Conf @@ -28,27 +79,29 @@ type Player struct { audio sync.RWMutex } -func NewPlayer(lc fx.Lifecycle, s *discordgo.Session, conf *cmd.Conf, log *zerolog.Logger) *Player { +// NewPlayer will create a new player from scratch using the provided +// arguments +func NewPlayer(s *discordgo.Session, conf *cmd.Conf, log zerolog.Logger, guildID string, storage *storage.StormDB, gs *guild.State) (*Player, error) { st := NewState() p := &Player{ - State: st, - Queue: NewQueue(st), - session: s, - log: log.With().Str("component", "player").Logger(), - conf: conf, - stop: make(chan bool), + State: st, + Queue: NewQueue(st), + Storage: storage, + Guild: guildID, + GuildState: gs, + session: s, + log: log.With().Str("component", "player").Logger(), + conf: conf, + stop: make(chan bool), } - lc.Append(fx.Hook{ - OnStop: func(ctx context.Context) error { - p.log.Debug().Msg("on stop called, cleaning up") - p.Stop() - p.Disconnect() // nolint:errcheck - return nil - }, - }) + return p, nil +} - return p +func (p *Player) Kill() { + p.log.Debug().Msg("kill called") + p.Stop() + p.Disconnect() // nolint:errcheck } // Disconnect will disconnect the player from the currently connected voice @@ -68,7 +121,7 @@ func (p *Player) Disconnect() error { // Connect will connect the player to the voice channel. func (p *Player) Connect() error { if p.session != nil { - voice, err := p.session.ChannelVoiceJoin(p.conf.Bot.Guild, p.conf.Bot.Channels.Voice, false, true) + voice, err := p.session.ChannelVoiceJoin(p.GuildState.ID, p.GuildState.VoiceChannel, false, true) if err != nil { return fmt.Errorf("unable to establish connection to vocal channel: %w", err) } diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000..1b1fcf0 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,53 @@ +package storage + +import ( + "context" + "fmt" + + "github.com/Depado/fox/cmd" + "github.com/Depado/fox/guild" + "github.com/asdine/storm/v3" + "github.com/rs/zerolog" + "go.uber.org/fx" +) + +type StormDB struct { + db *storm.DB + log zerolog.Logger +} + +func NewStormStorage(lc fx.Lifecycle, c *cmd.Conf, l *zerolog.Logger) (*StormDB, error) { + log := l.With().Str("component", "storage").Logger() + log.Debug().Str("path", c.Database.Path).Msg("opening database") + + db, err := storm.Open(c.Database.Path) + if err != nil { + return nil, fmt.Errorf("open database: %w", err) + } + + if err := db.Init(&guild.State{}); err != nil { + return nil, fmt.Errorf("init guild state model: %w", err) + } + + lc.Append(fx.Hook{ + OnStop: func(c context.Context) error { + return db.Close() + }, + }) + + return &StormDB{db, log}, nil +} + +func (s *StormDB) GetGuildState(guildID string) (*guild.State, error) { + gstate := &guild.State{} + return gstate, s.db.One("ID", guildID, gstate) +} + +func (s *StormDB) SaveGuildState(gs *guild.State) error { + return s.db.Save(gs) +} + +func (s *StormDB) NewGuildState(guildID string) (*guild.State, error) { + gs := guild.NewState(guildID) + return gs, s.db.Save(gs) +}