summaryrefslogtreecommitdiff
path: root/protocols/Facebook
diff options
context:
space:
mode:
authorGeorge Hazan <ghazan@miranda.im>2019-12-27 19:47:00 +0300
committerGeorge Hazan <ghazan@miranda.im>2019-12-27 19:47:00 +0300
commit42b48e4121e22cea97a9d4fc4c671b59004c77b9 (patch)
treeec02a9b233e8fac6993cd6841cc0f7c280929e00 /protocols/Facebook
parent16972f022733285ca95c14b829f1f982924de727 (diff)
Facebook: reading users' avatars
Diffstat (limited to 'protocols/Facebook')
-rw-r--r--protocols/Facebook/src/avatars.cpp331
-rw-r--r--protocols/Facebook/src/proto.cpp5
-rw-r--r--protocols/Facebook/src/proto.h8
-rw-r--r--protocols/Facebook/src/server.cpp20
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));