From f41cd01eae037b3021039e68dd37234126037ea8 Mon Sep 17 00:00:00 2001 From: George Hazan Date: Wed, 14 Aug 2024 18:15:52 +0300 Subject: Discord: offline files support for message attachments --- protocols/Discord/discord.vcxproj | 1 + protocols/Discord/discord.vcxproj.filters | 3 + protocols/Discord/src/files.cpp | 199 ++++++++++++++++++++++++++++++ protocols/Discord/src/proto.cpp | 112 +---------------- protocols/Discord/src/proto.h | 13 ++ protocols/Discord/src/utils.cpp | 42 +++++-- 6 files changed, 252 insertions(+), 118 deletions(-) create mode 100644 protocols/Discord/src/files.cpp (limited to 'protocols') diff --git a/protocols/Discord/discord.vcxproj b/protocols/Discord/discord.vcxproj index edf662c623..78e27176a0 100644 --- a/protocols/Discord/discord.vcxproj +++ b/protocols/Discord/discord.vcxproj @@ -29,6 +29,7 @@ + diff --git a/protocols/Discord/discord.vcxproj.filters b/protocols/Discord/discord.vcxproj.filters index 7316208d56..0fd41fa1cb 100644 --- a/protocols/Discord/discord.vcxproj.filters +++ b/protocols/Discord/discord.vcxproj.filters @@ -53,6 +53,9 @@ Source Files + + Source Files + diff --git a/protocols/Discord/src/files.cpp b/protocols/Discord/src/files.cpp new file mode 100644 index 0000000000..163f1c7c72 --- /dev/null +++ b/protocols/Discord/src/files.cpp @@ -0,0 +1,199 @@ +/* +Copyright © 2016-22 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" + +#pragma comment(lib, "opus.lib") + +///////////////////////////////////////////////////////////////////////////////////////// +// Receiving files + +static void __cdecl DownloadCallack(size_t iProgress, void *pParam) +{ + auto *ofd = (OFDTHREAD *)pParam; + + DBVARIANT dbv = { DBVT_DWORD }; + dbv.dVal = unsigned(iProgress); + db_event_setJson(ofd->hDbEvent, "ft", &dbv); +} + +void CDiscordProto::OfflineFileThread(void *param) +{ + auto *ofd = (OFDTHREAD *)param; + + DB::EventInfo dbei(ofd->hDbEvent); + if (m_bOnline && dbei && !strcmp(dbei.szModule, m_szModuleName) && dbei.eventType == EVENTTYPE_FILE) { + DB::FILE_BLOB blob(dbei); + + if (!ofd->bCopy) { + MHttpRequest nlhr(REQUEST_GET); + nlhr.flags = NLHRF_HTTP11 | NLHRF_SSL; + nlhr.m_szUrl = blob.getUrl(); + if (!m_szFileCookie.IsEmpty()) + nlhr.AddHeader("Cookie", m_szFileCookie); + + debugLogW(L"Saving to [%s]", ofd->wszPath.c_str()); + NLHR_PTR reply(Netlib_DownloadFile(m_hNetlibUser, &nlhr, ofd->wszPath, DownloadCallack, ofd)); + if (reply && reply->resultCode == 200) { + if (m_szFileCookie.IsEmpty()) + m_szFileCookie = reply->GetCookies(); + + struct _stat st; + _wstat(ofd->wszPath, &st); + + DBVARIANT dbv = { DBVT_DWORD }; + dbv.dVal = st.st_size; + db_event_setJson(ofd->hDbEvent, "ft", &dbv); + + ofd->Finish(); + } + } + else { + ofd->wszPath.Empty(); + ofd->wszPath.Append(_A2T(blob.getUrl())); + ofd->pCallback->Invoke(*ofd); + } + } + + delete ofd; +} + +INT_PTR CDiscordProto::SvcOfflineFile(WPARAM param, LPARAM) +{ + ForkThread((MyThreadFunc)&CDiscordProto::OfflineFileThread, (void *)param); + return 0; +} + +void CDiscordProto::OnReceiveOfflineFile(DB::FILE_BLOB &blob) +{ + if (auto *ft = (CDiscordAttachment *)blob.getUserInfo()) { + blob.setUrl(ft->szUrl.GetBuffer()); + blob.setSize(ft->iFileSize); + delete ft; + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Sending files + +struct SendFileThreadParam +{ + MCONTACT hContact; + CMStringW wszDescr, wszFileName; + + SendFileThreadParam(MCONTACT _p1, LPCWSTR _p2, LPCWSTR _p3) : + hContact(_p1), + wszFileName(_p2), + wszDescr(_p3) + {} +}; + +void CDiscordProto::SendFileThread(void *param) +{ + SendFileThreadParam *p = (SendFileThreadParam *)param; + + FILE *in = _wfopen(p->wszFileName, L"rb"); + if (in == nullptr) { + debugLogA("cannot open file %S for reading", p->wszFileName.c_str()); +LBL_Error: + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, param); + delete p; + return; + } + + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, param); + + char szRandom[16], szRandomText[33]; + Utils_GetRandom(szRandom, _countof(szRandom)); + bin2hex(szRandom, _countof(szRandom), szRandomText); + CMStringA szBoundary(FORMAT, "----Boundary%s", szRandomText); + + CMStringA szUrl(FORMAT, "/channels/%lld/messages", getId(p->hContact, DB_KEY_CHANNELID)); + AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnReceiveFile); + pReq->AddHeader("Content-Type", CMStringA("multipart/form-data; boundary=" + szBoundary)); + pReq->AddHeader("Accept", "*/*"); + + szBoundary.Insert(0, "--"); + + CMStringA szBody; + szBody.Append(szBoundary + "\r\n"); + szBody.Append("Content-Disposition: form-data; name=\"content\"\r\n\r\n"); + szBody.Append(ptrA(mir_utf8encodeW(p->wszDescr))); + szBody.Append("\r\n"); + + szBody.Append(szBoundary + "\r\n"); + szBody.Append("Content-Disposition: form-data; name=\"tts\"\r\n\r\nfalse\r\n"); + + wchar_t *pFileName = wcsrchr(p->wszFileName.GetBuffer(), '\\'); + if (pFileName != nullptr) + pFileName++; + else + pFileName = p->wszFileName.GetBuffer(); + + szBody.Append(szBoundary + "\r\n"); + szBody.AppendFormat("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n", ptrA(mir_utf8encodeW(pFileName)).get()); + szBody.AppendFormat("Content-Type: %S\r\n", ProtoGetAvatarMimeType(ProtoGetAvatarFileFormat(p->wszFileName))); + szBody.Append("\r\n"); + + size_t cbBytes = filelength(fileno(in)); + + szBoundary.Insert(0, "\r\n"); + szBoundary.Append("--\r\n"); + pReq->m_szParam.Truncate(int(szBody.GetLength() + szBoundary.GetLength() + cbBytes)); + + memcpy(pReq->m_szParam.GetBuffer(), szBody.c_str(), szBody.GetLength()); + size_t cbRead = fread(pReq->m_szParam.GetBuffer() + szBody.GetLength(), 1, cbBytes, in); + fclose(in); + if (cbBytes != cbRead) { + debugLogA("cannot read file %S: %d bytes read instead of %d", p->wszFileName.c_str(), cbRead, cbBytes); + delete pReq; + goto LBL_Error; + } + + memcpy(pReq->m_szParam.GetBuffer() + szBody.GetLength() + cbBytes, szBoundary, szBoundary.GetLength()); + pReq->pUserInfo = p; + Push(pReq); + + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTED, param); +} + +void CDiscordProto::OnReceiveFile(MHttpResponse *pReply, AsyncHttpRequest *pReq) +{ + SendFileThreadParam *p = (SendFileThreadParam *)pReq->pUserInfo; + if (pReply->resultCode != 200) { + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, p); + debugLogA("CDiscordProto::SendFile failed: %d", pReply->resultCode); + } + else { + ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, p); + debugLogA("CDiscordProto::SendFile succeeded"); + } + + delete p; +} + +HANDLE CDiscordProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) +{ + SnowFlake id = getId(hContact, DB_KEY_CHANNELID); + if (id == 0) + return nullptr; + + // we don't wanna block the main thread, right? + SendFileThreadParam *param = new SendFileThreadParam(hContact, ppszFiles[0], szDescription); + ForkThread(&CDiscordProto::SendFileThread, param); + return param; +} diff --git a/protocols/Discord/src/proto.cpp b/protocols/Discord/src/proto.cpp index 7bf3dbcf0d..96ba582377 100644 --- a/protocols/Discord/src/proto.cpp +++ b/protocols/Discord/src/proto.cpp @@ -80,6 +80,8 @@ CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) : CreateProtoService(PS_LEAVECHAT, &CDiscordProto::SvcLeaveChat); + CreateProtoService(PS_OFFLINEFILE, &CDiscordProto::SvcOfflineFile); + CreateProtoService(PS_VOICE_CAPS, &CDiscordProto::VoiceCaps); // Events @@ -686,113 +688,3 @@ INT_PTR CDiscordProto::RequestFriendship(WPARAM hContact, LPARAM) AuthRequest(hContact, 0); return 0; } - -///////////////////////////////////////////////////////////////////////////////////////// - -struct SendFileThreadParam -{ - MCONTACT hContact; - CMStringW wszDescr, wszFileName; - - SendFileThreadParam(MCONTACT _p1, LPCWSTR _p2, LPCWSTR _p3) : - hContact(_p1), - wszFileName(_p2), - wszDescr(_p3) - {} -}; - -void CDiscordProto::SendFileThread(void *param) -{ - SendFileThreadParam *p = (SendFileThreadParam*)param; - - FILE *in = _wfopen(p->wszFileName, L"rb"); - if (in == nullptr) { - debugLogA("cannot open file %S for reading", p->wszFileName.c_str()); - LBL_Error: - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, param); - delete p; - return; - } - - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, param); - - char szRandom[16], szRandomText[33]; - Utils_GetRandom(szRandom, _countof(szRandom)); - bin2hex(szRandom, _countof(szRandom), szRandomText); - CMStringA szBoundary(FORMAT, "----Boundary%s", szRandomText); - - CMStringA szUrl(FORMAT, "/channels/%lld/messages", getId(p->hContact, DB_KEY_CHANNELID)); - AsyncHttpRequest *pReq = new AsyncHttpRequest(this, REQUEST_POST, szUrl, &CDiscordProto::OnReceiveFile); - pReq->AddHeader("Content-Type", CMStringA("multipart/form-data; boundary=" + szBoundary)); - pReq->AddHeader("Accept", "*/*"); - - szBoundary.Insert(0, "--"); - - CMStringA szBody; - szBody.Append(szBoundary + "\r\n"); - szBody.Append("Content-Disposition: form-data; name=\"content\"\r\n\r\n"); - szBody.Append(ptrA(mir_utf8encodeW(p->wszDescr))); - szBody.Append("\r\n"); - - szBody.Append(szBoundary + "\r\n"); - szBody.Append("Content-Disposition: form-data; name=\"tts\"\r\n\r\nfalse\r\n"); - - wchar_t *pFileName = wcsrchr(p->wszFileName.GetBuffer(), '\\'); - if (pFileName != nullptr) - pFileName++; - else - pFileName = p->wszFileName.GetBuffer(); - - szBody.Append(szBoundary + "\r\n"); - szBody.AppendFormat("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n", ptrA(mir_utf8encodeW(pFileName)).get()); - szBody.AppendFormat("Content-Type: %S\r\n", ProtoGetAvatarMimeType(ProtoGetAvatarFileFormat(p->wszFileName))); - szBody.Append("\r\n"); - - size_t cbBytes = filelength(fileno(in)); - - szBoundary.Insert(0, "\r\n"); - szBoundary.Append("--\r\n"); - pReq->m_szParam.Truncate(int(szBody.GetLength() + szBoundary.GetLength() + cbBytes)); - - memcpy(pReq->m_szParam.GetBuffer(), szBody.c_str(), szBody.GetLength()); - size_t cbRead = fread(pReq->m_szParam.GetBuffer() + szBody.GetLength(), 1, cbBytes, in); - fclose(in); - if (cbBytes != cbRead) { - debugLogA("cannot read file %S: %d bytes read instead of %d", p->wszFileName.c_str(), cbRead, cbBytes); - delete pReq; - goto LBL_Error; - } - - memcpy(pReq->m_szParam.GetBuffer() + szBody.GetLength() + cbBytes, szBoundary, szBoundary.GetLength()); - pReq->pUserInfo = p; - Push(pReq); - - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTED, param); -} - -void CDiscordProto::OnReceiveFile(MHttpResponse *pReply, AsyncHttpRequest *pReq) -{ - SendFileThreadParam *p = (SendFileThreadParam*)pReq->pUserInfo; - if (pReply->resultCode != 200) { - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, p); - debugLogA("CDiscordProto::SendFile failed: %d", pReply->resultCode); - } - else { - ProtoBroadcastAck(p->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, p); - debugLogA("CDiscordProto::SendFile succeeded"); - } - - delete p; -} - -HANDLE CDiscordProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) -{ - SnowFlake id = getId(hContact, DB_KEY_CHANNELID); - if (id == 0) - return nullptr; - - // we don't wanna block the main thread, right? - SendFileThreadParam *param = new SendFileThreadParam(hContact, ppszFiles[0], szDescription); - ForkThread(&CDiscordProto::SendFileThread, param); - return param; -} diff --git a/protocols/Discord/src/proto.h b/protocols/Discord/src/proto.h index 4a5c17a49b..efee984afa 100644 --- a/protocols/Discord/src/proto.h +++ b/protocols/Discord/src/proto.h @@ -273,6 +273,14 @@ struct CDiscordGuild : public MZeroedObject ///////////////////////////////////////////////////////////////////////////////////////// +struct CDiscordAttachment : public MZeroedObject +{ + CMStringA szFileName, szUrl; + int iFileSize; +}; + +///////////////////////////////////////////////////////////////////////////////////////// + #define OPCODE_DISPATCH 0 #define OPCODE_HEARTBEAT 1 #define OPCODE_IDENTIFY 2 @@ -331,6 +339,7 @@ class CDiscordProto : public PROTO void __cdecl SearchThread(void *param); void __cdecl BatchChatCreate(void* param); void __cdecl GetAwayMsgThread(void *param); + void __cdecl OfflineFileThread(void *param); ////////////////////////////////////////////////////////////////////////////////////// // session control @@ -366,6 +375,7 @@ class CDiscordProto : public PROTO m_szGateway, // gateway url m_szGatewaySessionId, // current session id m_szCookie, // cookie used for all http queries + m_szFileCookie, // cookie used for files downloads from CDN m_szWSCookie; // cookie used for establishing websocket connection HNETLIBUSER m_hGatewayNetlibUser; // the separate netlib user handle for gateways @@ -550,6 +560,7 @@ public: MWindow OnCreateAccMgrUI(MWindow) override; void OnMarkRead(MCONTACT, MEVENT) override; void OnModulesLoaded() override; + void OnReceiveOfflineFile(DB::FILE_BLOB &blob); void OnShutdown() override; ////////////////////////////////////////////////////////////////////////////////////// @@ -557,6 +568,8 @@ public: INT_PTR __cdecl RequestFriendship(WPARAM, LPARAM); + INT_PTR __cdecl SvcOfflineFile(WPARAM, LPARAM); + INT_PTR __cdecl SvcLeaveChat(WPARAM, LPARAM); INT_PTR __cdecl SvcEmptyServerHistory(WPARAM, LPARAM); diff --git a/protocols/Discord/src/utils.cpp b/protocols/Discord/src/utils.cpp index 1790c934cf..3132407b03 100644 --- a/protocols/Discord/src/utils.cpp +++ b/protocols/Discord/src/utils.cpp @@ -360,19 +360,45 @@ CDiscordUser* CDiscordProto::PrepareUser(const JSONNode &user) CMStringW CDiscordProto::PrepareMessageText(const JSONNode &pRoot, CDiscordUser *pUser) { CMStringW wszText = pRoot["content"].as_mstring(); + CMStringA szUserId = pRoot["author"]["id"].as_mstring(); - bool bDelimiterAdded = false; + bool bFilesAdded = false; for (auto &it : pRoot["attachments"]) { - CMStringW wszUrl = it["url"].as_mstring(); - if (!wszUrl.IsEmpty()) { - if (!bDelimiterAdded) { - bDelimiterAdded = true; - wszText.Append(L"\n-----------------"); - } - wszText.AppendFormat(L"\n%s: %s", TranslateT("Attachment"), wszUrl.c_str()); + CMStringA szUrl = it["url"].as_mstring(); + if (szUrl.IsEmpty()) + continue; + + bFilesAdded = true; + CMStringA szId = it["id"].as_mstring(); + + auto *pFile = new CDiscordAttachment(); + pFile->szUrl = szUrl; + pFile->szFileName = it["filename"].as_mstring(); + pFile->iFileSize = it["size"].as_int(); + + T2Utf szDescr(wszText); + + DB::EventInfo dbei(db_event_getById(m_szModuleName, szId)); + dbei.flags = DBEF_TEMPORARY; + dbei.timestamp = (uint32_t)StringToDate(pRoot["timestamp"].as_mstring()); + dbei.szId = szId; + dbei.szUserId = szUserId; + if (_atoi64(szUserId) == m_ownId) + dbei.flags |= DBEF_READ | DBEF_SENT; + + if (dbei) { + DB::FILE_BLOB blob(dbei); + OnReceiveOfflineFile(blob); + blob.write(dbei); + db_event_edit(dbei.getEvent(), &dbei, true); + delete pFile; } + else ProtoChainRecvFile(pUser->hContact, DB::FILE_BLOB(pFile, pFile->szFileName, szDescr), dbei); } + if (bFilesAdded) + return L""; + for (auto &it : pRoot["embeds"]) { wszText.Append(L"\n-----------------"); -- cgit v1.2.3