/* Facebook plugin for Miranda NG Copyright © 2019 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() { 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); OnLoggedOut(); } void FacebookProto::OnLoggedIn() { m_bOnline = true; m_mid = 0; MqttPublish("/foreground_state", "{\"foreground\":true, \"keepalive_timeout\":60}"); 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) { auto *pReq = CreateRequestGQL(FB_API_QUERY_SEQ_ID); pReq << CHAR_PARAM("query_params", "{\"1\":\"0\"}"); pReq->CalcSig(); JsonReply reply(ExecuteRequest(pReq)); if (reply.error()) { ConnectionFailed(); return; } 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(); } ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); m_iStatus = m_iDesiredStatus; // connect message queue 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.write().c_str()); } else { query << INT64_PARAM("last_seq_id", m_sid) << CHAR_PARAM("sync_token", m_szSyncToken); MqttPublish("/messenger_sync_get_diffs", query.write().c_str()); } } void FacebookProto::OnLoggedOut() { OnShutdown(); m_bOnline = false; } bool FacebookProto::RefreshContacts() { auto *pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS); pReq << CHAR_PARAM("query_params", "{\"0\":[\"user\"],\"1\":\"" FB_API_CONTACTS_COUNT "\"}"); pReq->CalcSig(); JsonReply reply(ExecuteRequest(pReq)); if (reply.error()) return false; bool bNeedUpdate = false; for (auto &it : reply.data()["viewer"]["messenger_contacts"]["nodes"]) { auto &n = it["represented_profile"]; CMStringW wszId(n["id"].as_mstring()); __int64 id = _wtoi64(wszId); MCONTACT hContact; if (id != m_uid) { auto *pUser = FindUser(id); if (pUser == nullptr) { hContact = db_add_contact(); Proto_AddToContact(hContact, m_szModuleName); setWString(hContact, DBKEY_ID, wszId); Clist_SetGroup(hContact, m_wszDefaultGroup); m_users.insert(new FacebookUser(id, hContact)); } else 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, DBKEY_FIRST_NAME, wszName.Mid(offset, length)); else if (wszPart == L"last") setWString(hContact, DBKEY_LAST_NAME, 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(DBKEY_AVATAR)), wszNewUrl(nAva["uri"].as_mstring()); if (wszOldUrl != wszNewUrl) { bNeedUpdate = true; setByte(hContact, "UpdateNeeded", 1); setWString(hContact, DBKEY_AVATAR, wszNewUrl); } } } if (bNeedUpdate) ForkThread(&FacebookProto::AvatarsUpdate); return true; } bool FacebookProto::RefreshToken() { auto *pReq = CreateRequest("authenticate", "auth.login"); pReq->m_szUrl = FB_API_URL_AUTH; pReq << CHAR_PARAM("email", getMStringA(DBKEY_LOGIN)); pReq << CHAR_PARAM("password", getMStringA(DBKEY_PASS)); pReq->CalcSig(); JsonReply reply(ExecuteRequest(pReq)); if (reply.error()) return false; m_szAuthToken = reply.data()["access_token"].as_mstring(); setString(DBKEY_TOKEN, m_szAuthToken); m_uid = reply.data()["uid"].as_int(); CMStringA m_szUid = reply.data()["uid"].as_mstring(); setString(DBKEY_ID, m_szUid); return true; } ///////////////////////////////////////////////////////////////////////////////////////// void FacebookProto::ServerThread(void *) { m_szAuthToken = getMStringA(DBKEY_TOKEN); if (m_szAuthToken.IsEmpty()) { if (!RefreshToken()) { ConnectionFailed(); return; } } if (!RefreshContacts()) { ConnectionFailed(); return; } // connect to MQTT server NETLIBOPENCONNECTION nloc = {}; nloc.szHost = "mqtt.facebook.com"; nloc.wPort = 443; nloc.flags = NLOCF_SSL | NLOCF_V2; m_mqttConn = Netlib_OpenConnection(m_hNetlibUser, &nloc); if (m_mqttConn == nullptr) { debugLogA("connection failed, exiting"); ConnectionFailed(); return; } // send initial packet MqttLogin(); __int64 startTime = GetTickCount64(); 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; } __int64 currTime = GetTickCount64(); if (currTime - startTime > 60000) { startTime = currTime; MqttPing(); } // 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; int oldStatus = m_iStatus; m_iDesiredStatus = m_iStatus = ID_STATUS_OFFLINE; ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus); } ///////////////////////////////////////////////////////////////////////////////////////// void FacebookProto::OnPublish(const char *topic, const uint8_t *p, size_t cbLen) { FbThriftReader rdr; // that might be a zipped buffer if (cbLen >= 2) { if ((((p[0] << 8) | p[1]) % 31) == 0 && (p[0] & 0x0F) == 8) { // zip header ok size_t dataSize; void *pData = doUnzip(cbLen, p, dataSize); if (pData != nullptr) { debugLogA("UNZIP: <%s>", CMStringA((const char *)pData, (int)dataSize).c_str()); 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; 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 == 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((const char *)rdr.data()); 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 }, { "deltaSentMessage", &FacebookProto::OnPublishSentMessage } }; void FacebookProto::OnPublishMessage(FbThriftReader &rdr) { CMStringA szJson((const char *)rdr.data(), (int)rdr.size()); if (szJson[0] == 0) szJson.Delete(0); debugLogA("MS: <%s>", szJson.c_str()); JSONNode root = JSONNode::parse(szJson); 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 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; } CMStringA wszUserId(metadata["actorFbId"].as_mstring()); auto *pUser = FindUser(_atoi64(wszUserId)); if (pUser == nullptr) { debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); return; } std::string szBody(root["body"].as_string()); std::string szId(metadata["messageId"].as_string()); PROTORECVEVENT pre = {}; pre.timestamp = DWORD(_wtoi64(metadata["timestamp"].as_mstring()) / 1000); pre.szMessage = (char *)szBody.c_str(); pre.szMsgId = (char *)szId.c_str(); ProtoChainRecvMsg(pUser->hContact, &pre); } // my own message was sent void FacebookProto::OnPublishSentMessage(const JSONNode &root) { auto &metadata = root["messageMetadata"]; __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); std::string szId(metadata["messageId"].as_string()); CMStringA wszUserId(metadata["threadKey"]["otherUserFbId"].as_mstring()); auto *pUser = FindUser(_atoi64(wszUserId)); if (pUser == nullptr) { debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); return; } for (auto &it : arOwnMessages) if (it->msgId == offlineId) { ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)it->reqId, (LPARAM)szId.c_str()); arOwnMessages.remove(arOwnMessages.indexOf(&it)); break; } }