summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Hazan <ghazan@miranda.im>2017-01-08 22:47:46 +0300
committerGeorge Hazan <ghazan@miranda.im>2017-01-08 22:47:46 +0300
commitf05340be7d013ac713fd24c19ff8304712223a73 (patch)
tree93e3ba9441fb0f90a237011778038771e6ccb052
parent92f80d349f63f694d0fdcf003a852fd424557663 (diff)
avatar support for Discord
-rw-r--r--protocols/Discord/src/avatars.cpp218
-rw-r--r--protocols/Discord/src/proto.cpp5
-rw-r--r--protocols/Discord/src/proto.h12
-rw-r--r--protocols/Discord/src/server.cpp15
-rw-r--r--protocols/Discord/src/stdafx.h2
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"