/* Copyright © 2016-22 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "stdafx.h" ///////////////////////////////////////////////////////////////////////////////////////// // reads a presence block from json void CDiscordProto::ProcessPresence(const JSONNode &root) { auto userId = ::getId(root["user"]["id"]); CDiscordUser *pUser = FindUser(userId); if (pUser == nullptr) { debugLogA("Presence from unknown user id %lld ignored", userId); return; } CMStringA szVer("Discord"); for (auto &it : root["client_status"]) { if (!mir_strcmp(it.name(), "web")) szVer += " (website)"; else if (!mir_strcmp(it.name(), "mobile")) szVer += " (mobile)"; } if (szVer.GetLength() > 7) setString(pUser->hContact, "MirVer", szVer); else delSetting(pUser->hContact, "MirVer"); setWord(pUser->hContact, "Status", StrToStatus(root["status"].as_mstring())); CheckAvatarChange(pUser->hContact, root["user"]["avatar"].as_mstring()); for (auto &act : root["activities"]) { CMStringW wszStatus(act["state"].as_mstring()); if (!wszStatus.IsEmpty()) db_set_ws(pUser->hContact, "CList", "StatusMsg", wszStatus); } } ///////////////////////////////////////////////////////////////////////////////////////// static void sttSetGroupName(MCONTACT hContact, const wchar_t *pwszGroupName) { ptrW wszOldName(Clist_GetGroup(hContact)); if (wszOldName != nullptr) { CMStringW wszChatGroup(Chat_GetGroup()); if (mir_wstrcmpi(wszOldName, wszChatGroup)) return; // custom group, don't touch it } Clist_SetGroup(hContact, pwszGroupName); } void CDiscordProto::BatchChatCreate(void *param) { CDiscordGuild *pGuild = (CDiscordGuild*)param; for (auto &it : pGuild->arChannels) { if (m_bTerminated) break; if (!it->bIsPrivate && !it->bIsGroup) CreateChat(pGuild, it); } } void CDiscordProto::CreateChat(CDiscordGuild *pGuild, CDiscordUser *pUser) { auto *si = pUser->si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, pUser->wszUsername, pUser->wszChannelName); si->pParent = pGuild->pParentSi; pUser->hContact = si->hContact; setId(pUser->hContact, DB_KEY_ID, pUser->channelId); setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId); SnowFlake oldMsgId = getId(pUser->hContact, DB_KEY_LASTMSGID); if (oldMsgId == 0) RetrieveHistory(pUser, MSG_BEFORE, pUser->lastMsgId + 1, 20); else if (!pUser->bSynced && pUser->lastMsgId > oldMsgId) { pUser->bSynced = true; RetrieveHistory(pUser, MSG_AFTER, oldMsgId, 99); } if (m_bUseGuildGroups) { if (pUser->parentId) { CDiscordUser *pParent = FindUserByChannel(pUser->parentId); if (pParent != nullptr) sttSetGroupName(pUser->hContact, pParent->wszChannelName); } else sttSetGroupName(pUser->hContact, Clist_GroupGetName(pGuild->m_groupId)); } BuildStatusList(pGuild, si); Chat_Control(si, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); Chat_Control(si, SESSION_ONLINE); if (!pUser->wszTopic.IsEmpty()) { Chat_SetStatusbarText(si, pUser->wszTopic); GCEVENT gce = { si, GC_EVENT_TOPIC }; gce.time = time(0); gce.pszText.w = pUser->wszTopic; Chat_Event(&gce); } } void CDiscordProto::ProcessGuild(const JSONNode &pRoot) { if (!m_bUseGroupchats) return; SnowFlake guildId = ::getId(pRoot["id"]); CDiscordGuild *pGuild = FindGuild(guildId); if (pGuild == nullptr) { pGuild = new CDiscordGuild(guildId); pGuild->LoadFromFile(); arGuilds.insert(pGuild); } pGuild->m_ownerId = ::getId(pRoot["owner_id"]); pGuild->m_wszName = pRoot["name"].as_mstring(); if (m_bUseGuildGroups) pGuild->m_groupId = Clist_GroupCreate(Clist_GroupExists(m_wszDefaultGroup), pGuild->m_wszName); 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->m_hContact = si->hContact; setId(pGuild->m_hContact, DB_KEY_CHANNELID, guildId); pGuild->m_bEnableHistory = surelyGetBool(pGuild->m_hContact, DB_KEY_ENABLE_HIST); Chat_Control(si, WINDOW_HIDDEN); Chat_Control(si, SESSION_ONLINE); for (auto &it : pRoot["roles"]) pGuild->ProcessRole(it); BuildStatusList(pGuild, si); bool bEnableSync = getByte(si->hContact, DB_KEY_ENABLE_SYNC); if (!pGuild->m_bSynced && bEnableSync) GatewaySendGuildInfo(pGuild); // store all guild members for (auto &it : pRoot["members"]) { auto *pm = ProcessGuildUser(pGuild, it); CMStringW wszNick = it["nick"].as_mstring(); if (!wszNick.IsEmpty()) pm->wszNick = wszNick; 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"])); if (gm != nullptr) gm->iStatus = StrToStatus(it["status"].as_mstring()); } for (auto &it : pRoot["voice_states"]) pGuild->arVoiceStates.insert(new CDiscordVoiceState(it)); for (auto &it : pGuild->arChatUsers) AddGuildUser(pGuild, *it); if (!m_bTerminated && bEnableSync) ForkThread(&CDiscordProto::BatchChatCreate, pGuild); pGuild->m_bSynced = true; } ///////////////////////////////////////////////////////////////////////////////////////// CDiscordUser* CDiscordProto::ProcessGuildChannel(CDiscordGuild *pGuild, const JSONNode &pch) { CMStringW wszChannelId = pch["id"].as_mstring(); SnowFlake channelId = _wtoi64(wszChannelId); CMStringW wszName = pch["name"].as_mstring(); CDiscordUser *pUser; bool bIsVoice = false; // filter our all channels but the text ones switch (auto iChannelType = pch["type"].as_int()) { case 4: // channel group if (!m_bUseGuildGroups) // ignore groups when they aren't enabled return nullptr; pUser = FindUserByChannel(channelId); if (pUser == nullptr) { // missing channel - create it pUser = new CDiscordUser(channelId); pUser->bIsPrivate = false; pUser->channelId = channelId; pUser->bIsGroup = true; arUsers.insert(pUser); pGuild->arChannels.insert(pUser); MGROUP grpId = Clist_GroupCreate(pGuild->m_groupId, wszName); pUser->wszChannelName = Clist_GroupGetName(grpId); } return pUser; case 2: // voice channel bIsVoice = true; __fallthrough; case 0: // text channel case 5: // announcement channel // check permissions to enter the channel auto permissions = pGuild->CalcPermissionOverride(m_ownId, pch["permission_overwrites"]); if (!(permissions & Permission::VIEW_CHANNEL)) return nullptr; pUser = FindUserByChannel(channelId); if (pUser == nullptr) { // missing channel - create it pUser = new CDiscordUser(channelId); pUser->bIsPrivate = false; pUser->channelId = channelId; arUsers.insert(pUser); } if (pGuild->arChannels.find(pUser) == nullptr) pGuild->arChannels.insert(pUser); // make announcement channels read-only if (iChannelType == 5) Contact::Readonly(pUser->hContact, true); pUser->wszUsername = wszChannelId; if (m_bUseGuildGroups) pUser->wszChannelName = L"#" + wszName; else pUser->wszChannelName = pGuild->m_wszName + L"#" + wszName; pUser->wszTopic = pch["topic"].as_mstring(); pUser->pGuild = pGuild; pUser->bIsVoice = bIsVoice; pUser->lastMsgId = ::getId(pch["last_message_id"]); pUser->parentId = ::getId(pch["parent_id"]); return pUser; } return nullptr; } ///////////////////////////////////////////////////////////////////////////////////////// CDiscordGuildMember* CDiscordProto::ProcessGuildUser(CDiscordGuild *pGuild, const JSONNode &pRoot, bool *pbNew) { auto& pUser = pRoot["user"]; 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); pGuild->arChatUsers.insert(pm); bNew = true; } pm->wszDiscordId = getNick(pUser); pm->wszNick = pRoot["nick"].as_mstring(); if (pm->wszNick.IsEmpty()) pm->wszNick = pUser["username"].as_mstring(); else bNew = true; 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 (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; } if (pbNew) *pbNew = bNew; return pm; } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::ProcessChatUser(CDiscordUser *pChat, SnowFlake userId, const JSONNode &pRoot) { // input data control CDiscordGuild *pGuild = pChat->pGuild; if (pGuild == nullptr || userId == 0) return; // does user exist? if yes, there's nothing to do auto *pm = pGuild->FindUser(userId); if (pm != nullptr) return; // otherwise let's create a user and insert him into all guild's chats pm = new CDiscordGuildMember(userId); pm->wszDiscordId = getNick(pRoot["author"]); pm->wszNick = pRoot["nick"].as_mstring(); if (pm->wszNick.IsEmpty()) pm->wszNick = pRoot["author"]["username"].as_mstring(); pGuild->arChatUsers.insert(pm); debugLogA("add missing user to chat: id=%lld, nick=%S", userId, pm->wszNick.c_str()); AddGuildUser(pGuild, *pm); } ///////////////////////////////////////////////////////////////////////////////////////// void CDiscordProto::AddGuildUser(CDiscordGuild *pGuild, const CDiscordGuildMember &pUser) { int flags = 0; switch (pUser.iStatus) { case ID_STATUS_ONLINE: case ID_STATUS_NA: case ID_STATUS_DND: flags = 1; break; } auto *pStatus = g_chatApi.TM_FindStatus(pGuild->pParentSi->pStatuses, pUser.wszRole); wchar_t wszUserId[100]; _i64tow_s(pUser.userId, wszUserId, _countof(wszUserId), 10); auto *pu = g_chatApi.UM_AddUser(pGuild->pParentSi, wszUserId, pUser.wszNick, (pStatus) ? pStatus->iStatus : 0); pu->iStatusEx = flags; if (pUser.userId == m_ownId) pGuild->pParentSi->pMe = pu; } ///////////////////////////////////////////////////////////////////////////////////////// // 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); } static int compareVoiceState(const CDiscordVoiceState *p1, const CDiscordVoiceState *p2) { return compareInt64(p1->m_userId, p2->m_userId); } CDiscordGuild::CDiscordGuild(SnowFlake _id) : m_id(_id), arRoles(10, compareRoles), arChannels(10, compareUsers), arChatUsers(30, compareChatUsers), arVoiceStates(10, compareVoiceState) {} CDiscordGuild::~CDiscordGuild() { delete pVoiceCall; } 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() { JSONNode cached; if (!file2json(GetCacheFile(), cached)) return; for (auto &it : cached) { SnowFlake userId = getId(it["id"]); auto *pUser = FindUser(userId); if (pUser == nullptr) { pUser = new CDiscordGuildMember(userId); arChatUsers.insert(pUser); } pUser->wszNick = it["n"].as_mstring(); pUser->wszRole = it["r"].as_mstring(); } } void CDiscordGuild ::SaveToFile() { JSONNode members(JSON_ARRAY); for (auto &it : arChatUsers) { JSONNode member; member << INT64_PARAM("id", it->userId) << WCHAR_PARAM("n", it->wszNick) << WCHAR_PARAM("r", it->wszRole); members << member; } CMStringW wszFileName(GetCacheFile()); CreatePathToFileW(wszFileName); json2file(members, wszFileName); }