forked from OpenGamers/abaddon
Merge branch 'slowmode'
This commit is contained in:
commit
cd8cd97c9b
@ -44,8 +44,9 @@ bool ChatInput::ProcessKeyPress(GdkEventKey *event) {
|
||||
auto text = buf->get_text();
|
||||
// sometimes a message thats just newlines can sneak in if you hold down enter
|
||||
if (text.size() > 0 && !std::all_of(text.begin(), text.end(), [](gunichar c) -> bool { return c == gunichar('\n'); })) {
|
||||
buf->set_text("");
|
||||
m_signal_submit.emit(text);
|
||||
const bool accepted = m_signal_submit.emit(text);
|
||||
if (accepted)
|
||||
buf->set_text("");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ private:
|
||||
Gtk::TextView m_textview;
|
||||
|
||||
public:
|
||||
typedef sigc::signal<void, Glib::ustring> type_signal_submit;
|
||||
typedef sigc::signal<bool, Glib::ustring> type_signal_submit;
|
||||
typedef sigc::signal<void> type_signal_escape;
|
||||
|
||||
type_signal_submit signal_submit();
|
||||
|
@ -2,6 +2,7 @@
|
||||
#include "chatmessage.hpp"
|
||||
#include "../abaddon.hpp"
|
||||
#include "chatinputindicator.hpp"
|
||||
#include "ratelimitindicator.hpp"
|
||||
#include "chatinput.hpp"
|
||||
|
||||
constexpr static uint64_t SnowflakeSplitDifference = 600;
|
||||
@ -14,7 +15,16 @@ ChatWindow::ChatWindow() {
|
||||
m_scroll = Gtk::manage(new Gtk::ScrolledWindow);
|
||||
m_input = Gtk::manage(new ChatInput);
|
||||
m_input_indicator = Gtk::manage(new ChatInputIndicator);
|
||||
m_rate_limit_indicator = Gtk::manage(new RateLimitIndicator);
|
||||
m_meta = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL));
|
||||
|
||||
m_rate_limit_indicator->set_margin_end(5);
|
||||
m_rate_limit_indicator->set_hexpand(true);
|
||||
m_rate_limit_indicator->set_halign(Gtk::ALIGN_END);
|
||||
m_rate_limit_indicator->set_valign(Gtk::ALIGN_END);
|
||||
m_rate_limit_indicator->show();
|
||||
|
||||
m_input_indicator->set_halign(Gtk::ALIGN_START);
|
||||
m_input_indicator->set_valign(Gtk::ALIGN_END);
|
||||
m_input_indicator->show();
|
||||
|
||||
@ -88,11 +98,17 @@ ChatWindow::ChatWindow() {
|
||||
|
||||
m_completer.show();
|
||||
|
||||
m_meta->set_hexpand(true);
|
||||
m_meta->set_halign(Gtk::ALIGN_FILL);
|
||||
m_meta->show();
|
||||
|
||||
m_meta->add(*m_input_indicator);
|
||||
m_meta->add(*m_rate_limit_indicator);
|
||||
m_scroll->add(*m_list);
|
||||
m_main->add(*m_scroll);
|
||||
m_main->add(m_completer);
|
||||
m_main->add(*m_input);
|
||||
m_main->add(*m_input_indicator);
|
||||
m_main->add(*m_meta);
|
||||
m_main->show();
|
||||
}
|
||||
|
||||
@ -125,6 +141,7 @@ void ChatWindow::SetMessages(const std::set<Snowflake> &msgs) {
|
||||
void ChatWindow::SetActiveChannel(Snowflake id) {
|
||||
m_active_channel = id;
|
||||
m_input_indicator->SetActiveChannel(id);
|
||||
m_rate_limit_indicator->SetActiveChannel(id);
|
||||
if (m_is_replying)
|
||||
StopReplying();
|
||||
}
|
||||
@ -179,11 +196,16 @@ Snowflake ChatWindow::GetActiveChannel() const {
|
||||
return m_active_channel;
|
||||
}
|
||||
|
||||
void ChatWindow::OnInputSubmit(const Glib::ustring &text) {
|
||||
bool ChatWindow::OnInputSubmit(const Glib::ustring &text) {
|
||||
if (!m_rate_limit_indicator->CanSpeak())
|
||||
return false;
|
||||
|
||||
if (m_active_channel.IsValid())
|
||||
m_signal_action_chat_submit.emit(text, m_active_channel, m_replying_to); // m_replying_to is checked for invalid in the handler
|
||||
if (m_is_replying)
|
||||
StopReplying();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ChatWindow::OnKeyPressEvent(GdkEventKey *e) {
|
||||
@ -343,7 +365,7 @@ void ChatWindow::ScrollToBottom() {
|
||||
x->set_value(x->get_upper());
|
||||
}
|
||||
|
||||
void ChatWindow::OnMessageSendFail(const std::string &nonce) {
|
||||
void ChatWindow::OnMessageSendFail(const std::string &nonce, float retry_after) {
|
||||
for (auto [id, widget] : m_id_to_widget) {
|
||||
if (auto *container = dynamic_cast<ChatMessageItemContainer *>(widget); container->Nonce == nonce) {
|
||||
container->SetFailed();
|
||||
|
@ -9,6 +9,7 @@ class ChatMessageHeader;
|
||||
class ChatMessageItemContainer;
|
||||
class ChatInput;
|
||||
class ChatInputIndicator;
|
||||
class RateLimitIndicator;
|
||||
class ChatWindow {
|
||||
public:
|
||||
ChatWindow();
|
||||
@ -43,7 +44,7 @@ protected:
|
||||
|
||||
Snowflake m_active_channel;
|
||||
|
||||
void OnInputSubmit(const Glib::ustring &text);
|
||||
bool OnInputSubmit(const Glib::ustring &text);
|
||||
|
||||
bool OnKeyPressEvent(GdkEventKey *e);
|
||||
void OnScrollEdgeOvershot(Gtk::PositionType pos);
|
||||
@ -53,7 +54,7 @@ protected:
|
||||
void ScrollToBottom();
|
||||
bool m_should_scroll_to_bottom = true;
|
||||
|
||||
void OnMessageSendFail(const std::string &nonce);
|
||||
void OnMessageSendFail(const std::string &nonce, float retry_after);
|
||||
|
||||
Gtk::Box *m_main;
|
||||
Gtk::ListBox *m_list;
|
||||
@ -63,6 +64,8 @@ protected:
|
||||
|
||||
Completer m_completer;
|
||||
ChatInputIndicator *m_input_indicator;
|
||||
RateLimitIndicator *m_rate_limit_indicator;
|
||||
Gtk::Box *m_meta;
|
||||
|
||||
public:
|
||||
typedef sigc::signal<void, Snowflake, Snowflake> type_signal_action_message_delete;
|
||||
|
132
components/ratelimitindicator.cpp
Normal file
132
components/ratelimitindicator.cpp
Normal file
@ -0,0 +1,132 @@
|
||||
#include "ratelimitindicator.hpp"
|
||||
#include "../abaddon.hpp"
|
||||
#include <filesystem>
|
||||
|
||||
RateLimitIndicator::RateLimitIndicator()
|
||||
: Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) {
|
||||
m_label.set_text("");
|
||||
m_label.set_ellipsize(Pango::ELLIPSIZE_START);
|
||||
m_label.set_valign(Gtk::ALIGN_END);
|
||||
get_style_context()->add_class("ratelimit-indicator");
|
||||
|
||||
m_img.set_margin_start(7);
|
||||
|
||||
add(m_label);
|
||||
add(m_img);
|
||||
m_label.show();
|
||||
|
||||
if (std::filesystem::exists("./res/clock.png")) {
|
||||
try {
|
||||
const auto pixbuf = Gdk::Pixbuf::create_from_file("./res/clock.png");
|
||||
int w, h;
|
||||
GetImageDimensions(pixbuf->get_width(), pixbuf->get_height(), w, h, 20, 10);
|
||||
m_img.property_pixbuf() = pixbuf->scale_simple(w, h, Gdk::INTERP_BILINEAR);
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
Abaddon::Get().GetDiscordClient().signal_message_create().connect(sigc::mem_fun(*this, &RateLimitIndicator::OnMessageCreate));
|
||||
Abaddon::Get().GetDiscordClient().signal_message_send_fail().connect(sigc::mem_fun(*this, &RateLimitIndicator::OnMessageSendFail));
|
||||
Abaddon::Get().GetDiscordClient().signal_channel_update().connect(sigc::mem_fun(*this, &RateLimitIndicator::OnChannelUpdate));
|
||||
}
|
||||
|
||||
void RateLimitIndicator::SetActiveChannel(Snowflake id) {
|
||||
m_active_channel = id;
|
||||
const auto channel = *Abaddon::Get().GetDiscordClient().GetChannel(m_active_channel);
|
||||
if (channel.RateLimitPerUser.has_value())
|
||||
m_rate_limit = *channel.RateLimitPerUser;
|
||||
else
|
||||
m_rate_limit = 0;
|
||||
|
||||
UpdateIndicator();
|
||||
}
|
||||
|
||||
bool RateLimitIndicator::CanSpeak() const {
|
||||
const auto rate_limit = GetRateLimit();
|
||||
if (rate_limit == 0) return true;
|
||||
|
||||
const auto it = m_times.find(m_active_channel);
|
||||
if (it == m_times.end())
|
||||
return true;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto sec_diff = std::chrono::duration_cast<std::chrono::seconds>(it->second - now).count();
|
||||
return sec_diff <= 0;
|
||||
}
|
||||
|
||||
int RateLimitIndicator::GetTimeLeft() const {
|
||||
if (CanSpeak()) return 0;
|
||||
|
||||
auto it = m_times.find(m_active_channel);
|
||||
if (it == m_times.end()) return 0;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto sec_diff = std::chrono::duration_cast<std::chrono::seconds>(it->second - now).count();
|
||||
|
||||
if (sec_diff <= 0)
|
||||
return 0;
|
||||
else
|
||||
return sec_diff;
|
||||
}
|
||||
|
||||
int RateLimitIndicator::GetRateLimit() const {
|
||||
return m_rate_limit;
|
||||
}
|
||||
|
||||
bool RateLimitIndicator::UpdateIndicator() {
|
||||
if (const auto rate_limit = GetRateLimit(); rate_limit != 0) {
|
||||
m_img.show();
|
||||
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (discord.HasAnyChannelPermission(discord.GetUserData().ID, m_active_channel, Permission::MANAGE_MESSAGES | Permission::MANAGE_CHANNELS)) {
|
||||
m_label.set_text("You may bypass slowmode.");
|
||||
set_has_tooltip(false);
|
||||
} else {
|
||||
const auto time_left = GetTimeLeft();
|
||||
if (time_left > 0)
|
||||
m_label.set_text(std::to_string(time_left) + "s");
|
||||
else
|
||||
m_label.set_text("");
|
||||
set_tooltip_text("Slowmode is enabled. Members can send one message every " + std::to_string(rate_limit) + " seconds.");
|
||||
}
|
||||
} else {
|
||||
m_img.hide();
|
||||
|
||||
m_label.set_text("");
|
||||
set_has_tooltip(false);
|
||||
}
|
||||
|
||||
if (m_connection)
|
||||
m_connection.disconnect();
|
||||
m_connection = Glib::signal_timeout().connect_seconds(sigc::mem_fun(*this, &RateLimitIndicator::UpdateIndicator), 1);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void RateLimitIndicator::OnMessageCreate(const Message &message) {
|
||||
auto &discord = Abaddon::Get().GetDiscordClient();
|
||||
if (message.Author.ID != discord.GetUserData().ID) return;
|
||||
const bool can_bypass = discord.HasAnyChannelPermission(discord.GetUserData().ID, m_active_channel, Permission::MANAGE_MESSAGES | Permission::MANAGE_CHANNELS);
|
||||
const auto rate_limit = GetRateLimit();
|
||||
if (rate_limit > 0 && !can_bypass) {
|
||||
m_times[message.ChannelID] = std::chrono::steady_clock::now() + std::chrono::duration<int>(rate_limit + 1);
|
||||
UpdateIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
void RateLimitIndicator::OnMessageSendFail(const std::string &nonce, float retry_after) {
|
||||
if (retry_after != 0) { // failed to rate limit
|
||||
const auto msg = Abaddon::Get().GetDiscordClient().GetMessage(nonce);
|
||||
const auto channel_id = msg->ChannelID;
|
||||
m_times[channel_id] = std::chrono::steady_clock::now() + std::chrono::duration<int>(std::lroundf(retry_after + 0.5f) + 1); // + 0.5 will ceil it
|
||||
UpdateIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
void RateLimitIndicator::OnChannelUpdate(Snowflake channel_id) {
|
||||
const auto r = Abaddon::Get().GetDiscordClient().GetChannel(channel_id)->RateLimitPerUser;
|
||||
if (r.has_value())
|
||||
m_rate_limit = *r;
|
||||
else
|
||||
m_rate_limit = 0;
|
||||
UpdateIndicator();
|
||||
}
|
31
components/ratelimitindicator.hpp
Normal file
31
components/ratelimitindicator.hpp
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include <gtkmm.h>
|
||||
#include <unordered_map>
|
||||
#include <chrono>
|
||||
#include "../discord/message.hpp"
|
||||
|
||||
class RateLimitIndicator : public Gtk::Box {
|
||||
public:
|
||||
RateLimitIndicator();
|
||||
void SetActiveChannel(Snowflake id);
|
||||
|
||||
// even tho this probably isnt the right place for this im gonna do it anyway to reduce coad
|
||||
bool CanSpeak() const;
|
||||
|
||||
private:
|
||||
int GetTimeLeft() const;
|
||||
int GetRateLimit() const;
|
||||
bool UpdateIndicator();
|
||||
void OnMessageCreate(const Message &message);
|
||||
void OnMessageSendFail(const std::string &nonce, float rate_limit);
|
||||
void OnChannelUpdate(Snowflake channel_id);
|
||||
|
||||
Gtk::Image m_img;
|
||||
Gtk::Label m_label;
|
||||
|
||||
sigc::connection m_connection;
|
||||
|
||||
int m_rate_limit;
|
||||
Snowflake m_active_channel;
|
||||
std::unordered_map<Snowflake, std::chrono::time_point<std::chrono::steady_clock>> m_times; // time point of when next message can be sent
|
||||
};
|
@ -261,6 +261,14 @@ bool DiscordClient::HasGuildPermission(Snowflake user_id, Snowflake guild_id, Pe
|
||||
return (base & perm) == perm;
|
||||
}
|
||||
|
||||
bool DiscordClient::HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const {
|
||||
const auto channel = m_store.GetChannel(channel_id);
|
||||
if (!channel.has_value()) return false;
|
||||
const auto base = ComputePermissions(user_id, *channel->GuildID);
|
||||
const auto overwrites = ComputeOverwrites(base, user_id, channel_id);
|
||||
return (overwrites & perm) != Permission::NONE;
|
||||
}
|
||||
|
||||
bool DiscordClient::HasChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const {
|
||||
const auto channel = m_store.GetChannel(channel_id);
|
||||
if (!channel.has_value()) return false;
|
||||
@ -345,7 +353,16 @@ bool DiscordClient::CanManageMember(Snowflake guild_id, Snowflake actor, Snowfla
|
||||
|
||||
void DiscordClient::ChatMessageCallback(std::string nonce, const http::response_type &response) {
|
||||
if (!CheckCode(response)) {
|
||||
m_signal_message_send_fail.emit(nonce);
|
||||
if (response.status_code == http::TooManyRequests) {
|
||||
try { // not sure if this body is guaranteed
|
||||
RateLimitedResponse r = nlohmann::json::parse(response.text);
|
||||
m_signal_message_send_fail.emit(nonce, r.RetryAfter);
|
||||
} catch (...) {
|
||||
m_signal_message_send_fail.emit(nonce, 0);
|
||||
}
|
||||
} else {
|
||||
m_signal_message_send_fail.emit(nonce, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,6 +94,8 @@ public:
|
||||
std::unordered_set<Snowflake> GetChannelsInGuild(Snowflake id) const;
|
||||
|
||||
bool HasGuildPermission(Snowflake user_id, Snowflake guild_id, Permission perm) const;
|
||||
|
||||
bool HasAnyChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
|
||||
bool HasChannelPermission(Snowflake user_id, Snowflake channel_id, Permission perm) const;
|
||||
Permission ComputePermissions(Snowflake member_id, Snowflake guild_id) const;
|
||||
Permission ComputeOverwrites(Permission base, Snowflake member_id, Snowflake channel_id) const;
|
||||
@ -312,8 +314,8 @@ public:
|
||||
typedef sigc::signal<void, GuildJoinRequestUpdateData> type_signal_guild_join_request_update;
|
||||
typedef sigc::signal<void, GuildJoinRequestDeleteData> type_signal_guild_join_request_delete;
|
||||
typedef sigc::signal<void, Message> type_signal_message_sent;
|
||||
typedef sigc::signal<void, std::string> type_signal_message_send_fail;
|
||||
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
|
||||
typedef sigc::signal<void, std::string /* nonce */, float /* retry_after */> type_signal_message_send_fail; // retry after param will be 0 if it failed for a reason that isnt slowmode
|
||||
typedef sigc::signal<void, bool, GatewayCloseCode> type_signal_disconnected; // bool true if reconnecting
|
||||
typedef sigc::signal<void> type_signal_connected;
|
||||
|
||||
type_signal_gateway_ready signal_gateway_ready();
|
||||
|
@ -444,3 +444,10 @@ void to_json(nlohmann::json &j, const VerificationGateInfoObject &m) {
|
||||
JS_IF("version", m.Version);
|
||||
JS_IF("enabled", m.Enabled);
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &j, RateLimitedResponse &m) {
|
||||
JS_D("code", m.Code);
|
||||
JS_D("global", m.Global);
|
||||
JS_O("message", m.Message);
|
||||
JS_D("retry_after", m.RetryAfter);
|
||||
}
|
||||
|
@ -615,3 +615,13 @@ struct VerificationGateInfoObject {
|
||||
friend void from_json(const nlohmann::json &j, VerificationGateInfoObject &m);
|
||||
friend void to_json(nlohmann::json &j, const VerificationGateInfoObject &m);
|
||||
};
|
||||
|
||||
// not sure what the structure for this really is
|
||||
struct RateLimitedResponse {
|
||||
int Code;
|
||||
bool Global;
|
||||
std::optional<std::string> Message;
|
||||
float RetryAfter;
|
||||
|
||||
friend void from_json(const nlohmann::json &j, RateLimitedResponse &m);
|
||||
};
|
||||
|
BIN
res/clock.png
Normal file
BIN
res/clock.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Loading…
Reference in New Issue
Block a user