diff --git a/README.md b/README.md index 98638cf56c..0b2fd2cc7f 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,8 @@ You can find more examples in our [example page](https://dpp.dev/md_docpages_03_ ### Linux The library runs ideally on **Linux**. -### Mac OS X and FreeBSD -The library is well-functional and stable on **Mac OS X** and **FreeBSD** too. +### Mac OS X and FreeBSD and OpenBSD +The library is well-functional and stable on **Mac OS X** and **FreeBSD** and **OpenBSD** too. ### Raspberry Pi For running your bot on a **Raspberry Pi**, we offer a prebuilt .deb package for ARM64, ARM6, and ARM7 so that you do not have to wait for it to compile. diff --git a/include/dpp/guild.h b/include/dpp/guild.h index dd002319dc..15c76d6989 100644 --- a/include/dpp/guild.h +++ b/include/dpp/guild.h @@ -180,6 +180,8 @@ enum guild_member_flags : uint16_t { gm_bypasses_verification = 0b0000000010000000, /** Member has started onboarding */ gm_started_onboarding = 0b0000000100000000, + gm_roles_action = 0b0000001000000000, + gm_nickname_action = 0b0000010000000000, }; /** @@ -187,11 +189,16 @@ enum guild_member_flags : uint16_t { * This contains the user's nickname, guild roles, and any other guild-specific flags. */ class DPP_EXPORT guild_member { -public: +protected: /** Nickname, or empty string if they don't have a nickname on this guild */ std::string nickname; /** List of roles this user has on this guild */ std::vector roles; + /** A set of flags built from the bitmask defined by dpp::guild_member_flags */ + uint16_t flags; + + friend void from_json(const nlohmann::json& j, guild_member& gm); +public: /** Guild id */ snowflake guild_id; /** User id */ @@ -204,8 +211,6 @@ class DPP_EXPORT guild_member { time_t joined_at; /** Boosting since */ time_t premium_since; - /** A set of flags built from the bitmask defined by dpp::guild_member_flags */ - uint16_t flags; /** Default constructor */ guild_member(); @@ -322,6 +327,20 @@ class DPP_EXPORT guild_member { */ guild_member& set_nickname(const std::string& nick); + /** + * @brief Get the nickname + * + * @return std::string nickname + */ + std::string get_nickname() const; + + /** + * @brief Get the roles + * + * @return std::vector roles + */ + const std::vector& get_roles() const; + /** * @brief Find the dpp::user object for this member. This is an alias for dpp::find_user * @return dpp::user* Pointer to the user object. If not in cache, it returns nullptr @@ -378,6 +397,40 @@ class DPP_EXPORT guild_member { * @return std::string mention */ std::string get_mention() const; + + /** + * @brief Add a role to this member + * @note This call sets the role change bit, which causes the new role + * list to be sent if this is passed to dpp::cluster::guild_edit_member + * or dpp::cluster::guild_add_member + * + * @param role_id Role ID to add + * @return guild_member& Reference to self + */ + guild_member& add_role(dpp::snowflake role_id); + + /** + * @brief Remove a role from this member + * @note This call sets the role change bit, which causes the new role + * list to be sent if this is passed to dpp::cluster::guild_edit_member + * or dpp::cluster::guild_add_member + * + * @param role_id Role ID to remove + * @return guild_member& Reference to self + */ + guild_member& remove_role(dpp::snowflake role_id); + + /** + * @brief Set a new role list for this member + * @note This call sets the role change bit, which causes the new role + * list to be sent if this is passed to dpp::cluster::guild_edit_member + * or dpp::cluster::guild_add_member + * + * @param role_ids Roles to set + * @return guild_member& Reference to self + */ + guild_member& set_roles(const std::vector &role_ids); + }; /** diff --git a/src/dpp/guild.cpp b/src/dpp/guild.cpp index 27ba737ecf..cc0f4a01d2 100644 --- a/src/dpp/guild.cpp +++ b/src/dpp/guild.cpp @@ -97,12 +97,12 @@ guild::guild() : guild_member::guild_member() : + flags(0), guild_id(0), user_id(0), communication_disabled_until(0), joined_at(0), - premium_since(0), - flags(0) + premium_since(0) { } @@ -112,6 +112,34 @@ std::string guild_member::get_mention() const { guild_member& guild_member::set_nickname(const std::string& nick) { this->nickname = nick; + this->flags |= gm_nickname_action; + return *this; +} + +guild_member& guild_member::add_role(dpp::snowflake role_id) { + roles.emplace_back(role_id); + flags |= gm_roles_action; + return *this; +} + +guild_member& guild_member::remove_role(dpp::snowflake role_id) { + roles.erase(std::remove(roles.begin(), roles.end(), role_id), roles.end()); + flags |= gm_roles_action; + return *this; +} + +std::string guild_member::get_nickname() const { + return nickname; +} + +const std::vector& guild_member::get_roles() const { + return roles; +} + + +guild_member& guild_member::set_roles(const std::vector &role_ids) { + roles = role_ids; + flags |= gm_roles_action; return *this; } @@ -189,8 +217,8 @@ void from_json(const nlohmann::json& j, guild_member& gm) { std::string guild_member::get_avatar_url(uint16_t size, const image_type format, bool prefer_animated) const { if (this->guild_id && this->user_id && !this->avatar.to_string().empty()) { return utility::cdn_endpoint_url_hash({ i_jpg, i_png, i_webp, i_gif }, - "guilds/" + std::to_string(this->guild_id) + "/" + std::to_string(this->user_id), this->avatar.to_string(), - format, size, prefer_animated, has_animated_guild_avatar()); + "guilds/" + std::to_string(this->guild_id) + "/" + std::to_string(this->user_id), this->avatar.to_string(), + format, size, prefer_animated, has_animated_guild_avatar()); } else { return std::string(); } @@ -211,19 +239,19 @@ std::string guild_member::build_json(bool with_id) const { } } - if (!this->nickname.empty()) { - j["nick"] = this->nickname; - } else { - j["nick"] = json::value_t::null; + if (this->flags & gm_nickname_action) { + if (!this->nickname.empty()) { + j["nick"] = this->nickname; + } else { + j["nick"] = json::value_t::null; + } } - if (!this->roles.empty()) { + if (this->flags & gm_roles_action) { j["roles"] = {}; - for (auto & role : this->roles) { + for (const auto & role : this->roles) { j["roles"].push_back(std::to_string(role)); } - } else { - j["roles"] = {}; } if (this->flags & gm_voice_action) { @@ -718,7 +746,7 @@ permission guild::base_permissions(const guild_member &member) const { permission permissions = everyone->permissions; - for (auto& rid : member.roles) { + for (auto& rid : member.get_roles()) { role* r = dpp::find_role(rid); if (r) { permissions |= r->permissions; @@ -765,7 +793,7 @@ permission guild::permission_overwrites(const uint64_t base_permissions, const u uint64_t allow = 0; uint64_t deny = 0; - for (auto& rid : gm.roles) { + for (auto& rid : gm.get_roles()) { /* Skip \@everyone role to not break the hierarchy. It's calculated above */ if (rid == this->id) { @@ -821,7 +849,7 @@ permission guild::permission_overwrites(const guild_member &member, const channe uint64_t allow = 0; uint64_t deny = 0; - for (auto& rid : member.roles) { + for (auto& rid : member.get_roles()) { /* Skip \@everyone role to not break the hierarchy. It's calculated above */ if (rid == this->id) { diff --git a/src/dpp/role.cpp b/src/dpp/role.cpp index ca4ae672a5..24147055d4 100644 --- a/src/dpp/role.cpp +++ b/src/dpp/role.cpp @@ -413,8 +413,9 @@ members_container role::get_members() const { } for (auto & m : g->members) { /* Iterate all members and use std::find on their role list to see who has this role */ - auto i = std::find(m.second.roles.begin(), m.second.roles.end(), this->id); - if (i != m.second.roles.end()) { + const auto& r = m.second.get_roles(); + auto i = std::find(r.begin(), r.end(), this->id); + if (i != r.end()) { gm[m.second.user_id] = m.second; } } @@ -425,8 +426,8 @@ members_container role::get_members() const { std::string role::get_icon_url(uint16_t size, const image_type format) const { if (!this->icon.to_string().empty() && this->id) { return utility::cdn_endpoint_url({ i_jpg, i_png, i_webp }, - "role-icons/" + std::to_string(this->id) + "/" + this->icon.to_string(), - format, size); + "role-icons/" + std::to_string(this->id) + "/" + this->icon.to_string(), + format, size); } else { return std::string(); } diff --git a/src/unittest/coro.cpp b/src/unittest/coro.cpp index 677e19d32f..1e6e819cf0 100644 --- a/src/unittest/coro.cpp +++ b/src/unittest/coro.cpp @@ -460,7 +460,7 @@ void event_handler_test(dpp::cluster *bot) { if (!pair.first.has_value()) { co_return {}; } - const std::string& member_nick = pair.second.has_value() ? pair.second->nickname : ""; + const std::string& member_nick = pair.second.has_value() ? pair.second->get_nickname() : ""; const std::string& user_nick = pair.first->username; result = co_await bot->co_message_edit(msg.set_content("coro " + (member_nick.empty() ? user_nick : member_nick) + " " + std::to_string(i))); co_return result.is_error() ? dpp::snowflake{} : std::get(result.value).id; diff --git a/src/unittest/test.h b/src/unittest/test.h index 1b8de4c64a..c3b295ae45 100644 --- a/src/unittest/test.h +++ b/src/unittest/test.h @@ -131,7 +131,6 @@ DPP_TEST(OPTCHOICE_STRING, "command_option_choice::fill_from_json: string", tf_o DPP_TEST(HOSTINFO, "https_client::get_host_info()", tf_offline); DPP_TEST(HTTPS, "https_client HTTPS request", tf_online); DPP_TEST(HTTP, "https_client HTTP request", tf_offline); -DPP_TEST(MULTIHEADER, "multiheader cookie test", tf_offline); DPP_TEST(RUNONCE, "run_once", tf_offline); DPP_TEST(WEBHOOK, "webhook construct from URL", tf_offline); DPP_TEST(MD_ESC_1, "Markdown escaping (ignore code block contents)", tf_offline); @@ -221,6 +220,8 @@ DPP_TEST(INVITE_DELETE, "cluster::invite_delete", tf_online); /* Extended set -- Less important, skipped on the master branch due to rate limits and GitHub actions limitations*/ /* To execute, run unittests with "full" command line argument */ +DPP_TEST(MULTIHEADER, "multiheader cookie test", tf_offline | tf_extended); // Fails in the EU as cookies are not sent without acceptance + DPP_TEST(VOICECONN, "Connect to voice channel", tf_online | tf_extended); DPP_TEST(VOICESEND, "Send audio to voice channel", tf_online | tf_extended); // udp unreliable on gitbub DPP_TEST(MESSAGEPIN, "Pinning a channel message", tf_online | tf_extended);