diff --git a/abaddon.cpp b/abaddon.cpp index 768141d..08e6884 100644 --- a/abaddon.cpp +++ b/abaddon.cpp @@ -484,10 +484,14 @@ void Abaddon::ActionSetStatus() { const auto status = dlg.GetStatusType(); const auto activity_type = dlg.GetActivityType(); const auto activity_name = dlg.GetActivityName(); - ActivityData activity; - activity.Name = activity_name; - activity.Type = activity_type; - m_discord.UpdateStatus(status, false, activity); + if (activity_name == "") { + m_discord.UpdateStatus(status, false); + } else { + ActivityData activity; + activity.Name = activity_name; + activity.Type = activity_type; + m_discord.UpdateStatus(status, false, activity); + } } void Abaddon::ActionReactionAdd(Snowflake id, const Glib::ustring ¶m) { diff --git a/components/memberlist.cpp b/components/memberlist.cpp index 0d54448..be7e074 100644 --- a/components/memberlist.cpp +++ b/components/memberlist.cpp @@ -1,6 +1,8 @@ #include "memberlist.hpp" #include "../abaddon.hpp" #include "../util.hpp" +#include "lazyimage.hpp" +#include "statusindicator.hpp" MemberListUserRow::MemberListUserRow(Snowflake guild_id, const UserData *data) { ID = data->ID; @@ -8,6 +10,9 @@ MemberListUserRow::MemberListUserRow(Snowflake guild_id, const UserData *data) { m_box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); m_label = Gtk::manage(new Gtk::Label); m_avatar = Gtk::manage(new LazyImage(16, 16)); + m_status_indicator = Gtk::manage(new StatusIndicator(ID)); + + m_status_indicator->set_margin_start(3); if (data->HasAvatar()) m_avatar->SetURL(data->GetAvatarURL("png")); @@ -37,8 +42,10 @@ MemberListUserRow::MemberListUserRow(Snowflake guild_id, const UserData *data) { m_label->set_use_markup(true); m_label->set_markup("[unknown user]"); } + m_label->set_halign(Gtk::ALIGN_START); m_box->add(*m_avatar); + m_box->add(*m_status_indicator); m_box->add(*m_label); m_ev->add(*m_box); add(*m_ev); diff --git a/components/memberlist.hpp b/components/memberlist.hpp index c816b26..856d73c 100644 --- a/components/memberlist.hpp +++ b/components/memberlist.hpp @@ -3,8 +3,9 @@ #include #include #include "../discord/discord.hpp" -#include "lazyimage.hpp" +class LazyImage; +class StatusIndicator; class MemberListUserRow : public Gtk::ListBoxRow { public: MemberListUserRow(Snowflake guild_id, const UserData *data); @@ -15,6 +16,7 @@ private: Gtk::EventBox *m_ev; Gtk::Box *m_box; LazyImage *m_avatar; + StatusIndicator *m_status_indicator; Gtk::Label *m_label; }; diff --git a/components/statusindicator.cpp b/components/statusindicator.cpp new file mode 100644 index 0000000..abf389c --- /dev/null +++ b/components/statusindicator.cpp @@ -0,0 +1,136 @@ +#include "statusindicator.hpp" +#include "../abaddon.hpp" + +static const constexpr int Diameter = 8; +static const auto OnlineColor = Gdk::RGBA("#43B581"); +static const auto IdleColor = Gdk::RGBA("#FAA61A"); +static const auto DNDColor = Gdk::RGBA("#982929"); +static const auto OfflineColor = Gdk::RGBA("#808080"); + +StatusIndicator::StatusIndicator(Snowflake user_id) + : Glib::ObjectBase("statusindicator") + , Gtk::Widget() + , m_id(user_id) + , m_color(OfflineColor) { + set_has_window(true); + set_name("status-indicator"); + + Abaddon::Get().GetDiscordClient().signal_guild_member_list_update().connect(sigc::hide(sigc::mem_fun(*this, &StatusIndicator::CheckStatus))); + auto cb = [this](Snowflake id, PresenceStatus status) { + if (id == m_id) CheckStatus(); + }; + Abaddon::Get().GetDiscordClient().signal_presence_update().connect(sigc::track_obj(cb, *this)); + + CheckStatus(); +} + +StatusIndicator::~StatusIndicator() { +} + +void StatusIndicator::CheckStatus() { + const auto status = Abaddon::Get().GetDiscordClient().GetUserStatus(m_id); + if (status.has_value()) { + switch (*status) { + case PresenceStatus::Online: + m_color = OnlineColor; + break; + case PresenceStatus::Offline: + m_color = OfflineColor; + break; + case PresenceStatus::DND: + m_color = DNDColor; + break; + case PresenceStatus::Idle: + m_color = IdleColor; + break; + } + } else { + m_color = OfflineColor; + } + + queue_draw(); +} + +Gtk::SizeRequestMode StatusIndicator::get_request_mode_vfunc() const { + return Gtk::Widget::get_request_mode_vfunc(); +} + +void StatusIndicator::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const { + minimum_width = 0; + natural_width = Diameter; +} + +void StatusIndicator::get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const { + minimum_height = 0; + natural_height = Diameter; +} + +void StatusIndicator::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const { + minimum_height = 0; + natural_height = Diameter; +} + +void StatusIndicator::get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const { + minimum_width = 0; + natural_width = Diameter; +} + +void StatusIndicator::on_size_allocate(Gtk::Allocation &allocation) { + set_allocation(allocation); + + if (m_window) + m_window->move_resize(allocation.get_x(), allocation.get_y(), allocation.get_width(), allocation.get_height()); +} + +void StatusIndicator::on_map() { + Gtk::Widget::on_map(); +} + +void StatusIndicator::on_unmap() { + Gtk::Widget::on_unmap(); +} + +void StatusIndicator::on_realize() { + set_realized(true); + + if (!m_window) { + GdkWindowAttr attributes; + std::memset(&attributes, 0, sizeof(attributes)); + + auto allocation = get_allocation(); + + attributes.x = allocation.get_x(); + attributes.y = allocation.get_y(); + attributes.width = allocation.get_width(); + attributes.height = allocation.get_height(); + + attributes.event_mask = get_events() | Gdk::EXPOSURE_MASK; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.wclass = GDK_INPUT_OUTPUT; + + m_window = Gdk::Window::create(get_parent_window(), &attributes, GDK_WA_X | GDK_WA_Y); + set_window(m_window); + + m_window->set_user_data(gobj()); + } +} + +void StatusIndicator::on_unrealize() { + m_window.reset(); + + Gtk::Widget::on_unrealize(); +} + +bool StatusIndicator::on_draw(const Cairo::RefPtr &cr) { + const auto allocation = get_allocation(); + const auto width = allocation.get_width(); + const auto height = allocation.get_height(); + + cr->set_source_rgb(m_color.get_red(), m_color.get_green(), m_color.get_blue()); + cr->arc(width / 2, height / 2, width / 3, 0.0, 2 * (4 * std::atan(1))); + cr->close_path(); + cr->fill_preserve(); + cr->stroke(); + + return true; +} diff --git a/components/statusindicator.hpp b/components/statusindicator.hpp new file mode 100644 index 0000000..9c7382e --- /dev/null +++ b/components/statusindicator.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include "../discord/snowflake.hpp" + +class StatusIndicator : public Gtk::Widget { +public: + StatusIndicator(Snowflake user_id); + virtual ~StatusIndicator(); + +protected: + Gtk::SizeRequestMode get_request_mode_vfunc() const override; + void get_preferred_width_vfunc(int &minimum_width, int &natural_width) const override; + void get_preferred_height_for_width_vfunc(int width, int &minimum_height, int &natural_height) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; + void get_preferred_width_for_height_vfunc(int height, int &minimum_width, int &natural_width) const override; + void on_size_allocate(Gtk::Allocation &allocation) override; + void on_map() override; + void on_unmap() override; + void on_realize() override; + void on_unrealize() override; + bool on_draw(const Cairo::RefPtr &cr) override; + + Glib::RefPtr m_window; + +private: + void CheckStatus(); + + Snowflake m_id; + Gdk::RGBA m_color; +}; diff --git a/dialogs/setstatus.cpp b/dialogs/setstatus.cpp index 8fcabf4..5c9656e 100644 --- a/dialogs/setstatus.cpp +++ b/dialogs/setstatus.cpp @@ -54,8 +54,17 @@ ActivityType SetStatusDialog::GetActivityType() const { return static_cast(std::stoul(x)); } -std::string SetStatusDialog::GetStatusType() const { - return m_status_combo.get_active_id(); +PresenceStatus SetStatusDialog::GetStatusType() const { + const auto &x = m_status_combo.get_active_id(); + if (x == "online") + return PresenceStatus::Online; + else if (x == "idle") + return PresenceStatus::Idle; + else if (x == "dnd") + return PresenceStatus::DND; + else if (x == "offline") + return PresenceStatus::Offline; + return PresenceStatus::Online; } std::string SetStatusDialog::GetActivityName() const { diff --git a/dialogs/setstatus.hpp b/dialogs/setstatus.hpp index 16f0f94..97db906 100644 --- a/dialogs/setstatus.hpp +++ b/dialogs/setstatus.hpp @@ -6,7 +6,7 @@ class SetStatusDialog : public Gtk::Dialog { public: SetStatusDialog(Gtk::Window &parent); ActivityType GetActivityType() const; - std::string GetStatusType() const; + PresenceStatus GetStatusType() const; std::string GetActivityName() const; protected: diff --git a/discord/activity.cpp b/discord/activity.cpp index ff02758..95dda5d 100644 --- a/discord/activity.cpp +++ b/discord/activity.cpp @@ -100,9 +100,17 @@ void to_json(nlohmann::json &j, const ActivityData &m) { JS_IF("flags", m.Flags); } +void from_json(const nlohmann::json &j, PresenceData &m) { + JS_N("activities", m.Activities); + JS_D("status", m.Status); +} + void to_json(nlohmann::json &j, const PresenceData &m) { j["activities"] = m.Activities; j["status"] = m.Status; - j["afk"] = m.IsAFK; - j["since"] = m.Since; + JS_IF("afk", m.IsAFK); + if (m.Since.has_value()) + j["since"] = *m.Since; + else + j["since"] = 0; } diff --git a/discord/activity.hpp b/discord/activity.hpp index 1e323e3..2cbd5ce 100644 --- a/discord/activity.hpp +++ b/discord/activity.hpp @@ -5,6 +5,13 @@ #include "json.hpp" #include "snowflake.hpp" +enum class PresenceStatus : uint8_t { + Online, + Offline, + Idle, + DND, +}; + enum class ActivityType : int { Game = 0, Streaming = 1, @@ -28,8 +35,8 @@ struct Bitwise { }; struct ActivityTimestamps { - std::optional Start; // opt - std::optional End; // opt + std::optional Start; + std::optional End; friend void from_json(const nlohmann::json &j, ActivityTimestamps &m); friend void to_json(nlohmann::json &j, const ActivityTimestamps &m); @@ -94,8 +101,9 @@ struct ActivityData { struct PresenceData { std::vector Activities; // null (but never sent as such) std::string Status; - bool IsAFK; - int Since = 0; + std::optional IsAFK; + std::optional Since; + friend void from_json(const nlohmann::json &j, PresenceData &m); friend void to_json(nlohmann::json &j, const PresenceData &m); }; diff --git a/discord/discord.cpp b/discord/discord.cpp index 382e3ac..df326d8 100644 --- a/discord/discord.cpp +++ b/discord/discord.cpp @@ -401,13 +401,26 @@ void DiscordClient::BanUser(Snowflake user_id, Snowflake guild_id) { m_http.MakePUT("/guilds/" + std::to_string(guild_id) + "/bans/" + std::to_string(user_id), "{}", [](auto) {}); } -void DiscordClient::UpdateStatus(const std::string &status, bool is_afk, const ActivityData &obj) { +void DiscordClient::UpdateStatus(PresenceStatus status, bool is_afk) { UpdateStatusMessage msg; - msg.Presence.Status = status; - msg.Presence.IsAFK = is_afk; - msg.Presence.Activities.push_back(obj); + msg.Status = status; + msg.IsAFK = is_afk; m_websocket.Send(nlohmann::json(msg)); + // fake message cuz we dont receive messages for ourself + m_user_to_status[m_user_data.ID] = status; + m_signal_presence_update.emit(m_user_data.ID, status); +} + +void DiscordClient::UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj) { + UpdateStatusMessage msg; + msg.Status = status; + msg.IsAFK = is_afk; + msg.Activities.push_back(obj); + + m_websocket.Send(nlohmann::json(msg)); + m_user_to_status[m_user_data.ID] = status; + m_signal_presence_update.emit(m_user_data.ID, status); } void DiscordClient::CreateDM(Snowflake user_id) { @@ -589,6 +602,14 @@ void DiscordClient::SetUserAgent(std::string agent) { m_websocket.SetUserAgent(agent); } +std::optional DiscordClient::GetUserStatus(Snowflake id) const { + auto it = m_user_to_status.find(id); + if (it != m_user_to_status.end()) + return it->second; + + return std::nullopt; +} + void DiscordClient::HandleGatewayMessageRaw(std::string str) { // handles multiple zlib compressed messages, calling HandleGatewayMessage when a full message is received std::vector buf(str.begin(), str.end()); @@ -882,11 +903,27 @@ void DiscordClient::HandleGatewayGuildMemberUpdate(const GatewayMessage &msg) { void DiscordClient::HandleGatewayPresenceUpdate(const GatewayMessage &msg) { PresenceUpdateMessage data = msg.Data; - auto cur = m_store.GetUser(data.User.at("id").get()); + const auto user_id = data.User.at("id").get(); + + auto cur = m_store.GetUser(user_id); if (cur.has_value()) { - UserData::update_from_json(data.User, *cur); + cur->update_from_json(data.User); m_store.SetUser(cur->ID, *cur); } + + PresenceStatus e; + if (data.StatusMessage == "online") + e = PresenceStatus::Online; + else if (data.StatusMessage == "offline") + e = PresenceStatus::Offline; + else if (data.StatusMessage == "idle") + e = PresenceStatus::Idle; + else if (data.StatusMessage == "dnd") + e = PresenceStatus::DND; + + m_user_to_status[user_id] = e; + + m_signal_presence_update.emit(user_id, e); } void DiscordClient::HandleGatewayChannelDelete(const GatewayMessage &msg) { @@ -1178,6 +1215,17 @@ void DiscordClient::HandleGatewayGuildMemberListUpdate(const GatewayMessage &msg m_store.SetUser(member->User.ID, member->User); AddUserToGuild(member->User.ID, data.GuildID); m_store.SetGuildMember(data.GuildID, member->User.ID, member->GetAsMemberData()); + if (member->Presence.has_value()) { + const auto &s = member->Presence->Status; + if (s == "online") + m_user_to_status[member->User.ID] = PresenceStatus::Online; + else if (s == "offline") + m_user_to_status[member->User.ID] = PresenceStatus::Offline; + else if (s == "idle") + m_user_to_status[member->User.ID] = PresenceStatus::Idle; + else if (s == "dnd") + m_user_to_status[member->User.ID] = PresenceStatus::DND; + } } } } @@ -1476,3 +1524,7 @@ DiscordClient::type_signal_invite_create DiscordClient::signal_invite_create() { DiscordClient::type_signal_invite_delete DiscordClient::signal_invite_delete() { return m_signal_invite_delete; } + +DiscordClient::type_signal_presence_update DiscordClient::signal_presence_update() { + return m_signal_presence_update; +} diff --git a/discord/discord.hpp b/discord/discord.hpp index cef811e..d585e39 100644 --- a/discord/discord.hpp +++ b/discord/discord.hpp @@ -105,7 +105,8 @@ public: void LeaveGuild(Snowflake id); void KickUser(Snowflake user_id, Snowflake guild_id); void BanUser(Snowflake user_id, Snowflake guild_id); // todo: reason, delete messages - void UpdateStatus(const std::string &status, bool is_afk, const ActivityData &obj); + void UpdateStatus(PresenceStatus status, bool is_afk); + void UpdateStatus(PresenceStatus status, bool is_afk, const ActivityData &obj); void CreateDM(Snowflake user_id); std::optional FindDM(Snowflake user_id); // wont find group dms void AddReaction(Snowflake id, Glib::ustring param); @@ -132,6 +133,8 @@ public: void UpdateToken(std::string token); void SetUserAgent(std::string agent); + std::optional GetUserStatus(Snowflake id) const; + private: static const constexpr int InflateChunkSize = 0x10000; std::vector m_compressed_buf; @@ -192,6 +195,8 @@ private: std::unordered_map> m_guild_to_channels; + std::unordered_map m_user_to_status; + UserData m_user_data; UserSettings m_user_settings; @@ -246,6 +251,7 @@ public: typedef sigc::signal type_signal_guild_ban_add; // guild id, user id typedef sigc::signal type_signal_invite_create; typedef sigc::signal type_signal_invite_delete; + typedef sigc::signal type_signal_presence_update; typedef sigc::signal type_signal_disconnected; // bool true if reconnecting typedef sigc::signal type_signal_connected; @@ -271,6 +277,7 @@ public: 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_presence_update signal_presence_update(); type_signal_disconnected signal_disconnected(); type_signal_connected signal_connected(); @@ -297,6 +304,7 @@ protected: 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_presence_update m_signal_presence_update; type_signal_disconnected m_signal_disconnected; type_signal_connected m_signal_connected; }; diff --git a/discord/objects.cpp b/discord/objects.cpp index 4cc03e0..b08f796 100644 --- a/discord/objects.cpp +++ b/discord/objects.cpp @@ -44,6 +44,7 @@ void from_json(const nlohmann::json &j, GuildMemberListUpdateMessage::MemberItem JS_N("hoisted_role", m.HoistedRole); JS_ON("premium_since", m.PremiumSince); JS_ON("nick", m.Nickname); + JS_ON("presence", m.Presence); m.m_member_data = j; } @@ -85,7 +86,24 @@ void to_json(nlohmann::json &j, const LazyLoadRequestMessage &m) { void to_json(nlohmann::json &j, const UpdateStatusMessage &m) { j["op"] = GatewayOp::UpdateStatus; - j["d"] = m.Presence; + j["d"] = nlohmann::json::object(); + j["d"]["since"] = m.Since; + j["d"]["activities"] = m.Activities; + j["d"]["afk"] = m.IsAFK; + switch (m.Status) { + case PresenceStatus::Online: + j["d"]["status"] = "online"; + break; + case PresenceStatus::Offline: + j["d"]["status"] = "offline"; + break; + case PresenceStatus::Idle: + j["d"]["status"] = "idle"; + break; + case PresenceStatus::DND: + j["d"]["status"] = "dnd"; + break; + } } void from_json(const nlohmann::json &j, ReadyEventData &m) { @@ -170,7 +188,7 @@ void from_json(const nlohmann::json &j, GuildMemberUpdateMessage &m) { JS_D("joined_at", m.JoinedAt); } -void from_json(const nlohmann::json &j, ClientStatus &m) { +void from_json(const nlohmann::json &j, ClientStatusData &m) { JS_O("desktop", m.Desktop); JS_O("mobile", m.Mobile); JS_O("web", m.Web); @@ -180,8 +198,8 @@ void from_json(const nlohmann::json &j, PresenceUpdateMessage &m) { m.User = j.at("user"); JS_O("guild_id", m.GuildID); JS_D("status", m.StatusMessage); - // JS_D("activities", m.Activities); - JS_D("client_status", m.Status); + JS_D("activities", m.Activities); + JS_D("client_status", m.ClientStatus); } void to_json(nlohmann::json &j, const CreateDMObject &m) { diff --git a/discord/objects.hpp b/discord/objects.hpp index e7e574c..0b3e92d 100644 --- a/discord/objects.hpp +++ b/discord/objects.hpp @@ -129,15 +129,15 @@ struct GuildMemberListUpdateMessage { }; struct MemberItem : Item { - UserData User; // - std::vector Roles; // - // PresenceData Presence; // + UserData User; + std::vector Roles; + std::optional Presence; std::string PremiumSince; // opt std::string Nickname; // opt - bool IsMuted; // - std::string JoinedAt; // - std::string HoistedRole; // null - bool IsDefeaned; // + bool IsMuted; + std::string JoinedAt; + std::string HoistedRole; // null + bool IsDefeaned; GuildMember GetAsMemberData() const; @@ -177,7 +177,10 @@ struct LazyLoadRequestMessage { }; struct UpdateStatusMessage { - PresenceData Presence; + int Since = 0; + std::vector Activities; + PresenceStatus Status; + bool IsAFK = false; friend void to_json(nlohmann::json &j, const UpdateStatusMessage &m); }; @@ -278,20 +281,20 @@ struct GuildMemberUpdateMessage { friend void from_json(const nlohmann::json &j, GuildMemberUpdateMessage &m); }; -struct ClientStatus { - std::string Desktop; // opt - std::string Mobile; // opt - std::string Web; // opt +struct ClientStatusData { + std::optional Desktop; + std::optional Mobile; + std::optional Web; - friend void from_json(const nlohmann::json &j, ClientStatus &m); + friend void from_json(const nlohmann::json &j, ClientStatusData &m); }; struct PresenceUpdateMessage { nlohmann::json User; // the client updates an existing object from this data - Snowflake GuildID; // opt + std::optional GuildID; std::string StatusMessage; - // std::vector Activities; - ClientStatus Status; + std::vector Activities; + ClientStatusData ClientStatus; friend void from_json(const nlohmann::json &j, PresenceUpdateMessage &m); }; diff --git a/discord/user.cpp b/discord/user.cpp index 443365d..fc7995e 100644 --- a/discord/user.cpp +++ b/discord/user.cpp @@ -68,21 +68,21 @@ void to_json(nlohmann::json &j, const UserData &m) { JS_IF("phone", m.Phone); } -void UserData::update_from_json(const nlohmann::json &j, UserData &m) { - JS_RD("username", m.Username); - JS_RD("discriminator", m.Discriminator); - JS_RD("avatar", m.Avatar); - JS_RD("bot", m.IsBot); - JS_RD("system", m.IsSystem); - JS_RD("mfa_enabled", m.IsMFAEnabled); - JS_RD("locale", m.Locale); - JS_RD("verified", m.IsVerified); - JS_RD("email", m.Email); - JS_RD("flags", m.Flags); - JS_RD("premium_type", m.PremiumType); - JS_RD("public_flags", m.PublicFlags); - JS_RD("desktop", m.IsDesktop); - JS_RD("mobile", m.IsMobile); - JS_RD("nsfw_allowed", m.IsNSFWAllowed); - JS_RD("phone", m.Phone); +void UserData::update_from_json(const nlohmann::json &j) { + JS_RD("username", Username); + JS_RD("discriminator", Discriminator); + JS_RD("avatar", Avatar); + JS_RD("bot", IsBot); + JS_RD("system", IsSystem); + JS_RD("mfa_enabled", IsMFAEnabled); + JS_RD("locale", Locale); + JS_RD("verified", IsVerified); + JS_RD("email", Email); + JS_RD("flags", Flags); + JS_RD("premium_type", PremiumType); + JS_RD("public_flags", PublicFlags); + JS_RD("desktop", IsDesktop); + JS_RD("mobile", IsMobile); + JS_RD("nsfw_allowed", IsNSFWAllowed); + JS_RD("phone", Phone); } diff --git a/discord/user.hpp b/discord/user.hpp index b82a07b..4e6461e 100644 --- a/discord/user.hpp +++ b/discord/user.hpp @@ -26,7 +26,7 @@ struct UserData { friend void from_json(const nlohmann::json &j, UserData &m); friend void to_json(nlohmann::json &j, const UserData &m); - static void update_from_json(const nlohmann::json &j, UserData &m); + void update_from_json(const nlohmann::json &j); bool HasAvatar() const; bool HasAnimatedAvatar() const;