diff options
author | George Hazan <ghazan@miranda.im> | 2017-01-08 22:47:46 +0300 |
---|---|---|
committer | George Hazan <ghazan@miranda.im> | 2017-01-08 22:47:46 +0300 |
commit | f05340be7d013ac713fd24c19ff8304712223a73 (patch) | |
tree | 93e3ba9441fb0f90a237011778038771e6ccb052 | |
parent | 92f80d349f63f694d0fdcf003a852fd424557663 (diff) |
avatar support for Discord
-rw-r--r-- | protocols/Discord/src/avatars.cpp | 218 | ||||
-rw-r--r-- | protocols/Discord/src/proto.cpp | 5 | ||||
-rw-r--r-- | protocols/Discord/src/proto.h | 12 | ||||
-rw-r--r-- | protocols/Discord/src/server.cpp | 15 | ||||
-rw-r--r-- | protocols/Discord/src/stdafx.h | 2 |
5 files changed, 248 insertions, 4 deletions
diff --git a/protocols/Discord/src/avatars.cpp b/protocols/Discord/src/avatars.cpp new file mode 100644 index 0000000000..aac083789b --- /dev/null +++ b/protocols/Discord/src/avatars.cpp @@ -0,0 +1,218 @@ +/* +Copyright © 2016-17 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" + +CMStringW CDiscordProto::GetAvatarFilename(MCONTACT hContact) +{ + CMStringW wszResult(FORMAT, L"%s\\%S", VARSW(L"%miranda_avatarcache%"), m_szModuleName); + + DWORD dwAttributes = GetFileAttributes(wszResult); + if (dwAttributes == 0xffffffff || (dwAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + CreateDirectoryTreeW(wszResult); + + wszResult.AppendChar('\\'); + + const wchar_t* szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_PNG)); + wszResult.AppendFormat(L"%lld%s", getId(hContact, DB_KEY_ID), szFileType); + return wszResult; +} + +INT_PTR CDiscordProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + int res = 0; + + switch (wParam) { + case AF_MAXSIZE: + ((POINT*)lParam)->x = ((POINT*)lParam)->y = 128; + break; + + case AF_PROPORTION: + res = PIP_NONE; + break; + + case AF_FORMATSUPPORTED: + res = lParam == PA_FORMAT_PNG || lParam == PA_FORMAT_GIF || lParam == PA_FORMAT_JPEG; + break; + + case AF_ENABLED: + res = 1; + break; + } + + return res; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::OnReceiveAvatar(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq) +{ + PROTO_AVATAR_INFORMATION ai = { 0 }; + ai.format = PA_FORMAT_UNKNOWN; + ai.hContact = (MCONTACT)pReq->pUserInfo; + + if (reply->resultCode != 200) { +LBL_Error: + ProtoBroadcastAck(ai.hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, (HANDLE)&ai); + return; + } + + const char *pszMimeType = NULL; + for (int i = 0; i < reply->headersCount; i++) + if (!mir_strcmp(reply->headers[i].szName, "Content-Type")) { + pszMimeType = reply->headers[i].szValue; + break; + } + + if (!mir_strcmp(pszMimeType, "image/jpeg")) + ai.format = PA_FORMAT_JPEG; + else if (!mir_strcmp(pszMimeType, "image/png")) + ai.format = PA_FORMAT_PNG; + else if (!mir_strcmp(pszMimeType, "image/gif")) + ai.format = PA_FORMAT_GIF; + else if (!mir_strcmp(pszMimeType, "image/bmp")) + ai.format = PA_FORMAT_BMP; + else { + debugLogA("unknown avatar mime type: %s", pszMimeType); + goto LBL_Error; + } + + mir_wstrncpy(ai.filename, GetAvatarFilename(ai.hContact), _countof(ai.filename)); + + FILE *out = _wfopen(ai.filename, L"wb"); + if (out == NULL) { + debugLogA("cannot open avatar file %S for writing", ai.filename); + goto LBL_Error; + } + + fwrite(reply->pData, 1, reply->dataLength, out); + fclose(out); + + if (ai.hContact) + ProtoBroadcastAck(ai.hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, (HANDLE)&ai); + else + CallService(MS_AV_REPORTMYAVATARCHANGED, (WPARAM)m_szModuleName, 0); +} + +INT_PTR CDiscordProto::GetAvatarInfo(WPARAM wParam, LPARAM lParam) +{ + PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam; + + CMStringW wszFileName(GetAvatarFilename(pai->hContact)); + if (!wszFileName.IsEmpty()) { + mir_wstrncpy(pai->filename, wszFileName, _countof(pai->filename)); + + bool bFileExist = _waccess(wszFileName, 0) == 0; + + // if we still need to load an avatar + if ((wParam & GAIF_FORCE) || !bFileExist) { + ptrA szAvatarHash(getStringA(pai->hContact, DB_KEY_AVHASH)); + SnowFlake id = getId(pai->hContact, DB_KEY_ID); + if (id == 0 || szAvatarHash == NULL) + return GAIR_NOAVATAR; + + CMStringA szUrl(FORMAT, "https://cdn.discordapp.com/avatars/%lld/%s.jpg", id, szAvatarHash); + AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_GET, szUrl, &CDiscordProto::OnReceiveAvatar); + pReq->pUserInfo = (void*)pai->hContact; + Push(pReq); + return GAIR_WAITFOR; + } + if (bFileExist) + return GAIR_SUCCESS; + } + + return GAIR_NOAVATAR; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CDiscordProto::GetMyAvatar(WPARAM wParam, LPARAM lParam) +{ + if (!wParam || !lParam) + return -3; + + wchar_t* buf = (wchar_t*)wParam; + int size = (int)lParam; + + PROTO_AVATAR_INFORMATION ai = {}; + switch (GetAvatarInfo(0, (LPARAM)&ai)) { + case GAIR_SUCCESS: + wcsncpy_s(buf, size, ai.filename, _TRUNCATE); + return 0; + + case GAIR_WAITFOR: + return -1; + } + + return -2; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CDiscordProto::OnReceiveMyAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + MCONTACT hContact = (MCONTACT)pReq->pUserInfo; + if (pReply->resultCode != 200) + return; + + JSONNode root = JSONNode::parse(pReply->pData); + if (!root) + return; + + setWString(hContact, DB_KEY_AVHASH, root["avatar"].as_mstring()); + CallService(MS_AV_REPORTMYAVATARCHANGED, (WPARAM)m_szModuleName, 0); +} + +INT_PTR CDiscordProto::SetMyAvatar(WPARAM, LPARAM lParam) +{ + CMStringW wszFileName(GetAvatarFilename(NULL)); + + const wchar_t *pwszFilename = (const wchar_t*)lParam; + if (pwszFilename == NULL) { // remove my avatar file + delSetting(DB_KEY_AVHASH); + DeleteFile(wszFileName); + } + + CMStringA szPayload("data:"); + + int iFormat = ProtoGetAvatarFileFormat(pwszFilename); + switch (iFormat) { + case PA_FORMAT_BMP: szPayload.Append("image/bmp"); break; + case PA_FORMAT_GIF: szPayload.Append("image/gif"); break; + case PA_FORMAT_PNG: szPayload.Append("image/png"); break; + case PA_FORMAT_JPEG: szPayload.Append("image/jpeg"); break; + default: + debugLogA("invalid file format for avatar %S: %d", pwszFilename, iFormat); + return 1; + } + szPayload.Append(";base64,"); + FILE *in = _wfopen(pwszFilename, L"rb"); + if (in == NULL) { + debugLogA("cannot open avatar file %S for reading", pwszFilename); + return 2; + } + + int iFileLength = _filelength(_fileno(in)); + ptrA szFileContents((char*)mir_alloc(iFileLength)); + fread(szFileContents, 1, iFileLength, in); + fclose(in); + szPayload.Append(ptrA(mir_base64_encode((BYTE*)szFileContents.get(), iFileLength))); + + JSONNode root; root << CHAR_PARAM("avatar", szPayload); + Push(new AsyncHttpRequest(this, REQUEST_PATCH, "/users/@me", &CDiscordProto::OnReceiveMyAvatar, &root)); + return 0; +} diff --git a/protocols/Discord/src/proto.cpp b/protocols/Discord/src/proto.cpp index 79751ba1b3..71ed6dc8b2 100644 --- a/protocols/Discord/src/proto.cpp +++ b/protocols/Discord/src/proto.cpp @@ -38,6 +38,11 @@ CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) : // Services CreateProtoService(PS_GETSTATUS, &CDiscordProto::GetStatus); + CreateProtoService(PS_GETAVATARINFO, &CDiscordProto::GetAvatarInfo); + CreateProtoService(PS_GETAVATARCAPS, &CDiscordProto::GetAvatarCaps); + CreateProtoService(PS_GETMYAVATAR, &CDiscordProto::GetMyAvatar); + CreateProtoService(PS_SETMYAVATAR, &CDiscordProto::SetMyAvatar); + // Events HookProtoEvent(ME_OPT_INITIALISE, &CDiscordProto::OnOptionsInit); HookProtoEvent(ME_MSG_WINDOWEVENT, &CDiscordProto::OnSrmmEvent); diff --git a/protocols/Discord/src/proto.h b/protocols/Discord/src/proto.h index a9a948ce7d..6996b54aed 100644 --- a/protocols/Discord/src/proto.h +++ b/protocols/Discord/src/proto.h @@ -202,6 +202,10 @@ public: // Services INT_PTR __cdecl GetStatus(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); + INT_PTR __cdecl GetMyAvatar(WPARAM, LPARAM); + INT_PTR __cdecl SetMyAvatar(WPARAM, LPARAM); // Events int __cdecl OnModulesLoaded(WPARAM, LPARAM); @@ -219,12 +223,14 @@ public: void OnLoggedOut(); void OnReceiveAuth(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - void OnReceiveToken(NETLIBHTTPREQUEST*, AsyncHttpRequest*); - void OnReceiveGuilds(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + void OnReceiveAvatar(NETLIBHTTPREQUEST*, AsyncHttpRequest*); void OnReceiveChannels(NETLIBHTTPREQUEST*, AsyncHttpRequest*); void OnReceiveFriends(NETLIBHTTPREQUEST*, AsyncHttpRequest*); void OnReceiveGateway(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + void OnReceiveGuilds(NETLIBHTTPREQUEST*, AsyncHttpRequest*); void OnReceiveMessageAck(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + void OnReceiveMyAvatar(NETLIBHTTPREQUEST*, AsyncHttpRequest*); + void OnReceiveToken(NETLIBHTTPREQUEST*, AsyncHttpRequest*); void RetrieveUserInfo(MCONTACT hContact); void OnReceiveUserInfo(NETLIBHTTPREQUEST*, AsyncHttpRequest*); @@ -235,6 +241,8 @@ public: // Misc void SetServerStatus(int iStatus); + CMStringW GetAvatarFilename(MCONTACT hContact); + static void CALLBACK HeartbeatTimerProc(HWND hwnd, UINT msg, UINT_PTR id, DWORD); __forceinline int getHeartbeatInterval() const { return m_iHartbeatInterval; } }; diff --git a/protocols/Discord/src/server.cpp b/protocols/Discord/src/server.cpp index c2c539f4d9..9164107309 100644 --- a/protocols/Discord/src/server.cpp +++ b/protocols/Discord/src/server.cpp @@ -110,17 +110,28 @@ void CDiscordProto::OnReceiveUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpReques return; } + ptrW wszOldAvatar(getWStringA(hContact, DB_KEY_AVHASH)); + m_ownId = _wtoi64(root["id"].as_mstring()); setId(hContact, DB_KEY_ID, m_ownId); setByte(hContact, DB_KEY_MFA, root["mfa_enabled"].as_bool()); setDword(hContact, DB_KEY_DISCR, root["discriminator"].as_int()); setWString(hContact, DB_KEY_NICK, root["username"].as_mstring()); - setWString(hContact, DB_KEY_AVHASH, root["avatar"].as_mstring()); setWString(hContact, DB_KEY_EMAIL, root["email"].as_mstring()); - if (hContact == NULL) + CMStringW wszNewAvatar(root["avatar"].as_mstring()); + setWString(hContact, DB_KEY_AVHASH, wszNewAvatar); + + if (hContact == NULL) { + // if avatar's hash changed, we need to request a new one + if (mir_wstrcmp(wszNewAvatar, wszOldAvatar)) { + PROTO_AVATAR_INFORMATION ai = {}; + GetAvatarInfo(GAIF_FORCE, (LPARAM)&ai); + } + OnLoggedIn(); + } } ///////////////////////////////////////////////////////////////////////////////////////// diff --git a/protocols/Discord/src/stdafx.h b/protocols/Discord/src/stdafx.h index 4bfc9448ae..80d87bc6de 100644 --- a/protocols/Discord/src/stdafx.h +++ b/protocols/Discord/src/stdafx.h @@ -9,6 +9,7 @@ #include <Shlwapi.h> #include <Wincrypt.h> #include <stdio.h> +#include <io.h> #include <direct.h> #include <time.h> @@ -37,6 +38,7 @@ #include <m_utils.h> #include <m_hotkeys.h> #include <m_json.h> +#include <m_avatars.h> #include <win2k.h> #include "../../libs/zlib/src/zlib.h" |