Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VoiceStatusUpdate - Unable to determine what channel a user left from #823

Open
nickmcski opened this issue Oct 17, 2020 · 9 comments
Open

Comments

@nickmcski
Copy link
Contributor

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.

@colecrouter
Copy link

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
Copy link

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

@CarsonHoffman
Copy link
Collaborator

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.

@colecrouter
Copy link

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.

@N0realm
Copy link

N0realm commented Nov 5, 2020

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.

discordgo/state.go

Lines 894 to 904 in 92c52f3

case *MessageUpdate:
if s.MaxMessageCount != 0 {
var old *Message
old, err = s.Message(t.ChannelID, t.ID)
if err == nil {
oldCopy := *old
t.BeforeUpdate = &oldCopy
}
err = s.MessageAdd(t.Message)
}

@colecrouter
Copy link

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.

@nickmcski
Copy link
Contributor Author

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

nickmcski added a commit to nickmcski/discordgo that referenced this issue Nov 5, 2020
CarsonHoffman added a commit that referenced this issue Nov 20, 2020
Add previous state to VoiceStateUpdate event (#823)
@colecrouter
Copy link

colecrouter commented Dec 8, 2020

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
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants