diff --git a/cleanup.go b/cleanup.go index 822b0b3e7ff007ee03192a7cf96aa15cfb4a4956..82671654352cf03ebf59166afa67a2b7caa7ea1d 100644 --- a/cleanup.go +++ b/cleanup.go @@ -6,8 +6,19 @@ import ( ) func cleanup() { + // Close all chatInstance + instances := make(map[string]*chatInstance) + for key, value := range instancesChannel { + instances[key] = value + } + + for _, instance := range instances { + instance.destroy() + } + + // Close session if err := session.Close(); err != nil { - log.Fatalf("Error while closing session, %s", err) + log.Fatalf("Error closing session, %s", err) os.Exit(1) } } diff --git a/config.go b/config.go index 40ca7c18048a9c785ae1461bf2d1c98adbb59d37..7494bba2428884e8e00cadb12b6a0b340a8c420b 100644 --- a/config.go +++ b/config.go @@ -10,15 +10,17 @@ import ( var config configPayload var configPath string var defaultConfig = configPayload{ + Prefix: "!", Token: "TOKEN", + Timeout: 120, ChannelID: []string{}, - Prefix: "!", } type configPayload struct { + Prefix string Token string + Timeout int ChannelID []string - Prefix string } func init() { @@ -45,9 +47,32 @@ func parse() { } else { log.Infof("Loaded config at %s.", configPath) } +} +func configLate() { allowedChannels = make(map[string]bool) + categoryMap = make(map[string]string) + guildMap = make(map[string]string) for _, id := range config.ChannelID { + if channel, err := session.Channel(id); err != nil { + log.Errorf("Error getting channel %s, %s", id, err) + continue + } else { + if channel.ParentID == "" { + log.Warnf("Channel %s has no parent.", id) + continue + } else { + if guildMap[channel.GuildID] != "" { + log.Warnf("Multiple channels specified for guild %s.", channel.GuildID) + continue + } else { + log.Infof("Channel %s from guild %s with parent %s added.", + channel.ID, channel.GuildID, channel.ParentID) + guildMap[channel.GuildID] = channel.ID + categoryMap[channel.ID] = channel.ParentID + } + } + } allowedChannels[id] = true } } diff --git a/go.mod b/go.mod index 3c02d0d5c7465bb7aa0475c264fbbbef8e43eb08..087f0bc0b8e5271c89ea3f40d8ffd910b369a73a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.16 require ( git.randomchars.net/freenitori/embedutil v1.0.2 git.randomchars.net/freenitori/log v1.0.0 - git.randomchars.net/freenitori/multiplexer v1.0.12 + git.randomchars.net/freenitori/multiplexer v1.0.13 github.com/BurntSushi/toml v0.3.1 github.com/bwmarrin/discordgo v0.23.2 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index ede86a0e4a85a8526464e2d0f1f46174e7eeb9e2..98bc7aee205a6956a8b4cf44f05299e5bc3ff2b9 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ git.randomchars.net/freenitori/log v1.0.0 h1:hU99jGk940I1O5OcaTfnXOpN8ozXiarxhu6 git.randomchars.net/freenitori/log v1.0.0/go.mod h1:YZFRZgVWDIrbyDGHyDeRlIRWeq0DXamXONxIt12eq2Q= git.randomchars.net/freenitori/multiplexer v1.0.12 h1:XsMMSeeaeBtavlsMl7M6ZrW00wa2+zsx/w5gYWR5Qh0= git.randomchars.net/freenitori/multiplexer v1.0.12/go.mod h1:Bx9vu2RXDtBrsKBslrhrc8v3IJl5Dna7I/rsHF586w0= +git.randomchars.net/freenitori/multiplexer v1.0.13 h1:5alUJBGLSrZB2/PE79WTirOCQMCtmnurcUSCOYBr10k= +git.randomchars.net/freenitori/multiplexer v1.0.13/go.mod h1:Bx9vu2RXDtBrsKBslrhrc8v3IJl5Dna7I/rsHF586w0= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4= diff --git a/handler.go b/handler.go index 0efca9b38e8254df83e79a66965f0fec9020a7e1..11e2d22cd56b22089631e41b5f1b82b0e616e56e 100644 --- a/handler.go +++ b/handler.go @@ -7,28 +7,39 @@ import ( ) func init() { - m.VoiceStateUpdate = append(m.VoiceStateUpdate, handleVoiceUpdate) + m.VoiceStateUpdate = append(m.VoiceStateUpdate, handleChatInitiate) + m.VoiceStateUpdate = append(m.VoiceStateUpdate, handleInstanceChannelActivity) } var allowedChannels map[string]bool -func handleVoiceUpdate(context *multiplexer.Context) { - - if context.Channel == nil { - return - } - - if !allowedChannels[context.Channel.ID] { +func handleChatInitiate(context *multiplexer.Context) { + // Lookup channel in allowed map + if context.Channel == nil || !allowedChannels[context.Channel.ID] { return } + // Get event var event *discordgo.VoiceStateUpdate if e, ok := context.Event.(*discordgo.VoiceStateUpdate); !ok { return } else { event = e } + if event.VoiceState != nil { + if event.VoiceState.ChannelID == "" { + return + } + } else { + return + } + // Lookup already existing channel + if instancesUser[event.UserID] != nil { + return + } + + // Get member from cache and add to cache if not exist var member *discordgo.Member if u, err := context.Session.State.Member(event.GuildID, event.UserID); err != nil { if u, err = context.Session.GuildMember(event.GuildID, event.UserID); err != nil { @@ -41,16 +52,72 @@ func handleVoiceUpdate(context *multiplexer.Context) { } else { member = u } - if member == nil { return } - log.Infof("%s#%s (%s) has joined the designated channel #%s (%s).", + // Create new instance + instance := newInstance(member) + if instance == nil { + return + } + + log.Infof("%s#%s (%s) has created a new volatile channel #%s (%s) in guild %s (%s).", member.User.Username, member.User.Discriminator, member.User.ID, - context.Channel.Name, - context.Channel.ID, + instance.Channel.Name, + instance.Channel.ID, + context.Guild.Name, + context.Guild.ID, ) + + // Move user to newly created volatile channel + if err := session.GuildMemberMove(event.GuildID, event.UserID, &instance.Channel.ID); err != nil { + log.Errorf("Error moving user %s#%s (%s) to newly created volatile channel #%s (%s), %s", + member.User.Username, + member.User.Discriminator, + member.User.ID, + instance.Channel.Name, + instance.Channel.ID, + err) + } +} + +func handleInstanceChannelActivity(context *multiplexer.Context) { + // Get event + var event *discordgo.VoiceStateUpdate + if e, ok := context.Event.(*discordgo.VoiceStateUpdate); !ok { + return + } else { + event = e + } + + // Lookup channel in pre-event state + if event.BeforeUpdate == nil || instancesChannel[event.BeforeUpdate.ChannelID] == nil { + // Add member if join + instance := instancesChannel[event.ChannelID] + if instance != nil { + instance.Members[event.UserID] = true + } + return + } + + // If both before update and current state are same and registered, do not handle + if event.BeforeUpdate.ChannelID == event.ChannelID && instancesChannel[event.ChannelID] != nil { + return + } + + // Remove user from previous channel and destroy if zero length + instanceOld := instancesChannel[event.BeforeUpdate.ChannelID] + delete(instanceOld.Members, event.UserID) + if len(instanceOld.Members) == 0 { + instanceOld.destroy() + } + + // Record user in new channel if registered + instance := instancesChannel[event.ChannelID] + if instance != nil { + instance.Members[event.UserID] = true + } } diff --git a/instance.go b/instance.go new file mode 100644 index 0000000000000000000000000000000000000000..26485f0387ceee85db739a51fc9f0d31fe56e2b4 --- /dev/null +++ b/instance.go @@ -0,0 +1,87 @@ +package main + +import ( + "git.randomchars.net/freenitori/log" + "github.com/bwmarrin/discordgo" + "time" +) + +var ( + categoryMap map[string]string + guildMap map[string]string + timeout time.Duration +) + +type chatInstance struct { + Initiator *discordgo.Member + Channel *discordgo.Channel + PreviousAction time.Time + Members map[string]bool +} + +var ( + instancesChannel = make(map[string]*chatInstance) + instancesUser = make(map[string]*chatInstance) +) + +// destroy destroys the chatInstance and removes the corresponding Discord voice channel. +func (c *chatInstance) destroy() { + delete(instancesChannel, c.Channel.ID) + delete(instancesUser, c.Initiator.User.ID) + if _, err := session.ChannelDelete(c.Channel.ID); err != nil { + log.Errorf("Error destroying channel %s initiated by user %s in guild %s, %s", + c.Channel.ID, c.Initiator.User.ID, c.Initiator.GuildID, err) + } + log.Infof("Chat instance with channel %s initiated by user %s#%s (%s) destroyed.", + c.Channel.ID, c.Initiator.User.Username, c.Initiator.User.Discriminator, c.Initiator.User.ID) +} + +// actionFree checks if actions are free in chatInstance. +func (c *chatInstance) actionFree() bool { + return c.PreviousAction.Add(timeout).Before(time.Now()) +} + +// newInstance creates a new chatInstance on behalf of a discordgo.Member. +func newInstance(member *discordgo.Member) *chatInstance { + instance := setupInstance(member) + if instance != nil { + instancesChannel[instance.Channel.ID] = instance + instancesUser[instance.Initiator.User.ID] = instance + return instance + } + return nil +} + +// setupInstance sets up a new chatInstance on behalf of a discordgo.Member. +func setupInstance(member *discordgo.Member) *chatInstance { + channelID := guildMap[member.GuildID] + if channelID == "" { + return nil + } + + parentID := categoryMap[channelID] + if parentID == "" { + return nil + } + + channel, err := session.GuildChannelCreateComplex(member.GuildID, discordgo.GuildChannelCreateData{ + Name: member.User.Username + "#" + member.User.Discriminator, + Type: discordgo.ChannelTypeGuildVoice, + Topic: "Volatile channel created by " + member.User.ID, + Bitrate: 64000, + UserLimit: 10, + ParentID: parentID, + }) + if err != nil { + log.Errorf("Error creating channel on guild %s on behalf of member %s, %s", + member.GuildID, member.User.ID, err) + return nil + } + + return &chatInstance{ + Initiator: member, + Channel: channel, + PreviousAction: time.Unix(0, 0), + Members: make(map[string]bool), + } +} diff --git a/main.go b/main.go index abf5657226ced354a92de82e643c25b34e559692..d3b19c0f322ceff969f55ed78c4630136c159858 100644 --- a/main.go +++ b/main.go @@ -51,13 +51,26 @@ func main() { session.UserAgent = "DiscordBot (voice-bot)" session.Token = "Bot " + config.Token session.ShouldReconnectOnError = true - session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged + session.Identify.Intents = discordgo.IntentsAll + session.State.TrackVoice = true // Open session - if err := session.Open(); err != nil { - log.Fatalf("Error while opening session, %s", err) - os.Exit(1) - } + func() { + open: + if err := session.Open(); err != nil { + if session.Identify.Intents == discordgo.IntentsAll { + log.Warnf("Wasn't able to start with full intents, some stuff might not work (%s)", err) + session.Identify.Intents = discordgo.IntentsAllWithoutPrivileged + goto open + } + + log.Fatalf("Error while opening session, %s", err) + os.Exit(1) + } + }() + + // Execute late config options + configLate() // Setup multiplexer m.SessionRegisterHandlers(session)