discordgo icon indicating copy to clipboard operation
discordgo copied to clipboard

VoiceStatusUpdate - Unable to determine what channel a user left from

Open nickmcski opened this issue 3 years ago • 9 comments

I'm trying to run an action when a user disconnects from a specific voice channel. The discord API shows the disconnect by firing a Voice State Update with a channel ID of null, as such state tracking is required to determine which server the user left from.

I've tried using the guild.voiceStates to determine what channel the user was in previous, but this information is already cleared out before the VoiceStateUpdate handler is called

func voiceStateUpdate(s *discordgo.Session, m *discordgo.VoiceStateUpdate) {

	if m.ChannelID == "" { //User disconnected from a voice channel
		guild, _ := s.State.Guild(m.GuildID)

		for _, key := range guild.VoiceStates {
			if key.UserID == m.UserID {
				//This code is never reached as the user was already removed from the VoiceStates array
				println(key.UserID, " left channel ", key.ChannelID)
			}
		}
	}
}

I would like feedback on this potential solution before I submit a pull request:

Trigger the new event VoiceStateDisconnect from voiceStateUpdate whenever an entry is removed.

I'm still fairly new to Go so any feedback or alternative solutions are greatly appreciated.

nickmcski avatar Oct 17 '20 07:10 nickmcski

I think this would be incredibly useful. Here's a thought: Discord.js's "voiceStateUpdate" event sends two states, and "old" and a "new" one. I think that gets the best of both worlds: being able to track complex states, while also keeping it simple.

Here's a code example: Old:

type VoiceStateUpdate struct {
    *VoiceState
}

New:

type VoiceStateUpdate struct {
    OldState *VoiceState
    NewState *VoiceState
}

Technically this could be handled by the user, but functionality is looking poor. My first thought would be to just go through the guild object and read the voice states, but:

// This field is only present in GUILD_CREATE events and websocket    <---- here
// update events, and thus is only present in state-cached guilds.
VoiceStates []*VoiceState `json:"voice_states"`

From Discord's docs, it sounds like GUILD_CREATE fires on startup anyway, so you might be able to get away with tracking each individual user's voiceState on startup, then managing it internally from there. If not, you would have to deal with losing the current voiceState on restart.

If you're trying to distinguish between joining from nowhere, vs from another channel, you could keep an internal list of connected members (as mentioned above). When you receive a voiceStateUpdate from that user add them to the list. If you receive a voiceStateUpdate "disconnect", set a timer to check again, then delete them from the list. If they disconnect for more than X time, they won't be on the list. If they are on the list, then you know they probably came from another channel, or only briefly disconnected.

import "time"

...
time.AfterFunc(3*time.Second, somefunction)

colecrouter avatar Oct 28 '20 20:10 colecrouter

To update, I've been looking deeper into this, and I've found some pretty scary stuff. So, the "Ready" event is (was) supposed to contain guild info, but only contains the guild ID and nothing else. (as per #161). However I did not know this, and blew a couple hours on it initially.

Here's what really has me scratching my head: GUILD_CREATE is empty too! I even tried doing (Session*) Guild(id string), and that got me the NAME of the servers, but nothing else. Ok so lazy loading is broken? Doesn't appear to be, because I put it on timer, then cast for the Guild object again, and it still didn't work.

Then, I found #278, which says you have to try using GUILD_READY. However, while GuildReady is a valid handler, it never fires (neither does GuildCreate). What's gives?

I case anyone wanted to check, here's my code:

func ready(s *discordgo.Session, e *discordgo.Ready) {
    // Get all current voiceStates
    for _, g := range e.Guilds {
    gTemp, _ := s.Guild(g.ID)
        for _, v := range gTemp.VoiceStates {
	fmt.Println(v.ChannelID)
	    voiceStateList = append(voiceStateList, v)
	}
    }
}
func guildCreate(s *discordgo.Session, e *discordgo.GuildCreate) {
    for _, v := range e.VoiceStates {
        fmt.Println(v.UserID)
        voiceStateList = append(voiceStateList, v)
    }
}

And obv I swapped out guildCreate for guildReady

colecrouter avatar Oct 29 '20 01:10 colecrouter

Fetching a guild from the API is never going to work for getting a guild's voice states; see:

// A list of voice states for the guild.
// This field is only present in GUILD_CREATE events and websocket
// update events, and thus is only present in state-cached guilds.
VoiceStates []*VoiceState `json:"voice_states"`

Are you observing that the voice states are empty on guild creates, or that the guild create event never fires? You seemed to allude to both. What intents are you requesting, and which class of behavior are you observing?

In any case, feel free to drop by our support channel (also linked in the README); I'm around at most times and would be happy to try to dig into what's going on.

CarsonHoffman avatar Oct 29 '20 01:10 CarsonHoffman

Ah yea I contradicted myself there. guildCreate and guildReady both do not fire (as far as my best attempts go). Ready is empty, as well as getting a guild from the Session.

colecrouter avatar Oct 29 '20 03:10 colecrouter

I think a better solution would be to either include the old state, like Discord.js does, or do it like it is done for the MESSAGE_UPDATE event, where a copy of the previous message is assigned to a BeforeUpdate field. https://github.com/bwmarrin/discordgo/blob/92c52f3db1f8acaabacbbd1f0b60d7614af17e07/state.go#L894-L904

N0realm avatar Nov 05 '20 12:11 N0realm

Agreed. Storing an internal table of users voice states does work, but it's so impractical. A solution like this would simplify 99% of use cases. The other 1% can still make a state list.

colecrouter avatar Nov 05 '20 18:11 colecrouter

Thank you for all the feedback, I'll submit an updated pull request with previous state added to the VoiceStateUpdate event.

nickmcski avatar Nov 05 '20 18:11 nickmcski

I've been using this for a while, and it's a very welcome solution. However, there's a bit of a problem. If a user is in a voice channel when the bot starts up, and the user starts sharing their screen, the event data is indistinguishable from if the user had just joined (as far as I can tell). The obvious workaround would be to check if the user is sharing their screen or not, but I can't find anything in the docs that would do that. Or maybe I'm just crazy, and this isn't happening to anyone else. I'm not 100% sure, but I don't think muting or deafening affects it the same way. Any suggestions are appreciated.

EDIT: I should clarify about the streaming part. You can check see if a user is streaming via checking their Activity, but that's a in a separate event, defeating the purpose (for me at least).

colecrouter avatar Dec 08 '20 02:12 colecrouter

Ok I figured it out my issue. Excerpt from Discord docs:

On October 7, 2020 the events under the GUILD_PRESENCES and GUILD_MEMBERS intents will be turned off by default on all gateway versions. If you are using Gateway v6, you will receive those events if you have enabled the flags for those intents in the Developer Portal and have been verified if your bot is in 100 or more guilds. You do not need to use Intents on Gateway v6 to receive these events; you just need to enable the flags.

This ultimately was my issue. Specifying IntentsGuildMembers and IntentsGuildPresences wasn't enough for me, I had to use this: discordgo.MakeIntent(discordgo.IntentsGuilds | discordgo.IntentsGuildVoiceStates | discordgo.IntentsGuildMembers | discordgo.IntentsGuildPresences) and that made GUILD_CREATE fire properly. Alongside the BeforeState property, this works great perfectly.

The excerpt from Discord should probably be added to the discordgo docs as well to prevent confusion in the future.

colecrouter avatar Dec 15 '20 21:12 colecrouter