diff --git a/src/discord/discord.cpp b/src/discord/discord.cpp index 2ee8493..d99e57a 100644 --- a/src/discord/discord.cpp +++ b/src/discord/discord.cpp @@ -470,6 +470,10 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla return actor_highest->Position > target_highest->Position; } +bool DiscordClient::IsStageModerator(Snowflake user_id, Snowflake channel_id) const { + return HasChannelPermission(user_id, channel_id, Permission::MANAGE_CHANNELS | Permission::MOVE_MEMBERS | Permission::MUTE_MEMBERS); +} + void DiscordClient::ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot &callback) { if (!CheckCode(response)) { if (response.status_code == http::TooManyRequests) { @@ -1306,6 +1310,11 @@ bool DiscordClient::HasUserRequestedToSpeak(Snowflake user_id) const { return state.has_value() && state->second.RequestToSpeakTimestamp.has_value() && util::FlagSet(state->second.Flags, VoiceStateFlags::Suppressed); } +bool DiscordClient::IsUserInvitedToSpeak(Snowflake user_id) const { + const auto state = GetVoiceState(user_id); + return state.has_value() && state->second.RequestToSpeakTimestamp.has_value() && !util::FlagSet(state->second.Flags, VoiceStateFlags::Suppressed); +} + void DiscordClient::RequestToSpeak(Snowflake channel_id, bool want, const sigc::slot &callback) { if (want && !HasSelfChannelPermission(channel_id, Permission::REQUEST_TO_SPEAK)) return; const auto channel = GetChannel(channel_id); @@ -1327,6 +1336,25 @@ void DiscordClient::RequestToSpeak(Snowflake channel_id, bool want, const sigc:: }); } +void DiscordClient::SetStageSpeaking(Snowflake channel_id, bool want, const sigc::slot &callback) { + const auto channel = GetChannel(channel_id); + if (!channel.has_value() || !channel->GuildID.has_value()) return; + + ModifyCurrentUserVoiceStateObject d; + d.ChannelID = channel_id; + d.Suppress = !want; + if (want) { + d.RequestToSpeakTimestamp = ""; + } + m_http.MakePATCH("/guilds/" + std::to_string(*channel->GuildID) + "/voice-states/@me", nlohmann::json(d).dump(), [callback](const http::response_type &response) { + if (CheckCode(response, 204)) { + callback(DiscordError::NONE); + } else { + callback(GetCodeFromResponse(response)); + } + }); +} + DiscordVoiceClient &DiscordClient::GetVoiceClient() { return m_voice; } diff --git a/src/discord/discord.hpp b/src/discord/discord.hpp index 55bd308..42fa1cb 100644 --- a/src/discord/discord.hpp +++ b/src/discord/discord.hpp @@ -87,6 +87,7 @@ public: Permission ComputePermissions(Snowflake member_id, Snowflake guild_id) const; Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const; bool CanManageMember(Snowflake guild_id, Snowflake actor, Snowflake target) const; // kick, ban, edit nickname (cant think of a better name) + bool IsStageModerator(Snowflake user_id, Snowflake channel_id) const; void ChatMessageCallback(const std::string &nonce, const http::response_type &response, const sigc::slot &callback); void SendChatMessageNoAttachments(const ChatSubmitParams ¶ms, const sigc::slot &callback); @@ -204,8 +205,10 @@ public: [[nodiscard]] std::optional GetSSRCOfUser(Snowflake id) const; [[nodiscard]] bool IsUserSpeaker(Snowflake user_id) const; [[nodiscard]] bool HasUserRequestedToSpeak(Snowflake user_id) const; + [[nodiscard]] bool IsUserInvitedToSpeak(Snowflake user_id) const; void RequestToSpeak(Snowflake channel_id, bool want, const sigc::slot &callback); + void SetStageSpeaking(Snowflake channel_id, bool want, const sigc::slot &callback); DiscordVoiceClient &GetVoiceClient(); diff --git a/src/windows/voice/voicewindow.cpp b/src/windows/voice/voicewindow.cpp index bfea175..27dd734 100644 --- a/src/windows/voice/voicewindow.cpp +++ b/src/windows/voice/voicewindow.cpp @@ -21,7 +21,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) , m_deafen("Deafen") , m_noise_suppression("Suppress Noise") , m_mix_mono("Mix Mono") - , m_request_to_speak("Request to Speak") + , m_stage_command("Request to Speak") , m_disconnect("Disconnect") , m_channel_id(channel_id) , m_menu_view("View") @@ -213,10 +213,21 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) }, *this)); - m_request_to_speak.signal_clicked().connect([this]() { + m_stage_command.signal_clicked().connect([this]() { auto &discord = Abaddon::Get().GetDiscordClient(); - const bool requested = discord.HasUserRequestedToSpeak(discord.GetUserData().ID); - Abaddon::Get().GetDiscordClient().RequestToSpeak(m_channel_id, !requested, NOOP_CALLBACK); + const auto user_id = discord.GetUserData().ID; + const bool is_moderator = discord.IsStageModerator(user_id, m_channel_id); + const bool is_speaker = discord.IsUserSpeaker(user_id); + const bool is_invited_to_speak = discord.IsUserInvitedToSpeak(user_id); + + if (is_speaker) { + discord.SetStageSpeaking(m_channel_id, false, NOOP_CALLBACK); + } else if (is_moderator || is_invited_to_speak) { + discord.SetStageSpeaking(m_channel_id, true, NOOP_CALLBACK); + } else { + const bool requested = discord.HasUserRequestedToSpeak(user_id); + discord.RequestToSpeak(m_channel_id, !requested, NOOP_CALLBACK); + } }); m_TMP_speakers_label.set_markup("Speakers"); @@ -231,7 +242,7 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) m_controls.add(m_noise_suppression); m_controls.add(m_mix_mono); m_buttons.set_halign(Gtk::ALIGN_CENTER); - m_buttons.pack_start(m_request_to_speak, false, true); + m_buttons.pack_start(m_stage_command, false, true); m_buttons.pack_start(m_disconnect, false, true); m_main.pack_start(m_menu_bar, false, true); m_main.pack_start(m_TMP_stagelabel, false, true); @@ -246,6 +257,8 @@ VoiceWindow::VoiceWindow(Snowflake channel_id) show_all_children(); Glib::signal_timeout().connect(sigc::mem_fun(*this, &VoiceWindow::UpdateVoiceMeters), 40); + + UpdateStageCommand(); } void VoiceWindow::SetUsers(const std::unordered_set &user_ids) { @@ -335,6 +348,26 @@ void VoiceWindow::UpdateVADParamValue() { } } +void VoiceWindow::UpdateStageCommand() { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto user_id = discord.GetUserData().ID; + + m_has_requested_to_speak = discord.HasUserRequestedToSpeak(user_id); + const bool is_moderator = discord.IsStageModerator(user_id, m_channel_id); + const bool is_speaker = discord.IsUserSpeaker(user_id); + const bool is_invited_to_speak = discord.IsUserInvitedToSpeak(user_id); + + if (is_speaker) { + m_stage_command.set_label("Leave the Stage"); + } else if (is_moderator || is_invited_to_speak) { + m_stage_command.set_label("Speak on Stage"); + } else if (m_has_requested_to_speak) { + m_stage_command.set_label("Cancel Request"); + } else { + m_stage_command.set_label("Request to Speak"); + } +} + void VoiceWindow::OnUserConnect(Snowflake user_id, Snowflake to_channel_id) { if (m_channel_id == to_channel_id) { if (auto it = m_rows.find(user_id); it == m_rows.end()) { @@ -363,8 +396,9 @@ void VoiceWindow::OnSpeakerStateChanged(Snowflake channel_id, Snowflake user_id, void VoiceWindow::OnVoiceStateUpdate(Snowflake user_id, Snowflake channel_id, VoiceStateFlags flags) { auto &discord = Abaddon::Get().GetDiscordClient(); - m_has_requested_to_speak = discord.HasUserRequestedToSpeak(discord.GetUserData().ID); - m_request_to_speak.set_label(m_has_requested_to_speak ? "Cancel Request" : "Request to Speak"); + if (user_id != discord.GetUserData().ID) return; + + UpdateStageCommand(); } VoiceWindow::type_signal_mute VoiceWindow::signal_mute() { diff --git a/src/windows/voice/voicewindow.hpp b/src/windows/voice/voicewindow.hpp index 7803f85..5005b90 100644 --- a/src/windows/voice/voicewindow.hpp +++ b/src/windows/voice/voicewindow.hpp @@ -36,10 +36,9 @@ private: void OnDeafenChanged(); void TryDeleteRow(Snowflake id); - bool UpdateVoiceMeters(); - void UpdateVADParamValue(); + void UpdateStageCommand(); Gtk::Box m_main; Gtk::Box m_controls; @@ -65,7 +64,7 @@ private: Gtk::HBox m_buttons; Gtk::Button m_disconnect; - Gtk::Button m_request_to_speak; + Gtk::Button m_stage_command; bool m_has_requested_to_speak = false;