diff options
author | George Hazan <ghazan@miranda.im> | 2019-12-27 19:47:00 +0300 |
---|---|---|
committer | George Hazan <ghazan@miranda.im> | 2019-12-27 19:47:00 +0300 |
commit | 42b48e4121e22cea97a9d4fc4c671b59004c77b9 (patch) | |
tree | ec02a9b233e8fac6993cd6841cc0f7c280929e00 /protocols/Facebook | |
parent | 16972f022733285ca95c14b829f1f982924de727 (diff) |
Facebook: reading users' avatars
Diffstat (limited to 'protocols/Facebook')
-rw-r--r-- | protocols/Facebook/src/avatars.cpp | 331 | ||||
-rw-r--r-- | protocols/Facebook/src/proto.cpp | 5 | ||||
-rw-r--r-- | protocols/Facebook/src/proto.h | 8 | ||||
-rw-r--r-- | protocols/Facebook/src/server.cpp | 20 |
4 files changed, 361 insertions, 3 deletions
diff --git a/protocols/Facebook/src/avatars.cpp b/protocols/Facebook/src/avatars.cpp new file mode 100644 index 0000000000..624fb36f7e --- /dev/null +++ b/protocols/Facebook/src/avatars.cpp @@ -0,0 +1,331 @@ +/* + +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 <http://www.gnu.org/licenses/>. + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +void FacebookProto::GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName) +{ + WCHAR wszPath[MAX_PATH]; + mir_snwprintf(wszPath, MAX_PATH, L"%s\\%S", VARSW(L"%miranda_avatarcache%"), m_szModuleName); + + DWORD dwAttributes = GetFileAttributes(wszPath); + if (dwAttributes == 0xffffffff || (dwAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + 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; + + const char *szParams = m_bUseBigAvatars ? "width=200&height=200" : "width=80&height=80"; + + 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); + 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) { + for (int i = 0; i < pReply->headersCount; i++) + if (!mir_strcmp(pReply->headers[i].szName, "Content-Type")) { + ai.format = ProtoGetAvatarFormatByMimeType(pReply->headers[i].szValue); + break; + } + + 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; +} + + +/* +bool FacebookProto::GetDbAvatarInfo(PROTO_AVATAR_INFORMATION &pai, std::string *url) +{ + ptrA id(getStringA(pai.hContact, FACEBOOK_KEY_ID)); + if (id == nullptr) + return false; + + if (url) { + *url = FACEBOOK_URL_PICTURE; + utils::text::replace_first(url, "%s", std::string(id)); + } + + std::wstring filename = GetAvatarFolder() + L'\\' + std::wstring(_A2T(id)) + L".jpg"; + + wcsncpy_s(pai.filename, filename.c_str(), _TRUNCATE); + pai.format = ProtoGetAvatarFormat(pai.filename); + return true; +} + +void FacebookProto::CheckAvatarChange(MCONTACT hContact, const std::string &image_url) +{ + std::wstring::size_type pos = image_url.rfind("/"); + + // Facebook contacts always have some avatar - keep avatar in database even if we have loaded empty one (e.g. for 'On Mobile' contacts) + if (image_url.empty() || pos == std::wstring::npos) + return; + + // Get name of image + std::string image_name = image_url.substr(pos + 1); + + // Remove eventual parameters from name + pos = image_name.rfind("?"); + if (pos != std::wstring::npos) + image_name = image_name.substr(0, pos); + + // Append our parameters to allow comparing for avatar/settings change + if (getBool(FACEBOOK_KEY_BIG_AVATARS, DEFAULT_BIG_AVATARS)) + image_name += "?big"; + + // Check for avatar change + ptrA old_name(getStringA(hContact, FACEBOOK_KEY_AVATAR)); + bool update_required = (old_name == nullptr || image_name.compare(old_name) != 0); + + // TODO: Remove this in some newer version + if (old_name == nullptr) { + // Remove AvatarURL value, which was used in previous versions of plugin + delSetting(hContact, "AvatarURL"); + } + + if (update_required) + setString(hContact, FACEBOOK_KEY_AVATAR, image_name.c_str()); + + if (!hContact) { + PROTO_AVATAR_INFORMATION ai = { 0 }; + if (GetAvatarInfo(update_required ? GAIF_FORCE : 0, (LPARAM)&ai) != GAIR_WAITFOR) + ReportSelfAvatarChanged(); + } + else if (update_required) { + db_set_b(hContact, "ContactPhoto", "NeedUpdate", 1); + ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_STATUS, nullptr); + } +} + +void FacebookProto::UpdateAvatarWorker(void *) +{ + HNETLIBCONN nlc = nullptr; + + debugLogA("*** UpdateAvatarWorker"); + + std::string params = getBool(FACEBOOK_KEY_BIG_AVATARS, DEFAULT_BIG_AVATARS) ? "?width=200&height=200" : "?width=80&height=80"; + + for (;;) { + std::string url; + PROTO_AVATAR_INFORMATION ai = { 0 }; + ai.hContact = avatar_queue[0]; + + if (Miranda_IsTerminated()) { + debugLogA("*** Terminating avatar update early: %s", url.c_str()); + break; + } + + if (GetDbAvatarInfo(ai, &url)) { + debugLogA("*** Updating avatar: %s", url.c_str()); + bool success = facy.save_url(url + params, ai.filename, nlc); + + if (ai.hContact) + ProtoBroadcastAck(ai.hContact, ACKTYPE_AVATAR, success ? ACKRESULT_SUCCESS : ACKRESULT_FAILED, (HANDLE)&ai); + else if (success) + ReportSelfAvatarChanged(); + } + + mir_cslock s(avatar_lock_); + avatar_queue.erase(avatar_queue.begin()); + if (avatar_queue.empty()) + break; + } + Netlib_CloseHandle(nlc); +} + +std::wstring FacebookProto::GetAvatarFolder() +{ + wchar_t path[MAX_PATH]; + mir_snwprintf(path, L"%s\\%s", VARSW(L"%miranda_avatarcache%"), m_tszUserName); + return path; +} + +INT_PTR FacebookProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + int res = 0; + + switch (wParam) { + case AF_MAXSIZE: + ((POINT*)lParam)->x = -1; + ((POINT*)lParam)->y = -1; + break; + + case AF_FORMATSUPPORTED: + res = (lParam == PA_FORMAT_JPEG || lParam == PA_FORMAT_GIF); + break; + + case AF_DELAYAFTERFAIL: + res = 10 * 60 * 1000; + break; + + case AF_ENABLED: + case AF_FETCHIFPROTONOTVISIBLE: + case AF_FETCHIFCONTACTOFFLINE: + res = 1; + break; + } + + return res; +} + +INT_PTR FacebookProto::GetAvatarInfo(WPARAM wParam, LPARAM lParam) +{ + if (!lParam) + return GAIR_NOAVATAR; + + PROTO_AVATAR_INFORMATION* pai = (PROTO_AVATAR_INFORMATION*)lParam; + if (GetDbAvatarInfo(*pai, nullptr)) { + bool fileExist = _waccess(pai->filename, 0) == 0; + + bool needLoad; + if (pai->hContact) + needLoad = (wParam & GAIF_FORCE) && (!fileExist || db_get_b(pai->hContact, "ContactPhoto", "NeedUpdate", 0)); + else + needLoad = (wParam & GAIF_FORCE) || !fileExist; + + if (needLoad) { + debugLogA("*** Starting avatar request thread for %s", _T2A(pai->filename)); + mir_cslock s(avatar_lock_); + + if (std::find(avatar_queue.begin(), avatar_queue.end(), pai->hContact) == avatar_queue.end()) { + bool is_empty = avatar_queue.empty(); + avatar_queue.push_back(pai->hContact); + if (is_empty) + ForkThread(&FacebookProto::UpdateAvatarWorker, nullptr); + } + return GAIR_WAITFOR; + } + else if (fileExist) + return GAIR_SUCCESS; + + } + return GAIR_NOAVATAR; +} + +INT_PTR FacebookProto::GetMyAvatar(WPARAM wParam, LPARAM lParam) +{ + debugLogA("*** GetMyAvatar"); + + if (!wParam || !lParam) + return -3; + + wchar_t* buf = (wchar_t*)wParam; + int size = (int)lParam; + + PROTO_AVATAR_INFORMATION ai = { 0 }; + switch (GetAvatarInfo(0, (LPARAM)&ai)) { + case GAIR_SUCCESS: + wcsncpy(buf, ai.filename, size); + buf[size - 1] = 0; + return 0; + + case GAIR_WAITFOR: + return -1; + + default: + return -2; + } +} +*/
\ No newline at end of file diff --git a/protocols/Facebook/src/proto.cpp b/protocols/Facebook/src/proto.cpp index d5f153d3ba..381547bf10 100644 --- a/protocols/Facebook/src/proto.cpp +++ b/protocols/Facebook/src/proto.cpp @@ -31,6 +31,7 @@ static int CompareUsers(const FacebookUser *p1, const FacebookUser *p2) FacebookProto::FacebookProto(const char *proto_name, const wchar_t *username) : PROTO<FacebookProto>(proto_name, username), m_users(50, CompareUsers), + m_bUseBigAvatars(this, "UseBigAvatars", true), m_wszDefaultGroup(this, "DefaultGroup", L"Facebook") { for (auto &cc : AccContacts()) { @@ -85,8 +86,12 @@ FacebookProto::FacebookProto(const char *proto_name, const wchar_t *username) : nlu.szDescriptiveName.w = descr; 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_OPT_INITIALISE, &FacebookProto::OnOptionsInit); diff --git a/protocols/Facebook/src/proto.h b/protocols/Facebook/src/proto.h index 3e14326a14..6a91ca9fbd 100644 --- a/protocols/Facebook/src/proto.h +++ b/protocols/Facebook/src/proto.h @@ -290,7 +290,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. * 0: xma_id */ #define FB_API_QUERY_XMA 10153919431161729 -#define FB_API_QUERY_XMA_STR (#FB_API_QUERY_XMA) /** * FB_API_CONTACTS_COUNT: @@ -360,6 +359,10 @@ class FacebookProto : public PROTO<FacebookProto> AsyncHttpRequest *CreateRequestGQL(int64_t id); NETLIBHTTPREQUEST *ExecuteRequest(AsyncHttpRequest *pReq); + // Avatars + void __cdecl AvatarsUpdate(void *); + void GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName); + // MQTT void MqttLogin(); @@ -423,6 +426,7 @@ public: // options CMOption<wchar_t *> m_wszDefaultGroup; // clist group to store contacts + CMOption<BYTE> m_bUseBigAvatars; // use big or small avatars by default //////////////////////////////////////////////////////////////////////////////////////// // PROTO_INTERFACE @@ -442,6 +446,8 @@ public: //////////////////////////////////////////////////////////////////////////////////////// // Services + INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); }; diff --git a/protocols/Facebook/src/server.cpp b/protocols/Facebook/src/server.cpp index 5c440808b1..b87ba413ef 100644 --- a/protocols/Facebook/src/server.cpp +++ b/protocols/Facebook/src/server.cpp @@ -99,6 +99,8 @@ bool FacebookProto::RefreshContacts() 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()); @@ -140,9 +142,18 @@ bool FacebookProto::RefreshContacts() if (auto &nCity = n["current_city"]) setWString(hContact, "City", nCity["name"].as_mstring()); - if (auto &nAva = it["smallPictureUrl"]) - setWString(hContact, "Avatar", nAva["uri"].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; } @@ -396,6 +407,11 @@ void FacebookProto::OnPublishMessage(FbThriftReader &rdr) 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)); |