summaryrefslogtreecommitdiff
path: root/protocols/Discord/src/dispatch.cpp
diff options
context:
space:
mode:
authorGeorge Hazan <ghazan@miranda.im>2023-01-11 10:28:49 +0300
committerGeorge Hazan <ghazan@miranda.im>2023-01-11 10:28:49 +0300
commit9ab83c6bb239d88e54ae1ce7a8af49432543443a (patch)
treefe7dd11cdbba3f6ca71eb102dce126666a043744 /protocols/Discord/src/dispatch.cpp
parent605743d6e763b3aa2868133f50f99f15293a9a29 (diff)
Discord: not included into the build, but adapted for the current core version
Diffstat (limited to 'protocols/Discord/src/dispatch.cpp')
-rw-r--r--protocols/Discord/src/dispatch.cpp592
1 files changed, 592 insertions, 0 deletions
diff --git a/protocols/Discord/src/dispatch.cpp b/protocols/Discord/src/dispatch.cpp
new file mode 100644
index 0000000000..5d79feb9fe
--- /dev/null
+++ b/protocols/Discord/src/dispatch.cpp
@@ -0,0 +1,592 @@
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
+#include "stdafx.h"
+
+#pragma pack(4)
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+struct CDiscordCommand
+{
+ const wchar_t *szCommandId;
+ GatewayHandlerFunc pFunc;
+}
+static handlers[] = // these structures must me sorted alphabetically
+{
+ { L"CALL_CREATE", &CDiscordProto::OnCommandCallCreated },
+ { L"CALL_DELETE", &CDiscordProto::OnCommandCallDeleted },
+ { L"CALL_UPDATE", &CDiscordProto::OnCommandCallUpdated },
+
+ { L"CHANNEL_CREATE", &CDiscordProto::OnCommandChannelCreated },
+ { L"CHANNEL_DELETE", &CDiscordProto::OnCommandChannelDeleted },
+ { L"CHANNEL_UPDATE", &CDiscordProto::OnCommandChannelUpdated },
+
+ { L"GUILD_CREATE", &CDiscordProto::OnCommandGuildCreated },
+ { L"GUILD_DELETE", &CDiscordProto::OnCommandGuildDeleted },
+ { L"GUILD_MEMBER_ADD", &CDiscordProto::OnCommandGuildMemberAdded },
+ { L"GUILD_MEMBER_LIST_UPDATE", &CDiscordProto::OnCommandGuildMemberListUpdate },
+ { L"GUILD_MEMBER_REMOVE", &CDiscordProto::OnCommandGuildMemberRemoved },
+ { L"GUILD_MEMBER_UPDATE", &CDiscordProto::OnCommandGuildMemberUpdated },
+ { L"GUILD_ROLE_CREATE", &CDiscordProto::OnCommandRoleCreated },
+ { L"GUILD_ROLE_DELETE", &CDiscordProto::OnCommandRoleDeleted },
+ { L"GUILD_ROLE_UPDATE", &CDiscordProto::OnCommandRoleCreated },
+
+ { L"MESSAGE_ACK", &CDiscordProto::OnCommandMessageAck },
+ { L"MESSAGE_CREATE", &CDiscordProto::OnCommandMessageCreate },
+ { L"MESSAGE_DELETE", &CDiscordProto::OnCommandMessageDelete },
+ { L"MESSAGE_UPDATE", &CDiscordProto::OnCommandMessageUpdate },
+
+ { L"PRESENCE_UPDATE", &CDiscordProto::OnCommandPresence },
+
+ { L"READY", &CDiscordProto::OnCommandReady },
+
+ { L"RELATIONSHIP_ADD", &CDiscordProto::OnCommandFriendAdded },
+ { L"RELATIONSHIP_REMOVE", &CDiscordProto::OnCommandFriendRemoved },
+
+ { L"TYPING_START", &CDiscordProto::OnCommandTyping },
+
+ { L"USER_SETTINGS_UPDATE", &CDiscordProto::OnCommandUserSettingsUpdate },
+ { L"USER_UPDATE", &CDiscordProto::OnCommandUserUpdate },
+};
+
+static int __cdecl pSearchFunc(const void *p1, const void *p2)
+{
+ return wcscmp(((CDiscordCommand*)p1)->szCommandId, ((CDiscordCommand*)p2)->szCommandId);
+}
+
+GatewayHandlerFunc CDiscordProto::GetHandler(const wchar_t *pwszCommand)
+{
+ CDiscordCommand tmp = { pwszCommand, nullptr };
+ CDiscordCommand *p = (CDiscordCommand*)bsearch(&tmp, handlers, _countof(handlers), sizeof(handlers[0]), pSearchFunc);
+ return (p != nullptr) ? p->pFunc : nullptr;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// channel operations
+
+void CDiscordProto::OnCommandChannelCreated(const JSONNode &pRoot)
+{
+ SnowFlake guildId = ::getId(pRoot["guild_id"]);
+ if (guildId == 0)
+ PreparePrivateChannel(pRoot);
+ else {
+ // group channel for a guild
+ CDiscordGuild *pGuild = FindGuild(guildId);
+ if (pGuild && m_bUseGroupchats) {
+ CDiscordUser *pUser = ProcessGuildChannel(pGuild, pRoot);
+ if (pUser)
+ CreateChat(pGuild, pUser);
+ }
+ }
+}
+
+void CDiscordProto::OnCommandChannelDeleted(const JSONNode &pRoot)
+{
+ CDiscordUser *pUser = FindUserByChannel(::getId(pRoot["id"]));
+ if (pUser == nullptr)
+ return;
+
+ SnowFlake guildId = ::getId(pRoot["guild_id"]);
+ if (guildId == 0) {
+ pUser->channelId = pUser->lastMsgId = 0;
+ delSetting(pUser->hContact, DB_KEY_CHANNELID);
+ }
+ else {
+ CDiscordGuild *pGuild = FindGuild(guildId);
+ if (pGuild != nullptr)
+ Chat_Terminate(m_szModuleName, pUser->wszUsername, true);
+ }
+}
+
+void CDiscordProto::OnCommandChannelUpdated(const JSONNode &pRoot)
+{
+ CDiscordUser *pUser = FindUserByChannel(::getId(pRoot["id"]));
+ if (pUser == nullptr)
+ return;
+
+ pUser->lastMsgId = ::getId(pRoot["last_message_id"]);
+
+ SnowFlake guildId = ::getId(pRoot["guild_id"]);
+ if (guildId != 0) {
+ CDiscordGuild *pGuild = FindGuild(guildId);
+ if (pGuild == nullptr)
+ return;
+
+ CMStringW wszName = pRoot["name"].as_mstring();
+ if (!wszName.IsEmpty()) {
+ CMStringW wszNewName = pGuild->wszName + L"#" + wszName;
+ Chat_ChangeSessionName(m_szModuleName, pUser->wszUsername, wszNewName);
+ }
+
+ CMStringW wszTopic = pRoot["topic"].as_mstring();
+ Chat_SetStatusbarText(m_szModuleName, pUser->wszUsername, wszTopic);
+
+ GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TOPIC };
+ gce.pszID.w = pUser->wszUsername;
+ gce.pszText.w = wszTopic;
+ gce.time = time(0);
+ Chat_Event(&gce);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// reading a new message
+
+void CDiscordProto::OnCommandFriendAdded(const JSONNode &pRoot)
+{
+ CDiscordUser *pUser = PrepareUser(pRoot["user"]);
+ pUser->bIsPrivate = true;
+ ProcessType(pUser, pRoot);
+}
+
+void CDiscordProto::OnCommandFriendRemoved(const JSONNode &pRoot)
+{
+ SnowFlake id = ::getId(pRoot["id"]);
+ CDiscordUser *pUser = FindUser(id);
+ if (pUser != nullptr) {
+ if (pUser->hContact)
+ if (pUser->bIsPrivate)
+ db_delete_contact(pUser->hContact);
+
+ arUsers.remove(pUser);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// guild synchronization
+
+void CDiscordProto::OnCommandGuildCreated(const JSONNode &pRoot)
+{
+ if (m_bUseGroupchats)
+ ProcessGuild(pRoot);
+}
+
+void CDiscordProto::OnCommandGuildDeleted(const JSONNode &pRoot)
+{
+ CDiscordGuild *pGuild = FindGuild(::getId(pRoot["id"]));
+ if (pGuild == nullptr)
+ return;
+
+ for (auto &it : arUsers.rev_iter())
+ if (it->pGuild == pGuild) {
+ Chat_Terminate(m_szModuleName, it->wszUsername, true);
+ arUsers.removeItem(&it);
+ }
+
+ Chat_Terminate(m_szModuleName, pRoot["name"].as_mstring(), true);
+
+ arGuilds.remove(pGuild);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// guild members
+
+void CDiscordProto::OnCommandGuildMemberAdded(const JSONNode&)
+{
+}
+
+void CDiscordProto::OnCommandGuildMemberListUpdate(const JSONNode &pRoot)
+{
+ auto *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+ if (pGuild == nullptr)
+ return;
+
+ int iStatus = 0;
+
+ for (auto &ops: pRoot["ops"]) {
+ for (auto &it : ops["items"]) {
+ auto &item = it.at((size_t)0);
+ if (!mir_strcmp(item .name(), "group")) {
+ iStatus = item ["id"].as_string() == "online" ? ID_STATUS_ONLINE : ID_STATUS_OFFLINE;
+ continue;
+ }
+
+ if (!mir_strcmp(item .name(), "member")) {
+ bool bNew = false;
+ auto *pm = ProcessGuildUser(pGuild, item, &bNew);
+ pm->iStatus = iStatus;
+
+ if (bNew)
+ AddGuildUser(pGuild, *pm);
+ else if (iStatus) {
+ CMStringW wszUserId(FORMAT, L"%lld", pm->userId);
+
+ GCEVENT gce = { m_szModuleName, 0, GC_EVENT_SETCONTACTSTATUS };
+ gce.time = time(0);
+ gce.pszUID.w = wszUserId;
+
+ for (auto &cc : pGuild->arChannels) {
+ if (!cc->bIsGroup)
+ continue;
+
+ gce.pszID.w = cc->wszChannelName;
+ gce.dwItemData = iStatus;
+ Chat_Event(&gce);
+ }
+ }
+ }
+ }
+ }
+
+ pGuild->bSynced = true;
+}
+
+void CDiscordProto::OnCommandGuildMemberRemoved(const JSONNode &pRoot)
+{
+ CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+ if (pGuild == nullptr)
+ return;
+
+ CMStringW wszUserId = pRoot["user"]["id"].as_mstring();
+
+ for (auto &pUser : arUsers) {
+ if (pUser->pGuild != pGuild)
+ continue;
+
+ GCEVENT gce = { m_szModuleName, 0, GC_EVENT_PART };
+ gce.pszUID.w = pUser->wszUsername;
+ gce.time = time(0);
+ gce.pszUID.w = wszUserId;
+ Chat_Event(&gce);
+ }
+}
+
+void CDiscordProto::OnCommandGuildMemberUpdated(const JSONNode &pRoot)
+{
+ CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+ if (pGuild == nullptr)
+ return;
+
+ CMStringW wszUserId = pRoot["user"]["id"].as_mstring();
+ CDiscordGuildMember *gm = pGuild->FindUser(_wtoi64(wszUserId));
+ if (gm == nullptr)
+ return;
+
+ gm->wszDiscordId = pRoot["user"]["username"].as_mstring() + L"#" + pRoot["user"]["discriminator"].as_mstring();
+ gm->wszNick = pRoot["nick"].as_mstring();
+ if (gm->wszNick.IsEmpty())
+ gm->wszNick = pRoot["user"]["username"].as_mstring();
+
+ for (auto &it : arUsers) {
+ if (it->pGuild != pGuild)
+ continue;
+
+ CMStringW wszOldNick;
+ SESSION_INFO *si = g_chatApi.SM_FindSession(it->wszUsername, m_szModuleName);
+ if (si != nullptr) {
+ USERINFO *ui = g_chatApi.UM_FindUser(si, wszUserId);
+ if (ui != nullptr)
+ wszOldNick = ui->pszNick;
+ }
+
+ GCEVENT gce = { m_szModuleName, 0, GC_EVENT_NICK };
+ gce.pszID.w = it->wszUsername;
+ gce.time = time(0);
+ gce.pszUID.w = wszUserId;
+ gce.pszNick.w = wszOldNick;
+ gce.pszText.w = gm->wszNick;
+ Chat_Event(&gce);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// roles
+
+void CDiscordProto::OnCommandRoleCreated(const JSONNode &pRoot)
+{
+ CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+ if (pGuild != nullptr)
+ ProcessRole(pGuild, pRoot["role"]);
+}
+
+void CDiscordProto::OnCommandRoleDeleted(const JSONNode &pRoot)
+{
+ CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+ if (pGuild == nullptr)
+ return;
+
+ SnowFlake id = ::getId(pRoot["role_id"]);
+ CDiscordRole *pRole = pGuild->arRoles.find((CDiscordRole*)&id);
+ if (pRole == nullptr)
+ return;
+
+ int iOldPosition = pRole->position;
+ pGuild->arRoles.remove(pRole);
+
+ for (auto &it : pGuild->arRoles)
+ if (it->position > iOldPosition)
+ it->position--;
+
+ for (auto &it : arUsers) {
+ if (it->pGuild != pGuild)
+ continue;
+
+ SESSION_INFO *si = g_chatApi.SM_FindSession(it->wszUsername, m_szModuleName);
+ if (si != nullptr) {
+ g_chatApi.TM_RemoveAll(&si->pStatuses);
+ BuildStatusList(pGuild, si);
+ }
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// reading a new message
+
+void CDiscordProto::OnCommandMessageCreate(const JSONNode &pRoot)
+{
+ OnCommandMessage(pRoot, true);
+}
+
+void CDiscordProto::OnCommandMessageUpdate(const JSONNode &pRoot)
+{
+ OnCommandMessage(pRoot, false);
+}
+
+void CDiscordProto::OnCommandMessage(const JSONNode &pRoot, bool bIsNew)
+{
+ CMStringW wszMessageId = pRoot["id"].as_mstring();
+ CMStringW wszUserId = pRoot["author"]["id"].as_mstring();
+ SnowFlake userId = _wtoi64(wszUserId);
+ SnowFlake msgId = _wtoi64(wszMessageId);
+
+ // try to find a sender by his channel
+ SnowFlake channelId = ::getId(pRoot["channel_id"]);
+ CDiscordUser *pUser = FindUserByChannel(channelId);
+ if (pUser == nullptr) {
+ debugLogA("skipping message with unknown channel id=%lld", channelId);
+ return;
+ }
+
+ char szMsgId[100];
+ _i64toa_s(msgId, szMsgId, _countof(szMsgId), 10);
+
+ COwnMessage ownMsg(::getId(pRoot["nonce"]), 0);
+ COwnMessage *p = arOwnMessages.find(&ownMsg);
+ if (p != nullptr) { // own message? skip it
+ ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)p->reqId, (LPARAM)szMsgId);
+ debugLogA("skipping own message with nonce=%lld, id=%lld", ownMsg.nonce, msgId);
+ }
+ else {
+ CMStringW wszText = PrepareMessageText(pRoot);
+ if (wszText.IsEmpty())
+ return;
+
+ // old message? try to restore it from database
+ bool bOurMessage = userId == m_ownId;
+ if (!bIsNew) {
+ MEVENT hOldEvent = db_event_getById(m_szModuleName, szMsgId);
+ if (hOldEvent) {
+ DB::EventInfo dbei;
+ dbei.cbBlob = -1;
+ if (!db_event_get(hOldEvent, &dbei)) {
+ ptrW wszOldText(DbEvent_GetTextW(&dbei, CP_UTF8));
+ if (wszOldText)
+ wszText.Insert(0, wszOldText);
+ if (dbei.flags & DBEF_SENT)
+ bOurMessage = true;
+ }
+ }
+ }
+
+ const JSONNode &edited = pRoot["edited_timestamp"];
+ if (!edited.isnull())
+ wszText.AppendFormat(L" (%s %s)", TranslateT("edited at"), edited.as_mstring().c_str());
+
+ if (pUser->bIsPrivate && !pUser->bIsGroup) {
+ // if a message has myself as an author, add some flags
+ PROTORECVEVENT recv = {};
+ if (bOurMessage)
+ recv.flags = PREF_CREATEREAD | PREF_SENT;
+
+ debugLogA("store a message from private user %lld, channel id %lld", pUser->id, pUser->channelId);
+ ptrA buf(mir_utf8encodeW(wszText));
+
+ recv.timestamp = (uint32_t)StringToDate(pRoot["timestamp"].as_mstring());
+ recv.szMessage = buf;
+ recv.szMsgId = szMsgId;
+ ProtoChainRecvMsg(pUser->hContact, &recv);
+ }
+ else {
+ debugLogA("store a message into the group channel id %lld", channelId);
+
+ SESSION_INFO *si = g_chatApi.SM_FindSession(pUser->wszUsername, m_szModuleName);
+ if (si == nullptr) {
+ debugLogA("message to unknown channel %lld ignored", channelId);
+ return;
+ }
+
+ ProcessChatUser(pUser, wszUserId, pRoot);
+
+ ParseSpecialChars(si, wszText);
+ wszText.Replace(L"%", L"%%");
+
+ GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE };
+ gce.pszID.w = pUser->wszUsername;
+ gce.dwFlags = GCEF_ADDTOLOG;
+ gce.pszUID.w = wszUserId;
+ gce.pszText.w = wszText;
+ gce.time = (uint32_t)StringToDate(pRoot["timestamp"].as_mstring());
+ gce.bIsMe = bOurMessage;
+ Chat_Event(&gce);
+
+ debugLogW(L"New channel %s message from %s: %s", si->ptszID, gce.pszUID.w, gce.pszText.w);
+ }
+ }
+
+ pUser->lastMsgId = msgId;
+
+ SnowFlake lastId = getId(pUser->hContact, DB_KEY_LASTMSGID); // as stored in a database
+ if (lastId < msgId)
+ setId(pUser->hContact, DB_KEY_LASTMSGID, msgId);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// someone changed its status
+
+void CDiscordProto::OnCommandMessageAck(const JSONNode &pRoot)
+{
+ CDiscordUser *pUser = FindUserByChannel(pRoot["channel_id"]);
+ if (pUser != nullptr)
+ pUser->lastMsgId = ::getId(pRoot["message_id"]);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// message deleted
+
+void CDiscordProto::OnCommandMessageDelete(const JSONNode &pRoot)
+{
+ if (!m_bSyncDeleteMsgs)
+ return;
+
+ CMStringA msgid(pRoot["id"].as_mstring());
+ if (!msgid.IsEmpty()) {
+ MEVENT hEvent = db_event_getById(m_szModuleName, msgid);
+ if (hEvent)
+ db_event_delete(hEvent);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// someone changed its status
+
+void CDiscordProto::OnCommandPresence(const JSONNode &pRoot)
+{
+ auto *pGuild = FindGuild(::getId(pRoot["user"]["guild_id"]));
+ if (pGuild == nullptr)
+ ProcessPresence(pRoot);
+ // else
+ // pGuild->ProcessPresence(pRoot);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// gateway session start
+
+void CDiscordProto::OnCommandReady(const JSONNode &pRoot)
+{
+ OnLoggedIn();
+
+ GatewaySendHeartbeat();
+ m_impl.m_heartBeat.StartSafe(m_iHartbeatInterval);
+
+ m_szGatewaySessionId = pRoot["session_id"].as_mstring();
+
+ if (m_bUseGroupchats)
+ for (auto &it : pRoot["guilds"])
+ ProcessGuild(it);
+
+ for (auto &it : pRoot["relationships"]) {
+ CDiscordUser *pUser = PrepareUser(it["user"]);
+ ProcessType(pUser, it);
+ }
+
+ for (auto &it : pRoot["presences"])
+ ProcessPresence(it);
+
+ for (auto &it : pRoot["private_channels"])
+ PreparePrivateChannel(it);
+
+ for (auto &it : pRoot["read_state"]) {
+ CDiscordUser *pUser = FindUserByChannel(::getId(it["id"]));
+ if (pUser != nullptr)
+ pUser->lastReadId = ::getId(it["last_message_id"]);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// UTN support
+
+void CDiscordProto::OnCommandTyping(const JSONNode &pRoot)
+{
+ SnowFlake channelId = ::getId(pRoot["channel_id"]);
+ debugLogA("user typing notification: channelid=%lld", channelId);
+
+ CDiscordUser *pChannel = FindUserByChannel(channelId);
+ if (pChannel == nullptr) {
+ debugLogA("channel with id=%lld is not found", channelId);
+ return;
+ }
+
+ // both private groupchats & guild channels are chat rooms for Miranda
+ if (pChannel->pGuild) {
+ debugLogA("user is typing in a group channel");
+
+ CMStringW wszUerId = pRoot["user_id"].as_mstring();
+ ProcessGuildUser(pChannel->pGuild, pRoot); // never returns null
+
+ GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TYPING };
+ gce.pszID.w = pChannel->wszUsername;
+ gce.pszUID.w = wszUerId;
+ gce.dwItemData = 1;
+ gce.time = time(0);
+ Chat_Event(&gce);
+ }
+ else {
+ debugLogA("user is typing in his private channel");
+ CallService(MS_PROTO_CONTACTISTYPING, pChannel->hContact, 20);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// User info update
+
+void CDiscordProto::OnCommandUserUpdate(const JSONNode &pRoot)
+{
+ SnowFlake id = ::getId(pRoot["id"]);
+
+ MCONTACT hContact;
+ if (id != m_ownId) {
+ CDiscordUser *pUser = FindUser(id);
+ if (pUser == nullptr)
+ return;
+
+ hContact = pUser->hContact;
+ }
+ else hContact = 0;
+
+ // force rereading avatar
+ CheckAvatarChange(hContact, pRoot["avatar"].as_mstring());
+}
+
+void CDiscordProto::OnCommandUserSettingsUpdate(const JSONNode &pRoot)
+{
+ int iStatus = StrToStatus(pRoot["status"].as_mstring());
+ if (iStatus != 0) {
+ int iOldStatus = m_iStatus; m_iStatus = iStatus;
+ ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus);
+ }
+}