From de40f3be3f08487937525c2ef096dad665dda61d Mon Sep 17 00:00:00 2001 From: dartraiden Date: Sat, 14 Jan 2023 01:30:59 +0300 Subject: Convert sources to CR+LF --- protocols/WhatsApp/src/appsync.cpp | 644 +++++++++++------------ protocols/WhatsApp/src/chats.cpp | 376 ++++++------- protocols/WhatsApp/src/proto.cpp | 616 +++++++++++----------- protocols/WhatsApp/src/proto.h | 1016 ++++++++++++++++++------------------ protocols/WhatsApp/src/server.cpp | 836 ++++++++++++++--------------- 5 files changed, 1744 insertions(+), 1744 deletions(-) (limited to 'protocols/WhatsApp') diff --git a/protocols/WhatsApp/src/appsync.cpp b/protocols/WhatsApp/src/appsync.cpp index d298f880d2..9607045131 100644 --- a/protocols/WhatsApp/src/appsync.cpp +++ b/protocols/WhatsApp/src/appsync.cpp @@ -1,322 +1,322 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-23 George Hazan - -*/ - -#include "stdafx.h" - -////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::InitSync() -{ - m_arCollections.insert(new WACollection("regular")); - m_arCollections.insert(new WACollection("regular_high")); - m_arCollections.insert(new WACollection("regular_low")); - m_arCollections.insert(new WACollection("critical_block")); - m_arCollections.insert(new WACollection("critical_unblock_low")); - - for (auto &it : m_arCollections) { - CMStringW wszPath(GetTmpFileName("collection", it->szName)); - wszPath.Append(L".json"); - if (_waccess(wszPath, 0)) - continue; - - JSONNode root = JSONNode::parse(file2string(wszPath)); - it->version = root["version"].as_int(); - - auto szHash = decodeBinStr(root["hash"].as_string()); - if (szHash.size() == sizeof(it->hash.hash)) - memcpy(it->hash.hash, szHash.c_str(), sizeof(it->hash.hash)); - - for (auto &val : root["indexValueMap"]) - it->indexValueMap[decodeBinStr(val.name())] = decodeBinStr(val.as_string()); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnServerSync(const WANode &node) -{ - OBJLIST task(1); - - for (auto &it : node.getChildren()) - if (it->title == "collection") - task.insert(new WACollection(it->getAttr("name"), it->getAttrInt("version"))); - - ResyncServer(task); - SendAck(node); -} - -void WhatsAppProto::ResyncAll() -{ - ResyncServer(m_arCollections); -} - -void WhatsAppProto::ResyncServer(const OBJLIST &task) -{ - WANodeIq iq(IQ::SET, "w:sync:app:state"); - - auto *pList = iq.addChild("sync"); - for (auto &it : task) { - auto *pCollection = m_arCollections.find(it); - if (pCollection == nullptr) - m_arCollections.insert(pCollection = new WACollection(it->szName, 0)); - - if (!pCollection->version || pCollection->version < it->version) { - auto *pNode = pList->addChild("collection"); - *pNode << CHAR_PARAM("name", it->szName) << INT_PARAM("version", pCollection->version) - << CHAR_PARAM("return_snapshot", (!pCollection->version) ? "true" : "false"); - } - } - - if (pList->getFirstChild() != nullptr) - WSSendNode(iq, &WhatsAppProto::OnIqServerSync); -} - -void WhatsAppProto::OnIqServerSync(const WANode &node) -{ - for (auto &coll : node.getChild("sync")->getChildren()) { - if (coll->title != "collection") - continue; - - auto *pszName = coll->getAttr("name"); - - auto *pCollection = FindCollection(pszName); - if (pCollection == nullptr) { - pCollection = new WACollection(pszName, 0); - m_arCollections.insert(pCollection); - } - - int dwVersion = 0; - - CMStringW wszSnapshotPath(GetTmpFileName("collection", pszName)); - if (auto *pSnapshot = coll->getChild("snapshot")) { - proto::ExternalBlobReference body(pSnapshot->content); - if (!body->directpath || !body->has_mediakey) { - debugLogA("Invalid snapshot data, skipping"); - continue; - } - - MBinBuffer buf = DownloadEncryptedFile(directPath2url(body->directpath), body->mediakey, "App State"); - if (buf.isEmpty()) { - debugLogA("Invalid downloaded snapshot data, skipping"); - continue; - } - - proto::SyncdSnapshot snapshot(unpadBuffer16(buf)); - if (!snapshot) { - debugLogA("%s: unable to decode snapshot, skipping"); - continue; - } - - dwVersion = snapshot->version->version; - if (dwVersion > pCollection->version) { - pCollection->hash.init(); - debugLogA("%s: applying snapshot of version %d", pCollection->szName.get(), dwVersion); - for (int i=0; i < snapshot->n_records; i++) - ParsePatch(pCollection, snapshot->records[i], true); - } - else debugLogA("%s: skipping snapshot of version %d", pCollection->szName.get(), dwVersion); - } - - if (auto *pPatchList = coll->getChild("patches")) { - for (auto &it : pPatchList->getChildren()) { - proto::SyncdPatch patch(it->content); - if (!patch) { - debugLogA("%s: unable to decode patch, skipping"); - continue; - } - - dwVersion = patch->version->version; - if (dwVersion > pCollection->version) { - debugLogA("%s: applying patch of version %d", pCollection->szName.get(), dwVersion); - for (int i = 0; i < patch->n_mutations; i++) { - auto &jt = *patch->mutations[i]; - ParsePatch(pCollection, jt.record, jt.operation == WA__SYNCD_MUTATION__SYNCD_OPERATION__SET); - } - } - else debugLogA("%s: skipping patch of version %d", pCollection->szName.get(), dwVersion); - } - } - - JSONNode jsonRoot, jsonMap; - for (auto &it : pCollection->indexValueMap) - jsonMap << CHAR_PARAM(ptrA(mir_base64_encode(it.first.c_str(), it.first.size())), ptrA(mir_base64_encode(it.second.c_str(), it.second.size()))); - jsonRoot << INT_PARAM("version", dwVersion) << CHAR_PARAM("hash", ptrA(mir_base64_encode(pCollection->hash.hash, sizeof(pCollection->hash.hash)))) - << JSON_PARAM("indexValueMap", jsonMap); - - string2file(jsonRoot.write(), GetTmpFileName("collection", CMStringA(pszName) + ".json")); - } -} - -static uint8_t sttMutationInfo[] = "WhatsApp Mutation Keys"; - -void WhatsAppProto::ParsePatch(WACollection *pColl, const Wa__SyncdRecord *rec, bool bSet) -{ - int id = decodeBigEndian(rec->keyid->id); - auto &indexBlob = rec->index->blob; - auto &value = rec->value->blob; - - auto *macValue = value.data + value.len - 32; - std::string index((char *)indexBlob.data, indexBlob.len); - - MBinBuffer key(getBlob(CMStringA(FORMAT, "AppSyncKey%d", id))); - if (key.isEmpty()) { - debugLogA("No key with id=%d to decode a patch"); - return; - } - - struct - { - uint8_t indexKey[32]; - uint8_t encKey[32]; - uint8_t macKey[32]; - uint8_t snapshotMacKey[32]; - uint8_t patchMacKey[32]; - - } mutationKeys; - - HKDF(EVP_sha256(), (BYTE *)"", 0, key.data(), key.length(), sttMutationInfo, sizeof(sttMutationInfo) - 1, (BYTE *)&mutationKeys, sizeof(mutationKeys)); - - MBinBuffer decoded = aesDecrypt(EVP_aes_256_cbc(), mutationKeys.encKey, value.data, value.data + 16, value.len - 32); - if (decoded.isEmpty()) { - debugLogA("Unable to decode patch with key id=%d", id); - return; - } - - proto::SyncActionData data(unpadBuffer16(decoded)); - if (!data) { - debugLogA("Unable to decode action data with id=%d", id); - return; - } - - JSONNode jsonRoot = JSONNode::parse((char *)data->index.data); - - if (bSet) { - ApplyPatch(jsonRoot, data->value); - - pColl->hash.add(macValue, 32); - pColl->indexValueMap[index] = std::string((char*)macValue, 32); - } - else { - debugLogA("Removing data with index: %s", jsonRoot.write().c_str()); - - auto &prevVal = pColl->indexValueMap.find(index); - if (prevVal != pColl->indexValueMap.end()) { - pColl->hash.sub(prevVal->second.c_str(), prevVal->second.size()); - pColl->indexValueMap.erase(prevVal); - } - } -} - -void WhatsAppProto::ApplyPatch(const JSONNode &index, const Wa__SyncActionValue *data) -{ - debugLogA("Applying patch for %s: %s", index.write().c_str(), protobuf_c_text_to_string(data).c_str()); - - auto title = index.at((json_index_t)0).as_string(); - - if (title == "contact" && data->contactaction) { - auto *pUser = AddUser(index.at(1).as_string().c_str(), false); - - auto *pAction = data->contactaction; - auto &fullName = pAction->fullname; - if (fullName) - setUString(pUser->hContact, "Nick", fullName); - - if (pAction->firstname) { - CMStringA str(pAction->firstname); - str.TrimRight(); - setUString(pUser->hContact, "FirstName", str.c_str()); - setUString(pUser->hContact, "LastName", fullName + str.GetLength() + 1); - } - else if (fullName != nullptr) { - auto *p = strrchr(fullName, ' '); - if (p != 0) { - *p = 0; - setUString(pUser->hContact, "FirstName", fullName); - setUString(pUser->hContact, "LastName", p+1); - } - else { - setUString(pUser->hContact, "FirstName", ""); - setUString(pUser->hContact, "LastName", fullName); - } - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::ProcessHistorySync(const Wa__HistorySync *pSync) -{ - debugLogA("Got history sync: %s", protobuf_c_text_to_string(pSync).c_str()); - - switch (pSync->synctype) { - case WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__INITIAL_BOOTSTRAP: - case WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__RECENT: - for (int i = 0; i < pSync->n_conversations; i++) { - auto *pChat = pSync->conversations[i]; - - auto *pUser = AddUser(pChat->id, false); - for (int j = 0; j < pChat->n_messages; j++) { - auto *pMessage = pChat->messages[j]; - if (!pMessage->message) - continue; - - MEVENT hEvent = db_event_getById(m_szModuleName, pMessage->message->key->id); - if (hEvent) { - debugLogA("Event %s is already processed", pMessage->message->key->id); - continue; - } - - CMStringA szMessageText(GetMessageText(pMessage->message->message)); - if (!szMessageText.IsEmpty()) { - auto *key = pMessage->message->key; - - PROTORECVEVENT pre = {}; - pre.timestamp = pMessage->message->messagetimestamp; - pre.szMessage = szMessageText.GetBuffer(); - pre.szMsgId = key->id; - pre.flags = PREF_CREATEREAD; - if (key->fromme) - pre.flags |= PREF_SENT; - ProtoChainRecvMsg(pUser->hContact, &pre); - - if (pUser->bIsGroupChat) { - if (pChat->name) - setUString(pUser->hContact, "Nick", pChat->name); - - GCEVENT gce = {m_szModuleName, 0, GC_EVENT_MESSAGE}; - gce.dwFlags = GCEF_UTF8; - gce.pszID.a = pUser->szId; - gce.pszUID.a = key->participant; - gce.bIsMe = key->fromme; - gce.pszText.a = szMessageText.GetBuffer(); - gce.time = pMessage->message->messagetimestamp; - Chat_Event(&gce); - } - } - } - } - - if (pSync->synctype == WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__INITIAL_BOOTSTRAP) - GC_RefreshMetadata(); - break; - - case WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__PUSH_NAME: - for (int i = 0; i < pSync->n_pushnames; i++) { - auto *pName = pSync->pushnames[i]; - if (auto *pUser = AddUser(pName->id, false)) - setUString(pUser->hContact, "Nick", pName->pushname); - } - break; - - case WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__INITIAL_STATUS_V3: - for (int i = 0; i < pSync->n_statusv3messages; i++) { - // TODO - // auto *pStatus = pSync->statusv3messages[i]; - } - break; - } -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::InitSync() +{ + m_arCollections.insert(new WACollection("regular")); + m_arCollections.insert(new WACollection("regular_high")); + m_arCollections.insert(new WACollection("regular_low")); + m_arCollections.insert(new WACollection("critical_block")); + m_arCollections.insert(new WACollection("critical_unblock_low")); + + for (auto &it : m_arCollections) { + CMStringW wszPath(GetTmpFileName("collection", it->szName)); + wszPath.Append(L".json"); + if (_waccess(wszPath, 0)) + continue; + + JSONNode root = JSONNode::parse(file2string(wszPath)); + it->version = root["version"].as_int(); + + auto szHash = decodeBinStr(root["hash"].as_string()); + if (szHash.size() == sizeof(it->hash.hash)) + memcpy(it->hash.hash, szHash.c_str(), sizeof(it->hash.hash)); + + for (auto &val : root["indexValueMap"]) + it->indexValueMap[decodeBinStr(val.name())] = decodeBinStr(val.as_string()); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnServerSync(const WANode &node) +{ + OBJLIST task(1); + + for (auto &it : node.getChildren()) + if (it->title == "collection") + task.insert(new WACollection(it->getAttr("name"), it->getAttrInt("version"))); + + ResyncServer(task); + SendAck(node); +} + +void WhatsAppProto::ResyncAll() +{ + ResyncServer(m_arCollections); +} + +void WhatsAppProto::ResyncServer(const OBJLIST &task) +{ + WANodeIq iq(IQ::SET, "w:sync:app:state"); + + auto *pList = iq.addChild("sync"); + for (auto &it : task) { + auto *pCollection = m_arCollections.find(it); + if (pCollection == nullptr) + m_arCollections.insert(pCollection = new WACollection(it->szName, 0)); + + if (!pCollection->version || pCollection->version < it->version) { + auto *pNode = pList->addChild("collection"); + *pNode << CHAR_PARAM("name", it->szName) << INT_PARAM("version", pCollection->version) + << CHAR_PARAM("return_snapshot", (!pCollection->version) ? "true" : "false"); + } + } + + if (pList->getFirstChild() != nullptr) + WSSendNode(iq, &WhatsAppProto::OnIqServerSync); +} + +void WhatsAppProto::OnIqServerSync(const WANode &node) +{ + for (auto &coll : node.getChild("sync")->getChildren()) { + if (coll->title != "collection") + continue; + + auto *pszName = coll->getAttr("name"); + + auto *pCollection = FindCollection(pszName); + if (pCollection == nullptr) { + pCollection = new WACollection(pszName, 0); + m_arCollections.insert(pCollection); + } + + int dwVersion = 0; + + CMStringW wszSnapshotPath(GetTmpFileName("collection", pszName)); + if (auto *pSnapshot = coll->getChild("snapshot")) { + proto::ExternalBlobReference body(pSnapshot->content); + if (!body->directpath || !body->has_mediakey) { + debugLogA("Invalid snapshot data, skipping"); + continue; + } + + MBinBuffer buf = DownloadEncryptedFile(directPath2url(body->directpath), body->mediakey, "App State"); + if (buf.isEmpty()) { + debugLogA("Invalid downloaded snapshot data, skipping"); + continue; + } + + proto::SyncdSnapshot snapshot(unpadBuffer16(buf)); + if (!snapshot) { + debugLogA("%s: unable to decode snapshot, skipping"); + continue; + } + + dwVersion = snapshot->version->version; + if (dwVersion > pCollection->version) { + pCollection->hash.init(); + debugLogA("%s: applying snapshot of version %d", pCollection->szName.get(), dwVersion); + for (int i=0; i < snapshot->n_records; i++) + ParsePatch(pCollection, snapshot->records[i], true); + } + else debugLogA("%s: skipping snapshot of version %d", pCollection->szName.get(), dwVersion); + } + + if (auto *pPatchList = coll->getChild("patches")) { + for (auto &it : pPatchList->getChildren()) { + proto::SyncdPatch patch(it->content); + if (!patch) { + debugLogA("%s: unable to decode patch, skipping"); + continue; + } + + dwVersion = patch->version->version; + if (dwVersion > pCollection->version) { + debugLogA("%s: applying patch of version %d", pCollection->szName.get(), dwVersion); + for (int i = 0; i < patch->n_mutations; i++) { + auto &jt = *patch->mutations[i]; + ParsePatch(pCollection, jt.record, jt.operation == WA__SYNCD_MUTATION__SYNCD_OPERATION__SET); + } + } + else debugLogA("%s: skipping patch of version %d", pCollection->szName.get(), dwVersion); + } + } + + JSONNode jsonRoot, jsonMap; + for (auto &it : pCollection->indexValueMap) + jsonMap << CHAR_PARAM(ptrA(mir_base64_encode(it.first.c_str(), it.first.size())), ptrA(mir_base64_encode(it.second.c_str(), it.second.size()))); + jsonRoot << INT_PARAM("version", dwVersion) << CHAR_PARAM("hash", ptrA(mir_base64_encode(pCollection->hash.hash, sizeof(pCollection->hash.hash)))) + << JSON_PARAM("indexValueMap", jsonMap); + + string2file(jsonRoot.write(), GetTmpFileName("collection", CMStringA(pszName) + ".json")); + } +} + +static uint8_t sttMutationInfo[] = "WhatsApp Mutation Keys"; + +void WhatsAppProto::ParsePatch(WACollection *pColl, const Wa__SyncdRecord *rec, bool bSet) +{ + int id = decodeBigEndian(rec->keyid->id); + auto &indexBlob = rec->index->blob; + auto &value = rec->value->blob; + + auto *macValue = value.data + value.len - 32; + std::string index((char *)indexBlob.data, indexBlob.len); + + MBinBuffer key(getBlob(CMStringA(FORMAT, "AppSyncKey%d", id))); + if (key.isEmpty()) { + debugLogA("No key with id=%d to decode a patch"); + return; + } + + struct + { + uint8_t indexKey[32]; + uint8_t encKey[32]; + uint8_t macKey[32]; + uint8_t snapshotMacKey[32]; + uint8_t patchMacKey[32]; + + } mutationKeys; + + HKDF(EVP_sha256(), (BYTE *)"", 0, key.data(), key.length(), sttMutationInfo, sizeof(sttMutationInfo) - 1, (BYTE *)&mutationKeys, sizeof(mutationKeys)); + + MBinBuffer decoded = aesDecrypt(EVP_aes_256_cbc(), mutationKeys.encKey, value.data, value.data + 16, value.len - 32); + if (decoded.isEmpty()) { + debugLogA("Unable to decode patch with key id=%d", id); + return; + } + + proto::SyncActionData data(unpadBuffer16(decoded)); + if (!data) { + debugLogA("Unable to decode action data with id=%d", id); + return; + } + + JSONNode jsonRoot = JSONNode::parse((char *)data->index.data); + + if (bSet) { + ApplyPatch(jsonRoot, data->value); + + pColl->hash.add(macValue, 32); + pColl->indexValueMap[index] = std::string((char*)macValue, 32); + } + else { + debugLogA("Removing data with index: %s", jsonRoot.write().c_str()); + + auto &prevVal = pColl->indexValueMap.find(index); + if (prevVal != pColl->indexValueMap.end()) { + pColl->hash.sub(prevVal->second.c_str(), prevVal->second.size()); + pColl->indexValueMap.erase(prevVal); + } + } +} + +void WhatsAppProto::ApplyPatch(const JSONNode &index, const Wa__SyncActionValue *data) +{ + debugLogA("Applying patch for %s: %s", index.write().c_str(), protobuf_c_text_to_string(data).c_str()); + + auto title = index.at((json_index_t)0).as_string(); + + if (title == "contact" && data->contactaction) { + auto *pUser = AddUser(index.at(1).as_string().c_str(), false); + + auto *pAction = data->contactaction; + auto &fullName = pAction->fullname; + if (fullName) + setUString(pUser->hContact, "Nick", fullName); + + if (pAction->firstname) { + CMStringA str(pAction->firstname); + str.TrimRight(); + setUString(pUser->hContact, "FirstName", str.c_str()); + setUString(pUser->hContact, "LastName", fullName + str.GetLength() + 1); + } + else if (fullName != nullptr) { + auto *p = strrchr(fullName, ' '); + if (p != 0) { + *p = 0; + setUString(pUser->hContact, "FirstName", fullName); + setUString(pUser->hContact, "LastName", p+1); + } + else { + setUString(pUser->hContact, "FirstName", ""); + setUString(pUser->hContact, "LastName", fullName); + } + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::ProcessHistorySync(const Wa__HistorySync *pSync) +{ + debugLogA("Got history sync: %s", protobuf_c_text_to_string(pSync).c_str()); + + switch (pSync->synctype) { + case WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__INITIAL_BOOTSTRAP: + case WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__RECENT: + for (int i = 0; i < pSync->n_conversations; i++) { + auto *pChat = pSync->conversations[i]; + + auto *pUser = AddUser(pChat->id, false); + for (int j = 0; j < pChat->n_messages; j++) { + auto *pMessage = pChat->messages[j]; + if (!pMessage->message) + continue; + + MEVENT hEvent = db_event_getById(m_szModuleName, pMessage->message->key->id); + if (hEvent) { + debugLogA("Event %s is already processed", pMessage->message->key->id); + continue; + } + + CMStringA szMessageText(GetMessageText(pMessage->message->message)); + if (!szMessageText.IsEmpty()) { + auto *key = pMessage->message->key; + + PROTORECVEVENT pre = {}; + pre.timestamp = pMessage->message->messagetimestamp; + pre.szMessage = szMessageText.GetBuffer(); + pre.szMsgId = key->id; + pre.flags = PREF_CREATEREAD; + if (key->fromme) + pre.flags |= PREF_SENT; + ProtoChainRecvMsg(pUser->hContact, &pre); + + if (pUser->bIsGroupChat) { + if (pChat->name) + setUString(pUser->hContact, "Nick", pChat->name); + + GCEVENT gce = {m_szModuleName, 0, GC_EVENT_MESSAGE}; + gce.dwFlags = GCEF_UTF8; + gce.pszID.a = pUser->szId; + gce.pszUID.a = key->participant; + gce.bIsMe = key->fromme; + gce.pszText.a = szMessageText.GetBuffer(); + gce.time = pMessage->message->messagetimestamp; + Chat_Event(&gce); + } + } + } + } + + if (pSync->synctype == WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__INITIAL_BOOTSTRAP) + GC_RefreshMetadata(); + break; + + case WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__PUSH_NAME: + for (int i = 0; i < pSync->n_pushnames; i++) { + auto *pName = pSync->pushnames[i]; + if (auto *pUser = AddUser(pName->id, false)) + setUString(pUser->hContact, "Nick", pName->pushname); + } + break; + + case WA__HISTORY_SYNC__HISTORY_SYNC_TYPE__INITIAL_STATUS_V3: + for (int i = 0; i < pSync->n_statusv3messages; i++) { + // TODO + // auto *pStatus = pSync->statusv3messages[i]; + } + break; + } +} diff --git a/protocols/WhatsApp/src/chats.cpp b/protocols/WhatsApp/src/chats.cpp index 59f98d4548..b0423e5b20 100644 --- a/protocols/WhatsApp/src/chats.cpp +++ b/protocols/WhatsApp/src/chats.cpp @@ -1,188 +1,188 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-23 George Hazan - -*/ - -#include "stdafx.h" - -void WhatsAppProto::GC_RefreshMetadata() -{ - for (auto &it : m_arUsers) { - if (it->bIsGroupChat) { - GC_GetAllMetadata(); - break; - } - } -} - -void WhatsAppProto::GC_GetAllMetadata() -{ - WANodeIq iq(IQ::GET, "w:g2", "@g.us"); - auto *pRoot = iq.addChild("participating"); - *pRoot << XCHILD("participants") << XCHILD("description"); - WSSendNode(iq, &WhatsAppProto::OnIqGcGetAllMetadata); -} - -void WhatsAppProto::OnIqGcGetAllMetadata(const WANode &node) -{ - if (auto *pGroup = node.getChild("groups")) - for (auto &it : pGroup->getChildren()) - GC_ParseMetadata(it); -} - -void WhatsAppProto::GC_ParseMetadata(const WANode *pGroup) -{ - auto *pszId = pGroup->getAttr("id"); - if (pszId == nullptr) - return; - - auto *pChatUser = AddUser(CMStringA(pszId) + "@g.us", false); - if (pChatUser == nullptr) - return; - - CMStringW wszId(Utf2T(pChatUser->szId)); - - pChatUser->si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszId, getMStringW(pChatUser->hContact, "Nick")); - - Chat_AddGroup(pChatUser->si, TranslateT("Owner")); - Chat_AddGroup(pChatUser->si, TranslateT("SuperAdmin")); - Chat_AddGroup(pChatUser->si, TranslateT("Admin")); - Chat_AddGroup(pChatUser->si, TranslateT("Participant")); - - CMStringA szOwner(pGroup->getAttr("creator")), szNick, szRole; - - for (auto &it : pGroup->getChildren()) { - if (it->title == "description") { - CMStringA szDescr = it->getBody(); - if (!szDescr.IsEmpty()) { - GCEVENT gce = {m_szModuleName, 0, GC_EVENT_INFORMATION}; - gce.dwFlags = GCEF_UTF8; - gce.pszID.a = pChatUser->szId; - gce.pszText.a = szDescr.c_str(); - Chat_Event(&gce); - } - } - else if (it->title == "member_add_mode") { - szRole = it->getBody(); - } - else if (it->title == "participant") { - auto *jid = it->getAttr("jid"); - - // if role isn't specified, use the default one - auto *role = it->getAttr("type"); - if (role == nullptr) - role = szRole; - - GCEVENT gce = {m_szModuleName, 0, GC_EVENT_JOIN}; - gce.dwFlags = GCEF_UTF8; - gce.pszID.a = pChatUser->szId; - gce.pszUID.a = jid; - gce.bIsMe = (jid == m_szJid); - - if (jid == szOwner) - gce.pszStatus.a = "Owner"; - else if (!mir_strcmp(role, "superadmin")) - gce.pszStatus.a = "SuperAdmin"; - else if (!mir_strcmp(role, "adminadd")) - gce.pszStatus.a = "Admin"; - else - gce.pszStatus.a = "Participant"; - - if (gce.bIsMe) - szNick = ptrA(getUStringA(DBKEY_NICK)); - else if (auto *pUser = FindUser(jid)) - szNick = T2Utf(Clist_GetContactDisplayName(pUser->hContact)).get(); - else - szNick = WAJid(jid).user; - - gce.pszNick.a = szNick; - Chat_Event(&gce); - } - } - - if (auto *pszSubject = pGroup->getAttr("subject")) { - time_t iSubjectTime = pGroup->getAttrInt("s_t"); - auto *pszUser = pGroup->getAttr("s_o"); - if (m_szJid == pszUser) - szNick = ptrA(getUStringA(DBKEY_NICK)); - else if (auto *pUser = FindUser(pszUser)) - szNick = T2Utf(Clist_GetContactDisplayName(pUser->hContact)).get(); - else - szNick = WAJid(pszUser).user; - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TOPIC }; - gce.dwFlags = GCEF_UTF8; - gce.pszID.a = pChatUser->szId; - gce.pszUID.a = pszUser; - gce.pszText.a = pszSubject; - gce.time = iSubjectTime; - Chat_Event(&gce); - - setUString(pChatUser->hContact, "Nick", pszSubject); - } - - pChatUser->bInited = true; - Chat_Control(m_szModuleName, wszId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); - Chat_Control(m_szModuleName, wszId, SESSION_ONLINE); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int WhatsAppProto::GcEventHook(WPARAM, LPARAM lParam) -{ - GCHOOK *gch = (GCHOOK*)lParam; - if (gch == nullptr) - return 0; - - if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) - return 0; - - auto *pUser = FindUser(T2Utf(gch->si->ptszID)); - if (pUser == nullptr) - return 0; - - switch (gch->iType) { - case GC_USER_MESSAGE: - if (gch->ptszText && mir_wstrlen(gch->ptszText) > 0) { - rtrimw(gch->ptszText); - Chat_UnescapeTags(gch->ptszText); - SendTextMessage(pUser->szId, T2Utf(gch->ptszText)); - } - break; - - case GC_USER_PRIVMESS: - break; - - case GC_USER_LOGMENU: - break; - - case GC_USER_NICKLISTMENU: - break; - } - - return 1; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int WhatsAppProto::GcMenuHook(WPARAM, LPARAM lParam) -{ - GCMENUITEMS* gcmi = (GCMENUITEMS*)lParam; - if (gcmi == nullptr) - return 0; - - if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) - return 0; - - auto *pUser = FindUser(T2Utf(gcmi->pszID)); - if (pUser == nullptr) - return 0; - - if (gcmi->Type == MENU_ON_LOG) { - } - else if (gcmi->Type == MENU_ON_NICKLIST) { - } - return 0; -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +void WhatsAppProto::GC_RefreshMetadata() +{ + for (auto &it : m_arUsers) { + if (it->bIsGroupChat) { + GC_GetAllMetadata(); + break; + } + } +} + +void WhatsAppProto::GC_GetAllMetadata() +{ + WANodeIq iq(IQ::GET, "w:g2", "@g.us"); + auto *pRoot = iq.addChild("participating"); + *pRoot << XCHILD("participants") << XCHILD("description"); + WSSendNode(iq, &WhatsAppProto::OnIqGcGetAllMetadata); +} + +void WhatsAppProto::OnIqGcGetAllMetadata(const WANode &node) +{ + if (auto *pGroup = node.getChild("groups")) + for (auto &it : pGroup->getChildren()) + GC_ParseMetadata(it); +} + +void WhatsAppProto::GC_ParseMetadata(const WANode *pGroup) +{ + auto *pszId = pGroup->getAttr("id"); + if (pszId == nullptr) + return; + + auto *pChatUser = AddUser(CMStringA(pszId) + "@g.us", false); + if (pChatUser == nullptr) + return; + + CMStringW wszId(Utf2T(pChatUser->szId)); + + pChatUser->si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszId, getMStringW(pChatUser->hContact, "Nick")); + + Chat_AddGroup(pChatUser->si, TranslateT("Owner")); + Chat_AddGroup(pChatUser->si, TranslateT("SuperAdmin")); + Chat_AddGroup(pChatUser->si, TranslateT("Admin")); + Chat_AddGroup(pChatUser->si, TranslateT("Participant")); + + CMStringA szOwner(pGroup->getAttr("creator")), szNick, szRole; + + for (auto &it : pGroup->getChildren()) { + if (it->title == "description") { + CMStringA szDescr = it->getBody(); + if (!szDescr.IsEmpty()) { + GCEVENT gce = {m_szModuleName, 0, GC_EVENT_INFORMATION}; + gce.dwFlags = GCEF_UTF8; + gce.pszID.a = pChatUser->szId; + gce.pszText.a = szDescr.c_str(); + Chat_Event(&gce); + } + } + else if (it->title == "member_add_mode") { + szRole = it->getBody(); + } + else if (it->title == "participant") { + auto *jid = it->getAttr("jid"); + + // if role isn't specified, use the default one + auto *role = it->getAttr("type"); + if (role == nullptr) + role = szRole; + + GCEVENT gce = {m_szModuleName, 0, GC_EVENT_JOIN}; + gce.dwFlags = GCEF_UTF8; + gce.pszID.a = pChatUser->szId; + gce.pszUID.a = jid; + gce.bIsMe = (jid == m_szJid); + + if (jid == szOwner) + gce.pszStatus.a = "Owner"; + else if (!mir_strcmp(role, "superadmin")) + gce.pszStatus.a = "SuperAdmin"; + else if (!mir_strcmp(role, "adminadd")) + gce.pszStatus.a = "Admin"; + else + gce.pszStatus.a = "Participant"; + + if (gce.bIsMe) + szNick = ptrA(getUStringA(DBKEY_NICK)); + else if (auto *pUser = FindUser(jid)) + szNick = T2Utf(Clist_GetContactDisplayName(pUser->hContact)).get(); + else + szNick = WAJid(jid).user; + + gce.pszNick.a = szNick; + Chat_Event(&gce); + } + } + + if (auto *pszSubject = pGroup->getAttr("subject")) { + time_t iSubjectTime = pGroup->getAttrInt("s_t"); + auto *pszUser = pGroup->getAttr("s_o"); + if (m_szJid == pszUser) + szNick = ptrA(getUStringA(DBKEY_NICK)); + else if (auto *pUser = FindUser(pszUser)) + szNick = T2Utf(Clist_GetContactDisplayName(pUser->hContact)).get(); + else + szNick = WAJid(pszUser).user; + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_TOPIC }; + gce.dwFlags = GCEF_UTF8; + gce.pszID.a = pChatUser->szId; + gce.pszUID.a = pszUser; + gce.pszText.a = pszSubject; + gce.time = iSubjectTime; + Chat_Event(&gce); + + setUString(pChatUser->hContact, "Nick", pszSubject); + } + + pChatUser->bInited = true; + Chat_Control(m_szModuleName, wszId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); + Chat_Control(m_szModuleName, wszId, SESSION_ONLINE); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int WhatsAppProto::GcEventHook(WPARAM, LPARAM lParam) +{ + GCHOOK *gch = (GCHOOK*)lParam; + if (gch == nullptr) + return 0; + + if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) + return 0; + + auto *pUser = FindUser(T2Utf(gch->si->ptszID)); + if (pUser == nullptr) + return 0; + + switch (gch->iType) { + case GC_USER_MESSAGE: + if (gch->ptszText && mir_wstrlen(gch->ptszText) > 0) { + rtrimw(gch->ptszText); + Chat_UnescapeTags(gch->ptszText); + SendTextMessage(pUser->szId, T2Utf(gch->ptszText)); + } + break; + + case GC_USER_PRIVMESS: + break; + + case GC_USER_LOGMENU: + break; + + case GC_USER_NICKLISTMENU: + break; + } + + return 1; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int WhatsAppProto::GcMenuHook(WPARAM, LPARAM lParam) +{ + GCMENUITEMS* gcmi = (GCMENUITEMS*)lParam; + if (gcmi == nullptr) + return 0; + + if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) + return 0; + + auto *pUser = FindUser(T2Utf(gcmi->pszID)); + if (pUser == nullptr) + return 0; + + if (gcmi->Type == MENU_ON_LOG) { + } + else if (gcmi->Type == MENU_ON_NICKLIST) { + } + return 0; +} diff --git a/protocols/WhatsApp/src/proto.cpp b/protocols/WhatsApp/src/proto.cpp index 006f78dfc0..8fabeb098b 100644 --- a/protocols/WhatsApp/src/proto.cpp +++ b/protocols/WhatsApp/src/proto.cpp @@ -1,308 +1,308 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-23 George Hazan - -*/ - -#include "stdafx.h" - -struct SearchParam -{ - SearchParam(const wchar_t *_jid, LONG _id) : - jid(_jid), id(_id) - {} - - std::wstring jid; - LONG id; -}; - -static int CompareOwnMsgs(const WAOwnMessage *p1, const WAOwnMessage *p2) -{ - return strcmp(p1->szMessageId, p2->szMessageId); -} - -static int CompareUsers(const WAUser *p1, const WAUser *p2) -{ - return strcmp(p1->szId, p2->szId); -} - -static int CompareCollections(const WACollection *p1, const WACollection *p2) -{ - return strcmp(p1->szName, p2->szName); -} - -static int CompareRequests(const WARequestBase *p1, const WARequestBase *p2) -{ - return strcmp(p1->szPacketId, p2->szPacketId); -} - -WhatsAppProto::WhatsAppProto(const char *proto_name, const wchar_t *username) : - PROTO(proto_name, username), - m_impl(*this), - m_signalStore(this, ""), - m_szJid(getMStringA(DBKEY_ID)), - m_tszDefaultGroup(getWStringA(DBKEY_DEF_GROUP)), - m_arUsers(10, CompareUsers), - m_arOwnMsgs(1, CompareOwnMsgs), - m_arPersistent(1), - m_arPacketQueue(10, CompareRequests), - m_arCollections(10, CompareCollections), - - m_wszNick(this, "Nick"), - m_wszDeviceName(this, "DeviceName", L"Miranda NG"), - m_wszDefaultGroup(this, "DefaultGroup", L"WhatsApp"), - m_bUsePopups(this, "UsePopups", true), - m_bUseBbcodes(this, "UseBbcodes", true), - m_bHideGroupchats(this, "HideChats", true) -{ - db_set_resident(m_szModuleName, "StatusMsg"); - - CreateProtoService(PS_CREATEACCMGRUI, &WhatsAppProto::SvcCreateAccMgrUI); - - CreateProtoService(PS_GETAVATARINFO, &WhatsAppProto::GetAvatarInfo); - CreateProtoService(PS_GETAVATARCAPS, &WhatsAppProto::GetAvatarCaps); - CreateProtoService(PS_GETMYAVATAR, &WhatsAppProto::GetMyAvatar); - CreateProtoService(PS_SETMYAVATAR, &WhatsAppProto::SetMyAvatar); - - HookProtoEvent(ME_OPT_INITIALISE, &WhatsAppProto::OnOptionsInit); - - InitSync(); - InitPopups(); - InitPersistentHandlers(); - - // Create standard network connection - wchar_t descr[512]; - mir_snwprintf(descr, TranslateT("%s (server)"), m_tszUserName); - - NETLIBUSER nlu = {}; - nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; - nlu.szSettingsModule = m_szModuleName; - nlu.szDescriptiveName.w = descr; - m_hNetlibUser = Netlib_RegisterUser(&nlu); - - // Temporary folder - CreateDirectoryTreeW(CMStringW(VARSW(L"%miranda_userdata%")) + L"\\" + _A2T(m_szModuleName)); - - // Avatars folder - m_tszAvatarFolder = CMStringW(VARSW(L"%miranda_avatarcache%")) + L"\\" + _A2T(m_szModuleName); - DWORD dwAttributes = GetFileAttributes(m_tszAvatarFolder.c_str()); - if (dwAttributes == 0xffffffff || (dwAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) - CreateDirectoryTreeW(m_tszAvatarFolder.c_str()); - - // default contacts group - if (m_tszDefaultGroup == NULL) - m_tszDefaultGroup = mir_wstrdup(L"WhatsApp"); - Clist_GroupCreate(0, m_tszDefaultGroup); - - // groupchat initialization - GCREGISTER gcr = {}; - gcr.dwFlags = GC_TYPNOTIF | GC_DATABASE; - gcr.ptszDispName = m_tszUserName; - gcr.pszModule = m_szModuleName; - Chat_Register(&gcr); - - HookProtoEvent(ME_GC_EVENT, &WhatsAppProto::GcEventHook); - HookProtoEvent(ME_GC_BUILDMENU, &WhatsAppProto::GcMenuHook); -} - -WhatsAppProto::~WhatsAppProto() -{ -} - -///////////////////////////////////////////////////////////////////////////////////////// -// OnErase - remove temporary folder for account - -const char *pszNeededItems[] = { - "AM_BaseProto", "DefaultGroup", "DeviceName", "HideChats", "NLlog", "Nick" -}; - -static int sttEnumFunc(const char *szSetting, void *param) -{ - for (auto &it : pszNeededItems) - if (!mir_strcmp(it, szSetting)) - return 0; - - auto *pList = (LIST*)param; - pList->insert(mir_strdup(szSetting)); - return 0; -} - -void WhatsAppProto::OnErase() -{ - m_bUnregister = true; - ServerThreadWorker(); - - // remove all temporary data from database & disk folder - LIST arSettings(50); - db_enum_settings(0, sttEnumFunc, m_szModuleName, &arSettings); - for (auto &it : arSettings) { - delSetting(it); - mir_free(it); - } - - DeleteDirectoryTreeW(CMStringW(VARSW(L"%miranda_userdata%")) + L"\\" + _A2T(m_szModuleName), false); - - m_szJid.Empty(); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// OnModulesLoaded emulator for an account - -void WhatsAppProto::OnModulesLoaded() -{ - // initialize contacts cache - if (!m_szJid.IsEmpty()) - m_arUsers.insert(new WAUser(0, m_szJid, false)); - - for (auto &cc : AccContacts()) { - bool bIsChat = isChatRoom(cc); - CMStringA szId(getMStringA(cc, bIsChat ? "ChatRoomID" : DBKEY_ID)); - if (!szId.IsEmpty()) - m_arUsers.insert(new WAUser(cc, szId, bIsChat)); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// PROTO_INTERFACE implementation - -MCONTACT WhatsAppProto::AddToList(int flags, PROTOSEARCHRESULT *psr) -{ - if (psr->id.w == nullptr) - return NULL; - - auto *pUser = AddUser(T2Utf(psr->id.w), (flags & PALF_TEMPORARY) != 0); - db_unset(pUser->hContact, "CList", "NotOnList"); - - return pUser->hContact; -} - -INT_PTR WhatsAppProto::GetCaps(int type, MCONTACT) -{ - switch (type) { - case PFLAGNUM_1: - return PF1_IM | PF1_FILE | PF1_CHAT | PF1_BASICSEARCH | PF1_ADDSEARCHRES | PF1_MODEMSGRECV; - case PFLAGNUM_2: - return PF2_ONLINE; - case PFLAGNUM_3: - return 0; - case PFLAGNUM_4: - return PF4_NOCUSTOMAUTH | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE | PF4_OFFLINEFILES | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_SERVERMSGID; - case PFLAGNUM_5: - return 0; - case PFLAG_UNIQUEIDTEXT: - return (DWORD_PTR)L"WhatsApp ID"; - } - return 0; -} - -int WhatsAppProto::SetStatus(int iNewStatus) -{ - if (m_iDesiredStatus == iNewStatus) - return 0; - - if (!mir_wstrlen(m_wszNick)) { - Popup(0, LPGENW("Connection cannot be established! You have not completed all necessary fields (Nick)."), LPGENW("Error")); - return 0; - } - - int oldStatus = m_iStatus; - - // Routing statuses not supported by WhatsApp - switch (iNewStatus) { - case ID_STATUS_OFFLINE: - m_iDesiredStatus = iNewStatus; - break; - - case ID_STATUS_ONLINE: - case ID_STATUS_FREECHAT: - default: - m_iDesiredStatus = ID_STATUS_ONLINE; - break; - } - - if (m_iDesiredStatus == ID_STATUS_OFFLINE) { - SetServerStatus(m_iDesiredStatus); - - if (m_hServerConn != nullptr) - Netlib_Shutdown(m_hServerConn); - - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); - } - else if (m_hServerConn == nullptr && !IsStatusConnecting(m_iStatus)) { - m_iStatus = ID_STATUS_CONNECTING; - ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); - - ForkThread(&WhatsAppProto::ServerThread); - } - else if (m_hServerConn != nullptr) { - SetServerStatus(m_iDesiredStatus); - - m_iStatus = m_iDesiredStatus; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); - } - else ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); - - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int WhatsAppProto::SendMsg(MCONTACT hContact, int, const char *pszMsg) -{ - ptrA jid(getStringA(hContact, DBKEY_ID)); - if (jid == nullptr || pszMsg == nullptr) - return 0; - - if (!isOnline()) { - debugLogA("No connection"); - return 0; - } - - return SendTextMessage(jid, pszMsg); -} - -int WhatsAppProto::UserIsTyping(MCONTACT hContact, int type) -{ - if (hContact && isOnline()) { - ptrA jid(getStringA(hContact, DBKEY_ID)); - if (jid && isOnline()) { - WSSendNode( - WANode("chatstates") << CHAR_PARAM("to", jid) << XCHILD((type == PROTOTYPE_SELFTYPING_ON) ? "composing" : "paused")); - } - } - - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// contacts search - -void WhatsAppProto::SearchAckThread(void *targ) -{ - Sleep(100); - - SearchParam *param = (SearchParam*)targ; - PROTOSEARCHRESULT psr = {}; - psr.cbSize = sizeof(psr); - psr.flags = PSR_UNICODE; - psr.nick.w = psr.firstName.w = psr.lastName.w = L""; - psr.id.w = (wchar_t*)param->jid.c_str(); - - ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)param->id, (LPARAM)&psr); - ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)param->id, 0); - - delete param; -} - -HANDLE WhatsAppProto::SearchBasic(const wchar_t* id) -{ - if (!isOnline()) - return nullptr; - - // fake - we always accept search - SearchParam *param = new SearchParam(id, -1); - ForkThread(&WhatsAppProto::SearchAckThread, param); - return (HANDLE)param->id; -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +struct SearchParam +{ + SearchParam(const wchar_t *_jid, LONG _id) : + jid(_jid), id(_id) + {} + + std::wstring jid; + LONG id; +}; + +static int CompareOwnMsgs(const WAOwnMessage *p1, const WAOwnMessage *p2) +{ + return strcmp(p1->szMessageId, p2->szMessageId); +} + +static int CompareUsers(const WAUser *p1, const WAUser *p2) +{ + return strcmp(p1->szId, p2->szId); +} + +static int CompareCollections(const WACollection *p1, const WACollection *p2) +{ + return strcmp(p1->szName, p2->szName); +} + +static int CompareRequests(const WARequestBase *p1, const WARequestBase *p2) +{ + return strcmp(p1->szPacketId, p2->szPacketId); +} + +WhatsAppProto::WhatsAppProto(const char *proto_name, const wchar_t *username) : + PROTO(proto_name, username), + m_impl(*this), + m_signalStore(this, ""), + m_szJid(getMStringA(DBKEY_ID)), + m_tszDefaultGroup(getWStringA(DBKEY_DEF_GROUP)), + m_arUsers(10, CompareUsers), + m_arOwnMsgs(1, CompareOwnMsgs), + m_arPersistent(1), + m_arPacketQueue(10, CompareRequests), + m_arCollections(10, CompareCollections), + + m_wszNick(this, "Nick"), + m_wszDeviceName(this, "DeviceName", L"Miranda NG"), + m_wszDefaultGroup(this, "DefaultGroup", L"WhatsApp"), + m_bUsePopups(this, "UsePopups", true), + m_bUseBbcodes(this, "UseBbcodes", true), + m_bHideGroupchats(this, "HideChats", true) +{ + db_set_resident(m_szModuleName, "StatusMsg"); + + CreateProtoService(PS_CREATEACCMGRUI, &WhatsAppProto::SvcCreateAccMgrUI); + + CreateProtoService(PS_GETAVATARINFO, &WhatsAppProto::GetAvatarInfo); + CreateProtoService(PS_GETAVATARCAPS, &WhatsAppProto::GetAvatarCaps); + CreateProtoService(PS_GETMYAVATAR, &WhatsAppProto::GetMyAvatar); + CreateProtoService(PS_SETMYAVATAR, &WhatsAppProto::SetMyAvatar); + + HookProtoEvent(ME_OPT_INITIALISE, &WhatsAppProto::OnOptionsInit); + + InitSync(); + InitPopups(); + InitPersistentHandlers(); + + // Create standard network connection + wchar_t descr[512]; + mir_snwprintf(descr, TranslateT("%s (server)"), m_tszUserName); + + NETLIBUSER nlu = {}; + nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szSettingsModule = m_szModuleName; + nlu.szDescriptiveName.w = descr; + m_hNetlibUser = Netlib_RegisterUser(&nlu); + + // Temporary folder + CreateDirectoryTreeW(CMStringW(VARSW(L"%miranda_userdata%")) + L"\\" + _A2T(m_szModuleName)); + + // Avatars folder + m_tszAvatarFolder = CMStringW(VARSW(L"%miranda_avatarcache%")) + L"\\" + _A2T(m_szModuleName); + DWORD dwAttributes = GetFileAttributes(m_tszAvatarFolder.c_str()); + if (dwAttributes == 0xffffffff || (dwAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + CreateDirectoryTreeW(m_tszAvatarFolder.c_str()); + + // default contacts group + if (m_tszDefaultGroup == NULL) + m_tszDefaultGroup = mir_wstrdup(L"WhatsApp"); + Clist_GroupCreate(0, m_tszDefaultGroup); + + // groupchat initialization + GCREGISTER gcr = {}; + gcr.dwFlags = GC_TYPNOTIF | GC_DATABASE; + gcr.ptszDispName = m_tszUserName; + gcr.pszModule = m_szModuleName; + Chat_Register(&gcr); + + HookProtoEvent(ME_GC_EVENT, &WhatsAppProto::GcEventHook); + HookProtoEvent(ME_GC_BUILDMENU, &WhatsAppProto::GcMenuHook); +} + +WhatsAppProto::~WhatsAppProto() +{ +} + +///////////////////////////////////////////////////////////////////////////////////////// +// OnErase - remove temporary folder for account + +const char *pszNeededItems[] = { + "AM_BaseProto", "DefaultGroup", "DeviceName", "HideChats", "NLlog", "Nick" +}; + +static int sttEnumFunc(const char *szSetting, void *param) +{ + for (auto &it : pszNeededItems) + if (!mir_strcmp(it, szSetting)) + return 0; + + auto *pList = (LIST*)param; + pList->insert(mir_strdup(szSetting)); + return 0; +} + +void WhatsAppProto::OnErase() +{ + m_bUnregister = true; + ServerThreadWorker(); + + // remove all temporary data from database & disk folder + LIST arSettings(50); + db_enum_settings(0, sttEnumFunc, m_szModuleName, &arSettings); + for (auto &it : arSettings) { + delSetting(it); + mir_free(it); + } + + DeleteDirectoryTreeW(CMStringW(VARSW(L"%miranda_userdata%")) + L"\\" + _A2T(m_szModuleName), false); + + m_szJid.Empty(); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// OnModulesLoaded emulator for an account + +void WhatsAppProto::OnModulesLoaded() +{ + // initialize contacts cache + if (!m_szJid.IsEmpty()) + m_arUsers.insert(new WAUser(0, m_szJid, false)); + + for (auto &cc : AccContacts()) { + bool bIsChat = isChatRoom(cc); + CMStringA szId(getMStringA(cc, bIsChat ? "ChatRoomID" : DBKEY_ID)); + if (!szId.IsEmpty()) + m_arUsers.insert(new WAUser(cc, szId, bIsChat)); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// PROTO_INTERFACE implementation + +MCONTACT WhatsAppProto::AddToList(int flags, PROTOSEARCHRESULT *psr) +{ + if (psr->id.w == nullptr) + return NULL; + + auto *pUser = AddUser(T2Utf(psr->id.w), (flags & PALF_TEMPORARY) != 0); + db_unset(pUser->hContact, "CList", "NotOnList"); + + return pUser->hContact; +} + +INT_PTR WhatsAppProto::GetCaps(int type, MCONTACT) +{ + switch (type) { + case PFLAGNUM_1: + return PF1_IM | PF1_FILE | PF1_CHAT | PF1_BASICSEARCH | PF1_ADDSEARCHRES | PF1_MODEMSGRECV; + case PFLAGNUM_2: + return PF2_ONLINE; + case PFLAGNUM_3: + return 0; + case PFLAGNUM_4: + return PF4_NOCUSTOMAUTH | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE | PF4_OFFLINEFILES | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_SERVERMSGID; + case PFLAGNUM_5: + return 0; + case PFLAG_UNIQUEIDTEXT: + return (DWORD_PTR)L"WhatsApp ID"; + } + return 0; +} + +int WhatsAppProto::SetStatus(int iNewStatus) +{ + if (m_iDesiredStatus == iNewStatus) + return 0; + + if (!mir_wstrlen(m_wszNick)) { + Popup(0, LPGENW("Connection cannot be established! You have not completed all necessary fields (Nick)."), LPGENW("Error")); + return 0; + } + + int oldStatus = m_iStatus; + + // Routing statuses not supported by WhatsApp + switch (iNewStatus) { + case ID_STATUS_OFFLINE: + m_iDesiredStatus = iNewStatus; + break; + + case ID_STATUS_ONLINE: + case ID_STATUS_FREECHAT: + default: + m_iDesiredStatus = ID_STATUS_ONLINE; + break; + } + + if (m_iDesiredStatus == ID_STATUS_OFFLINE) { + SetServerStatus(m_iDesiredStatus); + + if (m_hServerConn != nullptr) + Netlib_Shutdown(m_hServerConn); + + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); + } + else if (m_hServerConn == nullptr && !IsStatusConnecting(m_iStatus)) { + m_iStatus = ID_STATUS_CONNECTING; + ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); + + ForkThread(&WhatsAppProto::ServerThread); + } + else if (m_hServerConn != nullptr) { + SetServerStatus(m_iDesiredStatus); + + m_iStatus = m_iDesiredStatus; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); + } + else ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int WhatsAppProto::SendMsg(MCONTACT hContact, int, const char *pszMsg) +{ + ptrA jid(getStringA(hContact, DBKEY_ID)); + if (jid == nullptr || pszMsg == nullptr) + return 0; + + if (!isOnline()) { + debugLogA("No connection"); + return 0; + } + + return SendTextMessage(jid, pszMsg); +} + +int WhatsAppProto::UserIsTyping(MCONTACT hContact, int type) +{ + if (hContact && isOnline()) { + ptrA jid(getStringA(hContact, DBKEY_ID)); + if (jid && isOnline()) { + WSSendNode( + WANode("chatstates") << CHAR_PARAM("to", jid) << XCHILD((type == PROTOTYPE_SELFTYPING_ON) ? "composing" : "paused")); + } + } + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// contacts search + +void WhatsAppProto::SearchAckThread(void *targ) +{ + Sleep(100); + + SearchParam *param = (SearchParam*)targ; + PROTOSEARCHRESULT psr = {}; + psr.cbSize = sizeof(psr); + psr.flags = PSR_UNICODE; + psr.nick.w = psr.firstName.w = psr.lastName.w = L""; + psr.id.w = (wchar_t*)param->jid.c_str(); + + ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)param->id, (LPARAM)&psr); + ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)param->id, 0); + + delete param; +} + +HANDLE WhatsAppProto::SearchBasic(const wchar_t* id) +{ + if (!isOnline()) + return nullptr; + + // fake - we always accept search + SearchParam *param = new SearchParam(id, -1); + ForkThread(&WhatsAppProto::SearchAckThread, param); + return (HANDLE)param->id; +} diff --git a/protocols/WhatsApp/src/proto.h b/protocols/WhatsApp/src/proto.h index dbcf57b597..8ec4593767 100644 --- a/protocols/WhatsApp/src/proto.h +++ b/protocols/WhatsApp/src/proto.h @@ -1,508 +1,508 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-23 George Hazan - -*/ - -#if !defined(PROTO_H) -#define PROTO_H - -#define S_WHATSAPP_NET "@s.whatsapp.net" -#define APP_VERSION "2.2230.15" -#define KEY_BUNDLE_TYPE "\x05" - -class WhatsAppProto; -typedef void (WhatsAppProto:: *WA_PKT_HANDLER)(const WANode &node); -typedef void (WhatsAppProto:: *WA_PKT_HANDLER_FULL)(const WANode &node, void *pUserInfo); - -struct WAMSG -{ - union { - uint32_t dwFlags = 0; - struct { - bool bPrivateChat : 1; - bool bGroupChat : 1; - bool bDirectStatus : 1; - bool bOtherStatus : 1; - bool bPeerBroadcast : 1; - bool bOtherBroadcast : 1; - bool bOffline : 1; - }; - }; -}; - -struct WAMediaKeys -{ - WAMediaKeys(const uint8_t *pKey, size_t keyLen, const char *pszMediaType); - - uint8_t iv[16]; - uint8_t cipherKey[32]; - uint8_t macKey[64]; -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// own requests - -struct WARequestBase -{ - WARequestBase(const CMStringA &_1) : - szPacketId(_1) - {} - virtual ~WARequestBase() {} - - CMStringA szPacketId; - - virtual void Execute(WhatsAppProto *ppro, const WANode &node) = 0; -}; - -class WARequestSimple : public WARequestBase -{ - WA_PKT_HANDLER pHandler; - -public: - WARequestSimple(const CMStringA &_1, WA_PKT_HANDLER _2) : - WARequestBase(_1), - pHandler(_2) - {} - - void Execute(WhatsAppProto *ppro, const WANode &node) override - { - (ppro->*pHandler)(node); - } -}; - -class WARequestParam : public WARequestBase -{ - WA_PKT_HANDLER_FULL pHandler; - void *pUserInfo; - -public: - WARequestParam(const CMStringA &_1, WA_PKT_HANDLER_FULL _2, void *_3) : - WARequestBase(_1), - pHandler(_2), - pUserInfo(_3) - {} - - void Execute(WhatsAppProto *ppro, const WANode &node) override - { - (ppro->*pHandler)(node, pUserInfo); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct WAPersistentHandler -{ - WAPersistentHandler(const char *_1, const char *_2, const char *_3, const char *_4, WA_PKT_HANDLER _5) : - pszTitle(_1), pszType(_2), pszXmlns(_3), pszChild(_4), pHandler(_5) - {} - - const char *pszTitle, *pszType, *pszXmlns, *pszChild; - WA_PKT_HANDLER pHandler; -}; - -struct WAUser -{ - WAUser(MCONTACT _1, const char *_2, bool _3 = false) : - hContact(_1), - szId(mir_strdup(_2)), - bIsGroupChat(_3), - arDevices(1) - { - } - - ~WAUser() - { - mir_free(szId); - } - - MCONTACT hContact; - DWORD dwModifyTag = 0; - char *szId; - bool bInited = false, bIsGroupChat, bDeviceInit = false; - SESSION_INFO *si = 0; - OBJLIST arDevices; - time_t m_timer1 = 0, m_timer2 = 0; -}; - -struct WAOwnMessage -{ - WAOwnMessage(int _1, const char *_2, const char *_3) : - pktId(_1), - szJid(_2), - szMessageId(_3) - {} - - int pktId; - CMStringA szJid, szMessageId; -}; - -struct WACollection -{ - WACollection(const char *_1, int _2 = 0) : - szName(mir_strdup(_1)), - version(_2) - {} - - ptrA szName; - int version; - - LT_HASH hash; - std::map indexValueMap; -}; - -class WANoise -{ - friend class WhatsAppProto; - - WhatsAppProto *ppro; - uint32_t readCounter = 0, writeCounter = 0; - bool bInitFinished = false, bSendIntro = false; - MBinBuffer salt, encKey, decKey; - uint8_t hash[32]; - - struct { - MBinBuffer priv, pub; - } noiseKeys, ephemeral; - - void deriveKey(const void *pData, size_t cbLen, MBinBuffer &write, MBinBuffer &read); - void mixIntoKey(const void *n, const void *p); - void updateHash(const void *pData, size_t cbLen); - -public: - WANoise(WhatsAppProto *_ppro); - - void finish(); - void init(); - - MBinBuffer decrypt(const void *pData, size_t cbLen); - MBinBuffer encrypt(const void *pData, size_t cbLen); - - size_t decodeFrame(const uint8_t *&pData, size_t &cbLen); - MBinBuffer encodeFrame(const void *pData, size_t cbLen); -}; - -class MSignalSession : public MZeroedObject -{ - friend class MSignalStore; - signal_protocol_address address; - session_cipher *cipher = nullptr; - -public: - CMStringA szName; - MBinBuffer sessionData; - - MSignalSession(const CMStringA &_1, int _2); - ~MSignalSession(); - - bool hasAddress(const char *name, size_t name_len) const; - - __forceinline session_cipher* getCipher(void) const { return cipher; } - __forceinline int getDeviceId() const { return address.device_id; } - CMStringA getSetting() const; -}; - -class MSignalStore -{ - void init(); - - signal_context *m_pContext; - signal_protocol_store_context *m_pStore; - - void importPublicKey(ec_public_key **result, MBinBuffer &buf); - -public: - PROTO_INTERFACE *pProto; - const char *prefix; - - OBJLIST arSessions; - - struct - { - MBinBuffer priv, pub; - } - signedIdentity; - - struct - { - MBinBuffer priv, pub, signature; - uint32_t keyid; - } - preKey; - - MSignalStore(PROTO_INTERFACE *_1, const char *_2); - ~MSignalStore(); - - __forceinline signal_context *CTX() const { return m_pContext; } - - MSignalSession* createSession(const CMStringA &szName, int deviceId); - MSignalSession* getSession(const signal_protocol_address *address); - void injectSession(const char *szJid, const WANode *pNode, const WANode *pKey); - - MBinBuffer decryptSignalProto(const CMStringA &from, const char *pszType, const MBinBuffer &encrypted); - MBinBuffer decryptGroupSignalProto(const CMStringA &from, const CMStringA &author, const MBinBuffer &encrypted); - - MBinBuffer encryptSenderKey(const WAJid &to, const CMStringA &from, const MBinBuffer &buf, MBinBuffer &skmsgKey); - MBinBuffer encryptSignalProto(const WAJid &to, const MBinBuffer &buf, int &type); - - MBinBuffer encodeSignedIdentity(bool); - void generatePrekeys(int count); - - void logError(int code, const char *szMessage); - - void processSenderKeyMessage(const CMStringA &author, const Wa__Message__SenderKeyDistributionMessage *msg); -}; - -class WhatsAppProto : public PROTO -{ - friend class WANoise; - friend class CWhatsAppQRDlg; - friend class COptionsDlg; - - class CWhatsAppProtoImpl - { - friend class WhatsAppProto; - WhatsAppProto &m_proto; - - CTimer m_keepAlive, m_resyncApp; - void OnKeepAlive(CTimer *) - { m_proto.SendKeepAlive(); - } - void OnResync(CTimer *pTimer) - { - pTimer->Stop(); - m_proto.ResyncAll(); - } - - CWhatsAppProtoImpl(WhatsAppProto &pro) : - m_proto(pro), - m_keepAlive(Miranda_GetSystemWindow(), UINT_PTR(this)), - m_resyncApp(Miranda_GetSystemWindow(), UINT_PTR(this)+1) - { - m_keepAlive.OnEvent = Callback(this, &CWhatsAppProtoImpl::OnKeepAlive); - m_resyncApp.OnEvent = Callback(this, &CWhatsAppProtoImpl::OnResync); - } - } m_impl; - - bool m_bTerminated, m_bRespawn, m_bUpdatedPrekeys, m_bUnregister; - ptrW m_tszDefaultGroup; - - CMStringA m_szJid; - CMStringW m_tszAvatarFolder; - - EVP_PKEY *m_pKeys; // private & public keys - WANoise *m_noise; - - void UploadMorePrekeys(); - - // App state management - OBJLIST m_arCollections; - - void InitSync(void); - void ApplyPatch(const JSONNode &index, const Wa__SyncActionValue *data); - void ParsePatch(WACollection *pColl, const Wa__SyncdRecord *rec, bool bSet); - void ProcessHistorySync(const Wa__HistorySync *pSync); - void ResyncServer(const OBJLIST &task); - void ResyncAll(void); - - __forceinline WACollection *FindCollection(const char *pszName) - { return m_arCollections.find((WACollection *)&pszName); - } - - // Contacts management ///////////////////////////////////////////////////////////////// - - mir_cs m_csUsers; - OBJLIST m_arUsers; - - mir_cs m_csOwnMessages; - OBJLIST m_arOwnMsgs; - - WAUser* FindUser(const char *szId); - WAUser* AddUser(const char *szId, bool bTemporary); - - // Group chats ///////////////////////////////////////////////////////////////////////// - - void GC_RefreshMetadata(); - void GC_GetAllMetadata(); - void GC_ParseMetadata(const WANode *pGroup); - - int __cdecl GcEventHook(WPARAM, LPARAM); - int __cdecl GcMenuHook(WPARAM, LPARAM); - - // UI ////////////////////////////////////////////////////////////////////////////////// - - void CloseQrDialog(); - bool ShowQrCode(const CMStringA &ref); - - /// Network //////////////////////////////////////////////////////////////////////////// - - time_t m_lastRecvTime; - HNETLIBCONN m_hServerConn; - - mir_cs m_csPacketQueue; - OBJLIST m_arPacketQueue; - - LIST m_arPersistent; - WA_PKT_HANDLER FindPersistentHandler(const WANode &node); - - int m_iPacketId; - uint16_t m_wMsgPrefix[2]; - CMStringA GenerateMessageId(); - CMStringA GetMessageText(const Wa__Message *pMessage); - void GetMessageContent(CMStringA &txt, const char *szType, const char *szUrl, const char *szMimetype, const char *szDirectPath, const ProtobufCBinaryData &szMediaKey, const char *szCaption = nullptr); - void ProcessMessage(WAMSG type, const Wa__WebMessageInfo &msg); - bool CreateMsgParticipant(WANode *pParticipants, const WAJid &jid, const MBinBuffer &orig); - - void ProcessReceipt(MCONTACT hContact, const char *msgId, bool bRead); - - bool WSReadPacket(const WSHeader &hdr, MBinBuffer &buf); - int WSSend(const ProtobufCMessage &msg); - int WSSendNode(WANode &node); - int WSSendNode(WANode &node, WA_PKT_HANDLER); - int WSSendNode(WANode &node, WA_PKT_HANDLER_FULL, void *pUserInfo); - - MBinBuffer DownloadEncryptedFile(const char *url, const ProtobufCBinaryData &mediaKeys, const char *pszType); - CMStringW GetTmpFileName(const char *pszClass, const char *addition); - - void OnLoggedIn(void); - void OnLoggedOut(void); - void ProcessFailure(int code); - void ServerThreadWorker(void); - void ShutdownSession(void); - - void SendAck(const WANode &node); - void SendReceipt(const char *pszTo, const char *pszParticipant, const char *pszId, const char *pszType); - void SendKeepAlive(void); - int SendTextMessage(const char *jid, const char *pszMsg); - void SendUnregister(void); - void SendUsync(const LIST &jids, void *pUserInfo); - void SetServerStatus(int iStatus); - - void FinishTask(WASendTask *pTask); - void SendTask(WASendTask *pTask); - - /// Popups ///////////////////////////////////////////////////////////////////////////// - - HANDLE m_hPopupClass; - CMOption m_bUsePopups; - - void InitPopups(void); - void Popup(MCONTACT hContact, const wchar_t *szMsg, const wchar_t *szTitle); - - /// Request handlers /////////////////////////////////////////////////////////////////// - - void OnProcessHandshake(const uint8_t *pData, int cbLen); - - void InitPersistentHandlers(); - void OnAccountSync(const WANode &node); - void OnIqBlockList(const WANode &node); - void OnIqCountPrekeys(const WANode &node); - void OnIqDoNothing(const WANode &node); - void OnIqGcGetAllMetadata(const WANode &node); - void OnIqGetAvatar(const WANode &node); - void OnIqGetKeys(const WANode &node, void *pUserInfo); - void OnIqGetUsync(const WANode &node, void *pUserInfo); - void OnIqPairDevice(const WANode &node); - void OnIqPairSuccess(const WANode &node); - void OnIqResult(const WANode &node); - void OnIqServerSync(const WANode &node); - void OnNotifyAny(const WANode &node); - void OnNotifyDevices(const WANode &node); - void OnNotifyEncrypt(const WANode &node); - void OnNotifyPicture(const WANode &node); - void OnReceiveAck(const WANode &node); - void OnReceiveChatState(const WANode &node); - void OnReceiveFailure(const WANode &node); - void OnReceiveInfo(const WANode &node); - void OnReceiveMessage(const WANode &node); - void OnReceiveReceipt(const WANode &node); - void OnServerSync(const WANode &node); - void OnStreamError(const WANode &node); - void OnSuccess(const WANode &node); - - // Signal - MSignalStore m_signalStore; - - // Binary packets - void ProcessBinaryPacket(const uint8_t *pData, size_t cbLen); - - // unzip operations - MBinBuffer unzip(const MBinBuffer &src); - - /// Avatars //////////////////////////////////////////////////////////////////////////// - CMStringW GetAvatarFileName(MCONTACT hContact); - void ServerFetchAvatar(const char *jid); - - INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); - INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); - INT_PTR __cdecl GetMyAvatar(WPARAM, LPARAM); - INT_PTR __cdecl SetMyAvatar(WPARAM, LPARAM); - -public: - WhatsAppProto(const char *proto_name, const wchar_t *username); - ~WhatsAppProto(); - - __forceinline bool isOnline() const - { return m_hServerConn != 0; - } - - __forceinline void writeStr(const char *pszSetting, const JSONNode &node) - { - CMStringW str(node.as_mstring()); - if (!str.IsEmpty()) - setWString(pszSetting, str); - } - - class CWhatsAppQRDlg *m_pQRDlg; - - // PROTO_INTERFACE ///////////////////////////////////////////////////////////////////// - - MCONTACT AddToList(int flags, PROTOSEARCHRESULT *psr) override; - INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; - HANDLE SearchBasic(const wchar_t* id) override; - int SendMsg(MCONTACT hContact, int flags, const char* msg) override; - int SetStatus(int iNewStatus) override; - int UserIsTyping(MCONTACT hContact, int type) override; - - void OnErase() override; - void OnModulesLoaded() override; - - // Services //////////////////////////////////////////////////////////////////////////// - - INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); - - // Events ////////////////////////////////////////////////////////////////////////////// - - int __cdecl OnOptionsInit(WPARAM, LPARAM); - int __cdecl OnBuildStatusMenu(WPARAM, LPARAM); - - // Options ///////////////////////////////////////////////////////////////////////////// - - CMOption m_wszNick; // your nick name in presence - CMOption m_wszDeviceName; // how do you see Miranda in mobile phone - CMOption m_wszDefaultGroup; // clist group to store contacts - CMOption m_bHideGroupchats; // do not open chat windows on creation - CMOption m_bUseBbcodes; // use extended markup for messages - - // Processing Threads ////////////////////////////////////////////////////////////////// - - void __cdecl SearchAckThread(void*); - void __cdecl ServerThread(void*); -}; - -struct CMPlugin : public ACCPROTOPLUGIN -{ - HNETLIBUSER hAvatarUser = nullptr; - HNETLIBCONN hAvatarConn = nullptr; - bool SaveFile(const char *pszUrl, PROTO_AVATAR_INFORMATION &ai); - - bool bHasMessageState = false; - - CMPlugin(); - - int Load() override; - int Unload() override; -}; - -#endif +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#if !defined(PROTO_H) +#define PROTO_H + +#define S_WHATSAPP_NET "@s.whatsapp.net" +#define APP_VERSION "2.2230.15" +#define KEY_BUNDLE_TYPE "\x05" + +class WhatsAppProto; +typedef void (WhatsAppProto:: *WA_PKT_HANDLER)(const WANode &node); +typedef void (WhatsAppProto:: *WA_PKT_HANDLER_FULL)(const WANode &node, void *pUserInfo); + +struct WAMSG +{ + union { + uint32_t dwFlags = 0; + struct { + bool bPrivateChat : 1; + bool bGroupChat : 1; + bool bDirectStatus : 1; + bool bOtherStatus : 1; + bool bPeerBroadcast : 1; + bool bOtherBroadcast : 1; + bool bOffline : 1; + }; + }; +}; + +struct WAMediaKeys +{ + WAMediaKeys(const uint8_t *pKey, size_t keyLen, const char *pszMediaType); + + uint8_t iv[16]; + uint8_t cipherKey[32]; + uint8_t macKey[64]; +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// own requests + +struct WARequestBase +{ + WARequestBase(const CMStringA &_1) : + szPacketId(_1) + {} + virtual ~WARequestBase() {} + + CMStringA szPacketId; + + virtual void Execute(WhatsAppProto *ppro, const WANode &node) = 0; +}; + +class WARequestSimple : public WARequestBase +{ + WA_PKT_HANDLER pHandler; + +public: + WARequestSimple(const CMStringA &_1, WA_PKT_HANDLER _2) : + WARequestBase(_1), + pHandler(_2) + {} + + void Execute(WhatsAppProto *ppro, const WANode &node) override + { + (ppro->*pHandler)(node); + } +}; + +class WARequestParam : public WARequestBase +{ + WA_PKT_HANDLER_FULL pHandler; + void *pUserInfo; + +public: + WARequestParam(const CMStringA &_1, WA_PKT_HANDLER_FULL _2, void *_3) : + WARequestBase(_1), + pHandler(_2), + pUserInfo(_3) + {} + + void Execute(WhatsAppProto *ppro, const WANode &node) override + { + (ppro->*pHandler)(node, pUserInfo); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct WAPersistentHandler +{ + WAPersistentHandler(const char *_1, const char *_2, const char *_3, const char *_4, WA_PKT_HANDLER _5) : + pszTitle(_1), pszType(_2), pszXmlns(_3), pszChild(_4), pHandler(_5) + {} + + const char *pszTitle, *pszType, *pszXmlns, *pszChild; + WA_PKT_HANDLER pHandler; +}; + +struct WAUser +{ + WAUser(MCONTACT _1, const char *_2, bool _3 = false) : + hContact(_1), + szId(mir_strdup(_2)), + bIsGroupChat(_3), + arDevices(1) + { + } + + ~WAUser() + { + mir_free(szId); + } + + MCONTACT hContact; + DWORD dwModifyTag = 0; + char *szId; + bool bInited = false, bIsGroupChat, bDeviceInit = false; + SESSION_INFO *si = 0; + OBJLIST arDevices; + time_t m_timer1 = 0, m_timer2 = 0; +}; + +struct WAOwnMessage +{ + WAOwnMessage(int _1, const char *_2, const char *_3) : + pktId(_1), + szJid(_2), + szMessageId(_3) + {} + + int pktId; + CMStringA szJid, szMessageId; +}; + +struct WACollection +{ + WACollection(const char *_1, int _2 = 0) : + szName(mir_strdup(_1)), + version(_2) + {} + + ptrA szName; + int version; + + LT_HASH hash; + std::map indexValueMap; +}; + +class WANoise +{ + friend class WhatsAppProto; + + WhatsAppProto *ppro; + uint32_t readCounter = 0, writeCounter = 0; + bool bInitFinished = false, bSendIntro = false; + MBinBuffer salt, encKey, decKey; + uint8_t hash[32]; + + struct { + MBinBuffer priv, pub; + } noiseKeys, ephemeral; + + void deriveKey(const void *pData, size_t cbLen, MBinBuffer &write, MBinBuffer &read); + void mixIntoKey(const void *n, const void *p); + void updateHash(const void *pData, size_t cbLen); + +public: + WANoise(WhatsAppProto *_ppro); + + void finish(); + void init(); + + MBinBuffer decrypt(const void *pData, size_t cbLen); + MBinBuffer encrypt(const void *pData, size_t cbLen); + + size_t decodeFrame(const uint8_t *&pData, size_t &cbLen); + MBinBuffer encodeFrame(const void *pData, size_t cbLen); +}; + +class MSignalSession : public MZeroedObject +{ + friend class MSignalStore; + signal_protocol_address address; + session_cipher *cipher = nullptr; + +public: + CMStringA szName; + MBinBuffer sessionData; + + MSignalSession(const CMStringA &_1, int _2); + ~MSignalSession(); + + bool hasAddress(const char *name, size_t name_len) const; + + __forceinline session_cipher* getCipher(void) const { return cipher; } + __forceinline int getDeviceId() const { return address.device_id; } + CMStringA getSetting() const; +}; + +class MSignalStore +{ + void init(); + + signal_context *m_pContext; + signal_protocol_store_context *m_pStore; + + void importPublicKey(ec_public_key **result, MBinBuffer &buf); + +public: + PROTO_INTERFACE *pProto; + const char *prefix; + + OBJLIST arSessions; + + struct + { + MBinBuffer priv, pub; + } + signedIdentity; + + struct + { + MBinBuffer priv, pub, signature; + uint32_t keyid; + } + preKey; + + MSignalStore(PROTO_INTERFACE *_1, const char *_2); + ~MSignalStore(); + + __forceinline signal_context *CTX() const { return m_pContext; } + + MSignalSession* createSession(const CMStringA &szName, int deviceId); + MSignalSession* getSession(const signal_protocol_address *address); + void injectSession(const char *szJid, const WANode *pNode, const WANode *pKey); + + MBinBuffer decryptSignalProto(const CMStringA &from, const char *pszType, const MBinBuffer &encrypted); + MBinBuffer decryptGroupSignalProto(const CMStringA &from, const CMStringA &author, const MBinBuffer &encrypted); + + MBinBuffer encryptSenderKey(const WAJid &to, const CMStringA &from, const MBinBuffer &buf, MBinBuffer &skmsgKey); + MBinBuffer encryptSignalProto(const WAJid &to, const MBinBuffer &buf, int &type); + + MBinBuffer encodeSignedIdentity(bool); + void generatePrekeys(int count); + + void logError(int code, const char *szMessage); + + void processSenderKeyMessage(const CMStringA &author, const Wa__Message__SenderKeyDistributionMessage *msg); +}; + +class WhatsAppProto : public PROTO +{ + friend class WANoise; + friend class CWhatsAppQRDlg; + friend class COptionsDlg; + + class CWhatsAppProtoImpl + { + friend class WhatsAppProto; + WhatsAppProto &m_proto; + + CTimer m_keepAlive, m_resyncApp; + void OnKeepAlive(CTimer *) + { m_proto.SendKeepAlive(); + } + void OnResync(CTimer *pTimer) + { + pTimer->Stop(); + m_proto.ResyncAll(); + } + + CWhatsAppProtoImpl(WhatsAppProto &pro) : + m_proto(pro), + m_keepAlive(Miranda_GetSystemWindow(), UINT_PTR(this)), + m_resyncApp(Miranda_GetSystemWindow(), UINT_PTR(this)+1) + { + m_keepAlive.OnEvent = Callback(this, &CWhatsAppProtoImpl::OnKeepAlive); + m_resyncApp.OnEvent = Callback(this, &CWhatsAppProtoImpl::OnResync); + } + } m_impl; + + bool m_bTerminated, m_bRespawn, m_bUpdatedPrekeys, m_bUnregister; + ptrW m_tszDefaultGroup; + + CMStringA m_szJid; + CMStringW m_tszAvatarFolder; + + EVP_PKEY *m_pKeys; // private & public keys + WANoise *m_noise; + + void UploadMorePrekeys(); + + // App state management + OBJLIST m_arCollections; + + void InitSync(void); + void ApplyPatch(const JSONNode &index, const Wa__SyncActionValue *data); + void ParsePatch(WACollection *pColl, const Wa__SyncdRecord *rec, bool bSet); + void ProcessHistorySync(const Wa__HistorySync *pSync); + void ResyncServer(const OBJLIST &task); + void ResyncAll(void); + + __forceinline WACollection *FindCollection(const char *pszName) + { return m_arCollections.find((WACollection *)&pszName); + } + + // Contacts management ///////////////////////////////////////////////////////////////// + + mir_cs m_csUsers; + OBJLIST m_arUsers; + + mir_cs m_csOwnMessages; + OBJLIST m_arOwnMsgs; + + WAUser* FindUser(const char *szId); + WAUser* AddUser(const char *szId, bool bTemporary); + + // Group chats ///////////////////////////////////////////////////////////////////////// + + void GC_RefreshMetadata(); + void GC_GetAllMetadata(); + void GC_ParseMetadata(const WANode *pGroup); + + int __cdecl GcEventHook(WPARAM, LPARAM); + int __cdecl GcMenuHook(WPARAM, LPARAM); + + // UI ////////////////////////////////////////////////////////////////////////////////// + + void CloseQrDialog(); + bool ShowQrCode(const CMStringA &ref); + + /// Network //////////////////////////////////////////////////////////////////////////// + + time_t m_lastRecvTime; + HNETLIBCONN m_hServerConn; + + mir_cs m_csPacketQueue; + OBJLIST m_arPacketQueue; + + LIST m_arPersistent; + WA_PKT_HANDLER FindPersistentHandler(const WANode &node); + + int m_iPacketId; + uint16_t m_wMsgPrefix[2]; + CMStringA GenerateMessageId(); + CMStringA GetMessageText(const Wa__Message *pMessage); + void GetMessageContent(CMStringA &txt, const char *szType, const char *szUrl, const char *szMimetype, const char *szDirectPath, const ProtobufCBinaryData &szMediaKey, const char *szCaption = nullptr); + void ProcessMessage(WAMSG type, const Wa__WebMessageInfo &msg); + bool CreateMsgParticipant(WANode *pParticipants, const WAJid &jid, const MBinBuffer &orig); + + void ProcessReceipt(MCONTACT hContact, const char *msgId, bool bRead); + + bool WSReadPacket(const WSHeader &hdr, MBinBuffer &buf); + int WSSend(const ProtobufCMessage &msg); + int WSSendNode(WANode &node); + int WSSendNode(WANode &node, WA_PKT_HANDLER); + int WSSendNode(WANode &node, WA_PKT_HANDLER_FULL, void *pUserInfo); + + MBinBuffer DownloadEncryptedFile(const char *url, const ProtobufCBinaryData &mediaKeys, const char *pszType); + CMStringW GetTmpFileName(const char *pszClass, const char *addition); + + void OnLoggedIn(void); + void OnLoggedOut(void); + void ProcessFailure(int code); + void ServerThreadWorker(void); + void ShutdownSession(void); + + void SendAck(const WANode &node); + void SendReceipt(const char *pszTo, const char *pszParticipant, const char *pszId, const char *pszType); + void SendKeepAlive(void); + int SendTextMessage(const char *jid, const char *pszMsg); + void SendUnregister(void); + void SendUsync(const LIST &jids, void *pUserInfo); + void SetServerStatus(int iStatus); + + void FinishTask(WASendTask *pTask); + void SendTask(WASendTask *pTask); + + /// Popups ///////////////////////////////////////////////////////////////////////////// + + HANDLE m_hPopupClass; + CMOption m_bUsePopups; + + void InitPopups(void); + void Popup(MCONTACT hContact, const wchar_t *szMsg, const wchar_t *szTitle); + + /// Request handlers /////////////////////////////////////////////////////////////////// + + void OnProcessHandshake(const uint8_t *pData, int cbLen); + + void InitPersistentHandlers(); + void OnAccountSync(const WANode &node); + void OnIqBlockList(const WANode &node); + void OnIqCountPrekeys(const WANode &node); + void OnIqDoNothing(const WANode &node); + void OnIqGcGetAllMetadata(const WANode &node); + void OnIqGetAvatar(const WANode &node); + void OnIqGetKeys(const WANode &node, void *pUserInfo); + void OnIqGetUsync(const WANode &node, void *pUserInfo); + void OnIqPairDevice(const WANode &node); + void OnIqPairSuccess(const WANode &node); + void OnIqResult(const WANode &node); + void OnIqServerSync(const WANode &node); + void OnNotifyAny(const WANode &node); + void OnNotifyDevices(const WANode &node); + void OnNotifyEncrypt(const WANode &node); + void OnNotifyPicture(const WANode &node); + void OnReceiveAck(const WANode &node); + void OnReceiveChatState(const WANode &node); + void OnReceiveFailure(const WANode &node); + void OnReceiveInfo(const WANode &node); + void OnReceiveMessage(const WANode &node); + void OnReceiveReceipt(const WANode &node); + void OnServerSync(const WANode &node); + void OnStreamError(const WANode &node); + void OnSuccess(const WANode &node); + + // Signal + MSignalStore m_signalStore; + + // Binary packets + void ProcessBinaryPacket(const uint8_t *pData, size_t cbLen); + + // unzip operations + MBinBuffer unzip(const MBinBuffer &src); + + /// Avatars //////////////////////////////////////////////////////////////////////////// + CMStringW GetAvatarFileName(MCONTACT hContact); + void ServerFetchAvatar(const char *jid); + + INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl GetMyAvatar(WPARAM, LPARAM); + INT_PTR __cdecl SetMyAvatar(WPARAM, LPARAM); + +public: + WhatsAppProto(const char *proto_name, const wchar_t *username); + ~WhatsAppProto(); + + __forceinline bool isOnline() const + { return m_hServerConn != 0; + } + + __forceinline void writeStr(const char *pszSetting, const JSONNode &node) + { + CMStringW str(node.as_mstring()); + if (!str.IsEmpty()) + setWString(pszSetting, str); + } + + class CWhatsAppQRDlg *m_pQRDlg; + + // PROTO_INTERFACE ///////////////////////////////////////////////////////////////////// + + MCONTACT AddToList(int flags, PROTOSEARCHRESULT *psr) override; + INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; + HANDLE SearchBasic(const wchar_t* id) override; + int SendMsg(MCONTACT hContact, int flags, const char* msg) override; + int SetStatus(int iNewStatus) override; + int UserIsTyping(MCONTACT hContact, int type) override; + + void OnErase() override; + void OnModulesLoaded() override; + + // Services //////////////////////////////////////////////////////////////////////////// + + INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); + + // Events ////////////////////////////////////////////////////////////////////////////// + + int __cdecl OnOptionsInit(WPARAM, LPARAM); + int __cdecl OnBuildStatusMenu(WPARAM, LPARAM); + + // Options ///////////////////////////////////////////////////////////////////////////// + + CMOption m_wszNick; // your nick name in presence + CMOption m_wszDeviceName; // how do you see Miranda in mobile phone + CMOption m_wszDefaultGroup; // clist group to store contacts + CMOption m_bHideGroupchats; // do not open chat windows on creation + CMOption m_bUseBbcodes; // use extended markup for messages + + // Processing Threads ////////////////////////////////////////////////////////////////// + + void __cdecl SearchAckThread(void*); + void __cdecl ServerThread(void*); +}; + +struct CMPlugin : public ACCPROTOPLUGIN +{ + HNETLIBUSER hAvatarUser = nullptr; + HNETLIBCONN hAvatarConn = nullptr; + bool SaveFile(const char *pszUrl, PROTO_AVATAR_INFORMATION &ai); + + bool bHasMessageState = false; + + CMPlugin(); + + int Load() override; + int Unload() override; +}; + +#endif diff --git a/protocols/WhatsApp/src/server.cpp b/protocols/WhatsApp/src/server.cpp index 7512b05caf..24d64e5aa7 100644 --- a/protocols/WhatsApp/src/server.cpp +++ b/protocols/WhatsApp/src/server.cpp @@ -1,418 +1,418 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-23 George Hazan - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// -// gateway worker thread - -void WhatsAppProto::ServerThread(void *) -{ - do { - m_bRespawn = m_bUnregister = false; - ServerThreadWorker(); - } - while (m_bRespawn); - - OnLoggedOut(); -} - -void WhatsAppProto::ServerThreadWorker() -{ - // connect websocket - NETLIBHTTPHEADER hdrs[] = - { - { "Origin", "https://web.whatsapp.com" }, - { 0, 0 } - }; - - NLHR_PTR pReply(WebSocket_Connect(m_hNetlibUser, "web.whatsapp.com/ws/chat", hdrs)); - if (pReply == nullptr) { - debugLogA("Server connection failed, exiting"); - return; - } - - if (pReply->resultCode != 101) - return; - - delete m_noise; - m_noise = new WANoise(this); - m_noise->init(); - - debugLogA("Server connection succeeded"); - m_hServerConn = pReply->nlc; - m_lastRecvTime = time(0); - m_iPacketId = 1; - - Utils_GetRandom(m_wMsgPrefix, sizeof(m_wMsgPrefix)); - - Wa__HandshakeMessage__ClientHello client; - client.ephemeral = {m_noise->ephemeral.pub.length(), m_noise->ephemeral.pub.data()}; - client.has_ephemeral = true; - - Wa__HandshakeMessage msg; - msg.clienthello = &client; - WSSend(msg); - - MBinBuffer netbuf; - - for (m_bTerminated = false; !m_bTerminated;) { - unsigned char buf[2048]; - int bufSize = Netlib_Recv(m_hServerConn, (char *)buf, _countof(buf), MSG_NODUMP); - if (bufSize == 0) { - debugLogA("Gateway connection gracefully closed"); - break; - } - if (bufSize < 0) { - debugLogA("Gateway connection error, exiting"); - break; - } - - netbuf.append(buf, bufSize); - - WSHeader hdr; - if (!WebSocket_InitHeader(hdr, netbuf.data(), netbuf.length())) - continue; - - // we lack some data, let's read them - if (netbuf.length() < hdr.headerSize + hdr.payloadSize) - if (!WSReadPacket(hdr, netbuf)) - break; - - // debugLogA("Got packet: buffer = %d, opcode = %d, headerSize = %d, payloadSize = %d, final = %d, masked = %d", - // netbuf.length(), hdr.opCode, hdr.headerSize, hdr.payloadSize, hdr.bIsFinal, hdr.bIsMasked); - // Netlib_Dump(m_hServerConn, netbuf.data(), netbuf.length(), false, 0); - - m_lastRecvTime = time(0); - - // read all payloads from the current buffer, one by one - while (true) { - MBinBuffer currPacket; - currPacket.assign(netbuf.data() + hdr.headerSize, hdr.payloadSize); - - switch (hdr.opCode) { - case 1: // json packet - debugLogA("Text packet, skipping"); - /* - currPacket.append("", 1); // add 0 to use strchr safely - CMStringA szJson(pos, (int)dataSize); - - JSONNode root = JSONNode::parse(szJson); - if (root) { - debugLogA("JSON received:\n%s", start); - - CMStringA szPrefix(start, int(pos - start - 1)); - auto *pReq = m_arPacketQueue.find((WARequest *)&szPrefix); - if (pReq != nullptr) { - root << CHAR_PARAM("$id$", szPrefix); - } - } - } - */ - break; - - case 2: // binary packet - if (hdr.payloadSize > 32) - ProcessBinaryPacket(currPacket.data(), hdr.payloadSize); - break; - - case 8: // close - debugLogA("server required to exit"); - m_bRespawn = m_bTerminated = true; // simply reconnect, don't exit - break; - - default: - Netlib_Dump(m_hServerConn, currPacket.data(), hdr.payloadSize, false, 0); - } - - netbuf.remove(hdr.headerSize + hdr.payloadSize); - // debugLogA("%d bytes removed from network buffer, %d bytes remain", hdr.headerSize + hdr.payloadSize, netbuf.length()); - if (netbuf.length() == 0) - break; - - // if we have not enough data for header, continue reading - if (!WebSocket_InitHeader(hdr, netbuf.data(), netbuf.length())) { - debugLogA("not enough data for header, continue reading"); - break; - } - - // if we have not enough data for data, continue reading - if (hdr.headerSize + hdr.payloadSize > netbuf.length()) { - debugLogA("not enough place for data (%d+%d > %d), continue reading", hdr.headerSize, hdr.payloadSize, netbuf.length()); - break; - } - - debugLogA("Got inner packet: buffer = %d, opcode = %d, headerSize = %d, payloadSize = %d, final = %d, masked = %d", - netbuf.length(), hdr.opCode, hdr.headerSize, hdr.payloadSize, hdr.bIsFinal, hdr.bIsMasked); - } - } - - debugLogA("Server connection dropped"); - Netlib_CloseHandle(m_hServerConn); - m_hServerConn = nullptr; -} - -bool WhatsAppProto::WSReadPacket(const WSHeader &hdr, MBinBuffer &res) -{ - size_t currPacketSize = res.length() - hdr.headerSize; - - char buf[1024]; - while (currPacketSize < hdr.payloadSize) { - int result = Netlib_Recv(m_hServerConn, buf, _countof(buf), MSG_NODUMP); - if (result == 0) { - debugLogA("Gateway connection gracefully closed"); - return false; - } - if (result < 0) { - debugLogA("Gateway connection error, exiting"); - return false; - } - - currPacketSize += result; - res.append(buf, result); - } - return true; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Binary data processing - -void WhatsAppProto::ProcessBinaryPacket(const uint8_t *pData, size_t cbDataLen) -{ - while (size_t payloadLen = m_noise->decodeFrame(pData, cbDataLen)) { - if (m_noise->bInitFinished) { - MBinBuffer buf = m_noise->decrypt(pData, payloadLen); - - WAReader rdr(buf.data(), buf.length()); - auto b = rdr.readInt8(); - if (b & 2) { - buf.remove(1); - buf = unzip(buf); - rdr = WAReader(buf.data(), buf.length()); - } - - if (WANode *pNode = rdr.readNode()) { - CMStringA szText; - pNode->print(szText); - debugLogA("Got binary node:\n%s", szText.c_str()); - - auto pHandler = FindPersistentHandler(*pNode); - if (pHandler) - (this->*pHandler)(*pNode); - else - debugLogA("cannot handle incoming message"); - - delete pNode; - } - else { - debugLogA("wrong or broken payload"); - Netlib_Dump(m_hServerConn, pData, cbDataLen, false, 0); - } - } - else OnProcessHandshake(pData, (int)payloadLen); - - pData = (BYTE*)pData + payloadLen; - cbDataLen -= payloadLen; - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::ProcessFailure(int code) -{ - switch (code) { - case 401: - debugLogA("Connection logged out from another device, exiting"); - Popup(0, TranslateT("This account was logged out from mobile phone, you need to link it again"), m_tszUserName); - - OnErase(); - break; - - case 408: - debugLogA("Connection lost, exiting"); - break; - - case 411: - debugLogA("Conflict between two devices, exiting"); - break; - - case 428: - debugLogA("Connection forcibly closed by the server, exiting"); - break; - - case 440: - debugLogA("Connection replaced from another device, exiting"); - break; - - case 515: - debugLogA("Server required to restart immediately, leaving thread"); - m_bRespawn = true; - break; - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnLoggedIn() -{ - debugLogA("WhatsAppProto::OnLoggedIn"); - - if (m_bUnregister) { - SendUnregister(); - m_bTerminated = true; - return; - } - - SetServerStatus(m_iDesiredStatus); - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); - m_iStatus = m_iDesiredStatus; - m_bUpdatedPrekeys = false; - - m_impl.m_keepAlive.Start(1000); - - // retrieve initial info - WANodeIq abt(IQ::GET, "abt"); - abt.addChild("props")->addAttr("protocol", "1"); - WSSendNode(abt, &WhatsAppProto::OnIqDoNothing); - - WSSendNode( - WANodeIq(IQ::GET, "w") << XCHILD("props"), - &WhatsAppProto::OnIqDoNothing); - - WSSendNode( - WANodeIq(IQ::GET, "blocklist"), - &WhatsAppProto::OnIqBlockList); - - WSSendNode( - WANodeIq(IQ::GET, "privacy") << XCHILD("privacy"), - &WhatsAppProto::OnIqDoNothing); - - GC_RefreshMetadata(); -} - -void WhatsAppProto::OnLoggedOut(void) -{ - m_impl.m_keepAlive.Stop(); - - debugLogA("WhatsAppProto::OnLoggedOut"); - m_bTerminated = true; - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - - setAllContactStatuses(ID_STATUS_OFFLINE, false); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Service packets sending - -void WhatsAppProto::SendAck(const WANode &node) -{ - WANode ack("ack"); - ack << CHAR_PARAM("to", node.getAttr("from")) << CHAR_PARAM("id", node.getAttr("id")) << CHAR_PARAM("class", node.title); - if (node.title != "message") - if (auto *param = node.getAttr("type")) - ack << CHAR_PARAM("type", param); - if (auto *param = node.getAttr("participant")) - ack << CHAR_PARAM("participant", param); - if (auto *param = node.getAttr("recipient")) - ack << CHAR_PARAM("recipient", param); - WSSendNode(ack); -} - -void WhatsAppProto::SendKeepAlive() -{ - time_t now = time(0); - if (now - m_lastRecvTime > 20) { - WSSendNode(WANodeIq(IQ::GET, "w:p") << XCHILD("ping"), &WhatsAppProto::OnIqDoNothing); - - m_lastRecvTime = now; - } - - for (auto &it : m_arUsers) { - if (it->m_timer1 && now - it->m_timer1 > 600) { - it->m_timer1 = 0; - it->m_timer2 = now; - setWord(it->hContact, "Status", ID_STATUS_AWAY); - } - else if (it->m_timer2 && now - it->m_timer2 > 600) { - it->m_timer2 = 0; - setWord(it->hContact, "Status", ID_STATUS_OFFLINE); - } - } -} - -void WhatsAppProto::SendReceipt(const char *pszTo, const char *pszParticipant, const char *pszId, const char *pszType) -{ - WANode receipt("receipt"); - receipt << CHAR_PARAM("id", pszId); - - if (!mir_strcmp(pszType, "read") || !mir_strcmp(pszType, "read-self")) - receipt << INT_PARAM("t", time(0)); - - if (!mir_strcmp(pszType, "sender") && WAJid(pszTo).isUser()) - receipt << CHAR_PARAM("to", pszParticipant) << CHAR_PARAM("recipient", pszTo); - else { - receipt << CHAR_PARAM("to", pszTo); - if (pszParticipant) - receipt << CHAR_PARAM("participant", pszParticipant); - } - - if (pszType) - receipt << CHAR_PARAM("type", pszType); - WSSendNode(receipt); -} - -void WhatsAppProto::SetServerStatus(int iStatus) -{ - if (mir_wstrlen(m_wszNick)) - WSSendNode( - WANode("presence") << CHAR_PARAM("name", T2Utf(m_wszNick)) << CHAR_PARAM("type", (iStatus == ID_STATUS_ONLINE) ? "available" : "unavailable"), - &WhatsAppProto::OnIqDoNothing); -} - -void WhatsAppProto::SendUnregister() -{ - WANodeIq iq(IQ::SET, "md"); - *iq.addChild("remove-companion-device") << CHAR_PARAM("jid", WAJid(m_szJid, getDword(DBKEY_DEVICE_ID)).toString()) << CHAR_PARAM("reason", "user's decision"); - WSSendNode(iq, &WhatsAppProto::OnIqDoNothing); - - m_bTerminated = true; -} - -void WhatsAppProto::SendUsync(const LIST &jids, void *pUserInfo) -{ - WANodeIq iq(IQ::GET, "usync"); - - auto *pNode1 = iq.addChild("usync"); - *pNode1 << CHAR_PARAM("sid", GenerateMessageId()) << CHAR_PARAM("mode", "query") << CHAR_PARAM("last", "true") - << CHAR_PARAM("index", "0") << CHAR_PARAM("context", "message"); - - pNode1->addChild("query")->addChild("devices")->addAttr("version", "2"); - auto *pList = pNode1->addChild("list"); - for (auto &it : jids) - pList->addChild("user")->addAttr("jid", it); - - WSSendNode(iq, &WhatsAppProto::OnIqGetUsync, pUserInfo); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::ShutdownSession() -{ - if (m_bTerminated) - return; - - debugLogA("WhatsAppProto::ShutdownSession"); - - // shutdown all resources - if (m_hServerConn) - Netlib_Shutdown(m_hServerConn); - - OnLoggedOut(); -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// +// gateway worker thread + +void WhatsAppProto::ServerThread(void *) +{ + do { + m_bRespawn = m_bUnregister = false; + ServerThreadWorker(); + } + while (m_bRespawn); + + OnLoggedOut(); +} + +void WhatsAppProto::ServerThreadWorker() +{ + // connect websocket + NETLIBHTTPHEADER hdrs[] = + { + { "Origin", "https://web.whatsapp.com" }, + { 0, 0 } + }; + + NLHR_PTR pReply(WebSocket_Connect(m_hNetlibUser, "web.whatsapp.com/ws/chat", hdrs)); + if (pReply == nullptr) { + debugLogA("Server connection failed, exiting"); + return; + } + + if (pReply->resultCode != 101) + return; + + delete m_noise; + m_noise = new WANoise(this); + m_noise->init(); + + debugLogA("Server connection succeeded"); + m_hServerConn = pReply->nlc; + m_lastRecvTime = time(0); + m_iPacketId = 1; + + Utils_GetRandom(m_wMsgPrefix, sizeof(m_wMsgPrefix)); + + Wa__HandshakeMessage__ClientHello client; + client.ephemeral = {m_noise->ephemeral.pub.length(), m_noise->ephemeral.pub.data()}; + client.has_ephemeral = true; + + Wa__HandshakeMessage msg; + msg.clienthello = &client; + WSSend(msg); + + MBinBuffer netbuf; + + for (m_bTerminated = false; !m_bTerminated;) { + unsigned char buf[2048]; + int bufSize = Netlib_Recv(m_hServerConn, (char *)buf, _countof(buf), MSG_NODUMP); + if (bufSize == 0) { + debugLogA("Gateway connection gracefully closed"); + break; + } + if (bufSize < 0) { + debugLogA("Gateway connection error, exiting"); + break; + } + + netbuf.append(buf, bufSize); + + WSHeader hdr; + if (!WebSocket_InitHeader(hdr, netbuf.data(), netbuf.length())) + continue; + + // we lack some data, let's read them + if (netbuf.length() < hdr.headerSize + hdr.payloadSize) + if (!WSReadPacket(hdr, netbuf)) + break; + + // debugLogA("Got packet: buffer = %d, opcode = %d, headerSize = %d, payloadSize = %d, final = %d, masked = %d", + // netbuf.length(), hdr.opCode, hdr.headerSize, hdr.payloadSize, hdr.bIsFinal, hdr.bIsMasked); + // Netlib_Dump(m_hServerConn, netbuf.data(), netbuf.length(), false, 0); + + m_lastRecvTime = time(0); + + // read all payloads from the current buffer, one by one + while (true) { + MBinBuffer currPacket; + currPacket.assign(netbuf.data() + hdr.headerSize, hdr.payloadSize); + + switch (hdr.opCode) { + case 1: // json packet + debugLogA("Text packet, skipping"); + /* + currPacket.append("", 1); // add 0 to use strchr safely + CMStringA szJson(pos, (int)dataSize); + + JSONNode root = JSONNode::parse(szJson); + if (root) { + debugLogA("JSON received:\n%s", start); + + CMStringA szPrefix(start, int(pos - start - 1)); + auto *pReq = m_arPacketQueue.find((WARequest *)&szPrefix); + if (pReq != nullptr) { + root << CHAR_PARAM("$id$", szPrefix); + } + } + } + */ + break; + + case 2: // binary packet + if (hdr.payloadSize > 32) + ProcessBinaryPacket(currPacket.data(), hdr.payloadSize); + break; + + case 8: // close + debugLogA("server required to exit"); + m_bRespawn = m_bTerminated = true; // simply reconnect, don't exit + break; + + default: + Netlib_Dump(m_hServerConn, currPacket.data(), hdr.payloadSize, false, 0); + } + + netbuf.remove(hdr.headerSize + hdr.payloadSize); + // debugLogA("%d bytes removed from network buffer, %d bytes remain", hdr.headerSize + hdr.payloadSize, netbuf.length()); + if (netbuf.length() == 0) + break; + + // if we have not enough data for header, continue reading + if (!WebSocket_InitHeader(hdr, netbuf.data(), netbuf.length())) { + debugLogA("not enough data for header, continue reading"); + break; + } + + // if we have not enough data for data, continue reading + if (hdr.headerSize + hdr.payloadSize > netbuf.length()) { + debugLogA("not enough place for data (%d+%d > %d), continue reading", hdr.headerSize, hdr.payloadSize, netbuf.length()); + break; + } + + debugLogA("Got inner packet: buffer = %d, opcode = %d, headerSize = %d, payloadSize = %d, final = %d, masked = %d", + netbuf.length(), hdr.opCode, hdr.headerSize, hdr.payloadSize, hdr.bIsFinal, hdr.bIsMasked); + } + } + + debugLogA("Server connection dropped"); + Netlib_CloseHandle(m_hServerConn); + m_hServerConn = nullptr; +} + +bool WhatsAppProto::WSReadPacket(const WSHeader &hdr, MBinBuffer &res) +{ + size_t currPacketSize = res.length() - hdr.headerSize; + + char buf[1024]; + while (currPacketSize < hdr.payloadSize) { + int result = Netlib_Recv(m_hServerConn, buf, _countof(buf), MSG_NODUMP); + if (result == 0) { + debugLogA("Gateway connection gracefully closed"); + return false; + } + if (result < 0) { + debugLogA("Gateway connection error, exiting"); + return false; + } + + currPacketSize += result; + res.append(buf, result); + } + return true; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Binary data processing + +void WhatsAppProto::ProcessBinaryPacket(const uint8_t *pData, size_t cbDataLen) +{ + while (size_t payloadLen = m_noise->decodeFrame(pData, cbDataLen)) { + if (m_noise->bInitFinished) { + MBinBuffer buf = m_noise->decrypt(pData, payloadLen); + + WAReader rdr(buf.data(), buf.length()); + auto b = rdr.readInt8(); + if (b & 2) { + buf.remove(1); + buf = unzip(buf); + rdr = WAReader(buf.data(), buf.length()); + } + + if (WANode *pNode = rdr.readNode()) { + CMStringA szText; + pNode->print(szText); + debugLogA("Got binary node:\n%s", szText.c_str()); + + auto pHandler = FindPersistentHandler(*pNode); + if (pHandler) + (this->*pHandler)(*pNode); + else + debugLogA("cannot handle incoming message"); + + delete pNode; + } + else { + debugLogA("wrong or broken payload"); + Netlib_Dump(m_hServerConn, pData, cbDataLen, false, 0); + } + } + else OnProcessHandshake(pData, (int)payloadLen); + + pData = (BYTE*)pData + payloadLen; + cbDataLen -= payloadLen; + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::ProcessFailure(int code) +{ + switch (code) { + case 401: + debugLogA("Connection logged out from another device, exiting"); + Popup(0, TranslateT("This account was logged out from mobile phone, you need to link it again"), m_tszUserName); + + OnErase(); + break; + + case 408: + debugLogA("Connection lost, exiting"); + break; + + case 411: + debugLogA("Conflict between two devices, exiting"); + break; + + case 428: + debugLogA("Connection forcibly closed by the server, exiting"); + break; + + case 440: + debugLogA("Connection replaced from another device, exiting"); + break; + + case 515: + debugLogA("Server required to restart immediately, leaving thread"); + m_bRespawn = true; + break; + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnLoggedIn() +{ + debugLogA("WhatsAppProto::OnLoggedIn"); + + if (m_bUnregister) { + SendUnregister(); + m_bTerminated = true; + return; + } + + SetServerStatus(m_iDesiredStatus); + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); + m_iStatus = m_iDesiredStatus; + m_bUpdatedPrekeys = false; + + m_impl.m_keepAlive.Start(1000); + + // retrieve initial info + WANodeIq abt(IQ::GET, "abt"); + abt.addChild("props")->addAttr("protocol", "1"); + WSSendNode(abt, &WhatsAppProto::OnIqDoNothing); + + WSSendNode( + WANodeIq(IQ::GET, "w") << XCHILD("props"), + &WhatsAppProto::OnIqDoNothing); + + WSSendNode( + WANodeIq(IQ::GET, "blocklist"), + &WhatsAppProto::OnIqBlockList); + + WSSendNode( + WANodeIq(IQ::GET, "privacy") << XCHILD("privacy"), + &WhatsAppProto::OnIqDoNothing); + + GC_RefreshMetadata(); +} + +void WhatsAppProto::OnLoggedOut(void) +{ + m_impl.m_keepAlive.Stop(); + + debugLogA("WhatsAppProto::OnLoggedOut"); + m_bTerminated = true; + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + + setAllContactStatuses(ID_STATUS_OFFLINE, false); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Service packets sending + +void WhatsAppProto::SendAck(const WANode &node) +{ + WANode ack("ack"); + ack << CHAR_PARAM("to", node.getAttr("from")) << CHAR_PARAM("id", node.getAttr("id")) << CHAR_PARAM("class", node.title); + if (node.title != "message") + if (auto *param = node.getAttr("type")) + ack << CHAR_PARAM("type", param); + if (auto *param = node.getAttr("participant")) + ack << CHAR_PARAM("participant", param); + if (auto *param = node.getAttr("recipient")) + ack << CHAR_PARAM("recipient", param); + WSSendNode(ack); +} + +void WhatsAppProto::SendKeepAlive() +{ + time_t now = time(0); + if (now - m_lastRecvTime > 20) { + WSSendNode(WANodeIq(IQ::GET, "w:p") << XCHILD("ping"), &WhatsAppProto::OnIqDoNothing); + + m_lastRecvTime = now; + } + + for (auto &it : m_arUsers) { + if (it->m_timer1 && now - it->m_timer1 > 600) { + it->m_timer1 = 0; + it->m_timer2 = now; + setWord(it->hContact, "Status", ID_STATUS_AWAY); + } + else if (it->m_timer2 && now - it->m_timer2 > 600) { + it->m_timer2 = 0; + setWord(it->hContact, "Status", ID_STATUS_OFFLINE); + } + } +} + +void WhatsAppProto::SendReceipt(const char *pszTo, const char *pszParticipant, const char *pszId, const char *pszType) +{ + WANode receipt("receipt"); + receipt << CHAR_PARAM("id", pszId); + + if (!mir_strcmp(pszType, "read") || !mir_strcmp(pszType, "read-self")) + receipt << INT_PARAM("t", time(0)); + + if (!mir_strcmp(pszType, "sender") && WAJid(pszTo).isUser()) + receipt << CHAR_PARAM("to", pszParticipant) << CHAR_PARAM("recipient", pszTo); + else { + receipt << CHAR_PARAM("to", pszTo); + if (pszParticipant) + receipt << CHAR_PARAM("participant", pszParticipant); + } + + if (pszType) + receipt << CHAR_PARAM("type", pszType); + WSSendNode(receipt); +} + +void WhatsAppProto::SetServerStatus(int iStatus) +{ + if (mir_wstrlen(m_wszNick)) + WSSendNode( + WANode("presence") << CHAR_PARAM("name", T2Utf(m_wszNick)) << CHAR_PARAM("type", (iStatus == ID_STATUS_ONLINE) ? "available" : "unavailable"), + &WhatsAppProto::OnIqDoNothing); +} + +void WhatsAppProto::SendUnregister() +{ + WANodeIq iq(IQ::SET, "md"); + *iq.addChild("remove-companion-device") << CHAR_PARAM("jid", WAJid(m_szJid, getDword(DBKEY_DEVICE_ID)).toString()) << CHAR_PARAM("reason", "user's decision"); + WSSendNode(iq, &WhatsAppProto::OnIqDoNothing); + + m_bTerminated = true; +} + +void WhatsAppProto::SendUsync(const LIST &jids, void *pUserInfo) +{ + WANodeIq iq(IQ::GET, "usync"); + + auto *pNode1 = iq.addChild("usync"); + *pNode1 << CHAR_PARAM("sid", GenerateMessageId()) << CHAR_PARAM("mode", "query") << CHAR_PARAM("last", "true") + << CHAR_PARAM("index", "0") << CHAR_PARAM("context", "message"); + + pNode1->addChild("query")->addChild("devices")->addAttr("version", "2"); + auto *pList = pNode1->addChild("list"); + for (auto &it : jids) + pList->addChild("user")->addAttr("jid", it); + + WSSendNode(iq, &WhatsAppProto::OnIqGetUsync, pUserInfo); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::ShutdownSession() +{ + if (m_bTerminated) + return; + + debugLogA("WhatsAppProto::ShutdownSession"); + + // shutdown all resources + if (m_hServerConn) + Netlib_Shutdown(m_hServerConn); + + OnLoggedOut(); +} -- cgit v1.2.3