diff options
-rw-r--r-- | components/completer.cpp | 20 | ||||
-rw-r--r-- | dialogs/joinguild.cpp | 28 | ||||
-rw-r--r-- | discord/discord.cpp | 90 | ||||
-rw-r--r-- | discord/discord.hpp | 16 | ||||
-rw-r--r-- | discord/guild.cpp | 45 | ||||
-rw-r--r-- | discord/guild.hpp | 51 | ||||
-rw-r--r-- | discord/invite.cpp | 19 | ||||
-rw-r--r-- | discord/invite.hpp | 26 | ||||
-rw-r--r-- | discord/objects.cpp | 20 | ||||
-rw-r--r-- | discord/objects.hpp | 26 | ||||
-rw-r--r-- | discord/store.cpp | 27 | ||||
-rw-r--r-- | util.cpp | 35 | ||||
-rw-r--r-- | util.hpp | 1 | ||||
-rw-r--r-- | windows/guildsettings/invitespane.cpp | 127 | ||||
-rw-r--r-- | windows/guildsettings/invitespane.hpp | 39 | ||||
-rw-r--r-- | windows/guildsettingswindow.cpp | 7 | ||||
-rw-r--r-- | windows/guildsettingswindow.hpp | 2 |
17 files changed, 476 insertions, 103 deletions
diff --git a/components/completer.cpp b/components/completer.cpp index f07bee6..26d5cf2 100644 --- a/components/completer.cpp +++ b/components/completer.cpp @@ -178,15 +178,17 @@ void Completer::CompleteEmojis(const Glib::ustring &term) { }; int i = 0; - for (const auto tmp : guild->Emojis) { - const auto emoji = discord.GetEmoji(tmp.ID); - if (!emoji.has_value()) continue; - if (emoji->IsAnimated.has_value() && *emoji->IsAnimated) continue; - if (term.size() > 0) - if (!StringContainsCaseless(emoji->Name, term)) continue; - if (i++ > MaxCompleterEntries) break; - - const auto entry = make_entry(emoji->Name, "<:" + emoji->Name + ":" + std::to_string(emoji->ID) + ">", emoji->GetURL()); + if (guild->Emojis.has_value()) { + for (const auto tmp : *guild->Emojis) { + const auto emoji = discord.GetEmoji(tmp.ID); + if (!emoji.has_value()) continue; + if (emoji->IsAnimated.has_value() && *emoji->IsAnimated) continue; + if (term.size() > 0) + if (!StringContainsCaseless(emoji->Name, term)) continue; + if (i++ > MaxCompleterEntries) break; + + const auto entry = make_entry(emoji->Name, "<:" + emoji->Name + ":" + std::to_string(emoji->ID) + ">", emoji->GetURL()); + } } // if <15 guild emojis match then load up stock diff --git a/dialogs/joinguild.cpp b/dialogs/joinguild.cpp index 6fd21a8..74b467d 100644 --- a/dialogs/joinguild.cpp +++ b/dialogs/joinguild.cpp @@ -56,25 +56,21 @@ void JoinGuildDialog::on_entry_changed() { } void JoinGuildDialog::CheckCode() { - // clang-format off - Abaddon::Get().GetDiscordClient().FetchInviteData( - m_code, - [this](Invite invite) { - m_ok.set_sensitive(true); - if (invite.Members != -1) - m_info.set_text(invite.Guild.Name + " (" + std::to_string(invite.Members) + " members)"); + auto cb = [this](const std::optional<InviteData> &invite) { + if (invite.has_value()) { + m_ok.set_sensitive(true); + if (invite->Guild.has_value()) { + if (invite->MemberCount.has_value()) + m_info.set_text(invite->Guild->Name + " (" + std::to_string(*invite->MemberCount) + " members)"); else - m_info.set_text(invite.Guild.Name); - }, - [this](bool not_found) { + m_info.set_text(invite->Guild->Name); + } + } else { m_ok.set_sensitive(false); - if (not_found) - m_info.set_text("Invalid invite"); - else - m_info.set_text("HTTP error (try again)"); + m_info.set_text("Invalid invite"); } - ); - // clang-format on + }; + Abaddon::Get().GetDiscordClient().FetchInvite(m_code, sigc::track_obj(cb, *this)); } bool JoinGuildDialog::IsCode(std::string str) { diff --git a/discord/discord.cpp b/discord/discord.cpp index d37e21d..5d20553 100644 --- a/discord/discord.cpp +++ b/discord/discord.cpp @@ -98,14 +98,17 @@ std::set<Snowflake> DiscordClient::GetMessagesForChannel(Snowflake id) const { return ret; } -void DiscordClient::FetchInviteData(std::string code, std::function<void(Invite)> cb, std::function<void(bool)> err) { - m_http.MakeGET("/invites/" + code + "?with_counts=true", [this, cb, err](cpr::Response r) { +void DiscordClient::FetchInvite(std::string code, sigc::slot<void(std::optional<InviteData>)> callback) { + sigc::signal<void, std::optional<InviteData>> signal; + signal.connect(callback); + m_http.MakeGET("/invites/" + code + "?with_counts=true", [this, callback](cpr::Response r) { if (!CheckCode(r)) { - err(r.status_code == 404); + if (r.status_code == 404) + callback(std::nullopt); return; }; - cb(nlohmann::json::parse(r.text)); + callback(nlohmann::json::parse(r.text).get<InviteData>()); }); } @@ -483,6 +486,18 @@ void DiscordClient::UnbanUser(Snowflake guild_id, Snowflake user_id, sigc::slot< }); } +void DiscordClient::DeleteInvite(const std::string &code) { + DeleteInvite(code, [](const auto) {}); +} + +void DiscordClient::DeleteInvite(const std::string &code, sigc::slot<void(bool success)> callback) { + sigc::signal<void, bool> signal; + signal.connect(callback); + m_http.MakeDELETE("/invites/" + code, [this, callback](const cpr::Response &response) { + callback(CheckCode(response)); + }); +} + std::vector<BanData> DiscordClient::GetBansInGuild(Snowflake guild_id) { return m_store.GetBans(guild_id); } @@ -491,7 +506,7 @@ void DiscordClient::FetchGuildBan(Snowflake guild_id, Snowflake user_id, sigc::s sigc::signal<void, BanData> signal; signal.connect(callback); m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/bans/" + std::to_string(user_id), [this, callback, guild_id](const cpr::Response &response) { - if (response.status_code != 200) return; + if (!CheckCode(response)) return; auto ban = nlohmann::json::parse(response.text).get<BanData>(); m_store.SetBan(guild_id, ban.User.ID, ban); m_store.SetUser(ban.User.ID, ban.User); @@ -503,6 +518,7 @@ void DiscordClient::FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vect sigc::signal<void, std::vector<BanData>> signal; signal.connect(callback); m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/bans", [this, callback, guild_id](const cpr::Response &response) { + if (!CheckCode(response)) return; auto bans = nlohmann::json::parse(response.text).get<std::vector<BanData>>(); m_store.BeginTransaction(); for (const auto &ban : bans) { @@ -514,6 +530,24 @@ void DiscordClient::FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vect }); } +void DiscordClient::FetchGuildInvites(Snowflake guild_id, sigc::slot<void(std::vector<InviteData>)> callback) { + sigc::signal<void, std::vector<InviteData>> signal; + signal.connect(callback); + m_http.MakeGET("/guilds/" + std::to_string(guild_id) + "/invites", [this, callback, guild_id](const cpr::Response &response) { + // store? + if (!CheckCode(response)) return; + auto invites = nlohmann::json::parse(response.text).get<std::vector<InviteData>>(); + + m_store.BeginTransaction(); + for (const auto &invite : invites) + if (invite.Inviter.has_value()) + m_store.SetUser(invite.Inviter->ID, *invite.Inviter); + m_store.EndTransaction(); + + callback(invites); + }); +} + void DiscordClient::UpdateToken(std::string token) { if (!IsStarted()) { m_token = token; @@ -676,6 +710,12 @@ void DiscordClient::HandleGatewayMessage(std::string str) { case GatewayEvent::GUILD_BAN_ADD: { HandleGatewayGuildBanAdd(m); } break; + case GatewayEvent::INVITE_CREATE: { + HandleGatewayInviteCreate(m); + } break; + case GatewayEvent::INVITE_DELETE: { + HandleGatewayInviteDelete(m); + } break; } } break; default: @@ -719,10 +759,10 @@ void DiscordClient::ProcessNewGuild(GuildData &guild) { } } - for (auto &r : guild.Roles) + for (auto &r : *guild.Roles) m_store.SetRole(r.ID, r); - for (auto &e : guild.Emojis) + for (auto &e : *guild.Emojis) m_store.SetEmoji(e.ID, e); m_store.EndTransaction(); @@ -1000,6 +1040,32 @@ void DiscordClient::HandleGatewayGuildBanAdd(const GatewayMessage &msg) { m_signal_guild_ban_add.emit(data.GuildID, data.User.ID); } +void DiscordClient::HandleGatewayInviteCreate(const GatewayMessage &msg) { + InviteCreateObject data = msg.Data; + InviteData invite; + invite.Code = std::move(data.Code); + invite.CreatedAt = std::move(data.CreatedAt); + invite.Channel = *m_store.GetChannel(data.ChannelID); + invite.Guild = *m_store.GetGuild(*invite.Channel->GuildID); + invite.Inviter = std::move(data.Inviter); + invite.IsTemporary = std::move(data.IsTemporary); + invite.MaxAge = std::move(data.MaxAge); + invite.MaxUses = std::move(data.MaxUses); + invite.TargetUser = std::move(data.TargetUser); + invite.TargetUserType = std::move(data.TargetUserType); + invite.Uses = std::move(data.Uses); + m_signal_invite_create.emit(invite); +} + +void DiscordClient::HandleGatewayInviteDelete(const GatewayMessage &msg) { + InviteDeleteObject data = msg.Data; + if (!data.GuildID.has_value()) { + const auto chan = GetChannel(data.ChannelID); + data.GuildID = chan->ID; + } + m_signal_invite_delete.emit(data); +} + void DiscordClient::HandleGatewayReconnect(const GatewayMessage &msg) { m_signal_disconnected.emit(true); inflateEnd(&m_zstream); @@ -1223,6 +1289,8 @@ void DiscordClient::LoadEventMap() { m_event_map["TYPING_START"] = GatewayEvent::TYPING_START; m_event_map["GUILD_BAN_REMOVE"] = GatewayEvent::GUILD_BAN_REMOVE; m_event_map["GUILD_BAN_ADD"] = GatewayEvent::GUILD_BAN_ADD; + m_event_map["INVITE_CREATE"] = GatewayEvent::INVITE_CREATE; + m_event_map["INVITE_DELETE"] = GatewayEvent::INVITE_DELETE; } DiscordClient::type_signal_gateway_ready DiscordClient::signal_gateway_ready() { @@ -1312,3 +1380,11 @@ DiscordClient::type_signal_guild_ban_remove DiscordClient::signal_guild_ban_remo DiscordClient::type_signal_guild_ban_add DiscordClient::signal_guild_ban_add() { return m_signal_guild_ban_add; } + +DiscordClient::type_signal_invite_create DiscordClient::signal_invite_create() { + return m_signal_invite_create; +} + +DiscordClient::type_signal_invite_delete DiscordClient::signal_invite_delete() { + return m_signal_invite_delete; +} diff --git a/discord/discord.hpp b/discord/discord.hpp index 27df1fa..11315f9 100644 --- a/discord/discord.hpp +++ b/discord/discord.hpp @@ -75,7 +75,6 @@ public: std::set<Snowflake> GetMessagesForChannel(Snowflake id) const; std::set<Snowflake> GetPrivateChannels() const; - void FetchInviteData(std::string code, std::function<void(Invite)> cb, std::function<void(bool)> err); void FetchMessagesInChannel(Snowflake id, std::function<void(const std::vector<Snowflake> &)> cb); void FetchMessagesInChannelBefore(Snowflake channel_id, Snowflake before_id, std::function<void(const std::vector<Snowflake> &)> cb); std::optional<Message> GetMessage(Snowflake id) const; @@ -117,12 +116,17 @@ public: void SetGuildIcon(Snowflake id, const std::string &data, sigc::slot<void(bool success)> callback); void UnbanUser(Snowflake guild_id, Snowflake user_id); void UnbanUser(Snowflake guild_id, Snowflake user_id, sigc::slot<void(bool success)> callback); + void DeleteInvite(const std::string &code); + void DeleteInvite(const std::string &code, sigc::slot<void(bool success)> callback); // FetchGuildBans fetches all bans+reasons via api, this func fetches stored bans (so usually just GUILD_BAN_ADD data) std::vector<BanData> GetBansInGuild(Snowflake guild_id); void FetchGuildBan(Snowflake guild_id, Snowflake user_id, sigc::slot<void(BanData)> callback); void FetchGuildBans(Snowflake guild_id, sigc::slot<void(std::vector<BanData>)> callback); + void FetchInvite(std::string code, sigc::slot<void(std::optional<InviteData>)> callback); + void FetchGuildInvites(Snowflake guild_id, sigc::slot<void(std::vector<InviteData>)> callback); + void UpdateToken(std::string token); void SetUserAgent(std::string agent); @@ -161,6 +165,8 @@ private: void HandleGatewayTypingStart(const GatewayMessage &msg); void HandleGatewayGuildBanRemove(const GatewayMessage &msg); void HandleGatewayGuildBanAdd(const GatewayMessage &msg); + void HandleGatewayInviteCreate(const GatewayMessage &msg); + void HandleGatewayInviteDelete(const GatewayMessage &msg); void HandleGatewayReconnect(const GatewayMessage &msg); void HeartbeatThread(); void SendIdentify(); @@ -231,7 +237,9 @@ public: typedef sigc::signal<void, Snowflake, Snowflake> type_signal_guild_member_update; // guild id, user id typedef sigc::signal<void, Snowflake, Snowflake> type_signal_guild_ban_remove; // guild id, user id typedef sigc::signal<void, Snowflake, Snowflake> type_signal_guild_ban_add; // guild id, user id - typedef sigc::signal<void, bool> type_signal_disconnected; // bool true if reconnecting + typedef sigc::signal<void, InviteData> type_signal_invite_create; + typedef sigc::signal<void, InviteDeleteObject> type_signal_invite_delete; + typedef sigc::signal<void, bool> type_signal_disconnected; // bool true if reconnecting typedef sigc::signal<void> type_signal_connected; type_signal_gateway_ready signal_gateway_ready(); @@ -254,6 +262,8 @@ public: type_signal_guild_member_update signal_guild_member_update(); type_signal_guild_ban_remove signal_guild_ban_remove(); type_signal_guild_ban_add signal_guild_ban_add(); + type_signal_invite_create signal_invite_create(); + type_signal_invite_delete signal_invite_delete(); // safe to assume guild id is set type_signal_disconnected signal_disconnected(); type_signal_connected signal_connected(); @@ -278,6 +288,8 @@ protected: type_signal_guild_member_update m_signal_guild_member_update; type_signal_guild_ban_remove m_signal_guild_ban_remove; type_signal_guild_ban_add m_signal_guild_ban_add; + type_signal_invite_create m_signal_invite_create; + type_signal_invite_delete m_signal_invite_delete; type_signal_disconnected m_signal_disconnected; type_signal_connected m_signal_connected; }; diff --git a/discord/guild.cpp b/discord/guild.cpp index 8b8a5c2..bf99943 100644 --- a/discord/guild.cpp +++ b/discord/guild.cpp @@ -13,29 +13,29 @@ void from_json(const nlohmann::json &j, GuildData &m) { JS_N("splash", m.Splash); JS_ON("discovery_splash", m.DiscoverySplash); JS_O("owner", m.IsOwner); - JS_D("owner_id", m.OwnerID); + JS_O("owner_id", m.OwnerID); std::optional<std::string> tmp; JS_O("permissions", tmp); if (tmp.has_value()) m.Permissions = std::stoull(*tmp); - JS_D("region", m.VoiceRegion); - JS_N("afk_channel_id", m.AFKChannelID); - JS_D("afk_timeout", m.AFKTimeout); + JS_O("region", m.VoiceRegion); + JS_ON("afk_channel_id", m.AFKChannelID); + JS_O("afk_timeout", m.AFKTimeout); JS_O("embed_enabled", m.IsEmbedEnabled); JS_ON("embed_channel_id", m.EmbedChannelID); - JS_D("verification_level", m.VerificationLevel); - JS_D("default_message_notifications", m.DefaultMessageNotifications); - JS_D("explicit_content_filter", m.ExplicitContentFilter); - JS_D("roles", m.Roles); - JS_D("emojis", m.Emojis); - JS_D("features", m.Features); - JS_D("mfa_level", m.MFALevel); - JS_N("application_id", m.ApplicationID); + JS_O("verification_level", m.VerificationLevel); + JS_O("default_message_notifications", m.DefaultMessageNotifications); + JS_O("explicit_content_filter", m.ExplicitContentFilter); + JS_O("roles", m.Roles); + JS_O("emojis", m.Emojis); + JS_O("features", m.Features); + JS_O("mfa_level", m.MFALevel); + JS_ON("application_id", m.ApplicationID); JS_O("widget_enabled", m.IsWidgetEnabled); JS_ON("widget_channel_id", m.WidgetChannelID); - JS_N("system_channel_id", m.SystemChannelID); - JS_D("system_channel_flags", m.SystemChannelFlags); - JS_N("rules_channel_id", m.RulesChannelID); + JS_ON("system_channel_id", m.SystemChannelID); + JS_O("system_channel_flags", m.SystemChannelFlags); + JS_ON("rules_channel_id", m.RulesChannelID); JS_O("joined_at", m.JoinedAt); JS_O("large", m.IsLarge); JS_O("unavailable", m.IsUnavailable); @@ -46,13 +46,13 @@ void from_json(const nlohmann::json &j, GuildData &m) { // JS_O("presences", m.Presences); JS_ON("max_presences", m.MaxPresences); JS_O("max_members", m.MaxMembers); - JS_N("vanity_url_code", m.VanityURL); - JS_N("description", m.Description); - JS_N("banner", m.BannerHash); - JS_D("premium_tier", m.PremiumTier); + JS_ON("vanity_url_code", m.VanityURL); + JS_ON("description", m.Description); + JS_ON("banner", m.BannerHash); + JS_O("premium_tier", m.PremiumTier); JS_O("premium_subscription_count", m.PremiumSubscriptionCount); - JS_D("preferred_locale", m.PreferredLocale); - JS_N("public_updates_channel_id", m.PublicUpdatesChannelID); + JS_O("preferred_locale", m.PreferredLocale); + JS_ON("public_updates_channel_id", m.PublicUpdatesChannelID); JS_O("max_video_channel_users", m.MaxVideoChannelUsers); JS_O("approximate_member_count", tmp); if (tmp.has_value()) @@ -119,7 +119,8 @@ void GuildData::update_from_json(const nlohmann::json &j) { } bool GuildData::HasFeature(const std::string &search_feature) { - for (const auto &feature : Features) + if (!Features.has_value()) return false; + for (const auto &feature : *Features) if (search_feature == feature) return true; return false; diff --git a/discord/guild.hpp b/discord/guild.hpp index eeb62cf..e8132e0 100644 --- a/discord/guild.hpp +++ b/discord/guild.hpp @@ -9,6 +9,9 @@ // a bot is apparently only supposed to receive the `id` and `unavailable` as false // but user tokens seem to get the full objects (minus users) + +// everythings optional cuz of muh partial guild object +// anything not marked optional in https://discord.com/developers/docs/resources/guild#guild-object is guaranteed to be set when returned from Store::GetGuild struct GuildData { Snowflake ID; std::string Name; @@ -16,44 +19,44 @@ struct GuildData { std::string Splash; // null std::optional<std::string> DiscoverySplash; // null std::optional<bool> IsOwner; - Snowflake OwnerID; + std::optional<Snowflake> OwnerID; std::optional<uint64_t> Permissions; std::optional<std::string> PermissionsNew; std::optional<std::string> VoiceRegion; - Snowflake AFKChannelID; // null - int AFKTimeout; + std::optional<Snowflake> AFKChannelID; // null + std::optional<int> AFKTimeout; std::optional<bool> IsEmbedEnabled; // deprecated std::optional<Snowflake> EmbedChannelID; // null, deprecated - int VerificationLevel; - int DefaultMessageNotifications; - int ExplicitContentFilter; - std::vector<RoleData> Roles; // only access id - std::vector<EmojiData> Emojis; // only access id - std::vector<std::string> Features; - int MFALevel; - Snowflake ApplicationID; // null + std::optional<int> VerificationLevel; + std::optional<int> DefaultMessageNotifications; + std::optional<int> ExplicitContentFilter; + std::optional<std::vector<RoleData>> Roles; // only access id + std::optional<std::vector<EmojiData>> Emojis; // only access id + std::optional<std::vector<std::string>> Features; + std::optional<int> MFALevel; + std::optional<Snowflake> ApplicationID; // null std::optional<bool> IsWidgetEnabled; std::optional<Snowflake> WidgetChannelID; // null - Snowflake SystemChannelID; // null - int SystemChannelFlags; - Snowflake RulesChannelID; // null - std::optional<std::string> JoinedAt; // * - std::optional<bool> IsLarge; // * - std::optional<bool> IsUnavailable; // * - std::optional<int> MemberCount; // * + std::optional<Snowflake> SystemChannelID; // null + std::optional<int> SystemChannelFlags; + std::optional<Snowflake> RulesChannelID; // null + std::optional<std::string> JoinedAt; // * + std::optional<bool> IsLarge; // * + std::optional<bool> IsUnavailable; // * + std::optional<int> MemberCount; // * // std::vector<VoiceStateData> VoiceStates; // opt* // std::vector<MemberData> Members; // opt* - incomplete anyways std::optional<std::vector<ChannelData>> Channels; // * // std::vector<PresenceUpdateData> Presences; // opt* std::optional<int> MaxPresences; // null std::optional<int> MaxMembers; - std::string VanityURL; // null - std::string Description; // null - std::string BannerHash; // null - int PremiumTier; + std::optional<std::string> VanityURL; // null + std::optional<std::string> Description; // null + std::optional<std::string> BannerHash; // null + std::optional<int> PremiumTier; std::optional<int> PremiumSubscriptionCount; - std::string PreferredLocale; - Snowflake PublicUpdatesChannelID; // null + std::optional<std::string> PreferredLocale; + std::optional<Snowflake> PublicUpdatesChannelID; // null std::optional<int> MaxVideoChannelUsers; std::optional<int> ApproximateMemberCount; std::optional<int> ApproximatePresenceCount; diff --git a/discord/invite.cpp b/discord/invite.cpp index a08bdef..641d113 100644 --- a/discord/invite.cpp +++ b/discord/invite.cpp @@ -1,14 +1,17 @@ #include "invite.hpp" -void from_json(const nlohmann::json &j, Invite &m) { +void from_json(const nlohmann::json &j, InviteData &m) { JS_D("code", m.Code); + JS_O("guild", m.Guild); JS_O("channel", m.Channel); JS_O("inviter", m.Inviter); - JS_O("approximate_member_count", m.Members); - - if (j.contains("guild")) { - auto x = j.at("guild"); - x.at("id").get_to(m.Guild.ID); - x.at("name").get_to(m.Guild.Name); - } + JS_O("target_user", m.TargetUser); + JS_O("target_user_type", m.TargetUserType); + JS_O("approximate_presence_count", m.PresenceCount); + JS_O("approximate_member_count", m.MemberCount); + JS_O("uses", m.Uses); + JS_O("max_uses", m.MaxUses); + JS_O("max_age", m.MaxAge); + JS_O("temporary", m.IsTemporary); + JS_O("created_at", m.CreatedAt); } diff --git a/discord/invite.hpp b/discord/invite.hpp index 5b42167..9c7a9c9 100644 --- a/discord/invite.hpp +++ b/discord/invite.hpp @@ -3,13 +3,25 @@ #include "guild.hpp" #include <string> -class Invite { +enum class ETargetUserType { + STREAM = 1 +}; + +class InviteData { public: - std::string Code; // - GuildData Guild; // opt - ChannelData Channel; // opt - UserData Inviter; // opt - int Members = -1; // opt + std::string Code; + std::optional<GuildData> Guild; + std::optional<ChannelData> Channel; + std::optional<UserData> Inviter; + std::optional<UserData> TargetUser; + std::optional<ETargetUserType> TargetUserType; + std::optional<int> PresenceCount; + std::optional<int> MemberCount; + std::optional<int> Uses; + std::optional<int> MaxUses; + std::optional<int> MaxAge; + std::optional<bool> IsTemporary; + std::optional<std::string> CreatedAt; - friend void from_json(const nlohmann::json &j, Invite &m); + friend void from_json(const nlohmann::json &j, InviteData &m); }; diff --git a/discord/objects.cpp b/discord/objects.cpp index 70fbd57..bcc7b04 100644 --- a/discord/objects.cpp +++ b/discord/objects.cpp @@ -262,3 +262,23 @@ void from_json(const nlohmann::json &j, GuildBanAddObject &m) { JS_D("guild_id", m.GuildID); JS_D("user", m.User); } + +void from_json(const nlohmann::json &j, InviteCreateObject &m) { + JS_D("channel_id", m.ChannelID); + JS_D("code", m.Code); + JS_D("created_at", m.CreatedAt); + JS_O("guild_id", m.GuildID); + JS_O("inviter", m.Inviter); + JS_D("max_age", m.MaxAge); + JS_D("max_uses", m.MaxUses); + JS_O("target_user", m.TargetUser); + JS_O("target_user_type", m.TargetUserType); + JS_D("temporary", m.IsTemporary); + JS_D("uses", m.Uses); +} + +void from_json(const nlohmann::json &j, InviteDeleteObject &m) { + JS_D("channel_id", m.ChannelID); + JS_O("guild_id", m.GuildID); + JS_D("code", m.Code); +} diff --git a/discord/objects.hpp b/discord/objects.hpp index 5aa92ec..6c04602 100644 --- a/discord/objects.hpp +++ b/discord/objects.hpp @@ -57,6 +57,8 @@ enum class GatewayEvent : int { TYPING_START, GUILD_BAN_REMOVE, GUILD_BAN_ADD, + INVITE_CREATE, + INVITE_DELETE, }; struct GatewayMessage { @@ -370,3 +372,27 @@ struct GuildBanAddObject { friend void from_json(const nlohmann::json &j, GuildBanAddObject &m); }; + +struct InviteCreateObject { + Snowflake ChannelID; + std::string Code; + std::string CreatedAt; + std::optional<Snowflake> GuildID; + std::optional<UserData> Inviter; + int MaxAge; + int MaxUses; + UserData TargetUser; + std::optional<ETargetUserType> TargetUserType; + bool IsTemporary; + int Uses; + + friend void from_json(const nlohmann::json &j, InviteCreateObject &m); +}; + +struct InviteDeleteObject { + Snowflake ChannelID; + std::optional<Snowflake> GuildID; + std::string Code; + + friend void from_json(const nlohmann::json &j, InviteDeleteObject &m); +}; diff --git a/discord/store.cpp b/discord/store.cpp index 0131486..44af4cd 100644 --- a/discord/store.cpp +++ b/discord/store.cpp @@ -148,12 +148,23 @@ void Store::SetGuild(Snowflake id, const GuildData &guild) { Bind(m_set_guild_stmt, 11, guild.VerificationLevel); Bind(m_set_guild_stmt, 12, guild.DefaultMessageNotifications); std::vector<Snowflake> snowflakes; - for (const auto &x : guild.Roles) snowflakes.push_back(x.ID); - Bind(m_set_guild_stmt, 13, nlohmann::json(snowflakes).dump()); + if (guild.Roles.has_value()) { + for (const auto &x : *guild.Roles) snowflakes.push_back(x.ID); + Bind(m_set_guild_stmt, 13, nlohmann::json(snowflakes).dump()); + } else { + Bind(m_set_guild_stmt, 13, "[]"s); + } snowflakes.clear(); - for (const auto &x : guild.Emojis) snowflakes.push_back(x.ID); - Bind(m_set_guild_stmt, 14, nlohmann::json(snowflakes).dump()); - Bind(m_set_guild_stmt, 15, nlohmann::json(guild.Features).dump()); + if (guild.Emojis.has_value()) { + for (const auto &x : *guild.Emojis) snowflakes.push_back(x.ID); + Bind(m_set_guild_stmt, 14, nlohmann::json(snowflakes).dump()); + } else { + Bind(m_set_guild_stmt, 14, "[]"s); + } + if (guild.Features.has_value()) + Bind(m_set_guild_stmt, 15, nlohmann::json(*guild.Features).dump()); + else + Bind(m_set_guild_stmt, 15, "[]"s); Bind(m_set_guild_stmt, 16, guild.MFALevel); Bind(m_set_guild_stmt, 17, guild.ApplicationID); Bind(m_set_guild_stmt, 18, guild.IsWidgetEnabled); @@ -430,11 +441,13 @@ std::optional<GuildData> Store::GetGuild(Snowflake id) const { Get(m_get_guild_stmt, 11, ret.DefaultMessageNotifications); std::string tmp; Get(m_get_guild_stmt, 12, tmp); + ret.Roles.emplace(); for (const auto &id : nlohmann::json::parse(tmp).get<std::vector<Snowflake>>()) - ret.Roles.emplace_back().ID = id; + ret.Roles->emplace_back().ID = id; Get(m_get_guild_stmt, 13, tmp); + ret.Emojis.emplace(); for (const auto &id : nlohmann::json::parse(tmp).get<std::vector<Snowflake>>()) - ret.Emojis.emplace_back().ID = id; + ret.Emojis->emplace_back().ID = id; Get(m_get_guild_stmt, 14, tmp); ret.Features = nlohmann::json::parse(tmp).get<std::vector<std::string>>(); Get(m_get_guild_stmt, 15, ret.MFALevel); @@ -64,6 +64,41 @@ std::string HumanReadableBytes(uint64_t bytes) { return std::to_string(bytes) + x[order]; } +int GetTimezoneOffset() { + std::time_t secs; + std::time(&secs); + std::tm *tptr = std::localtime(&secs); + std::time_t local_secs = std::mktime(tptr); + tptr = std::gmtime(&secs); + std::time_t gmt_secs = std::mktime(tptr); + return local_secs - gmt_secs; +} + +std::string FormatISO8601(const std::string &in, int extra_offset, const std::string &fmt) { + int yr, mon, day, hr, min, sec, tzhr, tzmin; + float milli; + std::sscanf(in.c_str(), "%d-%d-%dT%d:%d:%d%f+%d:%d", + &yr, &mon, &day, &hr, &min, &sec, &milli, &tzhr, &tzmin); + std::tm tm; + tm.tm_year = yr - 1900; + tm.tm_mon = mon - 1; + tm.tm_mday = day; + tm.tm_hour = hr; + tm.tm_min = min; + tm.tm_sec = sec; + tm.tm_wday = 0; + tm.tm_yday = 0; + tm.tm_isdst = -1; + int offset = GetTimezoneOffset(); + tm.tm_sec += offset + extra_offset; + mktime(&tm); + std::stringstream ss; + const static std::locale locale(""); + ss.imbue(locale); + ss << std::put_time(&tm, fmt.c_str()); + return ss.str(); +} + void ScrollListBoxToSelected(Gtk::ListBox &list) { auto cb = [&list]() -> bool { const auto selected = list.get_selected_row(); @@ -45,6 +45,7 @@ std::string GetExtension(std::string url); bool IsURLViewableImage(const std::string &url); std::vector<uint8_t> ReadWholeFile(std::string path); std::string HumanReadableBytes(uint64_t bytes); +std::string FormatISO8601(const std::string &in, int extra_offset = 0, const std::string &fmt = "%x %X"); template<typename T> struct Bitwise { diff --git a/windows/guildsettings/invitespane.cpp b/windows/guildsettings/invitespane.cpp new file mode 100644 index 0000000..a47460f --- /dev/null +++ b/windows/guildsettings/invitespane.cpp @@ -0,0 +1,127 @@ +#include "invitespane.hpp" +#include "../../abaddon.hpp" + +GuildSettingsInvitesPane::GuildSettingsInvitesPane(Snowflake id) + : GuildID(id) + , m_model(Gtk::ListStore::create(m_columns)) + , m_menu_delete("Delete") { + set_name("guild-invites-pane"); + set_hexpand(true); + set_vexpand(true); + + m_view.signal_button_press_event().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnTreeButtonPress), false); + + m_menu_delete.signal_activate().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnMenuDelete)); + m_menu.append(m_menu_delete); + m_menu.show_all(); + + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + + if (discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_GUILD)) + discord.FetchGuildInvites(GuildID, sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnInvitesFetch)); + + discord.signal_invite_create().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnInviteCreate)); + discord.signal_invite_delete().connect(sigc::mem_fun(*this, &GuildSettingsInvitesPane::OnInviteDelete)); + + m_view.show(); + add(m_view); + + m_view.set_model(m_model); + m_view.append_column("Code", m_columns.m_col_code); + m_view.append_column("Expires", m_columns.m_col_expires); + m_view.append_column("Created by", m_columns.m_col_inviter); + m_view.append_column("Uses", m_columns.m_col_uses); + m_view.append_column("Max uses", m_columns.m_col_max_uses); + m_view.append_column("Grants temporary membership", m_columns.m_col_temporary); + + for (const auto column : m_view.get_columns()) + column->set_resizable(true); +} + +void GuildSettingsInvitesPane::AppendInvite(const InviteData &invite) { + auto &discord = Abaddon::Get().GetDiscordClient(); + auto &row = *m_model->append(); + row[m_columns.m_col_code] = invite.Code; + if (invite.Inviter.has_value()) + row[m_columns.m_col_inviter] = invite.Inviter->Username + "#" + invite.Inviter->Discriminator; + + if (invite.MaxAge.has_value()) + if (*invite.MaxAge == 0) + row[m_columns.m_col_expires] = "Never"; + else + row[m_columns.m_col_expires] = FormatISO8601(*invite.CreatedAt, *invite.MaxAge); + + row[m_columns.m_col_uses] = *invite.Uses; + if (*invite.MaxUses == 0) + row[m_columns.m_col_max_uses] = "Unlimited"; + else + row[m_columns.m_col_max_uses] = std::to_string(*invite.MaxUses); + + row[m_columns.m_col_temporary] = *invite.IsTemporary ? "Yes" : "No"; +} + +void GuildSettingsInvitesPane::OnInviteFetch(const std::optional<InviteData> &invite) { + if (!invite.has_value()) return; + AppendInvite(*invite); +} + +void GuildSettingsInvitesPane::OnInvitesFetch(const std::vector<InviteData> &invites) { + auto &discord = Abaddon::Get().GetDiscordClient(); + for (const auto &invite : invites) + AppendInvite(invite); +} + +void GuildSettingsInvitesPane::OnInviteCreate(const InviteData &invite) { + if (invite.Guild->ID == GuildID) + OnInviteFetch(std::make_optional(invite)); +} + +void GuildSettingsInvitesPane::OnInviteDelete(const InviteDeleteObject &data) { + if (*data.GuildID == GuildID) + for (auto &row : m_model->children()) + if (row[m_columns.m_col_code] == data.Code) + m_model->erase(row); +} + +void GuildSettingsInvitesPane::OnMenuDelete() { + auto selected_row = *m_view.get_selection()->get_selected(); + if (selected_row) { + auto code = static_cast<Glib::ustring>(selected_row[m_columns.m_col_code]); + auto cb = [this](const bool success) { + if (!success) { + Gtk::MessageDialog dlg("Failed to delete invite", false, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true); + dlg.run(); + } + }; + Abaddon::Get().GetDiscordClient().DeleteInvite(code, sigc::track_obj(cb, *this)); + } +} + +bool GuildSettingsInvitesPane::OnTreeButtonPress(GdkEventButton *event) { + if (event->type == GDK_BUTTON_PRESS && event->button == GDK_BUTTON_SECONDARY) { + auto &discord = Abaddon::Get().GetDiscordClient(); + const auto self_id = discord.GetUserData().ID; + const auto can_manage = discord.HasGuildPermission(self_id, GuildID, Permission::MANAGE_GUILD); + m_menu_delete.set_sensitive(can_manage); + auto selection = m_view.get_selection(); + Gtk::TreeModel::Path path; + if (m_view.get_path_at_pos(event->x, event->y, path)) { + m_view.get_selection()->select(path); + m_menu.popup_at_pointer(reinterpret_cast<GdkEvent *>(event)); + } + + return true; + } + + return false; +} + +GuildSettingsInvitesPane::ModelColumns::ModelColumns() { + add(m_col_code); + add(m_col_expires); + add(m_col_inviter); + add(m_col_temporary); + add(m_col_uses); + add(m_col_max_uses); +} diff --git a/windows/guildsettings/invitespane.hpp b/windows/guildsettings/invitespane.hpp new file mode 100644 index 0000000..c32f194 --- /dev/null +++ b/windows/guildsettings/invitespane.hpp @@ -0,0 +1,39 @@ +#pragma once +#include <gtkmm.h> +#include "../../discord/objects.hpp" + +class GuildSettingsInvitesPane : public Gtk::ScrolledWindow { +public: + GuildSettingsInvitesPane(Snowflake id); + +private: + void AppendInvite(const InviteData &invite); + void OnInviteFetch(const std::optional<InviteData> &invite); + void OnInvitesFetch(const std::vector<InviteData> &invites); + void OnInviteCreate(const InviteData &invite); + void OnInviteDelete(const InviteDeleteObject &data); + void OnMenuDelete(); + bool OnTreeButtonPress(GdkEventButton *event); + + Gtk::TreeView m_view; + + Snowflake GuildID; + + class ModelColumns : public Gtk::TreeModel::ColumnRecord { + public: + ModelColumns(); + + Gtk::TreeModelColumn<Glib::ustring> m_col_code; + Gtk::TreeModelColumn<Glib::ustring> m_col_expires; + Gtk::TreeModelColumn<Glib::ustring> m_col_inviter; + Gtk::TreeModelColumn<Glib::ustring> m_col_temporary; + Gtk::TreeModelColumn<int> m_col_uses; + Gtk::TreeModelColumn<Glib::ustring> m_col_max_uses; + }; + + ModelColumns m_columns; + Glib::RefPtr<Gtk::ListStore> m_model; + + Gtk::Menu m_menu; + Gtk::MenuItem m_menu_delete; +}; diff --git a/windows/guildsettingswindow.cpp b/windows/guildsettingswindow.cpp index 4672729..2124a0e 100644 --- a/windows/guildsettingswindow.cpp +++ b/windows/guildsettingswindow.cpp @@ -5,7 +5,8 @@ GuildSettingsWindow::GuildSettingsWindow(Snowflake id) : m_main(Gtk::ORIENTATION_VERTICAL) , GuildID(id) , m_pane_info(id) - , m_pane_bans(id) { + , m_pane_bans(id) + , m_pane_invites(id) { auto &discord = Abaddon::Get().GetDiscordClient(); const auto guild = *discord.GetGuild(id); @@ -36,7 +37,10 @@ GuildSettingsWindow::GuildSettingsWindow(Snowflake id) m_pane_info.show(); m_pane_bans.show(); + m_pane_invites.show(); + m_stack.set_transition_duration(100); + m_stack.set_transition_type(Gtk::STACK_TRANSITION_TYPE_CROSSFADE); m_stack.set_margin_top(10); m_stack.set_margin_bottom(10); m_stack.set_margin_left(10); @@ -44,6 +48,7 @@ GuildSettingsWindow::GuildSettingsWindow(Snowflake id) m_stack.add(m_pane_info, "info", "Info"); m_stack.add(m_pane_bans, "bans", "Bans"); + m_stack.add(m_pane_invites, "invites", "Invites"); m_stack.show(); m_main.add(m_switcher); diff --git a/windows/guildsettingswindow.hpp b/windows/guildsettingswindow.hpp index dc189fb..811c937 100644 --- a/windows/guildsettingswindow.hpp +++ b/windows/guildsettingswindow.hpp @@ -3,6 +3,7 @@ #include "../discord/snowflake.hpp" #include "guildsettings/infopane.hpp" #include "guildsettings/banspane.hpp" +#include "guildsettings/invitespane.hpp" class GuildSettingsWindow : public Gtk::Window { public: @@ -15,6 +16,7 @@ private: GuildSettingsInfoPane m_pane_info; GuildSettingsBansPane m_pane_bans; + GuildSettingsInvitesPane m_pane_invites; Snowflake GuildID; }; |