diff options
author | George Hazan <george.hazan@gmail.com> | 2024-08-14 18:15:52 +0300 |
---|---|---|
committer | George Hazan <george.hazan@gmail.com> | 2024-08-14 18:15:52 +0300 |
commit | f41cd01eae037b3021039e68dd37234126037ea8 (patch) | |
tree | 7eeae941c8265e6cdf5bf9c600bea3b089d75bb7 /protocols | |
parent | 55b728fc501839e93dcf851db0501277e0600c4a (diff) |
Discord: offline files support for message attachments
Diffstat (limited to 'protocols')
-rw-r--r-- | protocols/Discord/discord.vcxproj | 1 | ||||
-rw-r--r-- | protocols/Discord/discord.vcxproj.filters | 3 | ||||
-rw-r--r-- | protocols/Discord/src/files.cpp | 199 | ||||
-rw-r--r-- | protocols/Discord/src/proto.cpp | 112 | ||||
-rw-r--r-- | protocols/Discord/src/proto.h | 13 | ||||
-rw-r--r-- | protocols/Discord/src/utils.cpp | 42 |
6 files changed, 252 insertions, 118 deletions
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 @@ <ClCompile Include="src\avatars.cpp" />
<ClCompile Include="src\connection.cpp" />
<ClCompile Include="src\dispatch.cpp" />
+ <ClCompile Include="src\files.cpp" />
<ClCompile Include="src\gateway.cpp" />
<ClCompile Include="src\groupchat.cpp" />
<ClCompile Include="src\guilds.cpp" />
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 @@ <ClCompile Include="src\voice_client.cpp">
<Filter>Source Files</Filter>
</ClCompile>
+ <ClCompile Include="src\files.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\proto.h">
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 <http://www.gnu.org/licenses/>. +*/ + +#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<CDiscordProto> 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<CDiscordProto> 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-----------------");
|