summaryrefslogtreecommitdiff
path: root/protocols
diff options
context:
space:
mode:
authorGeorge Hazan <george.hazan@gmail.com>2024-08-14 18:15:52 +0300
committerGeorge Hazan <george.hazan@gmail.com>2024-08-14 18:15:52 +0300
commitf41cd01eae037b3021039e68dd37234126037ea8 (patch)
tree7eeae941c8265e6cdf5bf9c600bea3b089d75bb7 /protocols
parent55b728fc501839e93dcf851db0501277e0600c4a (diff)
Discord: offline files support for message attachments
Diffstat (limited to 'protocols')
-rw-r--r--protocols/Discord/discord.vcxproj1
-rw-r--r--protocols/Discord/discord.vcxproj.filters3
-rw-r--r--protocols/Discord/src/files.cpp199
-rw-r--r--protocols/Discord/src/proto.cpp112
-rw-r--r--protocols/Discord/src/proto.h13
-rw-r--r--protocols/Discord/src/utils.cpp42
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-----------------");