From 1979fd80424d16b2e489f9b57d01d9c7811d25a2 Mon Sep 17 00:00:00 2001 From: dartraiden Date: Mon, 2 Jan 2023 21:10:29 +0300 Subject: Update copyrights --- protocols/Facebook/src/avatars.cpp | 262 ++-- protocols/Facebook/src/db.h | 88 +- protocols/Facebook/src/dialogs.cpp | 156 +-- protocols/Facebook/src/dialogs.h | 2 +- protocols/Facebook/src/groupchats.cpp | 510 ++++---- protocols/Facebook/src/http.cpp | 370 +++--- protocols/Facebook/src/main.cpp | 144 +-- protocols/Facebook/src/mqtt.cpp | 688 +++++------ protocols/Facebook/src/mqtt.h | 278 ++--- protocols/Facebook/src/options.cpp | 168 +-- protocols/Facebook/src/proto.cpp | 604 +++++----- protocols/Facebook/src/proto.h | 1114 ++++++++--------- protocols/Facebook/src/server.cpp | 2102 ++++++++++++++++----------------- protocols/Facebook/src/stdafx.cxx | 36 +- protocols/Facebook/src/stdafx.h | 134 +-- protocols/Facebook/src/thrift.cpp | 616 +++++----- protocols/Facebook/src/version.h | 2 +- 17 files changed, 3637 insertions(+), 3637 deletions(-) (limited to 'protocols/Facebook') diff --git a/protocols/Facebook/src/avatars.cpp b/protocols/Facebook/src/avatars.cpp index 0ced4b1b25..0e73a9241b 100644 --- a/protocols/Facebook/src/avatars.cpp +++ b/protocols/Facebook/src/avatars.cpp @@ -1,131 +1,131 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -void FacebookProto::GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName) -{ - wchar_t wszPath[MAX_PATH]; - mir_snwprintf(wszPath, MAX_PATH, L"%s\\%S", VARSW(L"%miranda_avatarcache%"), m_szModuleName); - CreateDirectoryTreeW(wszPath); - - CMStringW id(getMStringW(hContact, DBKEY_ID)); - mir_snwprintf(pwszFileName, MAX_PATH, L"%s\\%s.jpg", wszPath, id.c_str()); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void __cdecl FacebookProto::AvatarsUpdate(void *) -{ - NETLIBHTTPREQUEST req = {}; - req.cbSize = sizeof(req); - req.flags = NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; - req.requestType = REQUEST_GET; - - CMStringA szParams((m_bUseBigAvatars) ? "type=large" : "type=normal"); - szParams.AppendFormat("&access_token=%s", m_szAuthToken.c_str()); - - for (auto &cc : AccContacts()) { - if (Miranda_IsTerminated()) - break; - - if (!getByte(cc, "UpdateNeeded")) - continue; - - delSetting(cc, "UpdateNeeded"); - - CMStringA szUrl(FORMAT, "https://graph.facebook.com/%s/picture?%s", getMStringA(cc, DBKEY_ID).c_str(), szParams.c_str()); - req.szUrl = szUrl.GetBuffer(); - - NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(m_hNetlibUser, &req); - if (pReply == nullptr) { - debugLogA("Failed to retrieve avatar from url: %s", szUrl.c_str()); - continue; - } - - PROTO_AVATAR_INFORMATION ai; - ai.hContact = cc; - ai.format = PA_FORMAT_UNKNOWN; - GetAvatarFilename(cc, ai.filename); - - bool bSuccess = false; - if (pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { - if (auto *pszHdr = Netlib_GetHeader(pReply, "Content-Type")) - ai.format = ProtoGetAvatarFormatByMimeType(pszHdr); - - if (ai.format != PA_FORMAT_UNKNOWN) { - FILE *fout = _wfopen(ai.filename, L"wb"); - if (fout) { - fwrite(pReply->pData, 1, pReply->dataLength, fout); - fclose(fout); - bSuccess = true; - } - else debugLogA("Error saving avatar to file %S", ai.filename); - } - else debugLogA("unknown avatar mime type"); - } - else debugLogA("Error %d reading avatar from url: %s", pReply->resultCode, szUrl.c_str()); - - ProtoBroadcastAck(cc, ACKTYPE_AVATAR, bSuccess ? ACKRESULT_SUCCESS : ACKRESULT_FAILED, (HANDLE)&ai); - - Netlib_FreeHttpRequest(pReply); - } -} - -INT_PTR FacebookProto::GetAvatarInfo(WPARAM flags, LPARAM lParam) -{ - PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam; - GetAvatarFilename(pai->hContact, pai->filename); - - bool bFileExist = _waccess(pai->filename, 0) == 0; - - // if we still need to load an avatar - if ((flags & GAIF_FORCE) || !bFileExist) { - setByte(pai->hContact, "UpdateNeeded", 1); - ForkThread(&FacebookProto::AvatarsUpdate); - return GAIR_WAITFOR; - } - - return (bFileExist) ? GAIR_SUCCESS : GAIR_NOAVATAR; -} - -INT_PTR FacebookProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) -{ - int res = 0; - - switch (wParam) { - case AF_MAXSIZE: - ((POINT *)lParam)->x = ((POINT *)lParam)->y = 128; - break; - - case AF_FORMATSUPPORTED: - res = lParam == PA_FORMAT_PNG || lParam == PA_FORMAT_GIF || lParam == PA_FORMAT_JPEG; - break; - - case AF_ENABLED: - case AF_FETCHIFPROTONOTVISIBLE: - case AF_FETCHIFCONTACTOFFLINE: - return 1; - } - - return res; -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +void FacebookProto::GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName) +{ + wchar_t wszPath[MAX_PATH]; + mir_snwprintf(wszPath, MAX_PATH, L"%s\\%S", VARSW(L"%miranda_avatarcache%"), m_szModuleName); + CreateDirectoryTreeW(wszPath); + + CMStringW id(getMStringW(hContact, DBKEY_ID)); + mir_snwprintf(pwszFileName, MAX_PATH, L"%s\\%s.jpg", wszPath, id.c_str()); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void __cdecl FacebookProto::AvatarsUpdate(void *) +{ + NETLIBHTTPREQUEST req = {}; + req.cbSize = sizeof(req); + req.flags = NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; + req.requestType = REQUEST_GET; + + CMStringA szParams((m_bUseBigAvatars) ? "type=large" : "type=normal"); + szParams.AppendFormat("&access_token=%s", m_szAuthToken.c_str()); + + for (auto &cc : AccContacts()) { + if (Miranda_IsTerminated()) + break; + + if (!getByte(cc, "UpdateNeeded")) + continue; + + delSetting(cc, "UpdateNeeded"); + + CMStringA szUrl(FORMAT, "https://graph.facebook.com/%s/picture?%s", getMStringA(cc, DBKEY_ID).c_str(), szParams.c_str()); + req.szUrl = szUrl.GetBuffer(); + + NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(m_hNetlibUser, &req); + if (pReply == nullptr) { + debugLogA("Failed to retrieve avatar from url: %s", szUrl.c_str()); + continue; + } + + PROTO_AVATAR_INFORMATION ai; + ai.hContact = cc; + ai.format = PA_FORMAT_UNKNOWN; + GetAvatarFilename(cc, ai.filename); + + bool bSuccess = false; + if (pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { + if (auto *pszHdr = Netlib_GetHeader(pReply, "Content-Type")) + ai.format = ProtoGetAvatarFormatByMimeType(pszHdr); + + if (ai.format != PA_FORMAT_UNKNOWN) { + FILE *fout = _wfopen(ai.filename, L"wb"); + if (fout) { + fwrite(pReply->pData, 1, pReply->dataLength, fout); + fclose(fout); + bSuccess = true; + } + else debugLogA("Error saving avatar to file %S", ai.filename); + } + else debugLogA("unknown avatar mime type"); + } + else debugLogA("Error %d reading avatar from url: %s", pReply->resultCode, szUrl.c_str()); + + ProtoBroadcastAck(cc, ACKTYPE_AVATAR, bSuccess ? ACKRESULT_SUCCESS : ACKRESULT_FAILED, (HANDLE)&ai); + + Netlib_FreeHttpRequest(pReply); + } +} + +INT_PTR FacebookProto::GetAvatarInfo(WPARAM flags, LPARAM lParam) +{ + PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam; + GetAvatarFilename(pai->hContact, pai->filename); + + bool bFileExist = _waccess(pai->filename, 0) == 0; + + // if we still need to load an avatar + if ((flags & GAIF_FORCE) || !bFileExist) { + setByte(pai->hContact, "UpdateNeeded", 1); + ForkThread(&FacebookProto::AvatarsUpdate); + return GAIR_WAITFOR; + } + + return (bFileExist) ? GAIR_SUCCESS : GAIR_NOAVATAR; +} + +INT_PTR FacebookProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + int res = 0; + + switch (wParam) { + case AF_MAXSIZE: + ((POINT *)lParam)->x = ((POINT *)lParam)->y = 128; + break; + + case AF_FORMATSUPPORTED: + res = lParam == PA_FORMAT_PNG || lParam == PA_FORMAT_GIF || lParam == PA_FORMAT_JPEG; + break; + + case AF_ENABLED: + case AF_FETCHIFPROTONOTVISIBLE: + case AF_FETCHIFCONTACTOFFLINE: + return 1; + } + + return res; +} diff --git a/protocols/Facebook/src/db.h b/protocols/Facebook/src/db.h index eb31b19ef0..8095cfad65 100644 --- a/protocols/Facebook/src/db.h +++ b/protocols/Facebook/src/db.h @@ -1,44 +1,44 @@ -/* - -Facebook plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -#define MODULENAME "Facebook" - -// Contact DB keys -#define DBKEY_LOGIN "Email" -#define DBKEY_ID "ID" -#define DBKEY_SID "SID" -#define DBKEY_NICK "Nick" -#define DBKEY_PASS "Password" -#define DBKEY_CLIENT_ID "ClientID" -#define DBKEY_DEVICE_ID "DeviceID" -#define DBKEY_AVATAR "Avatar" -#define DBKEY_CONTACT_TYPE "ContactType" -#define DBKEY_TOKEN "Token" -#define DBKEY_SYNC_TOKEN "SyncToken" - -// Account DB keys -#define DBKEY_SET_MIRANDA_STATUS "SetMirandaStatus" - -// Hidden account DB keys (can't be changed through GUI) -#define DBKEY_LOCALE "Locale" // [HIDDEN] - (string) en_US, cs_CZ, etc. (requires restart to apply) +/* + +Facebook plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +#define MODULENAME "Facebook" + +// Contact DB keys +#define DBKEY_LOGIN "Email" +#define DBKEY_ID "ID" +#define DBKEY_SID "SID" +#define DBKEY_NICK "Nick" +#define DBKEY_PASS "Password" +#define DBKEY_CLIENT_ID "ClientID" +#define DBKEY_DEVICE_ID "DeviceID" +#define DBKEY_AVATAR "Avatar" +#define DBKEY_CONTACT_TYPE "ContactType" +#define DBKEY_TOKEN "Token" +#define DBKEY_SYNC_TOKEN "SyncToken" + +// Account DB keys +#define DBKEY_SET_MIRANDA_STATUS "SetMirandaStatus" + +// Hidden account DB keys (can't be changed through GUI) +#define DBKEY_LOCALE "Locale" // [HIDDEN] - (string) en_US, cs_CZ, etc. (requires restart to apply) diff --git a/protocols/Facebook/src/dialogs.cpp b/protocols/Facebook/src/dialogs.cpp index 4e9e215b86..1938c94448 100644 --- a/protocols/Facebook/src/dialogs.cpp +++ b/protocols/Facebook/src/dialogs.cpp @@ -1,78 +1,78 @@ -/* - -Facebook plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -INT_PTR CALLBACK FBAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) -{ - FacebookProto *proto = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); - - switch (message) { - - case WM_INITDIALOG: - { - TranslateDialogDefault(hwnd); - - proto = reinterpret_cast(lparam); - SetWindowLongPtr(hwnd, GWLP_USERDATA, lparam); - - ptrA login(db_get_sa(0, proto->ModuleName(), DBKEY_LOGIN)); - if (login != nullptr) - SetDlgItemTextA(hwnd, IDC_UN, login); - - ptrA password(db_get_sa(0, proto->ModuleName(), DBKEY_PASS)); - if (password != nullptr) - SetDlgItemTextA(hwnd, IDC_PW, password); - - //if (!proto->isOffline()) { - // SendDlgItemMessage(hwnd, IDC_UN, EM_SETREADONLY, 1, 0); - // SendDlgItemMessage(hwnd, IDC_PW, EM_SETREADONLY, 1, 0); - //} - return TRUE; - } - case WM_COMMAND: - if (HIWORD(wparam) == EN_CHANGE && reinterpret_cast(lparam) == GetFocus()) { - switch (LOWORD(wparam)) { - case IDC_UN: - case IDC_PW: - SendMessage(GetParent(hwnd), PSM_CHANGED, 0, 0); - } - } - break; - - case WM_NOTIFY: - if (reinterpret_cast(lparam)->code == PSN_APPLY) { - char str[128]; - - GetDlgItemTextA(hwnd, IDC_UN, str, _countof(str)); - db_set_s(0, proto->ModuleName(), DBKEY_LOGIN, str); - - GetDlgItemTextA(hwnd, IDC_PW, str, _countof(str)); - db_set_s(0, proto->ModuleName(), DBKEY_PASS, str); - return TRUE; - } - break; - - } - - return FALSE; -} +/* + +Facebook plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +INT_PTR CALLBACK FBAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) +{ + FacebookProto *proto = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + switch (message) { + + case WM_INITDIALOG: + { + TranslateDialogDefault(hwnd); + + proto = reinterpret_cast(lparam); + SetWindowLongPtr(hwnd, GWLP_USERDATA, lparam); + + ptrA login(db_get_sa(0, proto->ModuleName(), DBKEY_LOGIN)); + if (login != nullptr) + SetDlgItemTextA(hwnd, IDC_UN, login); + + ptrA password(db_get_sa(0, proto->ModuleName(), DBKEY_PASS)); + if (password != nullptr) + SetDlgItemTextA(hwnd, IDC_PW, password); + + //if (!proto->isOffline()) { + // SendDlgItemMessage(hwnd, IDC_UN, EM_SETREADONLY, 1, 0); + // SendDlgItemMessage(hwnd, IDC_PW, EM_SETREADONLY, 1, 0); + //} + return TRUE; + } + case WM_COMMAND: + if (HIWORD(wparam) == EN_CHANGE && reinterpret_cast(lparam) == GetFocus()) { + switch (LOWORD(wparam)) { + case IDC_UN: + case IDC_PW: + SendMessage(GetParent(hwnd), PSM_CHANGED, 0, 0); + } + } + break; + + case WM_NOTIFY: + if (reinterpret_cast(lparam)->code == PSN_APPLY) { + char str[128]; + + GetDlgItemTextA(hwnd, IDC_UN, str, _countof(str)); + db_set_s(0, proto->ModuleName(), DBKEY_LOGIN, str); + + GetDlgItemTextA(hwnd, IDC_PW, str, _countof(str)); + db_set_s(0, proto->ModuleName(), DBKEY_PASS, str); + return TRUE; + } + break; + + } + + return FALSE; +} diff --git a/protocols/Facebook/src/dialogs.h b/protocols/Facebook/src/dialogs.h index 23d821c697..5dcaeb667b 100644 --- a/protocols/Facebook/src/dialogs.h +++ b/protocols/Facebook/src/dialogs.h @@ -3,7 +3,7 @@ Facebook plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Facebook/src/groupchats.cpp b/protocols/Facebook/src/groupchats.cpp index 92266e3e2d..b43e05696a 100644 --- a/protocols/Facebook/src/groupchats.cpp +++ b/protocols/Facebook/src/groupchats.cpp @@ -1,255 +1,255 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// -// Invitation dialog - -class CGroupchatInviteDlg : public CFBDlgBase -{ - CCtrlClc m_clc; - SESSION_INFO *m_si; - - void FilterList(CCtrlClc *) - { - for (auto &hContact : Contacts()) { - char *proto = Proto_GetBaseAccountName(hContact); - if (mir_strcmp(proto, m_proto->m_szModuleName) || m_proto->isChatRoom(hContact)) - if (HANDLE hItem = m_clc.FindContact(hContact)) - m_clc.DeleteItem(hItem); - } - } - - void ResetListOptions(CCtrlClc *) - { - m_clc.SetHideEmptyGroups(1); - m_clc.SetHideOfflineRoot(1); - m_clc.SetOfflineModes(PF2_NONE); - } - -public: - CGroupchatInviteDlg(FacebookProto *ppro, SESSION_INFO *si) : - CFBDlgBase(ppro, IDD_GROUPCHAT_INVITE), - m_si(si), - m_clc(this, IDC_CLIST) - { - m_clc.OnNewContact = - m_clc.OnListRebuilt = Callback(this, &CGroupchatInviteDlg::FilterList); - m_clc.OnOptionsChanged = Callback(this, &CGroupchatInviteDlg::ResetListOptions); - } - - bool OnInitDialog() override - { - SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, - GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_SHOWHIDDEN | CLS_HIDEOFFLINE | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE | CLS_GROUPCHECKBOXES); - m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); - - ResetListOptions(&m_clc); - FilterList(&m_clc); - return true; - } - - bool OnApply() override - { - JSONNode list(JSON_ARRAY); - - for (auto &hContact : m_proto->AccContacts()) { - if (m_proto->isChatRoom(hContact)) - continue; - - if (HANDLE hItem = m_clc.FindContact(hContact)) { - if (m_clc.GetCheck(hItem)) { - JSONNode user; user << CHAR_PARAM("type", "id") << CHAR_PARAM("id", m_proto->getMStringA(hContact, DBKEY_ID)); - list << user; - } - } - } - - auto *pReq = m_proto->CreateRequest(FB_API_URL_PARTS, "addMembers", "POST"); - pReq << CHAR_PARAM("to", list.write().c_str()) << WCHAR_PARAM("id", CMStringW(FORMAT, L"t_%s", m_si->ptszID)); - pReq->CalcSig(); - - JsonReply reply(m_proto->ExecuteRequest(pReq)); - return true; - } -}; - -void FacebookProto::Chat_InviteUser(SESSION_INFO *si) -{ - CGroupchatInviteDlg dlg(this, si); - if (si->pDlg) - dlg.SetParent(((CDlgBase *)si->pDlg)->GetHwnd()); - dlg.DoModal(); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Group chats - -enum ChatMenuItems -{ - IDM_INVITE = 10, IDM_LEAVE, - - IDM_KICK = 20 -}; - -static gc_item sttLogListItems[] = -{ - { LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM }, - { nullptr, 0, MENU_SEPARATOR }, - { LPGENW("&Leave/destroy chat"), IDM_LEAVE, MENU_ITEM }, -}; - -static gc_item sttNickListItems[] = -{ - { LPGENW("&Kick user"), IDM_KICK, MENU_ITEM }, -}; - -int FacebookProto::GroupchatMenuHook(WPARAM, LPARAM lParam) -{ - GCMENUITEMS *gcmi = (GCMENUITEMS *)lParam; - if (gcmi == nullptr) - return 0; - - if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) - return 0; - - if (SESSION_INFO *si = g_chatApi.SM_FindSession(gcmi->pszID, gcmi->pszModule)) { - if (gcmi->Type == MENU_ON_LOG) - Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); - if (gcmi->Type == MENU_ON_NICKLIST) - Chat_AddMenuItems(gcmi->hMenu, _countof(sttNickListItems), sttNickListItems, &g_plugin); - } - - return 0; -} - -int FacebookProto::GroupchatEventHook(WPARAM, LPARAM lParam) -{ - GCHOOK *gch = (GCHOOK *)lParam; - if (gch == nullptr) - return 0; - - if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) - return 0; - - SESSION_INFO *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule); - if (si == nullptr) - return 1; - - switch (gch->iType) { - case GC_USER_MESSAGE: - rtrimw(gch->ptszText); - if (!mir_wstrlen(gch->ptszText)) - break; - - if (m_bOnline) { - wchar_t *wszText = NEWWSTR_ALLOCA(gch->ptszText); - Chat_UnescapeTags(wszText); - - int mid = SendMsg(si->hContact, 0, T2Utf(wszText)); - - mir_cslock lck(m_csOwnMessages); - for (auto &msg : arOwnMessages) - if (msg->reqId == mid) - msg->wszText = wszText; - } - break; - - case GC_USER_PRIVMESS: - Chat_SendPrivateMessage(gch); - break; - - case GC_USER_LOGMENU: - Chat_ProcessLogMenu(si, gch); - break; - - case GC_USER_NICKLISTMENU: - Chat_ProcessNickMenu(si, gch); - break; - } - - return 1; -} - -void FacebookProto::Chat_ProcessLogMenu(SESSION_INFO *si, GCHOOK *gch) -{ - switch (gch->dwData) { - case IDM_INVITE: - Chat_InviteUser(si); - break; - - case IDM_LEAVE: - Chat_Leave(si); - break; - } -} - -void FacebookProto::Chat_ProcessNickMenu(SESSION_INFO *si, GCHOOK *gch) -{ - switch (gch->dwData) { - case IDM_KICK: - Chat_KickUser(si, gch->ptszUID); - break; - } -} - -void FacebookProto::Chat_SendPrivateMessage(GCHOOK *gch) -{ - auto *pUser = FindUser(_wtoi64(gch->ptszUID)); - if (pUser == nullptr) { - pUser = AddContact(gch->ptszUID, true); - setWString(pUser->hContact, "Nick", gch->ptszNick); - db_set_b(pUser->hContact, "CList", "Hidden", 1); - db_set_dw(pUser->hContact, "Ignore", "Mask1", 0); - } - - CallService(MS_MSG_SENDMESSAGE, pUser->hContact, 0); -} - -int FacebookProto::Chat_KickUser(SESSION_INFO *si, const wchar_t *pwszUid) -{ - auto *pReq = CreateRequest(FB_API_URL_PARTS, "removeMembers", "DELETE"); - pReq << WCHAR_PARAM("id", CMStringW(FORMAT, L"t_%s", si->ptszID)); - if (pwszUid != nullptr) { - JSONNode list(JSON_ARRAY); - JSONNode user; user << CHAR_PARAM("type", "id") << WCHAR_PARAM("id", pwszUid); - list << user; - pReq << CHAR_PARAM("to", list.write().c_str()); - } - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - return reply.error(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static void __cdecl DestroyRoomThread(SESSION_INFO *si) -{ - ::Sleep(100); - Chat_Terminate(si->pszModule, si->ptszID, true); -} - -void FacebookProto::Chat_Leave(SESSION_INFO *si) -{ - if (Chat_KickUser(si, nullptr) == 0) - mir_forkThread(DestroyRoomThread, si); -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// +// Invitation dialog + +class CGroupchatInviteDlg : public CFBDlgBase +{ + CCtrlClc m_clc; + SESSION_INFO *m_si; + + void FilterList(CCtrlClc *) + { + for (auto &hContact : Contacts()) { + char *proto = Proto_GetBaseAccountName(hContact); + if (mir_strcmp(proto, m_proto->m_szModuleName) || m_proto->isChatRoom(hContact)) + if (HANDLE hItem = m_clc.FindContact(hContact)) + m_clc.DeleteItem(hItem); + } + } + + void ResetListOptions(CCtrlClc *) + { + m_clc.SetHideEmptyGroups(1); + m_clc.SetHideOfflineRoot(1); + m_clc.SetOfflineModes(PF2_NONE); + } + +public: + CGroupchatInviteDlg(FacebookProto *ppro, SESSION_INFO *si) : + CFBDlgBase(ppro, IDD_GROUPCHAT_INVITE), + m_si(si), + m_clc(this, IDC_CLIST) + { + m_clc.OnNewContact = + m_clc.OnListRebuilt = Callback(this, &CGroupchatInviteDlg::FilterList); + m_clc.OnOptionsChanged = Callback(this, &CGroupchatInviteDlg::ResetListOptions); + } + + bool OnInitDialog() override + { + SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, + GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_SHOWHIDDEN | CLS_HIDEOFFLINE | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE | CLS_GROUPCHECKBOXES); + m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); + + ResetListOptions(&m_clc); + FilterList(&m_clc); + return true; + } + + bool OnApply() override + { + JSONNode list(JSON_ARRAY); + + for (auto &hContact : m_proto->AccContacts()) { + if (m_proto->isChatRoom(hContact)) + continue; + + if (HANDLE hItem = m_clc.FindContact(hContact)) { + if (m_clc.GetCheck(hItem)) { + JSONNode user; user << CHAR_PARAM("type", "id") << CHAR_PARAM("id", m_proto->getMStringA(hContact, DBKEY_ID)); + list << user; + } + } + } + + auto *pReq = m_proto->CreateRequest(FB_API_URL_PARTS, "addMembers", "POST"); + pReq << CHAR_PARAM("to", list.write().c_str()) << WCHAR_PARAM("id", CMStringW(FORMAT, L"t_%s", m_si->ptszID)); + pReq->CalcSig(); + + JsonReply reply(m_proto->ExecuteRequest(pReq)); + return true; + } +}; + +void FacebookProto::Chat_InviteUser(SESSION_INFO *si) +{ + CGroupchatInviteDlg dlg(this, si); + if (si->pDlg) + dlg.SetParent(((CDlgBase *)si->pDlg)->GetHwnd()); + dlg.DoModal(); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Group chats + +enum ChatMenuItems +{ + IDM_INVITE = 10, IDM_LEAVE, + + IDM_KICK = 20 +}; + +static gc_item sttLogListItems[] = +{ + { LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM }, + { nullptr, 0, MENU_SEPARATOR }, + { LPGENW("&Leave/destroy chat"), IDM_LEAVE, MENU_ITEM }, +}; + +static gc_item sttNickListItems[] = +{ + { LPGENW("&Kick user"), IDM_KICK, MENU_ITEM }, +}; + +int FacebookProto::GroupchatMenuHook(WPARAM, LPARAM lParam) +{ + GCMENUITEMS *gcmi = (GCMENUITEMS *)lParam; + if (gcmi == nullptr) + return 0; + + if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) + return 0; + + if (SESSION_INFO *si = g_chatApi.SM_FindSession(gcmi->pszID, gcmi->pszModule)) { + if (gcmi->Type == MENU_ON_LOG) + Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); + if (gcmi->Type == MENU_ON_NICKLIST) + Chat_AddMenuItems(gcmi->hMenu, _countof(sttNickListItems), sttNickListItems, &g_plugin); + } + + return 0; +} + +int FacebookProto::GroupchatEventHook(WPARAM, LPARAM lParam) +{ + GCHOOK *gch = (GCHOOK *)lParam; + if (gch == nullptr) + return 0; + + if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) + return 0; + + SESSION_INFO *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule); + if (si == nullptr) + return 1; + + switch (gch->iType) { + case GC_USER_MESSAGE: + rtrimw(gch->ptszText); + if (!mir_wstrlen(gch->ptszText)) + break; + + if (m_bOnline) { + wchar_t *wszText = NEWWSTR_ALLOCA(gch->ptszText); + Chat_UnescapeTags(wszText); + + int mid = SendMsg(si->hContact, 0, T2Utf(wszText)); + + mir_cslock lck(m_csOwnMessages); + for (auto &msg : arOwnMessages) + if (msg->reqId == mid) + msg->wszText = wszText; + } + break; + + case GC_USER_PRIVMESS: + Chat_SendPrivateMessage(gch); + break; + + case GC_USER_LOGMENU: + Chat_ProcessLogMenu(si, gch); + break; + + case GC_USER_NICKLISTMENU: + Chat_ProcessNickMenu(si, gch); + break; + } + + return 1; +} + +void FacebookProto::Chat_ProcessLogMenu(SESSION_INFO *si, GCHOOK *gch) +{ + switch (gch->dwData) { + case IDM_INVITE: + Chat_InviteUser(si); + break; + + case IDM_LEAVE: + Chat_Leave(si); + break; + } +} + +void FacebookProto::Chat_ProcessNickMenu(SESSION_INFO *si, GCHOOK *gch) +{ + switch (gch->dwData) { + case IDM_KICK: + Chat_KickUser(si, gch->ptszUID); + break; + } +} + +void FacebookProto::Chat_SendPrivateMessage(GCHOOK *gch) +{ + auto *pUser = FindUser(_wtoi64(gch->ptszUID)); + if (pUser == nullptr) { + pUser = AddContact(gch->ptszUID, true); + setWString(pUser->hContact, "Nick", gch->ptszNick); + db_set_b(pUser->hContact, "CList", "Hidden", 1); + db_set_dw(pUser->hContact, "Ignore", "Mask1", 0); + } + + CallService(MS_MSG_SENDMESSAGE, pUser->hContact, 0); +} + +int FacebookProto::Chat_KickUser(SESSION_INFO *si, const wchar_t *pwszUid) +{ + auto *pReq = CreateRequest(FB_API_URL_PARTS, "removeMembers", "DELETE"); + pReq << WCHAR_PARAM("id", CMStringW(FORMAT, L"t_%s", si->ptszID)); + if (pwszUid != nullptr) { + JSONNode list(JSON_ARRAY); + JSONNode user; user << CHAR_PARAM("type", "id") << WCHAR_PARAM("id", pwszUid); + list << user; + pReq << CHAR_PARAM("to", list.write().c_str()); + } + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + return reply.error(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static void __cdecl DestroyRoomThread(SESSION_INFO *si) +{ + ::Sleep(100); + Chat_Terminate(si->pszModule, si->ptszID, true); +} + +void FacebookProto::Chat_Leave(SESSION_INFO *si) +{ + if (Chat_KickUser(si, nullptr) == 0) + mir_forkThread(DestroyRoomThread, si); +} diff --git a/protocols/Facebook/src/http.cpp b/protocols/Facebook/src/http.cpp index 7bc6e5082c..6f520c0486 100644 --- a/protocols/Facebook/src/http.cpp +++ b/protocols/Facebook/src/http.cpp @@ -1,185 +1,185 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -static int CompareParams(const AsyncHttpRequest::Param *p1, const AsyncHttpRequest::Param *p2) -{ - return strcmp(p1->key, p2->key); -} - -AsyncHttpRequest::AsyncHttpRequest() : - params(5, CompareParams) -{ -} - -void AsyncHttpRequest::CalcSig() -{ - CMStringA buf; - for (auto &it : params) - buf.AppendFormat("%s=%s", it->key.c_str(), it->val.c_str()); - - buf.Append(FB_API_SECRET); - - char szHash[33]; - uint8_t digest[16]; - mir_md5_hash((uint8_t*)buf.c_str(), buf.GetLength(), digest); - bin2hex(digest, sizeof(digest), szHash); - this << CHAR_PARAM("sig", szHash); - - for (auto &it : params) { - if (!m_szParam.IsEmpty()) - m_szParam.AppendChar('&'); - m_szParam.AppendFormat("%s=%s", it->key.c_str(), mir_urlEncode(it->val.c_str()).c_str()); - } -} - -AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const CHAR_PARAM ¶m) -{ - pReq->params.insert(new AsyncHttpRequest::Param(param.szName, param.szValue)); - return pReq; -} - -AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const INT_PARAM ¶m) -{ - char value[40]; - itoa(param.iValue, value, 10); - pReq->params.insert(new AsyncHttpRequest::Param(param.szName, value)); - return pReq; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = (*m_root)["error_code"].as_int(); -} - -JsonReply::~JsonReply() -{ - json_delete(m_root); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -AsyncHttpRequest* FacebookProto::CreateRequest(const char *szUrl, const char *szName, const char *szMethod) -{ - AsyncHttpRequest *pReq = new AsyncHttpRequest(); - pReq->m_szUrl = szUrl; - pReq->requestType = REQUEST_POST; - pReq << CHAR_PARAM("api_key", FB_API_KEY) - << CHAR_PARAM("device_id", m_szDeviceID) - << CHAR_PARAM("fb_api_req_friendly_name", szName) - << CHAR_PARAM("format", "json") - << CHAR_PARAM("method", szMethod); - - CMStringA szLocale = getMStringA(DBKEY_LOCALE); - if (szLocale.IsEmpty()) - szLocale = "en"; - pReq << CHAR_PARAM("locale", szLocale); - - if (!m_szAuthToken.IsEmpty()) { - pReq->flags |= NLHRF_NODUMPHEADERS; - pReq->AddHeader("Authorization", "OAuth " + m_szAuthToken); - } - - pReq->AddHeader("User-Agent", FB_API_AGENT); - pReq->AddHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); - return pReq; -} - -AsyncHttpRequest* FacebookProto::CreateRequestGQL(int64_t query_id) { - const char* szName; - - switch (query_id) { - case FB_API_QUERY_CONTACT: - szName = "UsersQuery"; - break; - case FB_API_QUERY_CONTACTS: - szName = "FetchContactsFullQuery"; - break; - case FB_API_QUERY_CONTACTS_AFTER: - szName = "FetchContactsFullWithAfterQuery"; - break; - case FB_API_QUERY_CONTACTS_DELTA: - szName = "FetchContactsDeltaQuery"; - break; - case FB_API_QUERY_STICKER: - szName = "FetchStickersWithPreviewsQuery"; - break; - case FB_API_QUERY_THREAD: - szName = "ThreadQuery"; - break; - case FB_API_QUERY_SEQ_ID: - case FB_API_QUERY_THREADS: - szName = "ThreadListQuery"; - break; - case FB_API_QUERY_XMA: - szName = "XMAQuery"; - break; - default: - return nullptr; - } - - AsyncHttpRequest* pReq = CreateRequest(FB_API_URL_GQL, szName, "get"); - pReq << INT64_PARAM("query_id", query_id); - return pReq; -} - -NETLIBHTTPREQUEST* FacebookProto::ExecuteRequest(AsyncHttpRequest *pReq) -{ - CMStringA str; - - pReq->flags |= NLHRF_HTTP11; - pReq->szUrl = pReq->m_szUrl.GetBuffer(); - if (!pReq->m_szParam.IsEmpty()) { - if (pReq->requestType == REQUEST_GET) { - str.Format("%s?%s", pReq->m_szUrl.c_str(), pReq->m_szParam.c_str()); - pReq->szUrl = str.GetBuffer(); - } - else { - pReq->dataLength = pReq->m_szParam.GetLength(); - pReq->pData = mir_strdup(pReq->m_szParam); - } - } - - debugLogA("Executing request:\n%s", pReq->szUrl); - - NETLIBHTTPREQUEST *reply = Netlib_HttpTransaction(m_hNetlibUser, pReq); - delete pReq; - return reply; -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +static int CompareParams(const AsyncHttpRequest::Param *p1, const AsyncHttpRequest::Param *p2) +{ + return strcmp(p1->key, p2->key); +} + +AsyncHttpRequest::AsyncHttpRequest() : + params(5, CompareParams) +{ +} + +void AsyncHttpRequest::CalcSig() +{ + CMStringA buf; + for (auto &it : params) + buf.AppendFormat("%s=%s", it->key.c_str(), it->val.c_str()); + + buf.Append(FB_API_SECRET); + + char szHash[33]; + uint8_t digest[16]; + mir_md5_hash((uint8_t*)buf.c_str(), buf.GetLength(), digest); + bin2hex(digest, sizeof(digest), szHash); + this << CHAR_PARAM("sig", szHash); + + for (auto &it : params) { + if (!m_szParam.IsEmpty()) + m_szParam.AppendChar('&'); + m_szParam.AppendFormat("%s=%s", it->key.c_str(), mir_urlEncode(it->val.c_str()).c_str()); + } +} + +AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const CHAR_PARAM ¶m) +{ + pReq->params.insert(new AsyncHttpRequest::Param(param.szName, param.szValue)); + return pReq; +} + +AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const INT_PARAM ¶m) +{ + char value[40]; + itoa(param.iValue, value, 10); + pReq->params.insert(new AsyncHttpRequest::Param(param.szName, value)); + return pReq; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = (*m_root)["error_code"].as_int(); +} + +JsonReply::~JsonReply() +{ + json_delete(m_root); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +AsyncHttpRequest* FacebookProto::CreateRequest(const char *szUrl, const char *szName, const char *szMethod) +{ + AsyncHttpRequest *pReq = new AsyncHttpRequest(); + pReq->m_szUrl = szUrl; + pReq->requestType = REQUEST_POST; + pReq << CHAR_PARAM("api_key", FB_API_KEY) + << CHAR_PARAM("device_id", m_szDeviceID) + << CHAR_PARAM("fb_api_req_friendly_name", szName) + << CHAR_PARAM("format", "json") + << CHAR_PARAM("method", szMethod); + + CMStringA szLocale = getMStringA(DBKEY_LOCALE); + if (szLocale.IsEmpty()) + szLocale = "en"; + pReq << CHAR_PARAM("locale", szLocale); + + if (!m_szAuthToken.IsEmpty()) { + pReq->flags |= NLHRF_NODUMPHEADERS; + pReq->AddHeader("Authorization", "OAuth " + m_szAuthToken); + } + + pReq->AddHeader("User-Agent", FB_API_AGENT); + pReq->AddHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + return pReq; +} + +AsyncHttpRequest* FacebookProto::CreateRequestGQL(int64_t query_id) { + const char* szName; + + switch (query_id) { + case FB_API_QUERY_CONTACT: + szName = "UsersQuery"; + break; + case FB_API_QUERY_CONTACTS: + szName = "FetchContactsFullQuery"; + break; + case FB_API_QUERY_CONTACTS_AFTER: + szName = "FetchContactsFullWithAfterQuery"; + break; + case FB_API_QUERY_CONTACTS_DELTA: + szName = "FetchContactsDeltaQuery"; + break; + case FB_API_QUERY_STICKER: + szName = "FetchStickersWithPreviewsQuery"; + break; + case FB_API_QUERY_THREAD: + szName = "ThreadQuery"; + break; + case FB_API_QUERY_SEQ_ID: + case FB_API_QUERY_THREADS: + szName = "ThreadListQuery"; + break; + case FB_API_QUERY_XMA: + szName = "XMAQuery"; + break; + default: + return nullptr; + } + + AsyncHttpRequest* pReq = CreateRequest(FB_API_URL_GQL, szName, "get"); + pReq << INT64_PARAM("query_id", query_id); + return pReq; +} + +NETLIBHTTPREQUEST* FacebookProto::ExecuteRequest(AsyncHttpRequest *pReq) +{ + CMStringA str; + + pReq->flags |= NLHRF_HTTP11; + pReq->szUrl = pReq->m_szUrl.GetBuffer(); + if (!pReq->m_szParam.IsEmpty()) { + if (pReq->requestType == REQUEST_GET) { + str.Format("%s?%s", pReq->m_szUrl.c_str(), pReq->m_szParam.c_str()); + pReq->szUrl = str.GetBuffer(); + } + else { + pReq->dataLength = pReq->m_szParam.GetLength(); + pReq->pData = mir_strdup(pReq->m_szParam); + } + } + + debugLogA("Executing request:\n%s", pReq->szUrl); + + NETLIBHTTPREQUEST *reply = Netlib_HttpTransaction(m_hNetlibUser, pReq); + delete pReq; + return reply; +} diff --git a/protocols/Facebook/src/main.cpp b/protocols/Facebook/src/main.cpp index 9e308016fc..2b9215e2ed 100644 --- a/protocols/Facebook/src/main.cpp +++ b/protocols/Facebook/src/main.cpp @@ -1,72 +1,72 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -#pragma comment(lib, "Rpcrt4.lib") - -CMPlugin g_plugin; - -bool g_bMessageState; - -///////////////////////////////////////////////////////////////////////////////////////// - -PLUGININFOEX pluginInfoEx = { - sizeof(PLUGININFOEX), - __PLUGIN_NAME, - PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), - __DESCRIPTION, - __AUTHOR, - __COPYRIGHT, - __AUTHORWEB, - UNICODE_AWARE, - // {86033E58-A1E3-43AD-AE8E-305E15E72A91} - { 0xee0543fb, 0x711d, 0x4ac8, { 0xb6, 0xc0, 0x1d, 0xda, 0x48, 0x38, 0x10, 0x7e }} -}; - -CMPlugin::CMPlugin() : - ACCPROTOPLUGIN(MODULENAME, pluginInfoEx) -{ - SetUniqueId(DBKEY_ID); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Interface information - -extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; - -///////////////////////////////////////////////////////////////////////////////////////// -// Load - -static int OnModuleLoaded(WPARAM, LPARAM) -{ - g_bMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE) != 0; - return 0; -} - -int CMPlugin::Load() -{ - HookEvent(ME_SYSTEM_MODULELOAD, OnModuleLoaded); - HookEvent(ME_SYSTEM_MODULEUNLOAD, OnModuleLoaded); - HookEvent(ME_SYSTEM_MODULESLOADED, OnModuleLoaded); - - // Initialize random generator (used only as fallback in utils) - return 0; -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +#pragma comment(lib, "Rpcrt4.lib") + +CMPlugin g_plugin; + +bool g_bMessageState; + +///////////////////////////////////////////////////////////////////////////////////////// + +PLUGININFOEX pluginInfoEx = { + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + // {86033E58-A1E3-43AD-AE8E-305E15E72A91} + { 0xee0543fb, 0x711d, 0x4ac8, { 0xb6, 0xc0, 0x1d, 0xda, 0x48, 0x38, 0x10, 0x7e }} +}; + +CMPlugin::CMPlugin() : + ACCPROTOPLUGIN(MODULENAME, pluginInfoEx) +{ + SetUniqueId(DBKEY_ID); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Interface information + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// +// Load + +static int OnModuleLoaded(WPARAM, LPARAM) +{ + g_bMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE) != 0; + return 0; +} + +int CMPlugin::Load() +{ + HookEvent(ME_SYSTEM_MODULELOAD, OnModuleLoaded); + HookEvent(ME_SYSTEM_MODULEUNLOAD, OnModuleLoaded); + HookEvent(ME_SYSTEM_MODULESLOADED, OnModuleLoaded); + + // Initialize random generator (used only as fallback in utils) + return 0; +} diff --git a/protocols/Facebook/src/mqtt.cpp b/protocols/Facebook/src/mqtt.cpp index 7c2eb5ef5f..b7fbe0a3bd 100644 --- a/protocols/Facebook/src/mqtt.cpp +++ b/protocols/Facebook/src/mqtt.cpp @@ -1,344 +1,344 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -uint8_t *FacebookProto::doZip(size_t cbData, const void *pData, size_t &cbRes) -{ - size_t dataSize = cbData + 100; - uint8_t *pRes = (uint8_t *)mir_alloc(dataSize); - - z_stream zStreamOut = {}; - deflateInit(&zStreamOut, Z_BEST_COMPRESSION); - zStreamOut.avail_in = (unsigned)cbData; - zStreamOut.next_in = (uint8_t *)pData; - zStreamOut.avail_out = (unsigned)dataSize; - zStreamOut.next_out = (uint8_t *)pRes; - deflate(&zStreamOut, Z_FINISH); - deflateEnd(&zStreamOut); - - cbRes = dataSize - zStreamOut.avail_out; - return pRes; -} - -uint8_t *FacebookProto::doUnzip(size_t cbData, const void *pData, size_t &cbRes) -{ - size_t dataSize = cbData * 10; - uint8_t *pRes = (uint8_t *)mir_alloc(dataSize); - - z_stream zStreamOut = {}; - inflateInit(&zStreamOut); - zStreamOut.avail_in = (unsigned)cbData; - zStreamOut.next_in = (uint8_t *)pData; - zStreamOut.avail_out = (unsigned)dataSize; - zStreamOut.next_out = (uint8_t *)pRes; - int rc = inflate(&zStreamOut, Z_FINISH); - inflateEnd(&zStreamOut); - - switch (rc) { - case Z_OK: - case Z_STREAM_END: - cbRes = dataSize - zStreamOut.avail_out; - return pRes; - } - - mir_free(pRes); - cbRes = 0; - return nullptr; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// MqttMessage class members - -MqttMessage::MqttMessage() : - m_leadingByte(0) -{ -} - -MqttMessage::MqttMessage(FbMqttMessageType type, uint8_t flags) -{ - m_leadingByte = ((type & 0x0F) << 4) | (flags & 0x0F); -} - -char* MqttMessage::readStr(const uint8_t *&pData) const -{ - u_short len = ntohs(*(u_short *)pData); pData += sizeof(u_short); - if (len == 0) - return nullptr; - - char *res = (char*)mir_alloc(len + 1); - memcpy(res, pData, len); - res[len] = 0; - pData += len; - return res; -} - -void MqttMessage::writeStr(const char *str) -{ - size_t len = mir_strlen(str); - writeInt16((uint16_t)len); - writeBuf(str, len); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// MQTT functions - -bool FacebookProto::MqttParse(const MqttMessage &payload) -{ - auto *pData = (const uint8_t *)payload.data(), *pBeg = pData; - int flags = payload.getFlags(); - uint16_t mid; - - switch (payload.getType()) { - case FB_MQTT_MESSAGE_TYPE_CONNACK: - if (pData[1] != 0) { // connection failed; - int iErrorCode = ntohs(*(u_short *)pData); - debugLogA("Login failed with error %d", iErrorCode); - - if (iErrorCode == 4) { // invalid login/password - delSetting(DBKEY_TOKEN); - m_szAuthToken.Empty(); - ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, 0, LOGINERR_WRONGPASSWORD); - } - else ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, 0, LOGINERR_WRONGPROTOCOL); - return false; - } - - OnLoggedIn(); - break; - - case FB_MQTT_MESSAGE_TYPE_PUBREL: - mid = ntohs(*(u_short *)pData); - pData += 2; - { - MqttMessage reply(FB_MQTT_MESSAGE_TYPE_PUBCOMP); - reply.writeInt16(mid); - MqttSend(reply); - } - break; - - case FB_MQTT_MESSAGE_TYPE_PUBLISH: - char *str = payload.readStr(pData); - - if ((flags & FB_MQTT_MESSAGE_FLAG_QOS1) || (flags & FB_MQTT_MESSAGE_FLAG_QOS2)) { - mid = ntohs(*(u_short *)pData); - pData += 2; - - MqttMessage reply((flags & FB_MQTT_MESSAGE_FLAG_QOS1) ? FB_MQTT_MESSAGE_TYPE_PUBACK : FB_MQTT_MESSAGE_TYPE_PUBREC); - reply.writeInt16(mid); - MqttSend(reply); - } - - OnPublish(str, pData, payload.size() - (pData - pBeg)); - mir_free(str); - break; - } - - return true; -} - -bool FacebookProto::MqttRead(MqttMessage &payload) -{ - uint8_t b; - int res = Netlib_Recv(m_mqttConn, (char *)&b, sizeof(b), MSG_NODUMP); - if (res != 1) - return false; - - payload.m_leadingByte = b; - - uint32_t m = 1, remainingBytes = 0; - do { - if ((res = Netlib_Recv(m_mqttConn, (char *)&b, sizeof(b), MSG_NODUMP)) != 1) - return false; - - remainingBytes += (b & 0x7F) * m; - m *= 128; - } while ((b & 0x80) != 0); - - debugLogA("Received message of type=%d, flags=%x, body length=%d", payload.getType(), payload.getFlags(), remainingBytes); - - if (remainingBytes != 0) { - while (remainingBytes > 0) { - uint8_t buf[1024]; - int size = min(remainingBytes, sizeof(buf)); - if ((res = Netlib_Recv(m_mqttConn, (char *)buf, size)) <= 0) - return false; - - payload.writeBuf(buf, res); - remainingBytes -= res; - } - } - - return true; -} - -void FacebookProto::MqttSend(const MqttMessage &payload) -{ - FbThrift msg; - msg << payload.m_leadingByte; - msg.writeIntV(payload.size()); - msg.writeBuf(payload.data(), payload.size()); - Netlib_Send(m_mqttConn, (char*)msg.data(), (unsigned)msg.size()); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// creates initial MQTT will and sends initialization packet - -void FacebookProto::MqttLogin() -{ - uint8_t zeroByte = 0; - Utils_GetRandom(&m_iMqttId, sizeof(m_iMqttId) / 2); - - FbThrift thrift; - thrift.writeField(FB_THRIFT_TYPE_STRING); // Client identifier - thrift << m_szClientID; - - thrift.writeField(FB_THRIFT_TYPE_STRUCT, 4, 1); - - thrift.writeField(FB_THRIFT_TYPE_I64); // User identifier - thrift.writeInt64(m_uid); - - thrift.writeField(FB_THRIFT_TYPE_STRING); // User agent - thrift << FB_API_MQTT_AGENT; - - thrift.writeField(FB_THRIFT_TYPE_I64); - thrift.writeInt64(23); - thrift.writeField(FB_THRIFT_TYPE_I64); - thrift.writeInt64(26); - thrift.writeField(FB_THRIFT_TYPE_I32); - thrift.writeInt32(1); - - thrift.writeBool(true); - thrift.writeBool(!m_bLoginInvisible); // visibility - - thrift.writeField(FB_THRIFT_TYPE_STRING); // device id - thrift << m_szDeviceID; - - thrift.writeBool(true); - thrift.writeField(FB_THRIFT_TYPE_I32); - thrift.writeInt32(1); - thrift.writeField(FB_THRIFT_TYPE_I32); - thrift.writeInt32(0); - thrift.writeField(FB_THRIFT_TYPE_I64); - thrift.writeInt64(m_iMqttId); - - thrift.writeField(FB_THRIFT_TYPE_LIST, 14, 12); - thrift.writeList(FB_THRIFT_TYPE_I32, 0); - thrift << zeroByte; - - thrift.writeField(FB_THRIFT_TYPE_STRING); - thrift << m_szAuthToken << zeroByte; - - size_t dataSize; - mir_ptr pData(doZip(thrift.size(), thrift.data(), dataSize)); - - uint8_t protocolVersion = 3; - uint8_t flags = FB_MQTT_CONNECT_FLAG_USER | FB_MQTT_CONNECT_FLAG_PASS | FB_MQTT_CONNECT_FLAG_CLR | FB_MQTT_CONNECT_FLAG_QOS1; - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_CONNECT); - payload.writeStr("MQTToT"); - payload << protocolVersion << flags; - payload.writeInt16(60); // timeout - payload.writeBuf(pData, dataSize); - MqttSend(payload); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// various MQTT send commands - -void FacebookProto::MqttPing() -{ - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_PINGREQ, FB_MQTT_MESSAGE_FLAG_QOS1); - MqttSend(payload); -} - -void FacebookProto::MqttPublish(const char *topic, const JSONNode &value) -{ - auto str = value.write(); - debugLogA("Publish: <%s> -> <%s>", topic, str.c_str()); - - size_t dataSize; - mir_ptr pData(doZip(str.length(), str.c_str(), dataSize)); - - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_PUBLISH, FB_MQTT_MESSAGE_FLAG_QOS1); - payload.writeStr(topic); - payload.writeInt16(++m_mid); - payload.writeBuf(pData, dataSize); - MqttSend(payload); -} - -void FacebookProto::MqttSubscribe(const char *topic, ...) -{ - uint8_t zeroByte = 0; - - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_SUBSCRIBE, FB_MQTT_MESSAGE_FLAG_QOS1); - payload.writeInt16(++m_mid); - payload.writeStr(topic); - payload << zeroByte; - - va_list ap; - va_start(ap, topic); - while ((topic = va_arg(ap, const char *)) != nullptr) { - payload.writeStr(topic); - payload << zeroByte; - } - va_end(ap); - - MqttSend(payload); -} - -void FacebookProto::MqttUnsubscribe(const char *topic, ...) -{ - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE, FB_MQTT_MESSAGE_FLAG_QOS1); - payload.writeInt16(++m_mid); - payload.writeStr(topic); - - va_list ap; - va_start(ap, topic); - while ((topic = va_arg(ap, const char *)) != nullptr) - payload.writeStr(topic); - va_end(ap); - - MqttSend(payload); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// MQTT queue - -void FacebookProto::MqttQueueConnect() -{ - JSONNode query; - query << INT_PARAM("delta_batch_size", 125) << INT_PARAM("max_deltas_able_to_process", 1000) << INT_PARAM("sync_api_version", 3) << CHAR_PARAM("encoding", "JSON"); - if (m_szSyncToken.IsEmpty()) { - JSONNode hashes; hashes.set_name("graphql_query_hashes"); hashes << CHAR_PARAM("xma_query_id", __STRINGIFY(FB_API_QUERY_XMA)); - - JSONNode xma; xma.set_name(__STRINGIFY(FB_API_QUERY_XMA)); xma << CHAR_PARAM("xma_id", ""); - JSONNode hql; hql.set_name("graphql_query_params"); hql << xma; - - JSONNode params; params.set_name("queue_params"); - params << CHAR_PARAM("buzz_on_deltas_enabled", "false") << hashes << hql; - - query << INT64_PARAM("initial_titan_sequence_id", m_sid) << CHAR_PARAM("device_id", m_szDeviceID) << INT64_PARAM("entity_fbid", m_uid) << params; - MqttPublish("/messenger_sync_create_queue", query); - } - else { - query << INT64_PARAM("last_seq_id", m_sid) << CHAR_PARAM("sync_token", m_szSyncToken); - MqttPublish("/messenger_sync_get_diffs", query); - } -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +uint8_t *FacebookProto::doZip(size_t cbData, const void *pData, size_t &cbRes) +{ + size_t dataSize = cbData + 100; + uint8_t *pRes = (uint8_t *)mir_alloc(dataSize); + + z_stream zStreamOut = {}; + deflateInit(&zStreamOut, Z_BEST_COMPRESSION); + zStreamOut.avail_in = (unsigned)cbData; + zStreamOut.next_in = (uint8_t *)pData; + zStreamOut.avail_out = (unsigned)dataSize; + zStreamOut.next_out = (uint8_t *)pRes; + deflate(&zStreamOut, Z_FINISH); + deflateEnd(&zStreamOut); + + cbRes = dataSize - zStreamOut.avail_out; + return pRes; +} + +uint8_t *FacebookProto::doUnzip(size_t cbData, const void *pData, size_t &cbRes) +{ + size_t dataSize = cbData * 10; + uint8_t *pRes = (uint8_t *)mir_alloc(dataSize); + + z_stream zStreamOut = {}; + inflateInit(&zStreamOut); + zStreamOut.avail_in = (unsigned)cbData; + zStreamOut.next_in = (uint8_t *)pData; + zStreamOut.avail_out = (unsigned)dataSize; + zStreamOut.next_out = (uint8_t *)pRes; + int rc = inflate(&zStreamOut, Z_FINISH); + inflateEnd(&zStreamOut); + + switch (rc) { + case Z_OK: + case Z_STREAM_END: + cbRes = dataSize - zStreamOut.avail_out; + return pRes; + } + + mir_free(pRes); + cbRes = 0; + return nullptr; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// MqttMessage class members + +MqttMessage::MqttMessage() : + m_leadingByte(0) +{ +} + +MqttMessage::MqttMessage(FbMqttMessageType type, uint8_t flags) +{ + m_leadingByte = ((type & 0x0F) << 4) | (flags & 0x0F); +} + +char* MqttMessage::readStr(const uint8_t *&pData) const +{ + u_short len = ntohs(*(u_short *)pData); pData += sizeof(u_short); + if (len == 0) + return nullptr; + + char *res = (char*)mir_alloc(len + 1); + memcpy(res, pData, len); + res[len] = 0; + pData += len; + return res; +} + +void MqttMessage::writeStr(const char *str) +{ + size_t len = mir_strlen(str); + writeInt16((uint16_t)len); + writeBuf(str, len); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// MQTT functions + +bool FacebookProto::MqttParse(const MqttMessage &payload) +{ + auto *pData = (const uint8_t *)payload.data(), *pBeg = pData; + int flags = payload.getFlags(); + uint16_t mid; + + switch (payload.getType()) { + case FB_MQTT_MESSAGE_TYPE_CONNACK: + if (pData[1] != 0) { // connection failed; + int iErrorCode = ntohs(*(u_short *)pData); + debugLogA("Login failed with error %d", iErrorCode); + + if (iErrorCode == 4) { // invalid login/password + delSetting(DBKEY_TOKEN); + m_szAuthToken.Empty(); + ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, 0, LOGINERR_WRONGPASSWORD); + } + else ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, 0, LOGINERR_WRONGPROTOCOL); + return false; + } + + OnLoggedIn(); + break; + + case FB_MQTT_MESSAGE_TYPE_PUBREL: + mid = ntohs(*(u_short *)pData); + pData += 2; + { + MqttMessage reply(FB_MQTT_MESSAGE_TYPE_PUBCOMP); + reply.writeInt16(mid); + MqttSend(reply); + } + break; + + case FB_MQTT_MESSAGE_TYPE_PUBLISH: + char *str = payload.readStr(pData); + + if ((flags & FB_MQTT_MESSAGE_FLAG_QOS1) || (flags & FB_MQTT_MESSAGE_FLAG_QOS2)) { + mid = ntohs(*(u_short *)pData); + pData += 2; + + MqttMessage reply((flags & FB_MQTT_MESSAGE_FLAG_QOS1) ? FB_MQTT_MESSAGE_TYPE_PUBACK : FB_MQTT_MESSAGE_TYPE_PUBREC); + reply.writeInt16(mid); + MqttSend(reply); + } + + OnPublish(str, pData, payload.size() - (pData - pBeg)); + mir_free(str); + break; + } + + return true; +} + +bool FacebookProto::MqttRead(MqttMessage &payload) +{ + uint8_t b; + int res = Netlib_Recv(m_mqttConn, (char *)&b, sizeof(b), MSG_NODUMP); + if (res != 1) + return false; + + payload.m_leadingByte = b; + + uint32_t m = 1, remainingBytes = 0; + do { + if ((res = Netlib_Recv(m_mqttConn, (char *)&b, sizeof(b), MSG_NODUMP)) != 1) + return false; + + remainingBytes += (b & 0x7F) * m; + m *= 128; + } while ((b & 0x80) != 0); + + debugLogA("Received message of type=%d, flags=%x, body length=%d", payload.getType(), payload.getFlags(), remainingBytes); + + if (remainingBytes != 0) { + while (remainingBytes > 0) { + uint8_t buf[1024]; + int size = min(remainingBytes, sizeof(buf)); + if ((res = Netlib_Recv(m_mqttConn, (char *)buf, size)) <= 0) + return false; + + payload.writeBuf(buf, res); + remainingBytes -= res; + } + } + + return true; +} + +void FacebookProto::MqttSend(const MqttMessage &payload) +{ + FbThrift msg; + msg << payload.m_leadingByte; + msg.writeIntV(payload.size()); + msg.writeBuf(payload.data(), payload.size()); + Netlib_Send(m_mqttConn, (char*)msg.data(), (unsigned)msg.size()); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// creates initial MQTT will and sends initialization packet + +void FacebookProto::MqttLogin() +{ + uint8_t zeroByte = 0; + Utils_GetRandom(&m_iMqttId, sizeof(m_iMqttId) / 2); + + FbThrift thrift; + thrift.writeField(FB_THRIFT_TYPE_STRING); // Client identifier + thrift << m_szClientID; + + thrift.writeField(FB_THRIFT_TYPE_STRUCT, 4, 1); + + thrift.writeField(FB_THRIFT_TYPE_I64); // User identifier + thrift.writeInt64(m_uid); + + thrift.writeField(FB_THRIFT_TYPE_STRING); // User agent + thrift << FB_API_MQTT_AGENT; + + thrift.writeField(FB_THRIFT_TYPE_I64); + thrift.writeInt64(23); + thrift.writeField(FB_THRIFT_TYPE_I64); + thrift.writeInt64(26); + thrift.writeField(FB_THRIFT_TYPE_I32); + thrift.writeInt32(1); + + thrift.writeBool(true); + thrift.writeBool(!m_bLoginInvisible); // visibility + + thrift.writeField(FB_THRIFT_TYPE_STRING); // device id + thrift << m_szDeviceID; + + thrift.writeBool(true); + thrift.writeField(FB_THRIFT_TYPE_I32); + thrift.writeInt32(1); + thrift.writeField(FB_THRIFT_TYPE_I32); + thrift.writeInt32(0); + thrift.writeField(FB_THRIFT_TYPE_I64); + thrift.writeInt64(m_iMqttId); + + thrift.writeField(FB_THRIFT_TYPE_LIST, 14, 12); + thrift.writeList(FB_THRIFT_TYPE_I32, 0); + thrift << zeroByte; + + thrift.writeField(FB_THRIFT_TYPE_STRING); + thrift << m_szAuthToken << zeroByte; + + size_t dataSize; + mir_ptr pData(doZip(thrift.size(), thrift.data(), dataSize)); + + uint8_t protocolVersion = 3; + uint8_t flags = FB_MQTT_CONNECT_FLAG_USER | FB_MQTT_CONNECT_FLAG_PASS | FB_MQTT_CONNECT_FLAG_CLR | FB_MQTT_CONNECT_FLAG_QOS1; + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_CONNECT); + payload.writeStr("MQTToT"); + payload << protocolVersion << flags; + payload.writeInt16(60); // timeout + payload.writeBuf(pData, dataSize); + MqttSend(payload); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// various MQTT send commands + +void FacebookProto::MqttPing() +{ + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_PINGREQ, FB_MQTT_MESSAGE_FLAG_QOS1); + MqttSend(payload); +} + +void FacebookProto::MqttPublish(const char *topic, const JSONNode &value) +{ + auto str = value.write(); + debugLogA("Publish: <%s> -> <%s>", topic, str.c_str()); + + size_t dataSize; + mir_ptr pData(doZip(str.length(), str.c_str(), dataSize)); + + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_PUBLISH, FB_MQTT_MESSAGE_FLAG_QOS1); + payload.writeStr(topic); + payload.writeInt16(++m_mid); + payload.writeBuf(pData, dataSize); + MqttSend(payload); +} + +void FacebookProto::MqttSubscribe(const char *topic, ...) +{ + uint8_t zeroByte = 0; + + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_SUBSCRIBE, FB_MQTT_MESSAGE_FLAG_QOS1); + payload.writeInt16(++m_mid); + payload.writeStr(topic); + payload << zeroByte; + + va_list ap; + va_start(ap, topic); + while ((topic = va_arg(ap, const char *)) != nullptr) { + payload.writeStr(topic); + payload << zeroByte; + } + va_end(ap); + + MqttSend(payload); +} + +void FacebookProto::MqttUnsubscribe(const char *topic, ...) +{ + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE, FB_MQTT_MESSAGE_FLAG_QOS1); + payload.writeInt16(++m_mid); + payload.writeStr(topic); + + va_list ap; + va_start(ap, topic); + while ((topic = va_arg(ap, const char *)) != nullptr) + payload.writeStr(topic); + va_end(ap); + + MqttSend(payload); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// MQTT queue + +void FacebookProto::MqttQueueConnect() +{ + JSONNode query; + query << INT_PARAM("delta_batch_size", 125) << INT_PARAM("max_deltas_able_to_process", 1000) << INT_PARAM("sync_api_version", 3) << CHAR_PARAM("encoding", "JSON"); + if (m_szSyncToken.IsEmpty()) { + JSONNode hashes; hashes.set_name("graphql_query_hashes"); hashes << CHAR_PARAM("xma_query_id", __STRINGIFY(FB_API_QUERY_XMA)); + + JSONNode xma; xma.set_name(__STRINGIFY(FB_API_QUERY_XMA)); xma << CHAR_PARAM("xma_id", ""); + JSONNode hql; hql.set_name("graphql_query_params"); hql << xma; + + JSONNode params; params.set_name("queue_params"); + params << CHAR_PARAM("buzz_on_deltas_enabled", "false") << hashes << hql; + + query << INT64_PARAM("initial_titan_sequence_id", m_sid) << CHAR_PARAM("device_id", m_szDeviceID) << INT64_PARAM("entity_fbid", m_uid) << params; + MqttPublish("/messenger_sync_create_queue", query); + } + else { + query << INT64_PARAM("last_seq_id", m_sid) << CHAR_PARAM("sync_token", m_szSyncToken); + MqttPublish("/messenger_sync_get_diffs", query); + } +} diff --git a/protocols/Facebook/src/mqtt.h b/protocols/Facebook/src/mqtt.h index 08849fbc4c..8e1b4964c7 100644 --- a/protocols/Facebook/src/mqtt.h +++ b/protocols/Facebook/src/mqtt.h @@ -1,139 +1,139 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -#define FACEBOOK_ORCA_AGENT FB_API_MQTT_AGENT - -#define FB_THRIFT_TYPE_STOP 0 -#define FB_THRIFT_TYPE_VOID 1 -#define FB_THRIFT_TYPE_BOOL 2 -#define FB_THRIFT_TYPE_BYTE 3 -#define FB_THRIFT_TYPE_DOUBLE 4 -#define FB_THRIFT_TYPE_I16 6 -#define FB_THRIFT_TYPE_I32 8 -#define FB_THRIFT_TYPE_I64 10 -#define FB_THRIFT_TYPE_STRING 11 -#define FB_THRIFT_TYPE_STRUCT 12 -#define FB_THRIFT_TYPE_MAP 13 -#define FB_THRIFT_TYPE_SET 14 -#define FB_THRIFT_TYPE_LIST 15 - -enum FbMqttMessageType -{ - FB_MQTT_MESSAGE_TYPE_CONNECT = 1, - FB_MQTT_MESSAGE_TYPE_CONNACK = 2, - FB_MQTT_MESSAGE_TYPE_PUBLISH = 3, - FB_MQTT_MESSAGE_TYPE_PUBACK = 4, - FB_MQTT_MESSAGE_TYPE_PUBREC = 5, - FB_MQTT_MESSAGE_TYPE_PUBREL = 6, - FB_MQTT_MESSAGE_TYPE_PUBCOMP = 7, - FB_MQTT_MESSAGE_TYPE_SUBSCRIBE = 8, - FB_MQTT_MESSAGE_TYPE_SUBACK = 9, - FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE = 10, - FB_MQTT_MESSAGE_TYPE_UNSUBACK = 11, - FB_MQTT_MESSAGE_TYPE_PINGREQ = 12, - FB_MQTT_MESSAGE_TYPE_PINGRESP = 13, - FB_MQTT_MESSAGE_TYPE_DISCONNECT = 14 -}; - -class FbThrift -{ - MBinBuffer m_buf; - -public: - __forceinline void* data() const { return m_buf.data(); } - __forceinline size_t size() const { return m_buf.length(); } - - __forceinline void reset(size_t cbLen, void *pData) - { m_buf.assign(pData, cbLen); - } - - FbThrift& operator<<(uint8_t); - FbThrift& operator<<(const char *); - - void writeBool(bool value); - void writeBuf(const void *pData, size_t cbLen); - void writeInt16(uint16_t value); - void writeInt32(int32_t value); - void writeInt64(int64_t value); - void writeIntV(uint64_t value); - void writeField(int type); - void writeField(int type, int id, int lastid); - void writeList(int iType, int size); -}; - -class FbThriftReader : public FbThrift -{ - bool m_lastBool = false, m_lastBval = false; - size_t offset = 0; - - uint8_t decodeType(int type); - -public: - __forceinline CMStringA rest() const { return CMStringA((char*)data() + offset, int(size() - offset)); } - - bool isStop(); - bool readBool(bool &bVal); - bool readByte(uint8_t &val); - bool readField(uint8_t &type, uint16_t &id); - bool readInt16(uint16_t &val); - bool readInt32(uint32_t &val); - bool readInt64(uint64_t &val); - bool readIntV(uint64_t &val); - bool readList(uint8_t &type, uint32_t &size); - bool readStr(char *&val); // val must be freed via mir_free() -}; - -class MqttMessage : public FbThrift -{ - friend class FacebookProto; - - uint8_t m_leadingByte; - -public: - MqttMessage(); - MqttMessage(FbMqttMessageType type, uint8_t flags = 0); - - __forceinline int getType() const - { - return m_leadingByte >> 4; - } - - __forceinline int getFlags() const - { return m_leadingByte & 0x0F; - } - - char* readStr(const uint8_t *&pData) const; - void writeStr(const char *str); -}; - -#define FB_MQTT_CONNECT_FLAG_CLR 0x0002 -#define FB_MQTT_CONNECT_FLAG_WILL 0x0004 -#define FB_MQTT_CONNECT_FLAG_QOS0 0x0000 -#define FB_MQTT_CONNECT_FLAG_QOS1 0x0008 -#define FB_MQTT_CONNECT_FLAG_QOS2 0x0010 -#define FB_MQTT_CONNECT_FLAG_RET 0x0020 -#define FB_MQTT_CONNECT_FLAG_PASS 0x0040 -#define FB_MQTT_CONNECT_FLAG_USER 0x0080 - -#define FB_MQTT_MESSAGE_FLAG_QOS0 0x0000 -#define FB_MQTT_MESSAGE_FLAG_QOS1 0x0002 -#define FB_MQTT_MESSAGE_FLAG_QOS2 0x0004 +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +#define FACEBOOK_ORCA_AGENT FB_API_MQTT_AGENT + +#define FB_THRIFT_TYPE_STOP 0 +#define FB_THRIFT_TYPE_VOID 1 +#define FB_THRIFT_TYPE_BOOL 2 +#define FB_THRIFT_TYPE_BYTE 3 +#define FB_THRIFT_TYPE_DOUBLE 4 +#define FB_THRIFT_TYPE_I16 6 +#define FB_THRIFT_TYPE_I32 8 +#define FB_THRIFT_TYPE_I64 10 +#define FB_THRIFT_TYPE_STRING 11 +#define FB_THRIFT_TYPE_STRUCT 12 +#define FB_THRIFT_TYPE_MAP 13 +#define FB_THRIFT_TYPE_SET 14 +#define FB_THRIFT_TYPE_LIST 15 + +enum FbMqttMessageType +{ + FB_MQTT_MESSAGE_TYPE_CONNECT = 1, + FB_MQTT_MESSAGE_TYPE_CONNACK = 2, + FB_MQTT_MESSAGE_TYPE_PUBLISH = 3, + FB_MQTT_MESSAGE_TYPE_PUBACK = 4, + FB_MQTT_MESSAGE_TYPE_PUBREC = 5, + FB_MQTT_MESSAGE_TYPE_PUBREL = 6, + FB_MQTT_MESSAGE_TYPE_PUBCOMP = 7, + FB_MQTT_MESSAGE_TYPE_SUBSCRIBE = 8, + FB_MQTT_MESSAGE_TYPE_SUBACK = 9, + FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE = 10, + FB_MQTT_MESSAGE_TYPE_UNSUBACK = 11, + FB_MQTT_MESSAGE_TYPE_PINGREQ = 12, + FB_MQTT_MESSAGE_TYPE_PINGRESP = 13, + FB_MQTT_MESSAGE_TYPE_DISCONNECT = 14 +}; + +class FbThrift +{ + MBinBuffer m_buf; + +public: + __forceinline void* data() const { return m_buf.data(); } + __forceinline size_t size() const { return m_buf.length(); } + + __forceinline void reset(size_t cbLen, void *pData) + { m_buf.assign(pData, cbLen); + } + + FbThrift& operator<<(uint8_t); + FbThrift& operator<<(const char *); + + void writeBool(bool value); + void writeBuf(const void *pData, size_t cbLen); + void writeInt16(uint16_t value); + void writeInt32(int32_t value); + void writeInt64(int64_t value); + void writeIntV(uint64_t value); + void writeField(int type); + void writeField(int type, int id, int lastid); + void writeList(int iType, int size); +}; + +class FbThriftReader : public FbThrift +{ + bool m_lastBool = false, m_lastBval = false; + size_t offset = 0; + + uint8_t decodeType(int type); + +public: + __forceinline CMStringA rest() const { return CMStringA((char*)data() + offset, int(size() - offset)); } + + bool isStop(); + bool readBool(bool &bVal); + bool readByte(uint8_t &val); + bool readField(uint8_t &type, uint16_t &id); + bool readInt16(uint16_t &val); + bool readInt32(uint32_t &val); + bool readInt64(uint64_t &val); + bool readIntV(uint64_t &val); + bool readList(uint8_t &type, uint32_t &size); + bool readStr(char *&val); // val must be freed via mir_free() +}; + +class MqttMessage : public FbThrift +{ + friend class FacebookProto; + + uint8_t m_leadingByte; + +public: + MqttMessage(); + MqttMessage(FbMqttMessageType type, uint8_t flags = 0); + + __forceinline int getType() const + { + return m_leadingByte >> 4; + } + + __forceinline int getFlags() const + { return m_leadingByte & 0x0F; + } + + char* readStr(const uint8_t *&pData) const; + void writeStr(const char *str); +}; + +#define FB_MQTT_CONNECT_FLAG_CLR 0x0002 +#define FB_MQTT_CONNECT_FLAG_WILL 0x0004 +#define FB_MQTT_CONNECT_FLAG_QOS0 0x0000 +#define FB_MQTT_CONNECT_FLAG_QOS1 0x0008 +#define FB_MQTT_CONNECT_FLAG_QOS2 0x0010 +#define FB_MQTT_CONNECT_FLAG_RET 0x0020 +#define FB_MQTT_CONNECT_FLAG_PASS 0x0040 +#define FB_MQTT_CONNECT_FLAG_USER 0x0080 + +#define FB_MQTT_MESSAGE_FLAG_QOS0 0x0000 +#define FB_MQTT_MESSAGE_FLAG_QOS1 0x0002 +#define FB_MQTT_MESSAGE_FLAG_QOS2 0x0004 diff --git a/protocols/Facebook/src/options.cpp b/protocols/Facebook/src/options.cpp index 0d8c1eb38a..2ed98b1dfc 100644 --- a/protocols/Facebook/src/options.cpp +++ b/protocols/Facebook/src/options.cpp @@ -1,85 +1,85 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -class CFacebookAccOptsDlg : public CFBDlgBase -{ - CCtrlEdit edtGroup; - CCtrlCheck chkEnableChats, chkHideChats, chkKeepUnread, chkLoginInvis, chkLoadAll; - CCtrlHyperlink linkMainPage; - -public: - CFacebookAccOptsDlg(FacebookProto *pThis) : - CFBDlgBase(pThis, IDD_OPTIONS), - edtGroup(this, IDC_GROUP), - chkLoadAll(this, IDC_LOADALL), - chkHideChats(this, IDC_HIDECHATS), - chkKeepUnread(this, IDC_KEEP_UNREAD), - chkLoginInvis(this, IDC_INVIS_LOGIN), - chkEnableChats(this, IDC_ENABLECHATS), - linkMainPage(this, IDC_NEWACCOUNTLINK, "https://www.facebook.com") - { - CreateLink(edtGroup, pThis->m_wszDefaultGroup); - CreateLink(chkLoadAll, pThis->m_bLoadAll); - CreateLink(chkHideChats, pThis->m_bHideGroupchats); - CreateLink(chkKeepUnread, pThis->m_bKeepUnread); - CreateLink(chkLoginInvis, pThis->m_bLoginInvisible); - CreateLink(chkEnableChats, pThis->m_bUseGroupchats); - } - - bool OnInitDialog() override - { - ptrA login(m_proto->getStringA(DBKEY_LOGIN)); - if (login != nullptr) - SetDlgItemTextA(m_hwnd, IDC_UN, login); - - ptrA password(m_proto->getStringA(DBKEY_PASS)); - if (password != nullptr) - SetDlgItemTextA(m_hwnd, IDC_PW, password); - return true; - } - - bool OnApply() override - { - char str[128]; - - GetDlgItemTextA(m_hwnd, IDC_UN, str, _countof(str)); - m_proto->setString(DBKEY_LOGIN, str); - - GetDlgItemTextA(m_hwnd, IDC_PW, str, _countof(str)); - m_proto->setString(DBKEY_PASS, str); - return true; - } -}; - -int FacebookProto::OnOptionsInit(WPARAM wParam, LPARAM) -{ - OPTIONSDIALOGPAGE odp = {}; - odp.position = -790000000; - odp.szTitle.w = m_tszUserName; - odp.szGroup.w = LPGENW("Network"); - odp.flags = ODPF_UNICODE; - - odp.szTab.w = LPGENW("Account"); - odp.pDialog = new CFacebookAccOptsDlg(this); - g_plugin.addOptions(wParam, &odp); - return 0; +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +class CFacebookAccOptsDlg : public CFBDlgBase +{ + CCtrlEdit edtGroup; + CCtrlCheck chkEnableChats, chkHideChats, chkKeepUnread, chkLoginInvis, chkLoadAll; + CCtrlHyperlink linkMainPage; + +public: + CFacebookAccOptsDlg(FacebookProto *pThis) : + CFBDlgBase(pThis, IDD_OPTIONS), + edtGroup(this, IDC_GROUP), + chkLoadAll(this, IDC_LOADALL), + chkHideChats(this, IDC_HIDECHATS), + chkKeepUnread(this, IDC_KEEP_UNREAD), + chkLoginInvis(this, IDC_INVIS_LOGIN), + chkEnableChats(this, IDC_ENABLECHATS), + linkMainPage(this, IDC_NEWACCOUNTLINK, "https://www.facebook.com") + { + CreateLink(edtGroup, pThis->m_wszDefaultGroup); + CreateLink(chkLoadAll, pThis->m_bLoadAll); + CreateLink(chkHideChats, pThis->m_bHideGroupchats); + CreateLink(chkKeepUnread, pThis->m_bKeepUnread); + CreateLink(chkLoginInvis, pThis->m_bLoginInvisible); + CreateLink(chkEnableChats, pThis->m_bUseGroupchats); + } + + bool OnInitDialog() override + { + ptrA login(m_proto->getStringA(DBKEY_LOGIN)); + if (login != nullptr) + SetDlgItemTextA(m_hwnd, IDC_UN, login); + + ptrA password(m_proto->getStringA(DBKEY_PASS)); + if (password != nullptr) + SetDlgItemTextA(m_hwnd, IDC_PW, password); + return true; + } + + bool OnApply() override + { + char str[128]; + + GetDlgItemTextA(m_hwnd, IDC_UN, str, _countof(str)); + m_proto->setString(DBKEY_LOGIN, str); + + GetDlgItemTextA(m_hwnd, IDC_PW, str, _countof(str)); + m_proto->setString(DBKEY_PASS, str); + return true; + } +}; + +int FacebookProto::OnOptionsInit(WPARAM wParam, LPARAM) +{ + OPTIONSDIALOGPAGE odp = {}; + odp.position = -790000000; + odp.szTitle.w = m_tszUserName; + odp.szGroup.w = LPGENW("Network"); + odp.flags = ODPF_UNICODE; + + odp.szTab.w = LPGENW("Account"); + odp.pDialog = new CFacebookAccOptsDlg(this); + g_plugin.addOptions(wParam, &odp); + return 0; } \ No newline at end of file diff --git a/protocols/Facebook/src/proto.cpp b/protocols/Facebook/src/proto.cpp index 7a1d8b1c93..db2d401e21 100644 --- a/protocols/Facebook/src/proto.cpp +++ b/protocols/Facebook/src/proto.cpp @@ -1,302 +1,302 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -static int CompareUsers(const FacebookUser *p1, const FacebookUser *p2) -{ - if (p1->id == p2->id) - return 0; - - return (p1->id < p2->id) ? -1 : 1; -} - -static int CompareMessages(const COwnMessage *p1, const COwnMessage *p2) -{ - if (p1->msgId == p2->msgId) - return 0; - - return (p1->msgId < p2->msgId) ? -1 : 1; -} - -FacebookProto::FacebookProto(const char *proto_name, const wchar_t *username) : - PROTO(proto_name, username), - m_impl(*this), - m_users(50, CompareUsers), - arOwnMessages(1, CompareMessages), - m_bLoadAll(this, "LoadAllContacts", false), - m_bKeepUnread(this, "KeepUnread", false), - m_bUseBigAvatars(this, "UseBigAvatars", true), - m_bUseGroupchats(this, "UseGroupChats", true), - m_bHideGroupchats(this, "HideGroupChats", true), - m_bLoginInvisible(this, "LoginInvisible", false), - m_wszDefaultGroup(this, "DefaultGroup", L"Facebook") -{ - // to upgrade previous settings - if (getByte("Compatibility") < 1) { - setByte("Compatibility", 1); - delSetting(DBKEY_DEVICE_ID); - } - - m_szDeviceID = getMStringA(DBKEY_DEVICE_ID); - if (m_szDeviceID.IsEmpty()) { - UUID deviceId; - UuidCreate(&deviceId); - RPC_CSTR szId; - UuidToStringA(&deviceId, &szId); - m_szDeviceID = szId; - setString(DBKEY_DEVICE_ID, m_szDeviceID); - RpcStringFreeA(&szId); - } - - m_szClientID = getMStringA(DBKEY_CLIENT_ID); - if (m_szClientID.IsEmpty()) { - for (int i = 0; i < 20; i++) { - uint32_t dwRandon; - Utils_GetRandom(&dwRandon, sizeof(dwRandon)); - int c = dwRandon % 62; - if (c >= 0 && c < 26) - c += 'a'; - else if (c >= 26 && c < 52) - c += 'A' - 26; - else if (c >= 52 && c < 62) - c += '0' - 52; - m_szClientID.AppendChar(c); - } - setString(DBKEY_CLIENT_ID, m_szClientID); - } - - m_uid = _atoi64(getMStringA(DBKEY_ID)); - m_sid = _atoi64(getMStringA(DBKEY_SID)); - m_szSyncToken = getMStringA(DBKEY_SYNC_TOKEN); - - // Create standard network connection - NETLIBUSER nlu = {}; - nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; - nlu.szSettingsModule = m_szModuleName; - nlu.szDescriptiveName.w = m_tszUserName; - m_hNetlibUser = Netlib_RegisterUser(&nlu); - - db_set_resident(m_szModuleName, "UpdateNeeded"); - - // Services - CreateProtoService(PS_CREATEACCMGRUI, &FacebookProto::SvcCreateAccMgrUI); - CreateProtoService(PS_GETAVATARINFO, &FacebookProto::GetAvatarInfo); - CreateProtoService(PS_GETAVATARCAPS, &FacebookProto::GetAvatarCaps); - - // Events - HookProtoEvent(ME_GC_EVENT, &FacebookProto::GroupchatEventHook); - HookProtoEvent(ME_GC_BUILDMENU, &FacebookProto::GroupchatMenuHook); - HookProtoEvent(ME_OPT_INITIALISE, &FacebookProto::OnOptionsInit); - HookProtoEvent(ME_DB_EVENT_MARKED_READ, &FacebookProto::OnMarkedRead); - - // Group chats - GCREGISTER gcr = {}; - gcr.dwFlags = GC_TYPNOTIF; - gcr.ptszDispName = m_tszUserName; - gcr.pszModule = m_szModuleName; - Chat_Register(&gcr); -} - -FacebookProto::~FacebookProto() -{ -} - -///////////////////////////////////////////////////////////////////////////////////////// -// protocol events - -void FacebookProto::OnContactAdded(MCONTACT hContact) -{ - __int64 userId = _atoi64(getMStringA(hContact, DBKEY_ID)); - if (userId && !FindUser(userId)) { - mir_cslock lck(m_csUsers); - m_users.insert(new FacebookUser(userId, hContact)); - } -} - -void FacebookProto::OnModulesLoaded() -{ - VARSW wszCache(L"%miranda_avatarcache%"); - - CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", wszCache.get(), m_szModuleName); - SMADD_CONT cont = { 2, m_szModuleName, wszPath }; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); - - wszPath.Format(L"%s\\%S\\Stickers\\*.webp", wszCache.get(), m_szModuleName); - cont.path = wszPath; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); - - // contacts cache - for (auto &cc : AccContacts()) { - CMStringA szId(getMStringA(cc, DBKEY_ID)); - if (!szId.IsEmpty()) - m_users.insert(new FacebookUser(_atoi64(szId), cc, isChatRoom(cc))); - } - - // Default group - Clist_GroupCreate(0, m_wszDefaultGroup); -} - -void FacebookProto::OnShutdown() -{ - if (m_mqttConn != nullptr) - Netlib_Shutdown(m_mqttConn); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MCONTACT FacebookProto::AddToList(int, PROTOSEARCHRESULT *psr) -{ - if (!mir_wstrlen(psr->id.w)) - return 0; - - if (auto *pUser = FindUser(_wtoi64(psr->id.w))) - return pUser->hContact; - - MCONTACT hContact = db_add_contact(); - setWString(hContact, DBKEY_ID, psr->id.w); - Proto_AddToContact(hContact, m_szModuleName); - return hContact; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR FacebookProto::GetCaps(int type, MCONTACT) -{ - switch (type) { - case PFLAGNUM_1: - { - DWORD_PTR flags = PF1_IM | PF1_CHAT | PF1_SERVERCLIST | PF1_AUTHREQ; - - if (getByte(DBKEY_SET_MIRANDA_STATUS)) - return flags |= PF1_MODEMSG; - else - return flags |= PF1_MODEMSGRECV; - } - - case PFLAGNUM_2: - return PF2_ONLINE | PF2_SHORTAWAY | PF2_INVISIBLE | PF2_IDLE; - - case PFLAGNUM_3: - if (getByte(DBKEY_SET_MIRANDA_STATUS)) - return PF2_ONLINE; // | PF2_SHORTAWAY; - else - return 0; - - case PFLAGNUM_4: - return PF4_NOCUSTOMAUTH | PF4_AVATARS | PF4_SUPPORTTYPING | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE | PF4_READNOTIFY; - - case PFLAG_MAXLENOFMESSAGE: - return FACEBOOK_MESSAGE_LIMIT; - - case PFLAG_UNIQUEIDTEXT: - return (INT_PTR) L"Facebook ID"; - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int FacebookProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) -{ - if (!m_bOnline) { - ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)1, (LPARAM)TranslateT("Protocol is offline or user isn't authorized yet")); - return 1; - } - - CMStringA userId(getMStringA(hContact, DBKEY_ID)); - - __int64 msgId; - Utils_GetRandom(&msgId, sizeof(msgId)); - msgId = abs(msgId); - - JSONNode root; root << CHAR_PARAM("body", pszSrc) << INT64_PARAM("msgid", msgId) << INT64_PARAM("sender_fbid", m_uid) << CHAR_PARAM("to", userId); - MqttPublish("/send_message2", root); - - mir_cslock lck(m_csOwnMessages); - arOwnMessages.insert(new COwnMessage(msgId, m_mid, hContact)); - return m_mid; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int FacebookProto::SetStatus(int iNewStatus) -{ - if (iNewStatus != ID_STATUS_OFFLINE && IsStatusConnecting(m_iStatus)) { - debugLogA("=== Status is already connecting, no change"); - return 0; - } - - // Routing statuses not supported by Facebook - switch (iNewStatus) { - case ID_STATUS_ONLINE: - case ID_STATUS_OFFLINE: - break; - - default: - iNewStatus = ID_STATUS_AWAY; - break; - } - - if (m_iStatus == iNewStatus) { - debugLogA("=== Statuses are same, no change"); - return 0; - } - - m_iDesiredStatus = iNewStatus; - - int iOldStatus = m_iStatus; - - // log off & free all resources - if (iNewStatus == ID_STATUS_OFFLINE) { - OnShutdown(); - - m_iStatus = ID_STATUS_OFFLINE; - } - else if (m_iStatus == ID_STATUS_OFFLINE) { // we gonna connect - debugLogA("*** Beginning SignOn process"); - - m_iStatus = ID_STATUS_CONNECTING; - - ForkThread(&FacebookProto::ServerThread); - } - else m_iStatus = iNewStatus; - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - return 0; -} - -////////////////////////////////////////////////////////////////////////////// - -int FacebookProto::UserIsTyping(MCONTACT hContact, int type) -{ - JSONNode root; root << INT_PARAM("state", type == PROTOTYPE_SELFTYPING_ON) << CHAR_PARAM("to", getMStringA(hContact, DBKEY_ID)); - MqttPublish("/typing", root); - return 0; -} - -////////////////////////////////////////////////////////////////////////////// -// Services - -INT_PTR FacebookProto::SvcCreateAccMgrUI(WPARAM, LPARAM lParam) -{ - return (INT_PTR) CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_FACEBOOKACCOUNT), - (HWND) lParam, FBAccountProc, (LPARAM) this); -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +static int CompareUsers(const FacebookUser *p1, const FacebookUser *p2) +{ + if (p1->id == p2->id) + return 0; + + return (p1->id < p2->id) ? -1 : 1; +} + +static int CompareMessages(const COwnMessage *p1, const COwnMessage *p2) +{ + if (p1->msgId == p2->msgId) + return 0; + + return (p1->msgId < p2->msgId) ? -1 : 1; +} + +FacebookProto::FacebookProto(const char *proto_name, const wchar_t *username) : + PROTO(proto_name, username), + m_impl(*this), + m_users(50, CompareUsers), + arOwnMessages(1, CompareMessages), + m_bLoadAll(this, "LoadAllContacts", false), + m_bKeepUnread(this, "KeepUnread", false), + m_bUseBigAvatars(this, "UseBigAvatars", true), + m_bUseGroupchats(this, "UseGroupChats", true), + m_bHideGroupchats(this, "HideGroupChats", true), + m_bLoginInvisible(this, "LoginInvisible", false), + m_wszDefaultGroup(this, "DefaultGroup", L"Facebook") +{ + // to upgrade previous settings + if (getByte("Compatibility") < 1) { + setByte("Compatibility", 1); + delSetting(DBKEY_DEVICE_ID); + } + + m_szDeviceID = getMStringA(DBKEY_DEVICE_ID); + if (m_szDeviceID.IsEmpty()) { + UUID deviceId; + UuidCreate(&deviceId); + RPC_CSTR szId; + UuidToStringA(&deviceId, &szId); + m_szDeviceID = szId; + setString(DBKEY_DEVICE_ID, m_szDeviceID); + RpcStringFreeA(&szId); + } + + m_szClientID = getMStringA(DBKEY_CLIENT_ID); + if (m_szClientID.IsEmpty()) { + for (int i = 0; i < 20; i++) { + uint32_t dwRandon; + Utils_GetRandom(&dwRandon, sizeof(dwRandon)); + int c = dwRandon % 62; + if (c >= 0 && c < 26) + c += 'a'; + else if (c >= 26 && c < 52) + c += 'A' - 26; + else if (c >= 52 && c < 62) + c += '0' - 52; + m_szClientID.AppendChar(c); + } + setString(DBKEY_CLIENT_ID, m_szClientID); + } + + m_uid = _atoi64(getMStringA(DBKEY_ID)); + m_sid = _atoi64(getMStringA(DBKEY_SID)); + m_szSyncToken = getMStringA(DBKEY_SYNC_TOKEN); + + // Create standard network connection + NETLIBUSER nlu = {}; + nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szSettingsModule = m_szModuleName; + nlu.szDescriptiveName.w = m_tszUserName; + m_hNetlibUser = Netlib_RegisterUser(&nlu); + + db_set_resident(m_szModuleName, "UpdateNeeded"); + + // Services + CreateProtoService(PS_CREATEACCMGRUI, &FacebookProto::SvcCreateAccMgrUI); + CreateProtoService(PS_GETAVATARINFO, &FacebookProto::GetAvatarInfo); + CreateProtoService(PS_GETAVATARCAPS, &FacebookProto::GetAvatarCaps); + + // Events + HookProtoEvent(ME_GC_EVENT, &FacebookProto::GroupchatEventHook); + HookProtoEvent(ME_GC_BUILDMENU, &FacebookProto::GroupchatMenuHook); + HookProtoEvent(ME_OPT_INITIALISE, &FacebookProto::OnOptionsInit); + HookProtoEvent(ME_DB_EVENT_MARKED_READ, &FacebookProto::OnMarkedRead); + + // Group chats + GCREGISTER gcr = {}; + gcr.dwFlags = GC_TYPNOTIF; + gcr.ptszDispName = m_tszUserName; + gcr.pszModule = m_szModuleName; + Chat_Register(&gcr); +} + +FacebookProto::~FacebookProto() +{ +} + +///////////////////////////////////////////////////////////////////////////////////////// +// protocol events + +void FacebookProto::OnContactAdded(MCONTACT hContact) +{ + __int64 userId = _atoi64(getMStringA(hContact, DBKEY_ID)); + if (userId && !FindUser(userId)) { + mir_cslock lck(m_csUsers); + m_users.insert(new FacebookUser(userId, hContact)); + } +} + +void FacebookProto::OnModulesLoaded() +{ + VARSW wszCache(L"%miranda_avatarcache%"); + + CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", wszCache.get(), m_szModuleName); + SMADD_CONT cont = { 2, m_szModuleName, wszPath }; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); + + wszPath.Format(L"%s\\%S\\Stickers\\*.webp", wszCache.get(), m_szModuleName); + cont.path = wszPath; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); + + // contacts cache + for (auto &cc : AccContacts()) { + CMStringA szId(getMStringA(cc, DBKEY_ID)); + if (!szId.IsEmpty()) + m_users.insert(new FacebookUser(_atoi64(szId), cc, isChatRoom(cc))); + } + + // Default group + Clist_GroupCreate(0, m_wszDefaultGroup); +} + +void FacebookProto::OnShutdown() +{ + if (m_mqttConn != nullptr) + Netlib_Shutdown(m_mqttConn); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MCONTACT FacebookProto::AddToList(int, PROTOSEARCHRESULT *psr) +{ + if (!mir_wstrlen(psr->id.w)) + return 0; + + if (auto *pUser = FindUser(_wtoi64(psr->id.w))) + return pUser->hContact; + + MCONTACT hContact = db_add_contact(); + setWString(hContact, DBKEY_ID, psr->id.w); + Proto_AddToContact(hContact, m_szModuleName); + return hContact; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR FacebookProto::GetCaps(int type, MCONTACT) +{ + switch (type) { + case PFLAGNUM_1: + { + DWORD_PTR flags = PF1_IM | PF1_CHAT | PF1_SERVERCLIST | PF1_AUTHREQ; + + if (getByte(DBKEY_SET_MIRANDA_STATUS)) + return flags |= PF1_MODEMSG; + else + return flags |= PF1_MODEMSGRECV; + } + + case PFLAGNUM_2: + return PF2_ONLINE | PF2_SHORTAWAY | PF2_INVISIBLE | PF2_IDLE; + + case PFLAGNUM_3: + if (getByte(DBKEY_SET_MIRANDA_STATUS)) + return PF2_ONLINE; // | PF2_SHORTAWAY; + else + return 0; + + case PFLAGNUM_4: + return PF4_NOCUSTOMAUTH | PF4_AVATARS | PF4_SUPPORTTYPING | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE | PF4_READNOTIFY; + + case PFLAG_MAXLENOFMESSAGE: + return FACEBOOK_MESSAGE_LIMIT; + + case PFLAG_UNIQUEIDTEXT: + return (INT_PTR) L"Facebook ID"; + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int FacebookProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) +{ + if (!m_bOnline) { + ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)1, (LPARAM)TranslateT("Protocol is offline or user isn't authorized yet")); + return 1; + } + + CMStringA userId(getMStringA(hContact, DBKEY_ID)); + + __int64 msgId; + Utils_GetRandom(&msgId, sizeof(msgId)); + msgId = abs(msgId); + + JSONNode root; root << CHAR_PARAM("body", pszSrc) << INT64_PARAM("msgid", msgId) << INT64_PARAM("sender_fbid", m_uid) << CHAR_PARAM("to", userId); + MqttPublish("/send_message2", root); + + mir_cslock lck(m_csOwnMessages); + arOwnMessages.insert(new COwnMessage(msgId, m_mid, hContact)); + return m_mid; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int FacebookProto::SetStatus(int iNewStatus) +{ + if (iNewStatus != ID_STATUS_OFFLINE && IsStatusConnecting(m_iStatus)) { + debugLogA("=== Status is already connecting, no change"); + return 0; + } + + // Routing statuses not supported by Facebook + switch (iNewStatus) { + case ID_STATUS_ONLINE: + case ID_STATUS_OFFLINE: + break; + + default: + iNewStatus = ID_STATUS_AWAY; + break; + } + + if (m_iStatus == iNewStatus) { + debugLogA("=== Statuses are same, no change"); + return 0; + } + + m_iDesiredStatus = iNewStatus; + + int iOldStatus = m_iStatus; + + // log off & free all resources + if (iNewStatus == ID_STATUS_OFFLINE) { + OnShutdown(); + + m_iStatus = ID_STATUS_OFFLINE; + } + else if (m_iStatus == ID_STATUS_OFFLINE) { // we gonna connect + debugLogA("*** Beginning SignOn process"); + + m_iStatus = ID_STATUS_CONNECTING; + + ForkThread(&FacebookProto::ServerThread); + } + else m_iStatus = iNewStatus; + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + return 0; +} + +////////////////////////////////////////////////////////////////////////////// + +int FacebookProto::UserIsTyping(MCONTACT hContact, int type) +{ + JSONNode root; root << INT_PARAM("state", type == PROTOTYPE_SELFTYPING_ON) << CHAR_PARAM("to", getMStringA(hContact, DBKEY_ID)); + MqttPublish("/typing", root); + return 0; +} + +////////////////////////////////////////////////////////////////////////////// +// Services + +INT_PTR FacebookProto::SvcCreateAccMgrUI(WPARAM, LPARAM lParam) +{ + return (INT_PTR) CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_FACEBOOKACCOUNT), + (HWND) lParam, FBAccountProc, (LPARAM) this); +} diff --git a/protocols/Facebook/src/proto.h b/protocols/Facebook/src/proto.h index 14dc343d18..7fbeebd2cb 100644 --- a/protocols/Facebook/src/proto.h +++ b/protocols/Facebook/src/proto.h @@ -1,557 +1,557 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - - -/** - * FB_API_AHOST: - * - * The HTTP host for the Facebook API. - */ -#define FB_API_AHOST "https://api.facebook.com" - -/** - * FB_API_BHOST: - * - * The HTTP host for the Facebook BAPI. - */ -#define FB_API_BHOST "https://b-api.facebook.com" - -/** - * FB_API_GHOST: - * - * The HTTP host for the Facebook Graph API. - */ -#define FB_API_GHOST "https://graph.facebook.com" - -/** - * FB_API_WHOST: - * - * The HTTP host for the Facebook website. - */ -#define FB_API_WHOST "https://www.facebook.com" - -/** - * FB_API_FBRPC_PREFIX - * - * The fbrpc URL prefix used in links shared from the mobile app. - */ -#define FB_API_FBRPC_PREFIX "fbrpc://facebook/nativethirdparty" - -/** - * FB_API_KEY: - * - * The Facebook API key. - */ -#define FB_API_KEY "256002347743983" - -/** - * FB_API_SECRET: - * - * The Facebook API secret. - */ -#define FB_API_SECRET "374e60f8b9bb6b8cbb30f78030438895" - -/** - * FB_ORCA_AGENT - * - * The part of the user agent that looks like the official client, since the - * server started checking this. - */ - -#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/537.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" - -/** - * FB_API_AGENT: - * - * The HTTP User-Agent header. - */ -#define FB_API_AGENT "Facebook plugin / Purple / 0.9.6 " FB_ORCA_AGENT - -/** - * FB_API_MQTT_AGENT - * - * The client information string sent in the MQTT CONNECT message - */ - -#define FB_API_MQTT_AGENT FB_API_AGENT - -/** - * FB_API_URL_ATTACH: - * - * The URL for attachment URL requests. - */ -#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.getAttachment" -//#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.attachmentRedirect" - -/** - * FB_API_URL_AUTH: - * - * The URL for authentication requests. - */ -#define FB_API_URL_AUTH FB_API_BHOST "/method/auth.login" - -/** - * FB_API_URL_GQL: - * - * The URL for GraphQL requests. - */ -#define FB_API_URL_GQL FB_API_GHOST "/graphql" - -/** - * FB_API_URL_MESSAGES: - * - * The URL for linking message threads. - */ -#define FB_API_URL_MESSAGES FB_API_WHOST "/messages" - -/** - * FB_API_URL_PARTS: - * - * The URL for participant management requests. - */ -#define FB_API_URL_PARTS FB_API_GHOST "/participants" - -/** - * FB_API_URL_THREADS: - * - * The URL for thread management requests. - */ -#define FB_API_URL_THREADS FB_API_GHOST "/me/group_threads" - -/** - * FB_API_URL_TOPIC: - * - * The URL for thread topic requests. - */ -#define FB_API_URL_TOPIC FB_API_AHOST "/method/messaging.setthreadname" - -/** - * FB_API_QUERY_CONTACT: - * - * The query hash for the `UsersQuery`. - * - * Key mapping: - * 0: user_fbids - * 1: include_full_user_info - * 2: profile_pic_large_size - * 3: profile_pic_medium_size - * 4: profile_pic_small_size - */ -#define FB_API_QUERY_CONTACT 10153915107411729 - -/** - * FB_API_QUERY_CONTACTS: - * - * The query hash for the `FetchContactsFullQuery`. - * - * Key mapping: - * 0: profile_types - * 1: limit - * 2: big_img_size - * 3: huge_img_size - * 4: small_img_size - */ -#define FB_API_QUERY_CONTACTS 10154444360806729 - -/** - * FB_API_QUERY_CONTACTS_AFTER: - * - * The query hash for the `FetchContactsFullWithAfterQuery`. - * - * Key mapping: - * 0: profile_types - * 1: after - * 2: limit - * 3: big_img_size - * 4: huge_img_size - * 5: small_img_size - */ -#define FB_API_QUERY_CONTACTS_AFTER 10154444360816729 - - -/** - * FB_API_QUERY_CONTACTS_DELTA: - * - * The query hash for the `FetchContactsDeltaQuery`. - * - * Key mapping: - * 0: after - * 1: profile_types - * 2: limit - * 3: big_img_size - * 4: huge_img_size - * 5: small_img_size - */ -#define FB_API_QUERY_CONTACTS_DELTA 10154444360801729 - -/** - * FB_API_QUERY_STICKER: - * - * The query hash for the `FetchStickersWithPreviewsQuery`. - * - * Key mapping: - * 0: sticker_ids - * 1: media_type - * 2: preview_size - * 3: scaling_factor - * 4: animated_media_type - */ -#define FB_API_QUERY_STICKER 10152877994321729 - -/** - * FB_API_QUERY_THREAD: - * - * The query hash for the `ThreadQuery`. - * - * Key mapping: - * 0: thread_ids - * 1: verification_type - * 2: hash_key - * 3: small_preview_size - * 4: large_preview_size - * 5: item_count - * 6: event_count - * 7: full_screen_height - * 8: full_screen_width - * 9: medium_preview_size - * 10: fetch_users_separately - * 11: include_message_info - * 12: msg_count - * 13: include_full_user_info - * 14: profile_pic_large_size - * 15: profile_pic_medium_size - * 16: profile_pic_small_size - */ -#define FB_API_QUERY_THREAD 10153919752036729 - -/** - * FB_API_QUERY_THREADS: - * - * The query hash for the `ThreadListQuery`. - * - * Key mapping: - * 0: folder_tag (INBOX, PENDING, ARCHIVED, OTHER, UNREAD) - * 1: thread_count (result is sorted from newest to oldest when parameter is sent) - * 2: include_thread_info - * 3: verification_type - * 4: hash_key - * 5: small_preview_size - * 6: large_preview_size - * 7: item_count - * 8: event_count - * 9: full_screen_height - * 10: full_screen_width - * 11: medium_preview_size - * 12: fetch_users_separately - * 13: include_message_info - * 14: msg_count - * 15: UNKNOWN - * 16: profile_pic_large_size - * 17: profile_pic_medium_size - * 18: profile_pic_small_size - */ -#define FB_API_QUERY_THREADS 10153919752026729 - -/** - * FB_API_QUERY_SEQ_ID: - * - * A variant of ThreadListQuery with sequence ID - * - * TODO: parameters. - */ - -#define FB_API_QUERY_SEQ_ID 10155268192741729 - -/** - * FB_API_QUERY_XMA: - * - * The query hash for the `XMAQuery`. - * - * Key mapping: - * 0: xma_id - */ -#define FB_API_QUERY_XMA 10153919431161729 - -/** - * FB_API_CONTACTS_COUNT: - * - * The maximum amount of contacts to fetch in a single request. If this - * value is set too high, HTTP request will fail. This is due to the - * request data being too large. - */ -#define FB_API_CONTACTS_COUNT 500 - -#define FACEBOOK_MESSAGE_LIMIT 100000 - -class FacebookProto; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct AsyncHttpRequest : public MTHttpRequest -{ - struct Param - { - Param(const char *p1, const char *p2) : - key(p1), val(p2) - {} - - CMStringA key, val; - }; - OBJLIST params; - - AsyncHttpRequest(); - - void CalcSig(); -}; - -AsyncHttpRequest *operator<<(AsyncHttpRequest *, const CHAR_PARAM &); -AsyncHttpRequest *operator<<(AsyncHttpRequest *, const INT_PARAM &); - -class JsonReply -{ - JSONNode *m_root = nullptr; - int m_errorCode = 0; - -public: - JsonReply(NETLIBHTTPREQUEST *); - ~JsonReply(); - - __forceinline JSONNode &data() const { return *m_root; } - __forceinline int error() const { return m_errorCode; } -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct FacebookUser -{ - FacebookUser(__int64 _p1, MCONTACT _p2, bool _p3 = false, bool _p4 = false) : - id(_p1), - hContact(_p2), - bIsChat(_p3), - bIsChatInitialized(_p4) - {} - - __int64 id; - MCONTACT hContact; - bool bIsChat; - bool bIsChatInitialized; -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct COwnMessage -{ - __int64 msgId; - int reqId; - MCONTACT hContact; - CMStringW wszText; - - COwnMessage() : - msgId(0), - reqId(0), - hContact(0) - { - } - - COwnMessage(__int64 _id, int _reqId, MCONTACT _hContact) : - msgId(_id), - reqId(_reqId), - hContact(_hContact) - { - } -}; - -class FacebookProto : public PROTO -{ - friend class CGroupchatInviteDlg; - - class FacebookImpl - { - friend class FacebookProto; - - FacebookProto &m_proto; - CTimer m_heartBeat; - - void OnHeartBeat(CTimer *) - { - m_proto.MqttPing(); - } - - FacebookImpl(FacebookProto &pro) : - m_proto(pro), - m_heartBeat(Miranda_GetSystemWindow(), (UINT_PTR)this) - { - m_heartBeat.OnEvent = Callback(this, &FacebookImpl::OnHeartBeat); - } - } m_impl; - - uint8_t *doZip(size_t cbData, const void *pData, size_t &cbRes); - uint8_t *doUnzip(size_t cbData, const void *pData, size_t &cbRes); - - void ConnectionFailed(int iErrorCode = 0); - - AsyncHttpRequest *CreateRequest(const char *szUrl, const char *szName, const char *szMethod); - AsyncHttpRequest *CreateRequestGQL(int64_t id); - NETLIBHTTPREQUEST *ExecuteRequest(AsyncHttpRequest *pReq); - - // Avatars - void __cdecl AvatarsUpdate(void *); - void GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName); - - // Group chats - void Chat_InviteUser(SESSION_INFO *si); - int Chat_KickUser(SESSION_INFO *si, const wchar_t *pwszUid); - void Chat_Leave(SESSION_INFO *si); - void Chat_SendPrivateMessage(GCHOOK *gch); - void Chat_ProcessLogMenu(SESSION_INFO *si, GCHOOK *gch); - void Chat_ProcessNickMenu(SESSION_INFO *si, GCHOOK *gch); - - // MQTT - void MqttLogin(); - - void MqttPing(); - void MqttPublish(const char *topic, const JSONNode &value); - void MqttSubscribe(const char *topic, ...); - void MqttUnsubscribe(const char *topic, ...); - - bool MqttRead(MqttMessage &payload); - bool MqttParse(const MqttMessage &payload); - void MqttSend(const MqttMessage &payload); - - void MqttQueueConnect(); - - void OnPublish(const char *str, const uint8_t *payLoad, size_t cbLen); - void OnPublishMessage(FbThriftReader &rdr); - void OnPublishPresence(FbThriftReader &rdr); - void OnPublishUtn(FbThriftReader &rdr); - - HNETLIBCONN m_mqttConn; - __int64 m_iMqttId; - int16_t m_mid; // MQTT message id - - // internal data - CMStringA m_szDeviceID; // stored, GUID that identifies this miranda's account - CMStringA m_szClientID; // stored, random alphanumeric string of 20 chars - __int64 m_uid; // stored, Facebook user id - - CMStringA m_szSyncToken; // stored, sequence query token - __int64 m_sid; // stored, Facebook sequence id - - int m_iUnread; - bool m_bOnline; - bool m_QueueCreated; - - CMStringA m_szAuthToken; // calculated - - mir_cs m_csOwnMessages; - OBJLIST arOwnMessages; - bool ExtractOwnMessage(__int64 msgId, COwnMessage &res); - - mir_cs m_csUsers; - OBJLIST m_users; - - FacebookUser* FindUser(__int64 id); - - FacebookUser *UserFromJson(const JSONNode &root, CMStringW &wszId, bool &bIsChat); - - bool CheckOwnMessage(FacebookUser *pUser, __int64 offlineId, const char *pszMsgId); - void FetchAttach(const CMStringA &mid, __int64 fbid, CMStringA &szBody); - - void OnLoggedIn(); - void OnLoggedOut(); - - FacebookUser* RefreshThread(JSONNode &n); - FacebookUser* RefreshThread(CMStringW &wszId); - bool RefreshSid(); - int RefreshToken(); - void RefreshThreads(); - int RefreshContacts(); - - FacebookUser *AddContact(const CMStringW &wszId, bool bTemp = true); - - void __cdecl ServerThread(void *); - -public: - FacebookProto(const char *proto_name, const wchar_t *username); - ~FacebookProto(); - - inline const char *ModuleName() const - { - return m_szModuleName; - } - - void OnPublishPrivateMessage(const JSONNode &json); - void OnPublishReadReceipt(const JSONNode &json); - void OnPublishSentMessage(const JSONNode &json); - void OnPublishThreadName(const JSONNode &json); - void OnPublishChatJoin(const JSONNode &json); - void OnPublishChatLeave(const JSONNode &json); - - ////////////////////////////////////////////////////////////////////////////////////// - // options - - CMOption m_wszDefaultGroup; // clist group to store contacts - CMOption m_bUseBigAvatars; // use big or small avatars by default - CMOption m_bUseGroupchats; // do we need group chats at all? - CMOption m_bHideGroupchats; // do not open chat windows on creation - CMOption m_bLoginInvisible; // login in the invisible mode - CMOption m_bKeepUnread; // do not mark incoming messages as read - CMOption m_bLoadAll; // load all contacts, not only those who have ARE_FRIENDS status - - //////////////////////////////////////////////////////////////////////////////////////// - // PROTO_INTERFACE - - void OnContactAdded(MCONTACT) override; - void OnModulesLoaded() override; - void OnShutdown() override; - - MCONTACT AddToList(int flags, PROTOSEARCHRESULT *psr) override; - INT_PTR GetCaps(int type, MCONTACT hContact) override; - int SendMsg(MCONTACT hContact, int flags, const char *pszSrc) override; - int SetStatus(int iNewStatus) override; - int UserIsTyping(MCONTACT hContact, int type) override; - - //////////////////////////////////////////////////////////////////////////////////////// - // Events - - int __cdecl OnMarkedRead(WPARAM, LPARAM); - int __cdecl OnOptionsInit(WPARAM, LPARAM); - - int __cdecl GroupchatMenuHook(WPARAM, LPARAM); - int __cdecl GroupchatEventHook(WPARAM, LPARAM); - - //////////////////////////////////////////////////////////////////////////////////////// - // Services - - INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); - INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); - INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); -}; - -typedef CProtoDlgBase CFBDlgBase; - -struct CMPlugin : public ACCPROTOPLUGIN -{ - CMPlugin(); - - int Load() override; -}; +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + + +/** + * FB_API_AHOST: + * + * The HTTP host for the Facebook API. + */ +#define FB_API_AHOST "https://api.facebook.com" + +/** + * FB_API_BHOST: + * + * The HTTP host for the Facebook BAPI. + */ +#define FB_API_BHOST "https://b-api.facebook.com" + +/** + * FB_API_GHOST: + * + * The HTTP host for the Facebook Graph API. + */ +#define FB_API_GHOST "https://graph.facebook.com" + +/** + * FB_API_WHOST: + * + * The HTTP host for the Facebook website. + */ +#define FB_API_WHOST "https://www.facebook.com" + +/** + * FB_API_FBRPC_PREFIX + * + * The fbrpc URL prefix used in links shared from the mobile app. + */ +#define FB_API_FBRPC_PREFIX "fbrpc://facebook/nativethirdparty" + +/** + * FB_API_KEY: + * + * The Facebook API key. + */ +#define FB_API_KEY "256002347743983" + +/** + * FB_API_SECRET: + * + * The Facebook API secret. + */ +#define FB_API_SECRET "374e60f8b9bb6b8cbb30f78030438895" + +/** + * FB_ORCA_AGENT + * + * The part of the user agent that looks like the official client, since the + * server started checking this. + */ + +#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/537.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" + +/** + * FB_API_AGENT: + * + * The HTTP User-Agent header. + */ +#define FB_API_AGENT "Facebook plugin / Purple / 0.9.6 " FB_ORCA_AGENT + +/** + * FB_API_MQTT_AGENT + * + * The client information string sent in the MQTT CONNECT message + */ + +#define FB_API_MQTT_AGENT FB_API_AGENT + +/** + * FB_API_URL_ATTACH: + * + * The URL for attachment URL requests. + */ +#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.getAttachment" +//#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.attachmentRedirect" + +/** + * FB_API_URL_AUTH: + * + * The URL for authentication requests. + */ +#define FB_API_URL_AUTH FB_API_BHOST "/method/auth.login" + +/** + * FB_API_URL_GQL: + * + * The URL for GraphQL requests. + */ +#define FB_API_URL_GQL FB_API_GHOST "/graphql" + +/** + * FB_API_URL_MESSAGES: + * + * The URL for linking message threads. + */ +#define FB_API_URL_MESSAGES FB_API_WHOST "/messages" + +/** + * FB_API_URL_PARTS: + * + * The URL for participant management requests. + */ +#define FB_API_URL_PARTS FB_API_GHOST "/participants" + +/** + * FB_API_URL_THREADS: + * + * The URL for thread management requests. + */ +#define FB_API_URL_THREADS FB_API_GHOST "/me/group_threads" + +/** + * FB_API_URL_TOPIC: + * + * The URL for thread topic requests. + */ +#define FB_API_URL_TOPIC FB_API_AHOST "/method/messaging.setthreadname" + +/** + * FB_API_QUERY_CONTACT: + * + * The query hash for the `UsersQuery`. + * + * Key mapping: + * 0: user_fbids + * 1: include_full_user_info + * 2: profile_pic_large_size + * 3: profile_pic_medium_size + * 4: profile_pic_small_size + */ +#define FB_API_QUERY_CONTACT 10153915107411729 + +/** + * FB_API_QUERY_CONTACTS: + * + * The query hash for the `FetchContactsFullQuery`. + * + * Key mapping: + * 0: profile_types + * 1: limit + * 2: big_img_size + * 3: huge_img_size + * 4: small_img_size + */ +#define FB_API_QUERY_CONTACTS 10154444360806729 + +/** + * FB_API_QUERY_CONTACTS_AFTER: + * + * The query hash for the `FetchContactsFullWithAfterQuery`. + * + * Key mapping: + * 0: profile_types + * 1: after + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_AFTER 10154444360816729 + + +/** + * FB_API_QUERY_CONTACTS_DELTA: + * + * The query hash for the `FetchContactsDeltaQuery`. + * + * Key mapping: + * 0: after + * 1: profile_types + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_DELTA 10154444360801729 + +/** + * FB_API_QUERY_STICKER: + * + * The query hash for the `FetchStickersWithPreviewsQuery`. + * + * Key mapping: + * 0: sticker_ids + * 1: media_type + * 2: preview_size + * 3: scaling_factor + * 4: animated_media_type + */ +#define FB_API_QUERY_STICKER 10152877994321729 + +/** + * FB_API_QUERY_THREAD: + * + * The query hash for the `ThreadQuery`. + * + * Key mapping: + * 0: thread_ids + * 1: verification_type + * 2: hash_key + * 3: small_preview_size + * 4: large_preview_size + * 5: item_count + * 6: event_count + * 7: full_screen_height + * 8: full_screen_width + * 9: medium_preview_size + * 10: fetch_users_separately + * 11: include_message_info + * 12: msg_count + * 13: include_full_user_info + * 14: profile_pic_large_size + * 15: profile_pic_medium_size + * 16: profile_pic_small_size + */ +#define FB_API_QUERY_THREAD 10153919752036729 + +/** + * FB_API_QUERY_THREADS: + * + * The query hash for the `ThreadListQuery`. + * + * Key mapping: + * 0: folder_tag (INBOX, PENDING, ARCHIVED, OTHER, UNREAD) + * 1: thread_count (result is sorted from newest to oldest when parameter is sent) + * 2: include_thread_info + * 3: verification_type + * 4: hash_key + * 5: small_preview_size + * 6: large_preview_size + * 7: item_count + * 8: event_count + * 9: full_screen_height + * 10: full_screen_width + * 11: medium_preview_size + * 12: fetch_users_separately + * 13: include_message_info + * 14: msg_count + * 15: UNKNOWN + * 16: profile_pic_large_size + * 17: profile_pic_medium_size + * 18: profile_pic_small_size + */ +#define FB_API_QUERY_THREADS 10153919752026729 + +/** + * FB_API_QUERY_SEQ_ID: + * + * A variant of ThreadListQuery with sequence ID + * + * TODO: parameters. + */ + +#define FB_API_QUERY_SEQ_ID 10155268192741729 + +/** + * FB_API_QUERY_XMA: + * + * The query hash for the `XMAQuery`. + * + * Key mapping: + * 0: xma_id + */ +#define FB_API_QUERY_XMA 10153919431161729 + +/** + * FB_API_CONTACTS_COUNT: + * + * The maximum amount of contacts to fetch in a single request. If this + * value is set too high, HTTP request will fail. This is due to the + * request data being too large. + */ +#define FB_API_CONTACTS_COUNT 500 + +#define FACEBOOK_MESSAGE_LIMIT 100000 + +class FacebookProto; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct AsyncHttpRequest : public MTHttpRequest +{ + struct Param + { + Param(const char *p1, const char *p2) : + key(p1), val(p2) + {} + + CMStringA key, val; + }; + OBJLIST params; + + AsyncHttpRequest(); + + void CalcSig(); +}; + +AsyncHttpRequest *operator<<(AsyncHttpRequest *, const CHAR_PARAM &); +AsyncHttpRequest *operator<<(AsyncHttpRequest *, const INT_PARAM &); + +class JsonReply +{ + JSONNode *m_root = nullptr; + int m_errorCode = 0; + +public: + JsonReply(NETLIBHTTPREQUEST *); + ~JsonReply(); + + __forceinline JSONNode &data() const { return *m_root; } + __forceinline int error() const { return m_errorCode; } +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct FacebookUser +{ + FacebookUser(__int64 _p1, MCONTACT _p2, bool _p3 = false, bool _p4 = false) : + id(_p1), + hContact(_p2), + bIsChat(_p3), + bIsChatInitialized(_p4) + {} + + __int64 id; + MCONTACT hContact; + bool bIsChat; + bool bIsChatInitialized; +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct COwnMessage +{ + __int64 msgId; + int reqId; + MCONTACT hContact; + CMStringW wszText; + + COwnMessage() : + msgId(0), + reqId(0), + hContact(0) + { + } + + COwnMessage(__int64 _id, int _reqId, MCONTACT _hContact) : + msgId(_id), + reqId(_reqId), + hContact(_hContact) + { + } +}; + +class FacebookProto : public PROTO +{ + friend class CGroupchatInviteDlg; + + class FacebookImpl + { + friend class FacebookProto; + + FacebookProto &m_proto; + CTimer m_heartBeat; + + void OnHeartBeat(CTimer *) + { + m_proto.MqttPing(); + } + + FacebookImpl(FacebookProto &pro) : + m_proto(pro), + m_heartBeat(Miranda_GetSystemWindow(), (UINT_PTR)this) + { + m_heartBeat.OnEvent = Callback(this, &FacebookImpl::OnHeartBeat); + } + } m_impl; + + uint8_t *doZip(size_t cbData, const void *pData, size_t &cbRes); + uint8_t *doUnzip(size_t cbData, const void *pData, size_t &cbRes); + + void ConnectionFailed(int iErrorCode = 0); + + AsyncHttpRequest *CreateRequest(const char *szUrl, const char *szName, const char *szMethod); + AsyncHttpRequest *CreateRequestGQL(int64_t id); + NETLIBHTTPREQUEST *ExecuteRequest(AsyncHttpRequest *pReq); + + // Avatars + void __cdecl AvatarsUpdate(void *); + void GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName); + + // Group chats + void Chat_InviteUser(SESSION_INFO *si); + int Chat_KickUser(SESSION_INFO *si, const wchar_t *pwszUid); + void Chat_Leave(SESSION_INFO *si); + void Chat_SendPrivateMessage(GCHOOK *gch); + void Chat_ProcessLogMenu(SESSION_INFO *si, GCHOOK *gch); + void Chat_ProcessNickMenu(SESSION_INFO *si, GCHOOK *gch); + + // MQTT + void MqttLogin(); + + void MqttPing(); + void MqttPublish(const char *topic, const JSONNode &value); + void MqttSubscribe(const char *topic, ...); + void MqttUnsubscribe(const char *topic, ...); + + bool MqttRead(MqttMessage &payload); + bool MqttParse(const MqttMessage &payload); + void MqttSend(const MqttMessage &payload); + + void MqttQueueConnect(); + + void OnPublish(const char *str, const uint8_t *payLoad, size_t cbLen); + void OnPublishMessage(FbThriftReader &rdr); + void OnPublishPresence(FbThriftReader &rdr); + void OnPublishUtn(FbThriftReader &rdr); + + HNETLIBCONN m_mqttConn; + __int64 m_iMqttId; + int16_t m_mid; // MQTT message id + + // internal data + CMStringA m_szDeviceID; // stored, GUID that identifies this miranda's account + CMStringA m_szClientID; // stored, random alphanumeric string of 20 chars + __int64 m_uid; // stored, Facebook user id + + CMStringA m_szSyncToken; // stored, sequence query token + __int64 m_sid; // stored, Facebook sequence id + + int m_iUnread; + bool m_bOnline; + bool m_QueueCreated; + + CMStringA m_szAuthToken; // calculated + + mir_cs m_csOwnMessages; + OBJLIST arOwnMessages; + bool ExtractOwnMessage(__int64 msgId, COwnMessage &res); + + mir_cs m_csUsers; + OBJLIST m_users; + + FacebookUser* FindUser(__int64 id); + + FacebookUser *UserFromJson(const JSONNode &root, CMStringW &wszId, bool &bIsChat); + + bool CheckOwnMessage(FacebookUser *pUser, __int64 offlineId, const char *pszMsgId); + void FetchAttach(const CMStringA &mid, __int64 fbid, CMStringA &szBody); + + void OnLoggedIn(); + void OnLoggedOut(); + + FacebookUser* RefreshThread(JSONNode &n); + FacebookUser* RefreshThread(CMStringW &wszId); + bool RefreshSid(); + int RefreshToken(); + void RefreshThreads(); + int RefreshContacts(); + + FacebookUser *AddContact(const CMStringW &wszId, bool bTemp = true); + + void __cdecl ServerThread(void *); + +public: + FacebookProto(const char *proto_name, const wchar_t *username); + ~FacebookProto(); + + inline const char *ModuleName() const + { + return m_szModuleName; + } + + void OnPublishPrivateMessage(const JSONNode &json); + void OnPublishReadReceipt(const JSONNode &json); + void OnPublishSentMessage(const JSONNode &json); + void OnPublishThreadName(const JSONNode &json); + void OnPublishChatJoin(const JSONNode &json); + void OnPublishChatLeave(const JSONNode &json); + + ////////////////////////////////////////////////////////////////////////////////////// + // options + + CMOption m_wszDefaultGroup; // clist group to store contacts + CMOption m_bUseBigAvatars; // use big or small avatars by default + CMOption m_bUseGroupchats; // do we need group chats at all? + CMOption m_bHideGroupchats; // do not open chat windows on creation + CMOption m_bLoginInvisible; // login in the invisible mode + CMOption m_bKeepUnread; // do not mark incoming messages as read + CMOption m_bLoadAll; // load all contacts, not only those who have ARE_FRIENDS status + + //////////////////////////////////////////////////////////////////////////////////////// + // PROTO_INTERFACE + + void OnContactAdded(MCONTACT) override; + void OnModulesLoaded() override; + void OnShutdown() override; + + MCONTACT AddToList(int flags, PROTOSEARCHRESULT *psr) override; + INT_PTR GetCaps(int type, MCONTACT hContact) override; + int SendMsg(MCONTACT hContact, int flags, const char *pszSrc) override; + int SetStatus(int iNewStatus) override; + int UserIsTyping(MCONTACT hContact, int type) override; + + //////////////////////////////////////////////////////////////////////////////////////// + // Events + + int __cdecl OnMarkedRead(WPARAM, LPARAM); + int __cdecl OnOptionsInit(WPARAM, LPARAM); + + int __cdecl GroupchatMenuHook(WPARAM, LPARAM); + int __cdecl GroupchatEventHook(WPARAM, LPARAM); + + //////////////////////////////////////////////////////////////////////////////////////// + // Services + + INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); + INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); +}; + +typedef CProtoDlgBase CFBDlgBase; + +struct CMPlugin : public ACCPROTOPLUGIN +{ + CMPlugin(); + + int Load() override; +}; diff --git a/protocols/Facebook/src/server.cpp b/protocols/Facebook/src/server.cpp index b35cc5461a..39ba67f83d 100644 --- a/protocols/Facebook/src/server.cpp +++ b/protocols/Facebook/src/server.cpp @@ -1,1051 +1,1051 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -void FacebookProto::ConnectionFailed(int iErrorCode) -{ - if (iErrorCode) { - POPUPDATAW popup; - popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); - wcscpy_s(popup.lpwzContactName, m_tszUserName); - mir_snwprintf(popup.lpwzText, TranslateT("Connection failed with error code %d"), iErrorCode); - PUAddPopupW(&popup); - } - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_FAILED, (HANDLE)m_iStatus, m_iDesiredStatus); - - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); - - OnShutdown(); -} - -bool FacebookProto::ExtractOwnMessage(__int64 msgId, COwnMessage &res) -{ - mir_cslock lck(m_csOwnMessages); - for (auto &it : arOwnMessages) - if (it->msgId == msgId) { - res = *it; - arOwnMessages.removeItem(&it); - return true; - } - - return false; -} - -void FacebookProto::OnLoggedIn() -{ - m_mid = 0; - - JSONNode root; root << BOOL_PARAM("foreground", true) << INT_PARAM("keepalive_timeout", 60); - MqttPublish("/foreground_state", root); - - MqttSubscribe("/inbox", "/mercury", "/messaging_events", "/orca_presence", "/orca_typing_notifications", "/pp", "/t_ms", "/t_p", "/t_rtc", "/webrtc", "/webrtc_response", 0); - MqttUnsubscribe("/orca_message_notifications", 0); - - // if sequence is not initialized, request SID from the server - if (m_sid == 0) { - if (!RefreshSid()) { - ConnectionFailed(); - return; - } - } - - // point of no return; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); - m_iStatus = m_iDesiredStatus; - m_bOnline = true; - m_impl.m_heartBeat.Start(60000); - - // connect message queue - MqttQueueConnect(); - - // request message threads if needed - if (m_bUseGroupchats) - RefreshThreads(); -} - -void FacebookProto::OnLoggedOut() -{ - m_impl.m_heartBeat.Stop(); - m_bOnline = false; - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - - setAllContactStatuses(ID_STATUS_OFFLINE, false); -} - -FacebookUser* FacebookProto::AddContact(const CMStringW &wszId, bool bTemp) -{ - MCONTACT hContact = db_add_contact(); - setWString(hContact, DBKEY_ID, wszId); - Proto_AddToContact(hContact, m_szModuleName); - Clist_SetGroup(hContact, m_wszDefaultGroup); - if (bTemp) - Contact::RemoveFromList(hContact); - - return FindUser(_wtoi64(wszId)); -} - -FacebookUser* FacebookProto::FindUser(__int64 id) -{ - mir_cslock lck(m_csUsers); - return m_users.find((FacebookUser *)&id); -} - -FacebookUser* FacebookProto::UserFromJson(const JSONNode &root, CMStringW &wszUserId, bool &bIsChat) -{ - bIsChat = false; - wszUserId = root["threadKey"]["otherUserFbId"].as_mstring(); - if (wszUserId.IsEmpty()) { - // if only thread id is present, it must be a group chat - wszUserId = root["threadKey"]["threadFbId"].as_mstring(); - bIsChat = true; - } - - auto *pUser = FindUser(_wtoi64(wszUserId)); - if (pUser == nullptr) { - debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); - return nullptr; - } - - if (pUser->bIsChat != bIsChat) { - debugLogA("Wrong chat user: %d vs %d for user %lld, ignored", pUser->bIsChat, bIsChat, pUser->id); - return nullptr; - } - - return pUser; -} - -int FacebookProto::RefreshContacts() -{ - CMStringA szCursor; - bool bNeedUpdate = false; - - while (true) { - JSONNode root; root << CHAR_PARAM("0", "user"); - - AsyncHttpRequest *pReq; - if (szCursor.IsEmpty()) { - pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS); - root << INT_PARAM("1", FB_API_CONTACTS_COUNT); - } - else { - pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS_AFTER); - root << CHAR_PARAM("1", szCursor) << INT_PARAM("2", FB_API_CONTACTS_COUNT); - } - pReq << CHAR_PARAM("query_params", root.write().c_str()); - pReq->flags |= NLHRF_NODUMPSEND; - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (int iErrorCode = reply.error()) - return iErrorCode; // unknown error - - bool bLoadAll = m_bLoadAll; - auto &data = reply.data()["viewer"]["messenger_contacts"]; - - for (auto &it : data["nodes"]) { - auto &n = it["represented_profile"]; - CMStringW wszId(n["id"].as_mstring()); - __int64 id = _wtoi64(wszId); - - MCONTACT hContact; - if (id != m_uid) { - bool bIsFriend = bLoadAll || n["friendship_status"].as_mstring() == L"ARE_FRIENDS"; - - auto *pUser = FindUser(id); - if (pUser == nullptr) { - if (!bIsFriend) - continue; - pUser = AddContact(wszId, false); - } - else if (!bIsFriend) - Contact::RemoveFromList(pUser->hContact); // adios! - - hContact = pUser->hContact; - } - else hContact = 0; - - if (auto &nName = it["structured_name"]) { - CMStringW wszName(nName["text"].as_mstring()); - setWString(hContact, DBKEY_NICK, wszName); - for (auto &nn : nName["parts"]) { - CMStringW wszPart(nn["part"].as_mstring()); - int offset = nn["offset"].as_int(), length = nn["length"].as_int(); - if (wszPart == L"first") - setWString(hContact, "FirstName", wszName.Mid(offset, length)); - else if (wszPart == L"last") - setWString(hContact, "LastName", wszName.Mid(offset, length)); - } - } - - if (auto &nBirth = n["birthdate"]) { - setDword(hContact, "BirthDay", nBirth["day"].as_int()); - setDword(hContact, "BirthMonth", nBirth["month"].as_int()); - } - - if (auto &nCity = n["current_city"]) - setWString(hContact, "City", nCity["name"].as_mstring()); - - if (auto &nAva = it[(m_bUseBigAvatars) ? "hugePictureUrl" : "bigPictureUrl"]) { - CMStringW wszOldUrl(getMStringW(hContact, DBKEY_AVATAR)), wszNewUrl(nAva["uri"].as_mstring()); - if (wszOldUrl != wszNewUrl) { - bNeedUpdate = true; - setByte(hContact, "UpdateNeeded", 1); - setWString(hContact, DBKEY_AVATAR, wszNewUrl); - } - } - } - - if (!data["page_info"]["has_next_page"].as_bool()) { - debugLogA("Got no next page, exiting", szCursor.c_str()); - break; - } - - szCursor = data["page_info"]["end_cursor"].as_mstring(); - debugLogA("Got cursor: %s", szCursor.c_str()); - } - - if (bNeedUpdate) - ForkThread(&FacebookProto::AvatarsUpdate); - return 0; -} - -bool FacebookProto::RefreshSid() -{ - auto *pReq = CreateRequestGQL(FB_API_QUERY_SEQ_ID); - JSONNode root; root << CHAR_PARAM("1", "0"); - pReq << CHAR_PARAM("query_params", root.write().c_str()); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (reply.error()) - return false; - - auto &n = reply.data()["viewer"]["message_threads"]; - CMStringW wszSid(n["sync_sequence_id"].as_mstring()); - setWString(DBKEY_SID, wszSid); - m_sid = _wtoi64(wszSid); - m_iUnread = n["unread_count"].as_int(); - return true; -} - -FacebookUser* FacebookProto::RefreshThread(JSONNode &n) -{ - if (!n["is_group_thread"].as_bool()) - return nullptr; - - CMStringW chatId(n["thread_key"]["thread_fbid"].as_mstring()); - CMStringW name(n["name"].as_mstring()); - if (name.IsEmpty()) { - for (auto &u : n["all_participants"]["nodes"]) { - auto &ur = u["messaging_actor"]; - CMStringW userId(ur["id"].as_mstring()); - if (_wtoi64(userId) == m_uid) - continue; - - if (!name.IsEmpty()) - name.Append(L", "); - name += ur["name"].as_mstring(); - } - - if (name.GetLength() > 128) { - name.Truncate(125); - name.Append(L"..."); - } - } - - auto *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, chatId, name); - if (si == nullptr) - return nullptr; - - setWString(si->hContact, DBKEY_ID, chatId); - Chat_AddGroup(si, TranslateT("Participant")); - - for (auto &u : n["all_participants"]["nodes"]) { - auto &ur = u["messaging_actor"]; - CMStringW userId(ur["id"].as_mstring()); - CMStringW userName(ur["name"].as_mstring()); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; - gce.pszID.w = chatId; - gce.pszUID.w = userId; - gce.pszNick.w = userName; - gce.bIsMe = _wtoi64(userId) == m_uid; - gce.time = time(0); - Chat_Event(&gce); - } - - Chat_Control(m_szModuleName, chatId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); - Chat_Control(m_szModuleName, chatId, SESSION_ONLINE); - - __int64 userId = _wtoi64(chatId); - auto *pUser = FindUser(userId); - - if (pUser == nullptr) { - mir_cslock lck(m_csUsers); - pUser = new FacebookUser(userId, si->hContact, true, true); - m_users.insert(pUser); - } - else { - pUser->hContact = si->hContact; - pUser->bIsChatInitialized = true; - } - - return pUser; -} - -FacebookUser* FacebookProto::RefreshThread(CMStringW &wszId) -{ - auto *pReq = CreateRequestGQL(FB_API_QUERY_THREAD); - pReq << WCHAR_PARAM("query_params", CMStringW(FORMAT, L"{\"0\":[\"%s\"], \"12\":0, \"13\":\"false\"}", wszId.c_str())); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (!reply.error()) { - auto &root = reply.data(); - for (auto &n : root) - return RefreshThread(n); - } - - return nullptr; -} - -void FacebookProto::RefreshThreads() -{ - int threadsLimit = 40; - - auto * pReq = CreateRequestGQL(FB_API_QUERY_THREADS); - JSONNode json; json << INT_PARAM("1", threadsLimit) << CHAR_PARAM("2", "true") << CHAR_PARAM("12", "false") << CHAR_PARAM("13", "false"); - pReq << CHAR_PARAM("query_params", json.write().c_str()); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (!reply.error()) { - auto &root = reply.data()["viewer"]["message_threads"]; - - for (auto &n : root["nodes"]) { - if (n["is_group_thread"].as_bool() && n["is_viewer_subscribed"].as_bool() && !n["has_viewer_archived"].as_bool()) - RefreshThread(n); - } - - // TODO: save timestamp of last message/action/... into DB - // TODO: lower threadsLimit to 10, load next pages if timestamp of last message is higher than timestamp in DB - } -} - -int FacebookProto::RefreshToken() -{ - auto *pReq = CreateRequest(FB_API_URL_AUTH, "authenticate", "auth.login"); - pReq->flags |= NLHRF_NODUMP; - pReq << CHAR_PARAM("email", getMStringA(DBKEY_LOGIN)); - pReq << CHAR_PARAM("password", getMStringA(DBKEY_PASS)); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (reply.error()) - return reply.error(); - - m_szAuthToken = reply.data()["access_token"].as_mstring(); - setString(DBKEY_TOKEN, m_szAuthToken); - - CMStringA m_szUid = reply.data()["uid"].as_mstring(); - setString(DBKEY_ID, m_szUid); - m_uid = _atoi64(m_szUid); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void FacebookProto::ServerThread(void *) -{ - m_QueueCreated = false; - -LBL_Begin: - m_szAuthToken = getMStringA(DBKEY_TOKEN); - if (m_szAuthToken.IsEmpty()) { - if (int iErrorCode = RefreshToken()) { - ConnectionFailed(iErrorCode); - return; - } - } - - int iErrorCode = RefreshContacts(); - if (iErrorCode != 0) { - if (iErrorCode == 401) { - delSetting(DBKEY_TOKEN); - goto LBL_Begin; - } - - ConnectionFailed(iErrorCode); - return; - } - - // connect to MQTT server - m_mqttConn = Netlib_OpenConnection(m_hNetlibUser, "mqtt.facebook.com", 443, 0, NLOCF_SSL); - if (m_mqttConn == nullptr) { - debugLogA("connection failed, exiting"); - ConnectionFailed(); - return; - } - - // send initial packet - MqttLogin(); - - while (!Miranda_IsTerminated()) { - NETLIBSELECT nls = {}; - nls.hReadConns[0] = m_mqttConn; - nls.dwTimeout = 1000; - int ret = Netlib_Select(&nls); - if (ret == SOCKET_ERROR) { - debugLogA("Netlib_Recv() failed, error=%d", WSAGetLastError()); - break; - } - - // no data, continue waiting - if (ret == 0) - continue; - - MqttMessage msg; - if (!MqttRead(msg)) { - debugLogA("MqttRead() failed"); - break; - } - - if (!MqttParse(msg)) { - debugLogA("MqttParse() failed"); - break; - } - } - - debugLogA("exiting ServerThread"); - - Netlib_CloseHandle(m_mqttConn); - m_mqttConn = nullptr; - - OnLoggedOut(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int FacebookProto::OnMarkedRead(WPARAM, LPARAM hDbEvent) -{ - MCONTACT hContact = db_event_getContact(hDbEvent); - if (!hContact) - return 0; - - // filter out only events of my protocol - const char *szProto = Proto_GetBaseAccountName(hContact); - if (mir_strcmp(szProto, m_szModuleName)) - return 0; - - if (m_bKeepUnread) - return 0; - - JSONNode root; root << BOOL_PARAM("state", true) << INT_PARAM("syncSeqId", m_sid) << CHAR_PARAM("mark", "read"); - if (isChatRoom(hContact)) - root << CHAR_PARAM("threadFbId", getMStringA(hContact, DBKEY_ID)); - else - root << CHAR_PARAM("otherUserFbId", getMStringA(hContact, DBKEY_ID)); - MqttPublish("/mark_thread", root); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void FacebookProto::OnPublish(const char *topic, const uint8_t *p, size_t cbLen) -{ - FbThriftReader rdr; - - // that might be a zipped buffer - if (cbLen >= 2) { - size_t dataSize; - void *pData = doUnzip(cbLen, p, dataSize); - if (pData != nullptr) { - debugLogA("UNZIP: %d bytes unzipped ok", dataSize); - Netlib_Dump(m_mqttConn, pData, dataSize, false, 0); - rdr.reset(dataSize, pData); - mir_free(pData); - } - } - - if (rdr.size() == 0) - rdr.reset(cbLen, (void*)p); - - if (!strcmp(topic, "/t_p")) - OnPublishPresence(rdr); - else if (!strcmp(topic, "/t_ms")) - OnPublishMessage(rdr); - else if (!strcmp(topic, "/orca_typing_notifications")) - OnPublishUtn(rdr); -} - -void FacebookProto::OnPublishPresence(FbThriftReader &rdr) -{ - char *str = nullptr; - rdr.readStr(str); - mir_free(str); - - bool bVal; - uint8_t fieldType; - uint16_t fieldId; - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_BOOL); - assert(fieldId == 1); - rdr.readBool(bVal); - - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_LIST); - assert(fieldId == 1); - - uint32_t size; - rdr.readList(fieldType, size); - assert(fieldType == FB_THRIFT_TYPE_STRUCT); - - debugLogA("Received list of presences: %d records", size); - for (uint32_t i = 0; i < size; i++) { - uint64_t userId, timestamp, voipBits; - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_I64); - assert(fieldId == 1); - rdr.readInt64(userId); - - uint32_t u32; - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_I32); - assert(fieldId == 1); - rdr.readInt32(u32); - - auto *pUser = FindUser(userId); - if (pUser == nullptr) - debugLogA("Skipping presence from unknown user %lld", userId); - else { - debugLogA("Presence from user %lld => %d", userId, u32); - setWord(pUser->hContact, "Status", (u32 != 0) ? ID_STATUS_ONLINE : ID_STATUS_OFFLINE); - } - - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_I64); - assert(fieldId == 1 || fieldId == 3 || fieldId == 4); - rdr.readInt64(timestamp); - - while (!rdr.isStop()) { - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_I64 || fieldType == FB_THRIFT_TYPE_I16 || fieldType == FB_THRIFT_TYPE_I32); - rdr.readIntV(voipBits); - } - - rdr.readByte(fieldType); - assert(fieldType == FB_THRIFT_TYPE_STOP); - } - - rdr.readByte(fieldType); - assert(fieldType == FB_THRIFT_TYPE_STOP); -} - -void FacebookProto::OnPublishUtn(FbThriftReader &rdr) -{ - JSONNode root = JSONNode::parse(rdr.rest()); - auto *pUser = FindUser(_wtoi64(root["sender_fbid"].as_mstring())); - if (pUser != nullptr) { - int length = (root["state"].as_int() == 0) ? PROTOTYPE_CONTACTTYPING_OFF : 60; - CallService(MS_PROTO_CONTACTISTYPING, pUser->hContact, length); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -struct -{ - const char *messageType; - void (FacebookProto:: *pFunc)(const JSONNode &); -} -static MsgHandlers[] = -{ - { "deltaNewMessage", &FacebookProto::OnPublishPrivateMessage }, - { "deltaThreadName", &FacebookProto::OnPublishThreadName }, - { "deltaSentMessage", &FacebookProto::OnPublishSentMessage }, - { "deltaReadReceipt", &FacebookProto::OnPublishReadReceipt }, - { "deltaParticipantsAddedToGroupThread", &FacebookProto::OnPublishChatJoin }, - { "deltaParticipantLeftGroupThread", &FacebookProto::OnPublishChatLeave }, -}; - -void FacebookProto::OnPublishMessage(FbThriftReader &rdr) -{ - uint8_t stop; - if (rdr.isStop()) - rdr.readByte(stop); - else { - uint8_t type; - uint16_t id; - rdr.readField(type, id); - _ASSERT(type == FB_THRIFT_TYPE_STRING); - _ASSERT(id == 1 || id == 2); - - char *szShit = nullptr; - rdr.readStr(szShit); - mir_free(szShit); - - rdr.readByte(stop); - } - - CMStringA szJson(rdr.rest()); - debugLogA("MS: <%s>", szJson.c_str()); - JSONNode root = JSONNode::parse(szJson); - - CMStringA errorCode = root["errorCode"].as_mstring(); - if (!errorCode.IsEmpty()) { - if (!m_QueueCreated && (errorCode == "ERROR_QUEUE_OVERFLOW" || errorCode == "ERROR_QUEUE_NOT_FOUND" || errorCode == "ERROR_QUEUE_LOST" || errorCode == "ERROR_QUEUE_EXCEEDS_MAX_DELTAS")) { - m_QueueCreated = true; // prevent queue creation request from being sent twice - delSetting(DBKEY_SYNC_TOKEN); m_szSyncToken.Empty(); - delSetting(DBKEY_SID); m_sid = 0; - if (!RefreshSid()) { - ConnectionFailed(); - return; - } - - MqttQueueConnect(); - } - } - - CMStringW str = root["lastIssuedSeqId"].as_mstring(); - if (!str.IsEmpty()) { - setWString(DBKEY_SID, str); - m_sid = _wtoi64(str); - } - - str = root["syncToken"].as_mstring(); - if (!str.IsEmpty()) { - m_szSyncToken = str; - setString(DBKEY_SYNC_TOKEN, m_szSyncToken); - return; - } - - for (auto &it : root["deltas"]) { - for (auto &handler : MsgHandlers) { - auto &json = it[handler.messageType]; - if (json) { - (this->*(handler.pFunc))(json); - break; - } - } - } -} - -// new message arrived -struct -{ - const char *szTag, *szClientVersion; -} -static facebookClients[] = -{ - { "source:titan:web", "Facebook (website)" }, - { "app_id:256002347743983", "Facebook (Facebook Messenger)" } -}; - -void FacebookProto::FetchAttach(const CMStringA &mid, __int64 fbid, CMStringA &szBody) -{ - for (int iAttempt = 0; iAttempt < 5; iAttempt++) { - auto *pReq = CreateRequest(FB_API_URL_ATTACH, "getAttachment", "messaging.getAttachment"); - pReq << CHAR_PARAM("mid", mid) << INT64_PARAM("aid", fbid); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - switch (reply.error()) { - case 0: - { - std::string uri = reply.data()["redirect_uri"].as_string(); - std::string type = reply.data()["content_type"].as_string(); - if (!uri.empty()) - szBody.AppendFormat("\r\n%s: %s", TranslateU(type.find("image/") != -1 ? "Picture attachment" : "File attachment"), uri.c_str()); - } - return; - - case 509: // attachment isn't ready, wait a bit and retry - ::Sleep(100); - continue; - - default: // shit happened, exiting - return; - } - } -} - -void FacebookProto::OnPublishPrivateMessage(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - if (!offlineId) { - debugLogA("We care about messages only, event skipped"); - return; - } - - bool bIsChat; - CMStringW wszUserId; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - - if (!bIsChat && pUser == nullptr) - pUser = AddContact(wszUserId, true); - else if (bIsChat && (pUser == nullptr || !pUser->bIsChatInitialized)) // chat room does not exists or is not initialized - pUser = RefreshThread(wszUserId); - - if (pUser == nullptr) { - debugLogA("User not found and adding failed, event skipped"); - return; - } - - for (auto &it : metadata["tags"]) { - auto *szTagName = it.name(); - for (auto &cli : facebookClients) { - if (!mir_strcmp(szTagName, cli.szTag)) { - setString(pUser->hContact, "MirVer", cli.szClientVersion); - break; - } - } - } - - CMStringA szId(metadata["messageId"].as_mstring()); - if (CheckOwnMessage(pUser, offlineId, szId)) { - debugLogA("own message <%s> skipped", szId.c_str()); - return; - } - - if (db_event_getById(m_szModuleName, szId)) { - debugLogA("this message <%s> was already stored, exiting", szId.c_str()); - return; - } - - // parse message body - CMStringA szBody(root["body"].as_string().c_str()); - if (szBody.IsEmpty()) - szBody = metadata["snippet"].as_string().c_str(); - - // parse stickers - CMStringA stickerId = root["stickerId"].as_mstring(); - if (!stickerId.IsEmpty()) { - if (ServiceExists(MS_SMILEYADD_LOADCONTACTSMILEYS)) { - CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); - CreateDirectoryTreeW(wszPath); - - bool bSuccess = false; - CMStringW wszFileName(FORMAT, L"%s\\STK{%S}.png", wszPath.c_str(), stickerId.c_str()); - uint32_t dwAttrib = GetFileAttributesW(wszFileName); - if (dwAttrib == INVALID_FILE_ATTRIBUTES) { - wszFileName.Format(L"%s\\STK{%S}.webp", wszPath.c_str(), stickerId.c_str()); - dwAttrib = GetFileAttributesW(wszFileName); - } - - // new sticker - if (dwAttrib == INVALID_FILE_ATTRIBUTES) { - auto *pReq = CreateRequestGQL(FB_API_QUERY_STICKER); - pReq << CHAR_PARAM("query_params", CMStringA(FORMAT, "{\"0\":[\"%s\"]}", stickerId.c_str())); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (!reply.error()) { - for (auto &sticker : reply.data()) { - // std::string szUrl = sticker["animated_image"]["uri"].as_string(); - // if (szUrl.empty()) - // szUrl = sticker["thread_image"]["uri"].as_string(); - // else - // wszFileName.Format(L"%s\\STK{%S}.webp", wszPath.c_str(), stickerId.c_str()); - std::string szUrl = sticker["thread_image"]["uri"].as_string(); - - NETLIBHTTPREQUEST req = {}; - req.cbSize = sizeof(req); - req.flags = NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; - req.requestType = REQUEST_GET; - req.szUrl = (char*)szUrl.c_str(); - - NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(m_hNetlibUser, &req); - if (pReply != nullptr && pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { - bSuccess = true; - FILE *out = _wfopen(wszFileName, L"wb"); - fwrite(pReply->pData, 1, pReply->dataLength, out); - fclose(out); - } - } - } - } - else bSuccess = true; - - if (bSuccess) { - if (!szBody.IsEmpty()) - szBody += "\r\n"; - szBody += "STK{" + stickerId + "}"; - - SMADD_CONT cont = { 1, m_szModuleName, wszFileName }; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); - } - else szBody += TranslateU("Sticker received"); - } - else szBody += TranslateU("SmileyAdd plugin required to support stickers"); - } - - // parse attachments (links, files, ...) - for (auto &it : root["attachments"]) { - // madness... json inside json - CMStringA szJson(it["xmaGraphQL"].as_mstring()); - if (szJson.IsEmpty()) { - __int64 fbid = _wtoi64(it["fbid"].as_mstring()); - if (fbid == 0) { - debugLogA("Neither a GQL nor an inline attachment, nothing to do"); - continue; - } - - // inline attachment, request its description - FetchAttach(szId, fbid, szBody); - continue; - } - - JSONROOT nBody(szJson); - if (!nBody) - continue; - - const JSONNode &attach = (*nBody).at((json_index_t)0)["story_attachment"]; - szBody += "\r\n-----------------------------------"; - - CMStringA str = attach["url"].as_mstring(); - if (!str.IsEmpty()) { - if (str.Left(8) == "fbrpc://") { - int iStart = str.Find("target_url="); - if (iStart != 0) { - CMStringA tmp; - - iStart += 11; - int iEnd = str.Find("&", iStart); - if (iEnd != -1) - tmp = str.Mid(iStart, iEnd - iStart); - else - tmp = str.Right(iStart); - - mir_urlDecode(tmp.GetBuffer()); - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("URL"), tmp.c_str()); - } - } - else szBody.AppendFormat("\r\n\t%s: %s", TranslateU("URL"), str.c_str()); - } - - str = attach["title"].as_string().c_str(); - if (!str.IsEmpty()) - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Title"), str.c_str()); - - str = attach["source"]["text"].as_string().c_str(); - if (!str.IsEmpty()) - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Source"), str.c_str()); - - str = attach["description"]["text"].as_string().c_str(); - if (!str.IsEmpty()) - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Description"), str.c_str()); - - str = attach["media"]["playable_url"].as_string().c_str(); - if (!str.IsEmpty()) - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Playable media"), str.c_str()); - } - - // if that's a group chat, send it to the room - CMStringW wszActorFbId(metadata["actorFbId"].as_mstring()); - __int64 actorFbId = _wtoi64(wszActorFbId); - - if (pUser->bIsChat) { - szBody.Replace("%", "%%"); - ptrW wszText(mir_utf8decodeW(szBody)); - - // TODO: GC_EVENT_JOIN for chat participants which are missing (for example added later during group chat) - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; - gce.pszID.w = wszUserId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszActorFbId; - gce.pszText.w = wszText; - gce.time = time(0); - gce.bIsMe = actorFbId == m_uid; - Chat_Event(&gce); - - debugLogA("New channel %lld message from %S: %s", pUser->id, gce.pszUID.w, gce.pszText.w); - } - else { // otherwise store a private message - PROTORECVEVENT pre = {}; - pre.timestamp = uint32_t(_wtoi64(metadata["timestamp"].as_mstring()) / 1000); - pre.szMessage = (char *)szBody.c_str(); - pre.szMsgId = (char *)szId.c_str(); - - if (m_uid == actorFbId) - pre.flags |= PREF_SENT; - - ProtoChainRecvMsg(pUser->hContact, &pre); - } -} - -// changing thread name -void FacebookProto::OnPublishThreadName(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - if (!offlineId) { - debugLogA("We care about messages only, event skipped"); - return; - } - - bool bIsChat; - CMStringW wszUserId; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - if (!bIsChat || pUser == nullptr) - return; - - CMStringW wszTitle = root["name"].as_mstring(); - if (!wszTitle.IsEmpty()) - setWString(pUser->hContact, DBKEY_NICK, wszTitle); - else - delSetting(pUser->hContact, DBKEY_NICK); -} - -// user joined chat -void FacebookProto::OnPublishChatJoin(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - if (!offlineId) { - debugLogA("We care about messages only, event skipped"); - return; - } - - bool bIsChat; - CMStringW wszUserId; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - if (!bIsChat || pUser == nullptr) - return; - - CMStringW wszText(metadata["adminText"].as_mstring()); - for (auto &it : root["addedParticipants"]) { - CMStringW wszNick(it["fullName"].as_mstring()), wszId(it["userFbId"].as_mstring()); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; - gce.pszID.w = wszUserId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszNick.w = wszNick; - gce.pszUID.w = wszId; - gce.pszText.w = wszText; - gce.time = time(0); - gce.bIsMe = _wtoi64(wszId) == m_uid; - Chat_Event(&gce); - } -} - -// user left chat -void FacebookProto::OnPublishChatLeave(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - if (!offlineId) { - debugLogA("We care about messages only, event skipped"); - return; - } - - bool bIsChat; - CMStringW wszUserId; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - if (!bIsChat || pUser == nullptr) - return; - - CMStringW wszText(metadata["adminText"].as_mstring()), wszId(root["leftParticipantFbId"].as_mstring()); - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_PART }; - gce.pszID.w = wszUserId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszId; - gce.pszText.w = wszText; - gce.time = time(0); - gce.bIsMe = _wtoi64(wszId) == m_uid; - Chat_Event(&gce); -} - -// read notification -void FacebookProto::OnPublishReadReceipt(const JSONNode &root) -{ - CMStringW wszUserId; - bool bIsChat; - auto *pUser = UserFromJson(root, wszUserId, bIsChat); - if (pUser == nullptr) { - debugLogA("Message from unknown contact %S, ignored", wszUserId.c_str()); - return; - } - - uint32_t timestamp = _wtoi64(root["watermarkTimestampMs"].as_mstring()); - for (MEVENT ev = db_event_firstUnread(pUser->hContact); ev != 0; ev = db_event_next(pUser->hContact, ev)) { - DBEVENTINFO dbei = {}; - if (db_event_get(ev, &dbei)) - continue; - - if (dbei.timestamp > timestamp) - break; - - if (!dbei.markedRead()) - db_event_markRead(pUser->hContact, ev); - } -} - -// my own message was sent -bool FacebookProto::CheckOwnMessage(FacebookUser *pUser, __int64 offlineId, const char *pszMsgId) -{ - COwnMessage tmp; - if (!ExtractOwnMessage(offlineId, tmp)) - return false; - - if (pUser->bIsChat) { - CMStringW wszId(FORMAT, L"%lld", m_uid); - tmp.wszText.Replace(L"%", L"%%"); - - wchar_t userId[100]; - _i64tow_s(pUser->id, userId, _countof(userId), 10); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; - gce.pszID.w = userId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszId; - gce.pszText.w = tmp.wszText; - gce.time = time(0); - gce.bIsMe = true; - Chat_Event(&gce); - } - else ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)tmp.reqId, (LPARAM)pszMsgId); - - return true; -} - -void FacebookProto::OnPublishSentMessage(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - - CMStringW wszUserId; - bool bIsChat; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - if (pUser == nullptr) { - debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); - return; - } - - std::string szMsgId(metadata["messageId"].as_string()); - CheckOwnMessage(pUser, offlineId, szMsgId.c_str()); -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +void FacebookProto::ConnectionFailed(int iErrorCode) +{ + if (iErrorCode) { + POPUPDATAW popup; + popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); + wcscpy_s(popup.lpwzContactName, m_tszUserName); + mir_snwprintf(popup.lpwzText, TranslateT("Connection failed with error code %d"), iErrorCode); + PUAddPopupW(&popup); + } + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_FAILED, (HANDLE)m_iStatus, m_iDesiredStatus); + + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); + + OnShutdown(); +} + +bool FacebookProto::ExtractOwnMessage(__int64 msgId, COwnMessage &res) +{ + mir_cslock lck(m_csOwnMessages); + for (auto &it : arOwnMessages) + if (it->msgId == msgId) { + res = *it; + arOwnMessages.removeItem(&it); + return true; + } + + return false; +} + +void FacebookProto::OnLoggedIn() +{ + m_mid = 0; + + JSONNode root; root << BOOL_PARAM("foreground", true) << INT_PARAM("keepalive_timeout", 60); + MqttPublish("/foreground_state", root); + + MqttSubscribe("/inbox", "/mercury", "/messaging_events", "/orca_presence", "/orca_typing_notifications", "/pp", "/t_ms", "/t_p", "/t_rtc", "/webrtc", "/webrtc_response", 0); + MqttUnsubscribe("/orca_message_notifications", 0); + + // if sequence is not initialized, request SID from the server + if (m_sid == 0) { + if (!RefreshSid()) { + ConnectionFailed(); + return; + } + } + + // point of no return; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); + m_iStatus = m_iDesiredStatus; + m_bOnline = true; + m_impl.m_heartBeat.Start(60000); + + // connect message queue + MqttQueueConnect(); + + // request message threads if needed + if (m_bUseGroupchats) + RefreshThreads(); +} + +void FacebookProto::OnLoggedOut() +{ + m_impl.m_heartBeat.Stop(); + m_bOnline = false; + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + + setAllContactStatuses(ID_STATUS_OFFLINE, false); +} + +FacebookUser* FacebookProto::AddContact(const CMStringW &wszId, bool bTemp) +{ + MCONTACT hContact = db_add_contact(); + setWString(hContact, DBKEY_ID, wszId); + Proto_AddToContact(hContact, m_szModuleName); + Clist_SetGroup(hContact, m_wszDefaultGroup); + if (bTemp) + Contact::RemoveFromList(hContact); + + return FindUser(_wtoi64(wszId)); +} + +FacebookUser* FacebookProto::FindUser(__int64 id) +{ + mir_cslock lck(m_csUsers); + return m_users.find((FacebookUser *)&id); +} + +FacebookUser* FacebookProto::UserFromJson(const JSONNode &root, CMStringW &wszUserId, bool &bIsChat) +{ + bIsChat = false; + wszUserId = root["threadKey"]["otherUserFbId"].as_mstring(); + if (wszUserId.IsEmpty()) { + // if only thread id is present, it must be a group chat + wszUserId = root["threadKey"]["threadFbId"].as_mstring(); + bIsChat = true; + } + + auto *pUser = FindUser(_wtoi64(wszUserId)); + if (pUser == nullptr) { + debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); + return nullptr; + } + + if (pUser->bIsChat != bIsChat) { + debugLogA("Wrong chat user: %d vs %d for user %lld, ignored", pUser->bIsChat, bIsChat, pUser->id); + return nullptr; + } + + return pUser; +} + +int FacebookProto::RefreshContacts() +{ + CMStringA szCursor; + bool bNeedUpdate = false; + + while (true) { + JSONNode root; root << CHAR_PARAM("0", "user"); + + AsyncHttpRequest *pReq; + if (szCursor.IsEmpty()) { + pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS); + root << INT_PARAM("1", FB_API_CONTACTS_COUNT); + } + else { + pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS_AFTER); + root << CHAR_PARAM("1", szCursor) << INT_PARAM("2", FB_API_CONTACTS_COUNT); + } + pReq << CHAR_PARAM("query_params", root.write().c_str()); + pReq->flags |= NLHRF_NODUMPSEND; + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (int iErrorCode = reply.error()) + return iErrorCode; // unknown error + + bool bLoadAll = m_bLoadAll; + auto &data = reply.data()["viewer"]["messenger_contacts"]; + + for (auto &it : data["nodes"]) { + auto &n = it["represented_profile"]; + CMStringW wszId(n["id"].as_mstring()); + __int64 id = _wtoi64(wszId); + + MCONTACT hContact; + if (id != m_uid) { + bool bIsFriend = bLoadAll || n["friendship_status"].as_mstring() == L"ARE_FRIENDS"; + + auto *pUser = FindUser(id); + if (pUser == nullptr) { + if (!bIsFriend) + continue; + pUser = AddContact(wszId, false); + } + else if (!bIsFriend) + Contact::RemoveFromList(pUser->hContact); // adios! + + hContact = pUser->hContact; + } + else hContact = 0; + + if (auto &nName = it["structured_name"]) { + CMStringW wszName(nName["text"].as_mstring()); + setWString(hContact, DBKEY_NICK, wszName); + for (auto &nn : nName["parts"]) { + CMStringW wszPart(nn["part"].as_mstring()); + int offset = nn["offset"].as_int(), length = nn["length"].as_int(); + if (wszPart == L"first") + setWString(hContact, "FirstName", wszName.Mid(offset, length)); + else if (wszPart == L"last") + setWString(hContact, "LastName", wszName.Mid(offset, length)); + } + } + + if (auto &nBirth = n["birthdate"]) { + setDword(hContact, "BirthDay", nBirth["day"].as_int()); + setDword(hContact, "BirthMonth", nBirth["month"].as_int()); + } + + if (auto &nCity = n["current_city"]) + setWString(hContact, "City", nCity["name"].as_mstring()); + + if (auto &nAva = it[(m_bUseBigAvatars) ? "hugePictureUrl" : "bigPictureUrl"]) { + CMStringW wszOldUrl(getMStringW(hContact, DBKEY_AVATAR)), wszNewUrl(nAva["uri"].as_mstring()); + if (wszOldUrl != wszNewUrl) { + bNeedUpdate = true; + setByte(hContact, "UpdateNeeded", 1); + setWString(hContact, DBKEY_AVATAR, wszNewUrl); + } + } + } + + if (!data["page_info"]["has_next_page"].as_bool()) { + debugLogA("Got no next page, exiting", szCursor.c_str()); + break; + } + + szCursor = data["page_info"]["end_cursor"].as_mstring(); + debugLogA("Got cursor: %s", szCursor.c_str()); + } + + if (bNeedUpdate) + ForkThread(&FacebookProto::AvatarsUpdate); + return 0; +} + +bool FacebookProto::RefreshSid() +{ + auto *pReq = CreateRequestGQL(FB_API_QUERY_SEQ_ID); + JSONNode root; root << CHAR_PARAM("1", "0"); + pReq << CHAR_PARAM("query_params", root.write().c_str()); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (reply.error()) + return false; + + auto &n = reply.data()["viewer"]["message_threads"]; + CMStringW wszSid(n["sync_sequence_id"].as_mstring()); + setWString(DBKEY_SID, wszSid); + m_sid = _wtoi64(wszSid); + m_iUnread = n["unread_count"].as_int(); + return true; +} + +FacebookUser* FacebookProto::RefreshThread(JSONNode &n) +{ + if (!n["is_group_thread"].as_bool()) + return nullptr; + + CMStringW chatId(n["thread_key"]["thread_fbid"].as_mstring()); + CMStringW name(n["name"].as_mstring()); + if (name.IsEmpty()) { + for (auto &u : n["all_participants"]["nodes"]) { + auto &ur = u["messaging_actor"]; + CMStringW userId(ur["id"].as_mstring()); + if (_wtoi64(userId) == m_uid) + continue; + + if (!name.IsEmpty()) + name.Append(L", "); + name += ur["name"].as_mstring(); + } + + if (name.GetLength() > 128) { + name.Truncate(125); + name.Append(L"..."); + } + } + + auto *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, chatId, name); + if (si == nullptr) + return nullptr; + + setWString(si->hContact, DBKEY_ID, chatId); + Chat_AddGroup(si, TranslateT("Participant")); + + for (auto &u : n["all_participants"]["nodes"]) { + auto &ur = u["messaging_actor"]; + CMStringW userId(ur["id"].as_mstring()); + CMStringW userName(ur["name"].as_mstring()); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; + gce.pszID.w = chatId; + gce.pszUID.w = userId; + gce.pszNick.w = userName; + gce.bIsMe = _wtoi64(userId) == m_uid; + gce.time = time(0); + Chat_Event(&gce); + } + + Chat_Control(m_szModuleName, chatId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); + Chat_Control(m_szModuleName, chatId, SESSION_ONLINE); + + __int64 userId = _wtoi64(chatId); + auto *pUser = FindUser(userId); + + if (pUser == nullptr) { + mir_cslock lck(m_csUsers); + pUser = new FacebookUser(userId, si->hContact, true, true); + m_users.insert(pUser); + } + else { + pUser->hContact = si->hContact; + pUser->bIsChatInitialized = true; + } + + return pUser; +} + +FacebookUser* FacebookProto::RefreshThread(CMStringW &wszId) +{ + auto *pReq = CreateRequestGQL(FB_API_QUERY_THREAD); + pReq << WCHAR_PARAM("query_params", CMStringW(FORMAT, L"{\"0\":[\"%s\"], \"12\":0, \"13\":\"false\"}", wszId.c_str())); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (!reply.error()) { + auto &root = reply.data(); + for (auto &n : root) + return RefreshThread(n); + } + + return nullptr; +} + +void FacebookProto::RefreshThreads() +{ + int threadsLimit = 40; + + auto * pReq = CreateRequestGQL(FB_API_QUERY_THREADS); + JSONNode json; json << INT_PARAM("1", threadsLimit) << CHAR_PARAM("2", "true") << CHAR_PARAM("12", "false") << CHAR_PARAM("13", "false"); + pReq << CHAR_PARAM("query_params", json.write().c_str()); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (!reply.error()) { + auto &root = reply.data()["viewer"]["message_threads"]; + + for (auto &n : root["nodes"]) { + if (n["is_group_thread"].as_bool() && n["is_viewer_subscribed"].as_bool() && !n["has_viewer_archived"].as_bool()) + RefreshThread(n); + } + + // TODO: save timestamp of last message/action/... into DB + // TODO: lower threadsLimit to 10, load next pages if timestamp of last message is higher than timestamp in DB + } +} + +int FacebookProto::RefreshToken() +{ + auto *pReq = CreateRequest(FB_API_URL_AUTH, "authenticate", "auth.login"); + pReq->flags |= NLHRF_NODUMP; + pReq << CHAR_PARAM("email", getMStringA(DBKEY_LOGIN)); + pReq << CHAR_PARAM("password", getMStringA(DBKEY_PASS)); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (reply.error()) + return reply.error(); + + m_szAuthToken = reply.data()["access_token"].as_mstring(); + setString(DBKEY_TOKEN, m_szAuthToken); + + CMStringA m_szUid = reply.data()["uid"].as_mstring(); + setString(DBKEY_ID, m_szUid); + m_uid = _atoi64(m_szUid); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void FacebookProto::ServerThread(void *) +{ + m_QueueCreated = false; + +LBL_Begin: + m_szAuthToken = getMStringA(DBKEY_TOKEN); + if (m_szAuthToken.IsEmpty()) { + if (int iErrorCode = RefreshToken()) { + ConnectionFailed(iErrorCode); + return; + } + } + + int iErrorCode = RefreshContacts(); + if (iErrorCode != 0) { + if (iErrorCode == 401) { + delSetting(DBKEY_TOKEN); + goto LBL_Begin; + } + + ConnectionFailed(iErrorCode); + return; + } + + // connect to MQTT server + m_mqttConn = Netlib_OpenConnection(m_hNetlibUser, "mqtt.facebook.com", 443, 0, NLOCF_SSL); + if (m_mqttConn == nullptr) { + debugLogA("connection failed, exiting"); + ConnectionFailed(); + return; + } + + // send initial packet + MqttLogin(); + + while (!Miranda_IsTerminated()) { + NETLIBSELECT nls = {}; + nls.hReadConns[0] = m_mqttConn; + nls.dwTimeout = 1000; + int ret = Netlib_Select(&nls); + if (ret == SOCKET_ERROR) { + debugLogA("Netlib_Recv() failed, error=%d", WSAGetLastError()); + break; + } + + // no data, continue waiting + if (ret == 0) + continue; + + MqttMessage msg; + if (!MqttRead(msg)) { + debugLogA("MqttRead() failed"); + break; + } + + if (!MqttParse(msg)) { + debugLogA("MqttParse() failed"); + break; + } + } + + debugLogA("exiting ServerThread"); + + Netlib_CloseHandle(m_mqttConn); + m_mqttConn = nullptr; + + OnLoggedOut(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int FacebookProto::OnMarkedRead(WPARAM, LPARAM hDbEvent) +{ + MCONTACT hContact = db_event_getContact(hDbEvent); + if (!hContact) + return 0; + + // filter out only events of my protocol + const char *szProto = Proto_GetBaseAccountName(hContact); + if (mir_strcmp(szProto, m_szModuleName)) + return 0; + + if (m_bKeepUnread) + return 0; + + JSONNode root; root << BOOL_PARAM("state", true) << INT_PARAM("syncSeqId", m_sid) << CHAR_PARAM("mark", "read"); + if (isChatRoom(hContact)) + root << CHAR_PARAM("threadFbId", getMStringA(hContact, DBKEY_ID)); + else + root << CHAR_PARAM("otherUserFbId", getMStringA(hContact, DBKEY_ID)); + MqttPublish("/mark_thread", root); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void FacebookProto::OnPublish(const char *topic, const uint8_t *p, size_t cbLen) +{ + FbThriftReader rdr; + + // that might be a zipped buffer + if (cbLen >= 2) { + size_t dataSize; + void *pData = doUnzip(cbLen, p, dataSize); + if (pData != nullptr) { + debugLogA("UNZIP: %d bytes unzipped ok", dataSize); + Netlib_Dump(m_mqttConn, pData, dataSize, false, 0); + rdr.reset(dataSize, pData); + mir_free(pData); + } + } + + if (rdr.size() == 0) + rdr.reset(cbLen, (void*)p); + + if (!strcmp(topic, "/t_p")) + OnPublishPresence(rdr); + else if (!strcmp(topic, "/t_ms")) + OnPublishMessage(rdr); + else if (!strcmp(topic, "/orca_typing_notifications")) + OnPublishUtn(rdr); +} + +void FacebookProto::OnPublishPresence(FbThriftReader &rdr) +{ + char *str = nullptr; + rdr.readStr(str); + mir_free(str); + + bool bVal; + uint8_t fieldType; + uint16_t fieldId; + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_BOOL); + assert(fieldId == 1); + rdr.readBool(bVal); + + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_LIST); + assert(fieldId == 1); + + uint32_t size; + rdr.readList(fieldType, size); + assert(fieldType == FB_THRIFT_TYPE_STRUCT); + + debugLogA("Received list of presences: %d records", size); + for (uint32_t i = 0; i < size; i++) { + uint64_t userId, timestamp, voipBits; + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_I64); + assert(fieldId == 1); + rdr.readInt64(userId); + + uint32_t u32; + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_I32); + assert(fieldId == 1); + rdr.readInt32(u32); + + auto *pUser = FindUser(userId); + if (pUser == nullptr) + debugLogA("Skipping presence from unknown user %lld", userId); + else { + debugLogA("Presence from user %lld => %d", userId, u32); + setWord(pUser->hContact, "Status", (u32 != 0) ? ID_STATUS_ONLINE : ID_STATUS_OFFLINE); + } + + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_I64); + assert(fieldId == 1 || fieldId == 3 || fieldId == 4); + rdr.readInt64(timestamp); + + while (!rdr.isStop()) { + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_I64 || fieldType == FB_THRIFT_TYPE_I16 || fieldType == FB_THRIFT_TYPE_I32); + rdr.readIntV(voipBits); + } + + rdr.readByte(fieldType); + assert(fieldType == FB_THRIFT_TYPE_STOP); + } + + rdr.readByte(fieldType); + assert(fieldType == FB_THRIFT_TYPE_STOP); +} + +void FacebookProto::OnPublishUtn(FbThriftReader &rdr) +{ + JSONNode root = JSONNode::parse(rdr.rest()); + auto *pUser = FindUser(_wtoi64(root["sender_fbid"].as_mstring())); + if (pUser != nullptr) { + int length = (root["state"].as_int() == 0) ? PROTOTYPE_CONTACTTYPING_OFF : 60; + CallService(MS_PROTO_CONTACTISTYPING, pUser->hContact, length); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +struct +{ + const char *messageType; + void (FacebookProto:: *pFunc)(const JSONNode &); +} +static MsgHandlers[] = +{ + { "deltaNewMessage", &FacebookProto::OnPublishPrivateMessage }, + { "deltaThreadName", &FacebookProto::OnPublishThreadName }, + { "deltaSentMessage", &FacebookProto::OnPublishSentMessage }, + { "deltaReadReceipt", &FacebookProto::OnPublishReadReceipt }, + { "deltaParticipantsAddedToGroupThread", &FacebookProto::OnPublishChatJoin }, + { "deltaParticipantLeftGroupThread", &FacebookProto::OnPublishChatLeave }, +}; + +void FacebookProto::OnPublishMessage(FbThriftReader &rdr) +{ + uint8_t stop; + if (rdr.isStop()) + rdr.readByte(stop); + else { + uint8_t type; + uint16_t id; + rdr.readField(type, id); + _ASSERT(type == FB_THRIFT_TYPE_STRING); + _ASSERT(id == 1 || id == 2); + + char *szShit = nullptr; + rdr.readStr(szShit); + mir_free(szShit); + + rdr.readByte(stop); + } + + CMStringA szJson(rdr.rest()); + debugLogA("MS: <%s>", szJson.c_str()); + JSONNode root = JSONNode::parse(szJson); + + CMStringA errorCode = root["errorCode"].as_mstring(); + if (!errorCode.IsEmpty()) { + if (!m_QueueCreated && (errorCode == "ERROR_QUEUE_OVERFLOW" || errorCode == "ERROR_QUEUE_NOT_FOUND" || errorCode == "ERROR_QUEUE_LOST" || errorCode == "ERROR_QUEUE_EXCEEDS_MAX_DELTAS")) { + m_QueueCreated = true; // prevent queue creation request from being sent twice + delSetting(DBKEY_SYNC_TOKEN); m_szSyncToken.Empty(); + delSetting(DBKEY_SID); m_sid = 0; + if (!RefreshSid()) { + ConnectionFailed(); + return; + } + + MqttQueueConnect(); + } + } + + CMStringW str = root["lastIssuedSeqId"].as_mstring(); + if (!str.IsEmpty()) { + setWString(DBKEY_SID, str); + m_sid = _wtoi64(str); + } + + str = root["syncToken"].as_mstring(); + if (!str.IsEmpty()) { + m_szSyncToken = str; + setString(DBKEY_SYNC_TOKEN, m_szSyncToken); + return; + } + + for (auto &it : root["deltas"]) { + for (auto &handler : MsgHandlers) { + auto &json = it[handler.messageType]; + if (json) { + (this->*(handler.pFunc))(json); + break; + } + } + } +} + +// new message arrived +struct +{ + const char *szTag, *szClientVersion; +} +static facebookClients[] = +{ + { "source:titan:web", "Facebook (website)" }, + { "app_id:256002347743983", "Facebook (Facebook Messenger)" } +}; + +void FacebookProto::FetchAttach(const CMStringA &mid, __int64 fbid, CMStringA &szBody) +{ + for (int iAttempt = 0; iAttempt < 5; iAttempt++) { + auto *pReq = CreateRequest(FB_API_URL_ATTACH, "getAttachment", "messaging.getAttachment"); + pReq << CHAR_PARAM("mid", mid) << INT64_PARAM("aid", fbid); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + switch (reply.error()) { + case 0: + { + std::string uri = reply.data()["redirect_uri"].as_string(); + std::string type = reply.data()["content_type"].as_string(); + if (!uri.empty()) + szBody.AppendFormat("\r\n%s: %s", TranslateU(type.find("image/") != -1 ? "Picture attachment" : "File attachment"), uri.c_str()); + } + return; + + case 509: // attachment isn't ready, wait a bit and retry + ::Sleep(100); + continue; + + default: // shit happened, exiting + return; + } + } +} + +void FacebookProto::OnPublishPrivateMessage(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + if (!offlineId) { + debugLogA("We care about messages only, event skipped"); + return; + } + + bool bIsChat; + CMStringW wszUserId; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + + if (!bIsChat && pUser == nullptr) + pUser = AddContact(wszUserId, true); + else if (bIsChat && (pUser == nullptr || !pUser->bIsChatInitialized)) // chat room does not exists or is not initialized + pUser = RefreshThread(wszUserId); + + if (pUser == nullptr) { + debugLogA("User not found and adding failed, event skipped"); + return; + } + + for (auto &it : metadata["tags"]) { + auto *szTagName = it.name(); + for (auto &cli : facebookClients) { + if (!mir_strcmp(szTagName, cli.szTag)) { + setString(pUser->hContact, "MirVer", cli.szClientVersion); + break; + } + } + } + + CMStringA szId(metadata["messageId"].as_mstring()); + if (CheckOwnMessage(pUser, offlineId, szId)) { + debugLogA("own message <%s> skipped", szId.c_str()); + return; + } + + if (db_event_getById(m_szModuleName, szId)) { + debugLogA("this message <%s> was already stored, exiting", szId.c_str()); + return; + } + + // parse message body + CMStringA szBody(root["body"].as_string().c_str()); + if (szBody.IsEmpty()) + szBody = metadata["snippet"].as_string().c_str(); + + // parse stickers + CMStringA stickerId = root["stickerId"].as_mstring(); + if (!stickerId.IsEmpty()) { + if (ServiceExists(MS_SMILEYADD_LOADCONTACTSMILEYS)) { + CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); + CreateDirectoryTreeW(wszPath); + + bool bSuccess = false; + CMStringW wszFileName(FORMAT, L"%s\\STK{%S}.png", wszPath.c_str(), stickerId.c_str()); + uint32_t dwAttrib = GetFileAttributesW(wszFileName); + if (dwAttrib == INVALID_FILE_ATTRIBUTES) { + wszFileName.Format(L"%s\\STK{%S}.webp", wszPath.c_str(), stickerId.c_str()); + dwAttrib = GetFileAttributesW(wszFileName); + } + + // new sticker + if (dwAttrib == INVALID_FILE_ATTRIBUTES) { + auto *pReq = CreateRequestGQL(FB_API_QUERY_STICKER); + pReq << CHAR_PARAM("query_params", CMStringA(FORMAT, "{\"0\":[\"%s\"]}", stickerId.c_str())); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (!reply.error()) { + for (auto &sticker : reply.data()) { + // std::string szUrl = sticker["animated_image"]["uri"].as_string(); + // if (szUrl.empty()) + // szUrl = sticker["thread_image"]["uri"].as_string(); + // else + // wszFileName.Format(L"%s\\STK{%S}.webp", wszPath.c_str(), stickerId.c_str()); + std::string szUrl = sticker["thread_image"]["uri"].as_string(); + + NETLIBHTTPREQUEST req = {}; + req.cbSize = sizeof(req); + req.flags = NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; + req.requestType = REQUEST_GET; + req.szUrl = (char*)szUrl.c_str(); + + NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(m_hNetlibUser, &req); + if (pReply != nullptr && pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { + bSuccess = true; + FILE *out = _wfopen(wszFileName, L"wb"); + fwrite(pReply->pData, 1, pReply->dataLength, out); + fclose(out); + } + } + } + } + else bSuccess = true; + + if (bSuccess) { + if (!szBody.IsEmpty()) + szBody += "\r\n"; + szBody += "STK{" + stickerId + "}"; + + SMADD_CONT cont = { 1, m_szModuleName, wszFileName }; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); + } + else szBody += TranslateU("Sticker received"); + } + else szBody += TranslateU("SmileyAdd plugin required to support stickers"); + } + + // parse attachments (links, files, ...) + for (auto &it : root["attachments"]) { + // madness... json inside json + CMStringA szJson(it["xmaGraphQL"].as_mstring()); + if (szJson.IsEmpty()) { + __int64 fbid = _wtoi64(it["fbid"].as_mstring()); + if (fbid == 0) { + debugLogA("Neither a GQL nor an inline attachment, nothing to do"); + continue; + } + + // inline attachment, request its description + FetchAttach(szId, fbid, szBody); + continue; + } + + JSONROOT nBody(szJson); + if (!nBody) + continue; + + const JSONNode &attach = (*nBody).at((json_index_t)0)["story_attachment"]; + szBody += "\r\n-----------------------------------"; + + CMStringA str = attach["url"].as_mstring(); + if (!str.IsEmpty()) { + if (str.Left(8) == "fbrpc://") { + int iStart = str.Find("target_url="); + if (iStart != 0) { + CMStringA tmp; + + iStart += 11; + int iEnd = str.Find("&", iStart); + if (iEnd != -1) + tmp = str.Mid(iStart, iEnd - iStart); + else + tmp = str.Right(iStart); + + mir_urlDecode(tmp.GetBuffer()); + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("URL"), tmp.c_str()); + } + } + else szBody.AppendFormat("\r\n\t%s: %s", TranslateU("URL"), str.c_str()); + } + + str = attach["title"].as_string().c_str(); + if (!str.IsEmpty()) + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Title"), str.c_str()); + + str = attach["source"]["text"].as_string().c_str(); + if (!str.IsEmpty()) + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Source"), str.c_str()); + + str = attach["description"]["text"].as_string().c_str(); + if (!str.IsEmpty()) + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Description"), str.c_str()); + + str = attach["media"]["playable_url"].as_string().c_str(); + if (!str.IsEmpty()) + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Playable media"), str.c_str()); + } + + // if that's a group chat, send it to the room + CMStringW wszActorFbId(metadata["actorFbId"].as_mstring()); + __int64 actorFbId = _wtoi64(wszActorFbId); + + if (pUser->bIsChat) { + szBody.Replace("%", "%%"); + ptrW wszText(mir_utf8decodeW(szBody)); + + // TODO: GC_EVENT_JOIN for chat participants which are missing (for example added later during group chat) + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; + gce.pszID.w = wszUserId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszActorFbId; + gce.pszText.w = wszText; + gce.time = time(0); + gce.bIsMe = actorFbId == m_uid; + Chat_Event(&gce); + + debugLogA("New channel %lld message from %S: %s", pUser->id, gce.pszUID.w, gce.pszText.w); + } + else { // otherwise store a private message + PROTORECVEVENT pre = {}; + pre.timestamp = uint32_t(_wtoi64(metadata["timestamp"].as_mstring()) / 1000); + pre.szMessage = (char *)szBody.c_str(); + pre.szMsgId = (char *)szId.c_str(); + + if (m_uid == actorFbId) + pre.flags |= PREF_SENT; + + ProtoChainRecvMsg(pUser->hContact, &pre); + } +} + +// changing thread name +void FacebookProto::OnPublishThreadName(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + if (!offlineId) { + debugLogA("We care about messages only, event skipped"); + return; + } + + bool bIsChat; + CMStringW wszUserId; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + if (!bIsChat || pUser == nullptr) + return; + + CMStringW wszTitle = root["name"].as_mstring(); + if (!wszTitle.IsEmpty()) + setWString(pUser->hContact, DBKEY_NICK, wszTitle); + else + delSetting(pUser->hContact, DBKEY_NICK); +} + +// user joined chat +void FacebookProto::OnPublishChatJoin(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + if (!offlineId) { + debugLogA("We care about messages only, event skipped"); + return; + } + + bool bIsChat; + CMStringW wszUserId; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + if (!bIsChat || pUser == nullptr) + return; + + CMStringW wszText(metadata["adminText"].as_mstring()); + for (auto &it : root["addedParticipants"]) { + CMStringW wszNick(it["fullName"].as_mstring()), wszId(it["userFbId"].as_mstring()); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; + gce.pszID.w = wszUserId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszNick.w = wszNick; + gce.pszUID.w = wszId; + gce.pszText.w = wszText; + gce.time = time(0); + gce.bIsMe = _wtoi64(wszId) == m_uid; + Chat_Event(&gce); + } +} + +// user left chat +void FacebookProto::OnPublishChatLeave(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + if (!offlineId) { + debugLogA("We care about messages only, event skipped"); + return; + } + + bool bIsChat; + CMStringW wszUserId; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + if (!bIsChat || pUser == nullptr) + return; + + CMStringW wszText(metadata["adminText"].as_mstring()), wszId(root["leftParticipantFbId"].as_mstring()); + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_PART }; + gce.pszID.w = wszUserId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszId; + gce.pszText.w = wszText; + gce.time = time(0); + gce.bIsMe = _wtoi64(wszId) == m_uid; + Chat_Event(&gce); +} + +// read notification +void FacebookProto::OnPublishReadReceipt(const JSONNode &root) +{ + CMStringW wszUserId; + bool bIsChat; + auto *pUser = UserFromJson(root, wszUserId, bIsChat); + if (pUser == nullptr) { + debugLogA("Message from unknown contact %S, ignored", wszUserId.c_str()); + return; + } + + uint32_t timestamp = _wtoi64(root["watermarkTimestampMs"].as_mstring()); + for (MEVENT ev = db_event_firstUnread(pUser->hContact); ev != 0; ev = db_event_next(pUser->hContact, ev)) { + DBEVENTINFO dbei = {}; + if (db_event_get(ev, &dbei)) + continue; + + if (dbei.timestamp > timestamp) + break; + + if (!dbei.markedRead()) + db_event_markRead(pUser->hContact, ev); + } +} + +// my own message was sent +bool FacebookProto::CheckOwnMessage(FacebookUser *pUser, __int64 offlineId, const char *pszMsgId) +{ + COwnMessage tmp; + if (!ExtractOwnMessage(offlineId, tmp)) + return false; + + if (pUser->bIsChat) { + CMStringW wszId(FORMAT, L"%lld", m_uid); + tmp.wszText.Replace(L"%", L"%%"); + + wchar_t userId[100]; + _i64tow_s(pUser->id, userId, _countof(userId), 10); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; + gce.pszID.w = userId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszId; + gce.pszText.w = tmp.wszText; + gce.time = time(0); + gce.bIsMe = true; + Chat_Event(&gce); + } + else ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)tmp.reqId, (LPARAM)pszMsgId); + + return true; +} + +void FacebookProto::OnPublishSentMessage(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + + CMStringW wszUserId; + bool bIsChat; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + if (pUser == nullptr) { + debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); + return; + } + + std::string szMsgId(metadata["messageId"].as_string()); + CheckOwnMessage(pUser, offlineId, szMsgId.c_str()); +} diff --git a/protocols/Facebook/src/stdafx.cxx b/protocols/Facebook/src/stdafx.cxx index d265a4c02e..8c570f6949 100644 --- a/protocols/Facebook/src/stdafx.cxx +++ b/protocols/Facebook/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation version 2 -of the License. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" +/* +Copyright (C) 2012-23 Miranda NG team (https://miranda-ng.org) + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation version 2 +of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" diff --git a/protocols/Facebook/src/stdafx.h b/protocols/Facebook/src/stdafx.h index 3d5f28057c..aadac15f0e 100644 --- a/protocols/Facebook/src/stdafx.h +++ b/protocols/Facebook/src/stdafx.h @@ -1,67 +1,67 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../../libs/zlib/src/zlib.h" - -#include "db.h" -#include "dialogs.h" -#include "mqtt.h" -#include "proto.h" -#include "resource.h" -#include "version.h" - -extern bool g_bMessageState; +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../libs/zlib/src/zlib.h" + +#include "db.h" +#include "dialogs.h" +#include "mqtt.h" +#include "proto.h" +#include "resource.h" +#include "version.h" + +extern bool g_bMessageState; diff --git a/protocols/Facebook/src/thrift.cpp b/protocols/Facebook/src/thrift.cpp index af0ceb4dd7..c7718c1a60 100644 --- a/protocols/Facebook/src/thrift.cpp +++ b/protocols/Facebook/src/thrift.cpp @@ -1,308 +1,308 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -///////////////////////////////////////////////////////////////////////////////////////// -// FbThrift class members - -#include "stdafx.h" - -static uint8_t encodeType(int type) -{ - switch (type) { - case FB_THRIFT_TYPE_BOOL: - return 2; - case FB_THRIFT_TYPE_BYTE: - return 3; - case FB_THRIFT_TYPE_I16: - return 4; - case FB_THRIFT_TYPE_I32: - return 5; - case FB_THRIFT_TYPE_I64: - return 6; - case FB_THRIFT_TYPE_DOUBLE: - return 7; - case FB_THRIFT_TYPE_STRING: - return 8; - case FB_THRIFT_TYPE_LIST: - return 9; - case FB_THRIFT_TYPE_SET: - return 10; - case FB_THRIFT_TYPE_MAP: - return 11; - case FB_THRIFT_TYPE_STRUCT: - return 12; - default: - return 0; - } -} - -FbThrift& FbThrift::operator<<(uint8_t value) -{ - m_buf.append(&value, 1); - return *this; -} - -FbThrift& FbThrift::operator<<(const char *str) -{ - size_t len = mir_strlen(str); - writeIntV(len); - m_buf.append(str, len); - return *this; -} - -void FbThrift::writeBool(bool bValue) -{ - uint8_t b = (bValue) ? 0x11 : 0x12; - m_buf.append(&b, 1); -} - -void FbThrift::writeBuf(const void *pData, size_t cbLen) -{ - m_buf.append(pData, cbLen); -} - -void FbThrift::writeField(int iType) -{ - uint8_t type = encodeType(iType) + 0x10; - m_buf.append(&type, 1); -} - -void FbThrift::writeField(int iType, int id, int lastid) -{ - uint8_t type = encodeType(iType); - uint8_t diff = uint8_t(id - lastid); - if (diff > 0x0F) { - m_buf.append(&type, 1); - writeInt64(id); - } - else { - type += (diff << 4); - m_buf.append(&type, 1); - } -} - -void FbThrift::writeList(int iType, int size) -{ - uint8_t type = encodeType(iType); - if (size > 14) { - writeIntV(size); - *this << (type | 0xF0); - } - else *this << (type | (size << 4)); -} - -void FbThrift::writeInt16(uint16_t value) -{ - value = htons(value); - m_buf.append(&value, sizeof(value)); -} - -void FbThrift::writeInt32(int32_t value) -{ - writeIntV((value << 1) ^ (value >> 31)); -} - -void FbThrift::writeInt64(int64_t value) -{ - writeIntV((value << 1) ^ (value >> 63)); -} - -void FbThrift::writeIntV(uint64_t value) -{ - bool bLast; - do { - bLast = (value & ~0x7F) == 0; - uint8_t b = value & 0x7F; - if (!bLast) { - b |= 0x80; - value >>= 7; - } - m_buf.append(&b, 1); - } while (!bLast); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// FbThriftReader class members - -uint8_t FbThriftReader::decodeType(int type) -{ - switch (type) { - case 0: - return FB_THRIFT_TYPE_STOP; - case 1: - m_lastBval = m_lastBool = true; - return FB_THRIFT_TYPE_BOOL; - case 2: - m_lastBool = true; - m_lastBval = false; - return FB_THRIFT_TYPE_BOOL; - case 3: - return FB_THRIFT_TYPE_BYTE; - case 4: - return FB_THRIFT_TYPE_I16; - case 5: - return FB_THRIFT_TYPE_I32; - case 6: - return FB_THRIFT_TYPE_I64; - case 7: - return FB_THRIFT_TYPE_DOUBLE; - case 8: - return FB_THRIFT_TYPE_STRING; - case 9: - return FB_THRIFT_TYPE_LIST; - case 10: - return FB_THRIFT_TYPE_SET; - case 11: - return FB_THRIFT_TYPE_MAP; - case 12: - return FB_THRIFT_TYPE_STRUCT; - default: - return 0; - } -} - -bool FbThriftReader::isStop() -{ - byte b; - if (!readByte(b)) - return true; - - offset--; - return b == FB_THRIFT_TYPE_STOP; -} - -bool FbThriftReader::readBool(bool &bVal) -{ - if (m_lastBool) { - bVal = m_lastBval; - m_lastBool = false; - return true; - } - - byte b; - if (!readByte(b)) - return false; - - bVal = b == 0x11; - return true; -} - -bool FbThriftReader::readByte(uint8_t &b) -{ - if (offset >= size()) - return false; - - b = *((uint8_t *)data() + offset); - offset++; - return true; -} - -bool FbThriftReader::readField(uint8_t &type, uint16_t &id) -{ - byte b; - if (!readByte(b)) - return false; - - type = decodeType(b & 0x0F); - id = (b >> 4); - return (id == 0) ? readInt16(id) : true; -} - -bool FbThriftReader::readIntV(uint64_t &val) -{ - uint8_t b; - unsigned i = 0; - val = 0; - - do { - if (!readByte(b)) - return false; - - val |= (uint64_t(b & 0x7F) << i); - i += 7; - } while ((b & 0x80) != 0); - - return true; -} - -bool FbThriftReader::readList(uint8_t &type, uint32_t &size) -{ - byte b; - if (!readByte(b)) - return false; - - type = decodeType(b & 0x0F); - size = b >> 4; - if (size == 0x0F) { - uint64_t tmp; - if (!readIntV(tmp)) - return false; - size = (uint32_t)tmp; - } - return true; -} - -bool FbThriftReader::readStr(char *&val) -{ - uint64_t tmp; - if (!readIntV(tmp)) - return false; - - uint32_t cbLen = (uint32_t)tmp; - if (offset + cbLen >= size()) - return false; - - if (cbLen > 0) { - val = mir_strndup((char *)data() + offset, cbLen); - offset += cbLen; - } - else val = nullptr; - return true; -} - -bool FbThriftReader::readInt16(uint16_t &val) -{ - if (offset + 2 >= size()) - return false; - - val = ntohs(*(u_short *)((char *)data() + offset)); - offset += 2; - return true; -} - -bool FbThriftReader::readInt32(uint32_t &val) -{ - uint64_t tmp; - if (!readIntV(tmp)) - return false; - - val = (uint32_t )tmp; - return true; -} - -bool FbThriftReader::readInt64(uint64_t &val) -{ - uint64_t tmp; - if (!readIntV(tmp)) - return false; - - val = (tmp >> 0x01) ^ -(tmp & 0x01); - return true; -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +///////////////////////////////////////////////////////////////////////////////////////// +// FbThrift class members + +#include "stdafx.h" + +static uint8_t encodeType(int type) +{ + switch (type) { + case FB_THRIFT_TYPE_BOOL: + return 2; + case FB_THRIFT_TYPE_BYTE: + return 3; + case FB_THRIFT_TYPE_I16: + return 4; + case FB_THRIFT_TYPE_I32: + return 5; + case FB_THRIFT_TYPE_I64: + return 6; + case FB_THRIFT_TYPE_DOUBLE: + return 7; + case FB_THRIFT_TYPE_STRING: + return 8; + case FB_THRIFT_TYPE_LIST: + return 9; + case FB_THRIFT_TYPE_SET: + return 10; + case FB_THRIFT_TYPE_MAP: + return 11; + case FB_THRIFT_TYPE_STRUCT: + return 12; + default: + return 0; + } +} + +FbThrift& FbThrift::operator<<(uint8_t value) +{ + m_buf.append(&value, 1); + return *this; +} + +FbThrift& FbThrift::operator<<(const char *str) +{ + size_t len = mir_strlen(str); + writeIntV(len); + m_buf.append(str, len); + return *this; +} + +void FbThrift::writeBool(bool bValue) +{ + uint8_t b = (bValue) ? 0x11 : 0x12; + m_buf.append(&b, 1); +} + +void FbThrift::writeBuf(const void *pData, size_t cbLen) +{ + m_buf.append(pData, cbLen); +} + +void FbThrift::writeField(int iType) +{ + uint8_t type = encodeType(iType) + 0x10; + m_buf.append(&type, 1); +} + +void FbThrift::writeField(int iType, int id, int lastid) +{ + uint8_t type = encodeType(iType); + uint8_t diff = uint8_t(id - lastid); + if (diff > 0x0F) { + m_buf.append(&type, 1); + writeInt64(id); + } + else { + type += (diff << 4); + m_buf.append(&type, 1); + } +} + +void FbThrift::writeList(int iType, int size) +{ + uint8_t type = encodeType(iType); + if (size > 14) { + writeIntV(size); + *this << (type | 0xF0); + } + else *this << (type | (size << 4)); +} + +void FbThrift::writeInt16(uint16_t value) +{ + value = htons(value); + m_buf.append(&value, sizeof(value)); +} + +void FbThrift::writeInt32(int32_t value) +{ + writeIntV((value << 1) ^ (value >> 31)); +} + +void FbThrift::writeInt64(int64_t value) +{ + writeIntV((value << 1) ^ (value >> 63)); +} + +void FbThrift::writeIntV(uint64_t value) +{ + bool bLast; + do { + bLast = (value & ~0x7F) == 0; + uint8_t b = value & 0x7F; + if (!bLast) { + b |= 0x80; + value >>= 7; + } + m_buf.append(&b, 1); + } while (!bLast); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// FbThriftReader class members + +uint8_t FbThriftReader::decodeType(int type) +{ + switch (type) { + case 0: + return FB_THRIFT_TYPE_STOP; + case 1: + m_lastBval = m_lastBool = true; + return FB_THRIFT_TYPE_BOOL; + case 2: + m_lastBool = true; + m_lastBval = false; + return FB_THRIFT_TYPE_BOOL; + case 3: + return FB_THRIFT_TYPE_BYTE; + case 4: + return FB_THRIFT_TYPE_I16; + case 5: + return FB_THRIFT_TYPE_I32; + case 6: + return FB_THRIFT_TYPE_I64; + case 7: + return FB_THRIFT_TYPE_DOUBLE; + case 8: + return FB_THRIFT_TYPE_STRING; + case 9: + return FB_THRIFT_TYPE_LIST; + case 10: + return FB_THRIFT_TYPE_SET; + case 11: + return FB_THRIFT_TYPE_MAP; + case 12: + return FB_THRIFT_TYPE_STRUCT; + default: + return 0; + } +} + +bool FbThriftReader::isStop() +{ + byte b; + if (!readByte(b)) + return true; + + offset--; + return b == FB_THRIFT_TYPE_STOP; +} + +bool FbThriftReader::readBool(bool &bVal) +{ + if (m_lastBool) { + bVal = m_lastBval; + m_lastBool = false; + return true; + } + + byte b; + if (!readByte(b)) + return false; + + bVal = b == 0x11; + return true; +} + +bool FbThriftReader::readByte(uint8_t &b) +{ + if (offset >= size()) + return false; + + b = *((uint8_t *)data() + offset); + offset++; + return true; +} + +bool FbThriftReader::readField(uint8_t &type, uint16_t &id) +{ + byte b; + if (!readByte(b)) + return false; + + type = decodeType(b & 0x0F); + id = (b >> 4); + return (id == 0) ? readInt16(id) : true; +} + +bool FbThriftReader::readIntV(uint64_t &val) +{ + uint8_t b; + unsigned i = 0; + val = 0; + + do { + if (!readByte(b)) + return false; + + val |= (uint64_t(b & 0x7F) << i); + i += 7; + } while ((b & 0x80) != 0); + + return true; +} + +bool FbThriftReader::readList(uint8_t &type, uint32_t &size) +{ + byte b; + if (!readByte(b)) + return false; + + type = decodeType(b & 0x0F); + size = b >> 4; + if (size == 0x0F) { + uint64_t tmp; + if (!readIntV(tmp)) + return false; + size = (uint32_t)tmp; + } + return true; +} + +bool FbThriftReader::readStr(char *&val) +{ + uint64_t tmp; + if (!readIntV(tmp)) + return false; + + uint32_t cbLen = (uint32_t)tmp; + if (offset + cbLen >= size()) + return false; + + if (cbLen > 0) { + val = mir_strndup((char *)data() + offset, cbLen); + offset += cbLen; + } + else val = nullptr; + return true; +} + +bool FbThriftReader::readInt16(uint16_t &val) +{ + if (offset + 2 >= size()) + return false; + + val = ntohs(*(u_short *)((char *)data() + offset)); + offset += 2; + return true; +} + +bool FbThriftReader::readInt32(uint32_t &val) +{ + uint64_t tmp; + if (!readIntV(tmp)) + return false; + + val = (uint32_t )tmp; + return true; +} + +bool FbThriftReader::readInt64(uint64_t &val) +{ + uint64_t tmp; + if (!readIntV(tmp)) + return false; + + val = (tmp >> 0x01) ^ -(tmp & 0x01); + return true; +} diff --git a/protocols/Facebook/src/version.h b/protocols/Facebook/src/version.h index 4a483c02f1..660e8cfe94 100644 --- a/protocols/Facebook/src/version.h +++ b/protocols/Facebook/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Facebook protocol support for Miranda NG." #define __AUTHOR "Miranda NG Team" #define __AUTHORWEB "https://miranda-ng.org/p/Facebook" -#define __COPYRIGHT "© 2019-22 Miranda NG team" +#define __COPYRIGHT "© 2019-23 Miranda NG team" -- cgit v1.2.3