From 0f77b6f84b7fb4eca715ef3191a13ed0291f37c0 Mon Sep 17 00:00:00 2001 From: George Hazan Date: Tue, 2 Jan 2024 15:49:18 +0300 Subject: fixes #4091 (Discord: do not display hidden channels) --- protocols/Discord/src/dispatch.cpp | 9 +- protocols/Discord/src/gateway.cpp | 2 +- protocols/Discord/src/groupchat.cpp | 11 +- protocols/Discord/src/guilds.cpp | 224 +++++++++++++++++++++++++----------- protocols/Discord/src/proto.cpp | 2 +- protocols/Discord/src/proto.h | 85 +++++++++++--- protocols/Discord/src/stdafx.h | 1 + protocols/Discord/src/version.h | 6 +- 8 files changed, 248 insertions(+), 92 deletions(-) (limited to 'protocols') diff --git a/protocols/Discord/src/dispatch.cpp b/protocols/Discord/src/dispatch.cpp index 2f3686af46..536eff688f 100644 --- a/protocols/Discord/src/dispatch.cpp +++ b/protocols/Discord/src/dispatch.cpp @@ -131,7 +131,7 @@ void CDiscordProto::OnCommandChannelUpdated(const JSONNode &pRoot) CMStringW wszName = pRoot["name"].as_mstring(); if (!wszName.IsEmpty()) { - CMStringW wszNewName = pGuild->wszName + L"#" + wszName; + CMStringW wszNewName = pGuild->m_wszName + L"#" + wszName; Chat_ChangeSessionName(pUser->si, wszNewName); } @@ -245,7 +245,7 @@ void CDiscordProto::OnCommandGuildMemberListUpdate(const JSONNode &pRoot) } } - pGuild->bSynced = true; + pGuild->m_bSynced = true; } void CDiscordProto::OnCommandGuildMemberRemoved(const JSONNode &pRoot) @@ -310,9 +310,8 @@ void CDiscordProto::OnCommandGuildMemberUpdated(const JSONNode &pRoot) void CDiscordProto::OnCommandRoleCreated(const JSONNode &pRoot) { - CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"])); - if (pGuild != nullptr) - ProcessRole(pGuild, pRoot["role"]); + if (auto *pGuild = FindGuild(::getId(pRoot["guild_id"]))) + pGuild->ProcessRole(pRoot["role"]); } void CDiscordProto::OnCommandRoleDeleted(const JSONNode &pRoot) diff --git a/protocols/Discord/src/gateway.cpp b/protocols/Discord/src/gateway.cpp index 04958142be..543020f105 100644 --- a/protocols/Discord/src/gateway.cpp +++ b/protocols/Discord/src/gateway.cpp @@ -257,7 +257,7 @@ void CDiscordProto::GatewaySendGuildInfo(CDiscordGuild *pGuild) JSONNode channels; channels.set_name("channels"); channels << chl; JSONNode payload; payload.set_name("d"); - payload << SINT64_PARAM("guild_id", pGuild->id) << BOOL_PARAM("typing", true) << BOOL_PARAM("activities", true) << BOOL_PARAM("presences", true) << channels; + payload << SINT64_PARAM("guild_id", pGuild->m_id) << BOOL_PARAM("typing", true) << BOOL_PARAM("activities", true) << BOOL_PARAM("presences", true) << channels; JSONNode root; root << INT_PARAM("op", OPCODE_REQUEST_SYNC_CHANNEL) << payload; diff --git a/protocols/Discord/src/groupchat.cpp b/protocols/Discord/src/groupchat.cpp index 67cf07d2fb..22f2981858 100644 --- a/protocols/Discord/src/groupchat.cpp +++ b/protocols/Discord/src/groupchat.cpp @@ -26,11 +26,20 @@ enum { ///////////////////////////////////////////////////////////////////////////////////////// +static int SortRolesByPosition(const CDiscordRole *p1, const CDiscordRole *p2) +{ + return p1->position - p2->position; +} + void BuildStatusList(const CDiscordGuild *pGuild, SESSION_INFO *si) { Chat_AddGroup(si, L"@owner"); + LIST roles(pGuild->arRoles.getCount(), SortRolesByPosition); for (auto &it : pGuild->arRoles) + roles.insert(it); + + for (auto &it : roles) Chat_AddGroup(si, it->wszName); } @@ -146,7 +155,7 @@ void CDiscordProto::Chat_ProcessLogMenu(GCHOOK *gch) es.recentCount = 5; if (EnterString(&es)) { JSONNode root; root << WCHAR_PARAM("nick", es.ptszResult); - CMStringA szUrl(FORMAT, "/guilds/%lld/members/@me/nick", pUser->pGuild->id); + CMStringA szUrl(FORMAT, "/guilds/%lld/members/@me/nick", pUser->pGuild->m_id); Push(new AsyncHttpRequest(this, REQUEST_PATCH, szUrl, nullptr, &root)); mir_free(es.ptszResult); } diff --git a/protocols/Discord/src/guilds.cpp b/protocols/Discord/src/guilds.cpp index d4b8999a13..a8f76bbfbe 100644 --- a/protocols/Discord/src/guilds.cpp +++ b/protocols/Discord/src/guilds.cpp @@ -17,36 +17,6 @@ along with this program. If not, see . #include "stdafx.h" -int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2); - -static int compareRoles(const CDiscordRole *p1, const CDiscordRole *p2) -{ - return compareInt64(p1->id, p2->id); -} - -static int compareChatUsers(const CDiscordGuildMember *p1, const CDiscordGuildMember *p2) -{ - return compareInt64(p1->userId, p2->userId); -} - -CDiscordGuild::CDiscordGuild(SnowFlake _id) : - id(_id), - arChannels(10, compareUsers), - arChatUsers(30, compareChatUsers), - arRoles(10, compareRoles) -{ -} - -CDiscordGuild::~CDiscordGuild() -{ -} - -CDiscordUser::~CDiscordUser() -{ - if (pGuild != nullptr) - pGuild->arChannels.remove(this); -} - ///////////////////////////////////////////////////////////////////////////////////////// // reads a presence block from json @@ -70,25 +40,6 @@ void CDiscordProto::ProcessPresence(const JSONNode &root) } } -///////////////////////////////////////////////////////////////////////////////////////// -// reads a role from json - -void CDiscordProto::ProcessRole(CDiscordGuild *guild, const JSONNode &role) -{ - SnowFlake id = ::getId(role["id"]); - CDiscordRole *p = guild->arRoles.find((CDiscordRole*)&id); - if (p == nullptr) { - p = new CDiscordRole(); - p->id = id; - guild->arRoles.insert(p); - } - - p->color = role["color"].as_int(); - p->position = role["position"].as_int(); - p->permissions = role["permissions"].as_int(); - p->wszName = role["name"].as_mstring(); -} - ///////////////////////////////////////////////////////////////////////////////////////// static void sttSetGroupName(MCONTACT hContact, const wchar_t *pwszGroupName) @@ -134,7 +85,7 @@ void CDiscordProto::CreateChat(CDiscordGuild *pGuild, CDiscordUser *pUser) if (pParent != nullptr) sttSetGroupName(pUser->hContact, pParent->wszChannelName); } - else sttSetGroupName(pUser->hContact, Clist_GroupGetName(pGuild->groupId)); + else sttSetGroupName(pUser->hContact, Clist_GroupGetName(pGuild->m_groupId)); } BuildStatusList(pGuild, si); @@ -163,31 +114,28 @@ void CDiscordProto::ProcessGuild(const JSONNode &pRoot) arGuilds.insert(pGuild); } - pGuild->ownerId = ::getId(pRoot["owner_id"]); - pGuild->wszName = pRoot["name"].as_mstring(); + pGuild->m_ownerId = ::getId(pRoot["owner_id"]); + pGuild->m_wszName = pRoot["name"].as_mstring(); if (m_bUseGuildGroups) - pGuild->groupId = Clist_GroupCreate(Clist_GroupExists(m_wszDefaultGroup), pGuild->wszName); + pGuild->m_groupId = Clist_GroupCreate(Clist_GroupExists(m_wszDefaultGroup), pGuild->m_wszName); - SESSION_INFO *si = Chat_NewSession(GCW_SERVER, m_szModuleName, pGuild->wszName, pGuild->wszName, pGuild); + SESSION_INFO *si = Chat_NewSession(GCW_SERVER, m_szModuleName, pGuild->m_wszName, pGuild->m_wszName, pGuild); if (si == nullptr) return; pGuild->pParentSi = (SESSION_INFO*)si; - pGuild->hContact = si->hContact; - setId(pGuild->hContact, DB_KEY_CHANNELID, guildId); + pGuild->m_hContact = si->hContact; + setId(pGuild->m_hContact, DB_KEY_CHANNELID, guildId); Chat_Control(si, WINDOW_HIDDEN); Chat_Control(si, SESSION_ONLINE); for (auto &it : pRoot["roles"]) - ProcessRole(pGuild, it); + pGuild->ProcessRole(it); BuildStatusList(pGuild, si); - for (auto &it : pRoot["channels"]) - ProcessGuildChannel(pGuild, it); - - if (!pGuild->bSynced && getByte(si->hContact, "EnableSync")) + if (!pGuild->m_bSynced && getByte(si->hContact, "EnableSync")) GatewaySendGuildInfo(pGuild); // store all guild members @@ -201,6 +149,10 @@ void CDiscordProto::ProcessGuild(const JSONNode &pRoot) pm->iStatus = ID_STATUS_OFFLINE; } + // parse channels + for (auto &it : pRoot["channels"]) + ProcessGuildChannel(pGuild, it); + // parse online statuses for (auto &it : pRoot["presences"]) { CDiscordGuildMember *gm = pGuild->FindUser(::getId(it["user"]["id"])); @@ -214,7 +166,7 @@ void CDiscordProto::ProcessGuild(const JSONNode &pRoot) if (!m_bTerminated) ForkThread(&CDiscordProto::BatchChatCreate, pGuild); - pGuild->bSynced = true; + pGuild->m_bSynced = true; } ///////////////////////////////////////////////////////////////////////////////////////// @@ -226,6 +178,11 @@ CDiscordUser* CDiscordProto::ProcessGuildChannel(CDiscordGuild *pGuild, const JS CMStringW wszName = pch["name"].as_mstring(); CDiscordUser *pUser; + // check permissions to enter the channel + auto permissions = pGuild->CalcPermissionOverride(m_ownId, pch["permission_overwrites"]); + if (!(permissions & Permission::VIEW_CHANNEL)) + return nullptr; + // filter our all channels but the text ones switch (pch["type"].as_int()) { case 4: // channel group @@ -243,7 +200,7 @@ CDiscordUser* CDiscordProto::ProcessGuildChannel(CDiscordGuild *pGuild, const JS pGuild->arChannels.insert(pUser); - MGROUP grpId = Clist_GroupCreate(pGuild->groupId, wszName); + MGROUP grpId = Clist_GroupCreate(pGuild->m_groupId, wszName); pUser->wszChannelName = Clist_GroupGetName(grpId); } return pUser; @@ -265,7 +222,7 @@ CDiscordUser* CDiscordProto::ProcessGuildChannel(CDiscordGuild *pGuild, const JS if (m_bUseGuildGroups) pUser->wszChannelName = L"#" + wszName; else - pUser->wszChannelName = pGuild->wszName + L"#" + wszName; + pUser->wszChannelName = pGuild->m_wszName + L"#" + wszName; pUser->wszTopic = pch["topic"].as_mstring(); pUser->pGuild = pGuild; pUser->lastMsgId = ::getId(pch["last_message_id"]); @@ -282,9 +239,10 @@ CDiscordGuildMember* CDiscordProto::ProcessGuildUser(CDiscordGuild *pGuild, cons { auto& pUser = pRoot["user"]; - bool bNew = false; CMStringW wszUserId = pUser["id"].as_mstring(); SnowFlake userId = _wtoi64(wszUserId); + bool bNew = false, bIsMe = userId == m_ownId; + CDiscordGuildMember *pm = pGuild->FindUser(userId); if (pm == nullptr) { pm = new CDiscordGuildMember(userId); @@ -299,14 +257,25 @@ CDiscordGuildMember* CDiscordProto::ProcessGuildUser(CDiscordGuild *pGuild, cons else bNew = true; - if (userId == pGuild->ownerId) + if (userId == pGuild->m_ownerId) { pm->wszRole = L"@owner"; + pm->permissions = Permission::ALL; + } else { + pm->permissions = pGuild->m_permissions; + CDiscordRole *pRole = nullptr; for (auto &itr : pRoot["roles"]) { SnowFlake roleId = ::getId(itr); - if (pRole = pGuild->arRoles.find((CDiscordRole *)&roleId)) - break; + + if (auto *p = pGuild->arRoles.find((CDiscordRole *)&roleId)) { + pm->permissions |= p->permissions; + if (pRole == nullptr) + pRole = p; + + if (bIsMe) + p->bIsMe = true; + } } pm->wszRole = (pRole == nullptr) ? L"@everyone" : pRole->wszName; } @@ -365,6 +334,125 @@ void CDiscordProto::AddGuildUser(CDiscordGuild *pGuild, const CDiscordGuildMembe } ///////////////////////////////////////////////////////////////////////////////////////// +// CDiscordGuild members + +int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2); + +static int compareRoles(const CDiscordRole *p1, const CDiscordRole *p2) +{ + return compareInt64(p1->id, p2->id); +} + +static int compareChatUsers(const CDiscordGuildMember *p1, const CDiscordGuildMember *p2) +{ + return compareInt64(p1->userId, p2->userId); +} + +CDiscordGuild::CDiscordGuild(SnowFlake _id) : + m_id(_id), + arChannels(10, compareUsers), + arChatUsers(30, compareChatUsers), + arRoles(10, compareRoles) +{} + +CDiscordGuild::~CDiscordGuild() +{} + +CDiscordUser::~CDiscordUser() +{ + if (pGuild != nullptr) + pGuild->arChannels.remove(this); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// calculates effective rights + +uint64_t CDiscordGuild::CalcPermissionOverride(SnowFlake myUserId, const JSONNode &json) +{ + if (myUserId == m_ownerId) + return Permission::ALL; + + uint64_t permissions = m_permissions; + + if (auto *pUser = FindUser(myUserId)) { + if (pUser->permissions & Permission::ADMIN) + return Permission::ALL; + + permissions = pUser->permissions; + } + + struct Item + { + Item() : + allow(0), + deny(0) + {} + + Item(SnowFlake _1, SnowFlake _2) : + allow(_1), + deny(_2) + {} + + SnowFlake allow, deny; + }; + std::map items; + + // verify permissions + for (auto &it : json) { + if (it["type"].as_int() != 0) + continue; + + SnowFlake id = ::getId(it["id"]); + items[id] = Item(::getId(it["allow"]), ::getId(it["deny"])); + } + + auto everyone = items[m_id]; + permissions &= ~everyone.deny; + permissions |= everyone.allow; + + uint64_t allow = 0, deny = 0; + for (auto &it : arRoles) { + if (it->bIsMe) { + auto role = items[it->id]; + deny |= role.deny; + allow |= role.allow; + } + } + + permissions &= ~deny; + permissions |= allow; + + auto personal = items[myUserId]; + permissions &= ~personal.deny; + permissions |= personal.allow; + + return permissions; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// reads a role from json + +void CDiscordGuild::ProcessRole(const JSONNode &role) +{ + SnowFlake id = ::getId(role["id"]); + CDiscordRole *p = arRoles.find((CDiscordRole *)&id); + if (p == nullptr) { + p = new CDiscordRole(); + p->id = id; + arRoles.insert(p); + } + + p->color = role["color"].as_int(); + p->position = role["position"].as_int(); + p->permissions = ::getId(role["permissions"]); + p->wszName = role["name"].as_mstring(); + + if (m_id == id) + m_permissions = p->permissions; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Persistence manager void CDiscordGuild::LoadFromFile() { diff --git a/protocols/Discord/src/proto.cpp b/protocols/Discord/src/proto.cpp index 7e55ea5d65..1e76ead10d 100644 --- a/protocols/Discord/src/proto.cpp +++ b/protocols/Discord/src/proto.cpp @@ -34,7 +34,7 @@ int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2) static int compareGuilds(const CDiscordGuild *p1, const CDiscordGuild *p2) { - return compareInt64(p1->id, p2->id); + return compareInt64(p1->m_id, p2->m_id); } CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) : diff --git a/protocols/Discord/src/proto.h b/protocols/Discord/src/proto.h index 3ff1f626e5..e34f6cbfa9 100644 --- a/protocols/Discord/src/proto.h +++ b/protocols/Discord/src/proto.h @@ -3,6 +3,58 @@ #define EVENT_INCOMING_CALL 10001 #define EVENT_CALL_FINISHED 10002 +enum Permission : uint64_t +{ + CREATE_INVITE = (1ll << 0), // Allows creation of instant invites + KICK_MEMBERS = (1ll << 1), // Allows kicking members + BAN_MEMBERS = (1ll << 2), // Allows banning members + ADMIN = (1ll << 3), // Allows all permissions and bypasses channel permission overwrites + MANAGE_CHANNELS = (1ll << 4), // Allows management and editing of channels + MANAGE_GUILD = (1ll << 5), // Allows management and editing of the guild + ADD_REACTIONS = (1ll << 6), // Allows for the addition of reactions to messages + VIEW_AUDIT_LOG = (1ll << 7), // Allows for viewing of audit logs + PRIORITY_SPEAKER = (1ll << 8), // Allows for using priority speaker in a voice channel + STREAM = (1ll << 9), // Allows the user to go live + VIEW_CHANNEL = (1ll << 10), // Allows guild members to view a channel, which includes reading messages in text channels and joining voice channels + SEND_MESSAGES = (1ll << 11), // Allows for sending messages in a channel and creating threads in a forum (does not allow sending messages in threads) + SEND_TTS_MESSAGES = (1ll << 12), // Allows for sending of /tts messages + MANAGE_MESSAGES = (1ll << 13), // Allows for deletion of other users messages + EMBED_LINKS = (1ll << 14), // Links sent by users with this permission will be auto-embedded + ATTACH_FILES = (1ll << 15), // Allows for uploading images and files + READ_HISTORY = (1ll << 16), // Allows for reading of message history + MENTION_EVERYONE = (1ll << 17), // Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel + USE_EXT_EMOJI = (1ll << 18), // Allows the usage of custom emojis from other servers + VIEW_INSIGHTS = (1ll << 19), // Allows for viewing guild insights + VOICE_CONNECT = (1ll << 20), // Allows for joining of a voice channel + VOICE_SPEAK = (1ll << 21), // Allows for speaking in a voice channel + VOICE_MUTE = (1ll << 22), // Allows for muting members in a voice channel + VOICE_DEAFEN = (1ll << 23), // Allows for deafening of members in a voice channel + VOICE_MOVE = (1ll << 24), // Allows for moving of members between voice channels + USE_VAD = (1ll << 25), // Allows for using voice-activity-detection in a voice channel + CHANGE_NICKNAME = (1ll << 26), // Allows for modification of own nickname + MANAGE_NICKS = (1ll << 27), // Allows for modification of other users nicknames + MANAGE_ROLES = (1ll << 28), // Allows management and editing of roles + MANAGE_WEBHOOKS = (1ll << 29), // + MANAGE_EMOJI = (1ll << 30), // Allows for editing and deleting emojis, stickers, and soundboard sounds created by all users + USE_APP_COMMANDS = (1ll << 31), // Allows members to use application commands, including slash commands and context menu commands. + REQUEST_TO_SPEAK = (1ll << 32), // Allows for requesting to speak in stage channels. + MANAGE_EVENTS = (1ll << 33), // Allows for editing and deleting scheduled events created by all users + MANAGE_THREADS = (1ll << 34), // Allows for deleting and archiving threads, and viewing all private threads + PUBLIC_THREADS = (1ll << 35), // Allows for creating public and announcement threads + PRIVATE_THREADS = (1ll << 36), // Allows for creating private threads + USE_EXT_STICKERS = (1ll << 37), // Allows the usage of custom stickers from other servers + SEND_THREADS = (1ll << 38), // Allows for sending messages in threads + EMBED_ACTIVITY = (1ll << 39), // Allows for using Activities (applications with the EMBEDDED flag) in a voice channel + MODERATE_MEMBERS = (1ll << 40), // Allows for timing out users to prevent them from sending or reacting to messages in chat and threads, and from speaking in voice and stage channels + VIEW_MONETIZATION = (1ll << 41), // Allows for viewing role subscription insights + USE_SOUNDBOARD = (1ll << 42), // Allows for using soundboard in a voice channel + CREATE_EXPRESSIONS = (1ll << 43), // Allows for creating emojis, stickers, and soundboard sounds, and editing and deleting those created by the current user. + CREATE_EVEMTS = (1ll << 44), // Allows for creating scheduled events, and editing and deleting those created by the current user. + USE_EXT_SOUNDS = (1ll << 45), // Allows the usage of custom soundboard sounds from other servers + SEND_VOICE = (1ll << 46), // Allows sending voice messages + ALL = -1ll +}; + typedef __int64 SnowFlake; __forceinline int compareInt64(const SnowFlake i1, const SnowFlake i2) @@ -41,9 +93,10 @@ public: struct CDiscordRole : public MZeroedObject { SnowFlake id; + uint64_t permissions; COLORREF color; - uint32_t permissions; int position; + bool bIsMe; CMStringW wszName; }; @@ -104,6 +157,7 @@ struct CDiscordGuildMember : public MZeroedObject {} SnowFlake userId; + uint64_t permissions; CMStringW wszDiscordId, wszNick, wszRole; int iStatus; }; @@ -113,27 +167,33 @@ struct CDiscordGuild : public MZeroedObject CDiscordGuild(SnowFlake _id); ~CDiscordGuild(); - __forceinline CDiscordGuildMember* FindUser(SnowFlake userId) - { + __forceinline CDiscordGuildMember* FindUser(SnowFlake userId) { return arChatUsers.find((CDiscordGuildMember *)&userId); } - __inline CMStringW GetCacheFile() const - { - return CMStringW(FORMAT, L"%s\\DiscordCache\\%lld.json", VARSW(L"%miranda_userdata%").get(), id); + __forceinline CDiscordRole* FindRole(SnowFlake id) { + return arRoles.find((CDiscordRole *)&id); } - SnowFlake id, ownerId; - CMStringW wszName; - MCONTACT hContact; - MGROUP groupId; - bool bSynced = false; - LIST arChannels; + __forceinline CMStringW GetCacheFile() const { + return CMStringW(FORMAT, L"%s\\DiscordCache\\%lld.json", VARSW(L"%miranda_userdata%").get(), m_id); + } + SnowFlake m_id, m_ownerId; + uint64_t m_permissions = 0; // my effective permissions + CMStringW m_wszName; + MCONTACT m_hContact; + MGROUP m_groupId; + bool m_bSynced = false; + SESSION_INFO *pParentSi; + LIST arChannels; OBJLIST arChatUsers; OBJLIST arRoles; // guild roles + uint64_t CalcPermissionOverride(SnowFlake myUserId, const JSONNode &json); + void ProcessRole(const JSONNode &json); + void LoadFromFile(); void SaveToFile(); }; @@ -315,7 +375,6 @@ class CDiscordProto : public PROTO void AddGuildUser(CDiscordGuild *guild, const CDiscordGuildMember &pUser); void ProcessGuild(const JSONNode &json); void ProcessPresence(const JSONNode &json); - void ProcessRole(CDiscordGuild *guild, const JSONNode &json); void ProcessType(CDiscordUser *pUser, const JSONNode &json); CDiscordUser* ProcessGuildChannel(CDiscordGuild *guild, const JSONNode &json); diff --git a/protocols/Discord/src/stdafx.h b/protocols/Discord/src/stdafx.h index 48d68292dd..7588a978d8 100644 --- a/protocols/Discord/src/stdafx.h +++ b/protocols/Discord/src/stdafx.h @@ -16,6 +16,7 @@ #include #include +#include #include #include "resource.h" diff --git a/protocols/Discord/src/version.h b/protocols/Discord/src/version.h index 1a33efa401..e1bdff0418 100644 --- a/protocols/Discord/src/version.h +++ b/protocols/Discord/src/version.h @@ -1,7 +1,7 @@ #define __MAJOR_VERSION 0 -#define __MINOR_VERSION 6 -#define __RELEASE_NUM 2 -#define __BUILD_NUM 11 +#define __MINOR_VERSION 96 +#define __RELEASE_NUM 5 +#define __BUILD_NUM 1 #include -- cgit v1.2.3