summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--protocols/Teams/Teams.vcxproj18
-rw-r--r--protocols/Teams/Teams.vcxproj.filters50
-rw-r--r--protocols/Teams/src/main.cpp24
-rw-r--r--protocols/Teams/src/proto.h102
-rw-r--r--protocols/Teams/src/requests/capabilities.h41
-rw-r--r--protocols/Teams/src/requests/chatrooms.h92
-rw-r--r--protocols/Teams/src/requests/contacts.h89
-rw-r--r--protocols/Teams/src/requests/history.h56
-rw-r--r--protocols/Teams/src/requests/poll.h38
-rw-r--r--protocols/Teams/src/requests/profile.h33
-rw-r--r--protocols/Teams/src/requests/search.h33
-rw-r--r--protocols/Teams/src/requests/status.h32
-rw-r--r--protocols/Teams/src/requests/subscriptions.h56
-rw-r--r--protocols/Teams/src/resource.h42
-rw-r--r--protocols/Teams/src/stdafx.h99
-rw-r--r--protocols/Teams/src/teams_avatars.cpp232
-rw-r--r--protocols/Teams/src/teams_chatrooms.cpp605
-rw-r--r--protocols/Teams/src/teams_contacts.cpp290
-rw-r--r--protocols/Teams/src/teams_endpoint.cpp227
-rw-r--r--protocols/Teams/src/teams_files.cpp297
-rw-r--r--protocols/Teams/src/teams_history.cpp151
-rw-r--r--protocols/Teams/src/teams_http.cpp41
-rw-r--r--protocols/Teams/src/teams_login.cpp22
-rw-r--r--protocols/Teams/src/teams_menus.cpp99
-rw-r--r--protocols/Teams/src/teams_menus.h30
-rw-r--r--protocols/Teams/src/teams_messages.cpp326
-rw-r--r--protocols/Teams/src/teams_mood.cpp146
-rw-r--r--protocols/Teams/src/teams_options.cpp37
-rw-r--r--protocols/Teams/src/teams_polling.cpp176
-rw-r--r--protocols/Teams/src/teams_popups.cpp83
-rw-r--r--protocols/Teams/src/teams_profile.cpp147
-rw-r--r--protocols/Teams/src/teams_proto.cpp201
-rw-r--r--protocols/Teams/src/teams_proto.h388
-rw-r--r--protocols/Teams/src/teams_search.cpp62
-rw-r--r--protocols/Teams/src/teams_utils.cpp885
-rw-r--r--protocols/Teams/src/teams_utils.h73
36 files changed, 5157 insertions, 166 deletions
diff --git a/protocols/Teams/Teams.vcxproj b/protocols/Teams/Teams.vcxproj
index 00464f85ee..ad579a582d 100644
--- a/protocols/Teams/Teams.vcxproj
+++ b/protocols/Teams/Teams.vcxproj
@@ -30,13 +30,29 @@
<ClCompile Include="src\stdafx.cxx">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
+ <ClCompile Include="src\teams_avatars.cpp" />
+ <ClCompile Include="src\teams_chatrooms.cpp" />
+ <ClCompile Include="src\teams_contacts.cpp" />
+ <ClCompile Include="src\teams_endpoint.cpp" />
+ <ClCompile Include="src\teams_files.cpp" />
+ <ClCompile Include="src\teams_history.cpp" />
<ClCompile Include="src\teams_http.cpp" />
<ClCompile Include="src\teams_login.cpp" />
+ <ClCompile Include="src\teams_menus.cpp" />
+ <ClCompile Include="src\teams_messages.cpp" />
+ <ClCompile Include="src\teams_mood.cpp" />
<ClCompile Include="src\teams_options.cpp" />
+ <ClCompile Include="src\teams_polling.cpp" />
+ <ClCompile Include="src\teams_popups.cpp" />
+ <ClCompile Include="src\teams_profile.cpp" />
<ClCompile Include="src\teams_proto.cpp" />
- <ClInclude Include="src\proto.h" />
+ <ClCompile Include="src\teams_search.cpp" />
+ <ClCompile Include="src\teams_utils.cpp" />
<ClInclude Include="src\resource.h" />
<ClInclude Include="src\stdafx.h" />
+ <ClInclude Include="src\teams_menus.h" />
+ <ClInclude Include="src\teams_proto.h" />
+ <ClInclude Include="src\teams_utils.h" />
<ClInclude Include="src\version.h" />
</ItemGroup>
<ItemGroup>
diff --git a/protocols/Teams/Teams.vcxproj.filters b/protocols/Teams/Teams.vcxproj.filters
index b30f82a50a..b2582eb02b 100644
--- a/protocols/Teams/Teams.vcxproj.filters
+++ b/protocols/Teams/Teams.vcxproj.filters
@@ -19,6 +19,48 @@
<ClCompile Include="src\teams_http.cpp">
<Filter>Source Files</Filter>
</ClCompile>
+ <ClCompile Include="src\teams_avatars.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_chatrooms.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_contacts.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_files.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_history.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_menus.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_messages.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_mood.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_polling.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_popups.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_profile.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_utils.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_endpoint.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="src\teams_search.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\resource.h">
@@ -30,7 +72,13 @@
<ClInclude Include="src\version.h">
<Filter>Header Files</Filter>
</ClInclude>
- <ClInclude Include="src\proto.h">
+ <ClInclude Include="src\teams_menus.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ <ClInclude Include="src\teams_utils.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ <ClInclude Include="src\teams_proto.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
diff --git a/protocols/Teams/src/main.cpp b/protocols/Teams/src/main.cpp
index 5301dc7ecf..d2ac241040 100644
--- a/protocols/Teams/src/main.cpp
+++ b/protocols/Teams/src/main.cpp
@@ -2,6 +2,8 @@
CMPlugin g_plugin;
+char g_szMirVer[100];
+HANDLE g_hCallEvent;
HANDLE hExtraXStatus;
/////////////////////////////////////////////////////////////////////////////////////////
@@ -33,11 +35,29 @@ extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOC
/////////////////////////////////////////////////////////////////////////////////////////
static IconItem iconList[] = {
- { LPGEN("Protocol icon"), "main", IDI_TEAMS },
+ { LPGEN("Protocol icon"), "main", IDI_TEAMS },
+ { LPGEN("Create new chat icon"), "conference", IDI_CONFERENCE },
+ { LPGEN("Block user icon"), "user_block", IDI_BLOCKUSER },
+ { LPGEN("Unblock user icon"), "user_unblock", IDI_UNBLOCKUSER },
+ { LPGEN("Incoming call icon"), "inc_call", IDI_CALL },
+ { LPGEN("Notification icon"), "notify", IDI_NOTIFY },
+ { LPGEN("Error icon"), "error", IDI_ERRORICON },
+ { LPGEN("Action icon"), "me_action", IDI_ACTION_ME }
+
};
int CMPlugin::Load()
{
- g_plugin.registerIcon("Protocols/" MODULENAME, iconList, MODULENAME);
+ registerIcon("Protocols/" MODULENAME, iconList, MODULENAME);
+
+ g_hCallEvent = CreateHookableEvent(MODULENAME "/IncomingCall");
+ return 0;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+int CMPlugin::Unload()
+{
+ DestroyHookableEvent(g_hCallEvent);
return 0;
}
diff --git a/protocols/Teams/src/proto.h b/protocols/Teams/src/proto.h
deleted file mode 100644
index 78a57f422f..0000000000
--- a/protocols/Teams/src/proto.h
+++ /dev/null
@@ -1,102 +0,0 @@
-#define TEAMS_CLIENT_ID "8ec6bc83-69c8-4392-8f08-b3c986009232"
-
-enum HostType {
- HOST_OTHER = 0,
- HOST_LOGIN = 1,
- HOST_TEAMS = 2,
-};
-
-struct AsyncHttpRequest : public MTHttpRequest<CTeamsProto>
-{
- AsyncHttpRequest(int iRequestType, HostType host, const char *pszUrl, MTHttpRequestHandler pFunc = 0);
-
- HostType m_host;
-};
-
-class CTeamsProto : public PROTO<CTeamsProto>
-{
- friend class COptionsMainDlg;
- friend class CDeviceCodeDlg;
-
- class CTeamsProtoImpl
- {
- friend class CTeamsProto;
- CTeamsProto &m_proto;
-
- CTimer m_loginPoll;
- void OnLoginPoll(CTimer *)
- {
- m_proto.LoginPoll();
- }
-
- CTeamsProtoImpl(CTeamsProto &pro) :
- m_proto(pro),
- m_loginPoll(Miranda_GetSystemWindow(), UINT_PTR(this) + 1)
- {
- m_loginPoll.OnEvent = Callback(this, &CTeamsProtoImpl::OnLoginPoll);
- }
- } m_impl;
-
- // http queue
- bool m_isTerminated = true;
- mir_cs m_requestQueueLock;
- LIST<AsyncHttpRequest> m_requests;
- MEventHandle m_hRequestQueueEvent;
- HANDLE m_hRequestQueueThread;
- CMStringA m_szAccessToken, m_szSubstrateToken, m_szSkypeToken;
-
- void __cdecl WorkerThread(void *);
-
- void StartQueue();
- void StopQueue();
-
- MHttpResponse* DoSend(AsyncHttpRequest *request);
-
- void Execute(AsyncHttpRequest *request);
- void PushRequest(AsyncHttpRequest *request);
-
- // login
- CMStringW m_wszUserCode;
- CMStringA m_szDeviceCode, m_szDeviceCookie, m_szVerificationUrl;
- time_t m_iLoginExpires;
-
- void Login();
- void LoggedIn();
- void LoginPoll();
- void LoginError();
-
- void OauthRefreshServices();
- void RefreshToken(const char *pszScope, AsyncHttpRequest::MTHttpRequestHandler pFunc);
-
- void OnReceiveSkypeToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
- void OnReceiveDevicePoll(MHttpResponse *response, AsyncHttpRequest *pRequest);
- void OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
- void OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
- void OnRefreshSkypeToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
- void OnRefreshSubstrate(MHttpResponse *response, AsyncHttpRequest *pRequest);
-
- // options
- int __cdecl OnOptionsInit(WPARAM, LPARAM);
-
- // settings
- CMOption<wchar_t *> m_wstrCListGroup;
-
-public:
- // constructor
- CTeamsProto(const char *protoName, const wchar_t *userName);
- ~CTeamsProto();
-
- MWindow OnCreateAccMgrUI(MWindow) override;
-
- INT_PTR GetCaps(int type, MCONTACT) override;
- int SetStatus(int iNewStatus) override;
-};
-
-typedef CProtoDlgBase<CTeamsProto> CTeamsDlgBase;
-
-struct CMPlugin : public ACCPROTOPLUGIN<CTeamsProto>
-{
- CMPlugin();
-
- int Load() override;
-};
diff --git a/protocols/Teams/src/requests/capabilities.h b/protocols/Teams/src/requests/capabilities.h
new file mode 100644
index 0000000000..566d946e3e
--- /dev/null
+++ b/protocols/Teams/src/requests/capabilities.h
@@ -0,0 +1,41 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_REQUEST_CAPS_H_
+#define _SKYPE_REQUEST_CAPS_H_
+
+struct SendCapabilitiesRequest : public AsyncHttpRequest
+{
+ SendCapabilitiesRequest(const char *hostname, CTeamsProto *ppro) :
+ AsyncHttpRequest(REQUEST_PUT, HOST_DEFAULT, "/users/ME/endpoints/" + mir_urlEncode(ppro->m_szId) + "/presenceDocs/messagingService", &CTeamsProto::OnCapabilitiesSended)
+ {
+ JSONNode privateInfo; privateInfo.set_name("privateInfo");
+ privateInfo << CHAR_PARAM("epname", hostname);
+
+ JSONNode publicInfo; publicInfo.set_name("publicInfo");
+ publicInfo << CHAR_PARAM("capabilities", "Audio|Video") << INT_PARAM("typ", 125)
+ << CHAR_PARAM("skypeNameVersion", "Miranda NG Skype") << CHAR_PARAM("nodeInfo", "xx") << CHAR_PARAM("version", g_szMirVer);
+
+ JSONNode node;
+ node << CHAR_PARAM("id", "messagingService") << CHAR_PARAM("type", "EndpointPresenceDoc")
+ << CHAR_PARAM("selfLink", "uri") << privateInfo << publicInfo;
+
+ m_szParam = node.write().c_str();
+ }
+};
+
+#endif //_SKYPE_REQUEST_CAPS_H_
diff --git a/protocols/Teams/src/requests/chatrooms.h b/protocols/Teams/src/requests/chatrooms.h
new file mode 100644
index 0000000000..cfef7dd8e0
--- /dev/null
+++ b/protocols/Teams/src/requests/chatrooms.h
@@ -0,0 +1,92 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_REQUEST_CHATS_H_
+#define _SKYPE_REQUEST_CHATS_H_
+
+struct CreateChatroomRequest : public AsyncHttpRequest
+{
+ CreateChatroomRequest(const LIST<char> &skypenames, CTeamsProto *ppro) :
+ AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT, "/threads")
+ {
+ //{"members":[{"id":"8:user3","role":"User"},{"id":"8:user2","role":"User"},{"id":"8:user1","role":"Admin"}]}
+ JSONNode node;
+ JSONNode members(JSON_ARRAY); members.set_name("members");
+
+ for (auto &it : skypenames) {
+ JSONNode member;
+ member << CHAR_PARAM("id", it) << CHAR_PARAM("role", !mir_strcmpi(it, ppro->m_szSkypename) ? "Admin" : "User");
+ members << member;
+ }
+ node << members;
+ m_szParam = node.write().c_str();
+ }
+};
+
+struct GetChatMembersRequest : public AsyncHttpRequest
+{
+ GetChatMembersRequest(const LIST<char> &ids, SESSION_INFO *si) :
+ AsyncHttpRequest(REQUEST_POST, HOST_PEOPLE, "/profiles", &CTeamsProto::OnGetChatMembers)
+ {
+ JSONNode node, mris(JSON_ARRAY); mris.set_name("mris");
+ for (auto &it : ids)
+ mris.push_back(JSONNode("", it));
+ node << mris << CHAR_PARAM("locale", "en-US");
+ m_szParam = node.write().c_str();
+
+ pUserInfo = si;
+ }
+};
+
+struct GetChatInfoRequest : public AsyncHttpRequest
+{
+ GetChatInfoRequest(const wchar_t *chatId) :
+ AsyncHttpRequest(REQUEST_GET, HOST_DEFAULT, 0, &CTeamsProto::OnGetChatInfo)
+ {
+ m_szUrl.AppendFormat("/threads/%S", chatId);
+
+ this << CHAR_PARAM("view", "msnp24Equivalent");
+ }
+};
+
+struct InviteUserToChatRequest : public AsyncHttpRequest
+{
+ InviteUserToChatRequest(const char *chatId, const char *skypename, const char *role) :
+ AsyncHttpRequest(REQUEST_PUT, HOST_DEFAULT)
+ {
+ m_szUrl.AppendFormat("/threads/%s/members/%s", chatId, skypename);
+
+ JSONNode node;
+ node << CHAR_PARAM("role", role);
+ m_szParam = node.write().c_str();
+ }
+};
+
+struct SetChatPropertiesRequest : public AsyncHttpRequest
+{
+ SetChatPropertiesRequest(const char *chatId, const char *propname, const char *value) :
+ AsyncHttpRequest(REQUEST_PUT, HOST_DEFAULT)
+ {
+ m_szUrl.AppendFormat("/threads/%s/properties?name=%s", chatId, propname);
+
+ JSONNode node;
+ node << CHAR_PARAM(propname, value);
+ m_szParam = node.write().c_str();
+ }
+};
+
+#endif //_SKYPE_REQUEST_CHATS_H_ \ No newline at end of file
diff --git a/protocols/Teams/src/requests/contacts.h b/protocols/Teams/src/requests/contacts.h
new file mode 100644
index 0000000000..f0614f10b6
--- /dev/null
+++ b/protocols/Teams/src/requests/contacts.h
@@ -0,0 +1,89 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_REQUEST_CONTACTS_H_
+#define _SKYPE_REQUEST_CONTACTS_H_
+
+struct GetContactListRequest : public AsyncHttpRequest
+{
+ GetContactListRequest() :
+ AsyncHttpRequest(REQUEST_GET, HOST_CONTACTS, "/users/SELF/contacts", &CTeamsProto::LoadContactList)
+ {
+ }
+};
+
+struct GetContactsAuthRequest : public AsyncHttpRequest
+{
+ GetContactsAuthRequest() :
+ AsyncHttpRequest(REQUEST_GET, HOST_CONTACTS, "/users/SELF/invites", &CTeamsProto::LoadContactsAuth)
+ {
+ }
+};
+
+struct AddContactRequest : public AsyncHttpRequest
+{
+ AddContactRequest(const char *who, const char *greeting = "") :
+ AsyncHttpRequest(REQUEST_PUT, HOST_CONTACTS, "/users/SELF/contacts")
+ {
+ JSONNode node;
+ node << CHAR_PARAM("mri", who) << CHAR_PARAM("greeting", greeting);
+ m_szParam = node.write().c_str();
+ }
+};
+
+struct AuthAcceptRequest : public AsyncHttpRequest
+{
+ AuthAcceptRequest(const char *who) :
+ AsyncHttpRequest(REQUEST_PUT, HOST_CONTACTS)
+ {
+ m_szUrl.AppendFormat("/users/SELF/invites/%s/accept", mir_urlEncode(who).c_str());
+ }
+};
+
+struct AuthDeclineRequest : public AsyncHttpRequest
+{
+ AuthDeclineRequest(const char *who) :
+ AsyncHttpRequest(REQUEST_PUT, HOST_CONTACTS)
+ {
+ m_szUrl.AppendFormat("/users/SELF/invites/%s/decline", mir_urlEncode(who).c_str());
+ }
+};
+
+struct BlockContactRequest : public AsyncHttpRequest
+{
+ BlockContactRequest(CTeamsProto *ppro, MCONTACT hContact) :
+ AsyncHttpRequest(REQUEST_PUT, HOST_CONTACTS, "/users/SELF/contacts/blocklist/" + ppro->getId(hContact), &CTeamsProto::OnBlockContact)
+ {
+ m_szParam = "{\"report_abuse\":\"false\",\"ui_version\":\"skype.com\"}";
+ pUserInfo = (void *)hContact;
+ }
+};
+
+struct UnblockContactRequest : public AsyncHttpRequest
+{
+ UnblockContactRequest(CTeamsProto *ppro, MCONTACT hContact) :
+ AsyncHttpRequest(REQUEST_DELETE, HOST_CONTACTS, 0, &CTeamsProto::OnUnblockContact)
+ {
+ m_szUrl.AppendFormat("/users/SELF/contacts/blocklist/%s", ppro->getId(hContact).c_str());
+ pUserInfo = (void *)hContact;
+
+ // TODO: user ip address
+ this << CHAR_PARAM("reporterIp", "123.123.123.123") << CHAR_PARAM("uiVersion", g_szMirVer);
+ }
+};
+
+#endif //_SKYPE_REQUEST_CONTACTS_H_ \ No newline at end of file
diff --git a/protocols/Teams/src/requests/history.h b/protocols/Teams/src/requests/history.h
new file mode 100644
index 0000000000..44276e78e0
--- /dev/null
+++ b/protocols/Teams/src/requests/history.h
@@ -0,0 +1,56 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_REQUEST_HISTORY_H_
+#define _SKYPE_REQUEST_HISTORY_H_
+
+struct SyncConversations : public AsyncHttpRequest
+{
+ SyncConversations() :
+ AsyncHttpRequest(REQUEST_GET, HOST_DEFAULT, "/users/ME/conversations", &CTeamsProto::OnSyncConversations)
+ {
+ this << INT_PARAM("startTime", 0) << INT_PARAM("pageSize", 100)
+ << CHAR_PARAM("view", "msnp24Equivalent") << CHAR_PARAM("targetType", "Passport|Skype|Lync|Thread");
+ }
+};
+
+struct GetHistoryRequest : public AsyncHttpRequest
+{
+ CMStringA m_who;
+
+ GetHistoryRequest(MCONTACT _1, const char *who, int pageSize, int64_t timestamp, bool bOperative) :
+ AsyncHttpRequest(REQUEST_GET, HOST_DEFAULT, "/users/ME/conversations/" + mir_urlEncode(who) + "/messages", &CTeamsProto::OnGetServerHistory),
+ m_who(who)
+ {
+ hContact = _1;
+ if (bOperative)
+ pUserInfo = this;
+
+ this << INT64_PARAM("startTime", timestamp) << INT_PARAM("pageSize", pageSize)
+ << CHAR_PARAM("view", "msnp24Equivalent") << CHAR_PARAM("targetType", "Passport|Skype|Lync|Thread");
+ }
+};
+
+struct EmptyHistoryRequest : public AsyncHttpRequest
+{
+ EmptyHistoryRequest(const char *who) :
+ AsyncHttpRequest(REQUEST_DELETE, HOST_DEFAULT, "/users/ME/conversations/" + mir_urlEncode(who) + "/messages")
+ {
+ }
+};
+
+#endif //_SKYPE_REQUEST_HISTORY_H_
diff --git a/protocols/Teams/src/requests/poll.h b/protocols/Teams/src/requests/poll.h
new file mode 100644
index 0000000000..b2573a43e2
--- /dev/null
+++ b/protocols/Teams/src/requests/poll.h
@@ -0,0 +1,38 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_POLL_H_
+#define _SKYPE_POLL_H_
+
+struct PollRequest : public AsyncHttpRequest
+{
+ PollRequest(CTeamsProto *ppro) :
+ AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT, "/users/ME/endpoints/" + mir_urlEncode(ppro->m_szId) + "/subscriptions/0/poll")
+ {
+ flags |= NLHRF_PERSISTENT;
+ timeout = 120000;
+
+ if (ppro->m_iPollingId != -1)
+ m_szUrl.AppendFormat("?ackId=%d", ppro->m_iPollingId);
+
+ AddHeader("Referer", "https://web.skype.com/main");
+ AddHeader("ClientInfo", "os=Windows; osVer=8.1; proc=Win32; lcid=en-us; deviceType=1; country=n/a; clientName=swx-skype.com; clientVer=908/1.85.0.29");
+ AddHeader("Accept", "application/json");
+ AddHeader("Accept-Language", "en, C");
+ }
+};
+#endif //_SKYPE_POLL_H_ \ No newline at end of file
diff --git a/protocols/Teams/src/requests/profile.h b/protocols/Teams/src/requests/profile.h
new file mode 100644
index 0000000000..a19772e3cc
--- /dev/null
+++ b/protocols/Teams/src/requests/profile.h
@@ -0,0 +1,33 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_REQUEST_PROFILE_H_
+#define _SKYPE_REQUEST_PROFILE_H_
+
+struct GetProfileRequest : public AsyncHttpRequest
+{
+ GetProfileRequest(CTeamsProto *ppro, MCONTACT hContact) :
+ AsyncHttpRequest(REQUEST_GET, HOST_API, 0, &CTeamsProto::LoadProfile)
+ {
+ m_szUrl.AppendFormat("/users/%s/profile", (hContact == 0) ? "self" : mir_urlEncode(ppro->getId(hContact)));
+ pUserInfo = (void *)hContact;
+
+ AddHeader("Accept", "application/json");
+ }
+};
+
+#endif //_SKYPE_REQUEST_PROFILE_H_
diff --git a/protocols/Teams/src/requests/search.h b/protocols/Teams/src/requests/search.h
new file mode 100644
index 0000000000..701b158339
--- /dev/null
+++ b/protocols/Teams/src/requests/search.h
@@ -0,0 +1,33 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_REQUEST_SEARCH_H_
+#define _SKYPE_REQUEST_SEARCH_H_
+
+struct GetSearchRequest : public AsyncHttpRequest
+{
+ GetSearchRequest(const char *string) :
+ AsyncHttpRequest(REQUEST_GET, HOST_GRAPH, "/v2.0/search/", &CTeamsProto::OnSearch)
+ {
+ this << CHAR_PARAM("requestid", Utils_GenerateUUID())
+ << CHAR_PARAM("locale", "en-US") << CHAR_PARAM("searchstring", string);
+
+ AddHeader("Accept", "application/json");
+ }
+};
+
+#endif //_SKYPE_REQUEST_SEARCH_H_
diff --git a/protocols/Teams/src/requests/status.h b/protocols/Teams/src/requests/status.h
new file mode 100644
index 0000000000..375b32dc25
--- /dev/null
+++ b/protocols/Teams/src/requests/status.h
@@ -0,0 +1,32 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_REQUEST_STATUS_H_
+#define _SKYPE_REQUEST_STATUS_H_
+
+struct SetStatusRequest : public AsyncHttpRequest
+{
+ SetStatusRequest(const char *status) :
+ AsyncHttpRequest(REQUEST_PUT, HOST_DEFAULT, "/users/ME/presenceDocs/messagingService", &CTeamsProto::OnStatusChanged)
+ {
+ JSONNode node(JSON_NODE);
+ node << CHAR_PARAM("status", status);
+ m_szParam = node.write().c_str();
+ }
+};
+
+#endif //_SKYPE_REQUEST_STATUS_H_
diff --git a/protocols/Teams/src/requests/subscriptions.h b/protocols/Teams/src/requests/subscriptions.h
new file mode 100644
index 0000000000..81f8cefdb7
--- /dev/null
+++ b/protocols/Teams/src/requests/subscriptions.h
@@ -0,0 +1,56 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_REQUEST_SUBSCIPTIONS_H_
+#define _SKYPE_REQUEST_SUBSCIPTIONS_H_
+
+struct CreateSubscriptionsRequest : public AsyncHttpRequest
+{
+ CreateSubscriptionsRequest() :
+ AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT, "/users/ME/endpoints/SELF/subscriptions", &CTeamsProto::OnSubscriptionsCreated)
+ {
+ JSONNode interestedResources(JSON_ARRAY); interestedResources.set_name("interestedResources");
+ interestedResources << CHAR_PARAM("", "/v1/users/ME/conversations/ALL/properties")
+ << CHAR_PARAM("", "/v1/users/ME/conversations/ALL/messages")
+ << CHAR_PARAM("", "/v1/users/ME/contacts/ALL")
+ << CHAR_PARAM("", "/v1/threads/ALL");
+
+ JSONNode node;
+ node << CHAR_PARAM("channelType", "httpLongPoll") << CHAR_PARAM("template", "raw") << interestedResources;
+ m_szParam = node.write().c_str();
+ }
+};
+
+struct CreateContactsSubscriptionRequest : public AsyncHttpRequest
+{
+ CreateContactsSubscriptionRequest(const LIST<char> &skypenames) :
+ AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT, "/users/ME/contacts")
+ {
+ JSONNode contacts(JSON_ARRAY); contacts.set_name("contacts");
+ for (auto &it : skypenames) {
+ JSONNode contact;
+ contact << CHAR_PARAM("id", it);
+ contacts << contact;
+ }
+
+ JSONNode node;
+ node << contacts;
+ m_szParam = node.write().c_str();
+ }
+};
+
+#endif //_SKYPE_REQUEST_SUBSCIPTIONS_H_
diff --git a/protocols/Teams/src/resource.h b/protocols/Teams/src/resource.h
index dd0cecf12b..5ea05c18d7 100644
--- a/protocols/Teams/src/resource.h
+++ b/protocols/Teams/src/resource.h
@@ -1,23 +1,45 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
-// Used by W:\miranda-ng\protocols\Teams\res\Resource.rc
+// Used by W:\miranda-ng\protocols\SkypeWeb\res\resource.rc
//
#define IDI_TEAMS 100
-#define IDD_ACCOUNT_MANAGER 101
-#define IDD_OPTIONS_MAIN 102
-#define IDD_DEVICECODE 103
-#define IDC_LOGIN 1001
-#define IDC_PASSWORD 1002
-#define IDC_GROUP 1003
-#define IDC_TEXT 1004
+#define IDC_LOGIN 101
+#define IDC_PASSWORD 102
+#define IDD_ACCOUNT_MANAGER 103
+#define IDD_OPTIONS_MAIN 104
+#define IDD_MOOD 105
+#define IDC_GROUP 106
+#define IDD_DEVICECODE 107
+#define IDD_GC_CREATE 111
+#define IDD_GC_INVITE 112
+#define IDI_CONFERENCE 114
+#define IDI_BLOCKUSER 118
+#define IDI_UNBLOCKUSER 119
+#define IDI_CALL 120
+#define IDI_NOTIFY 121
+#define IDI_ERRORICON 122
+#define IDI_ACTION_ME 123
+#define IDC_TEXT 1001
+#define IDC_AUTOSYNC 1028
+#define IDC_LOCALTIME 1029
+#define IDC_CLIST 1030
+#define IDC_TITLE 1031
+#define IDC_CONTACT 1032
+#define IDC_PLACE 1034
+#define IDC_USEHOST 1035
+#define IDC_BBCODES 1036
+#define IDC_MOOD_COMBO 1037
+#define IDC_CHANGEPASS 1038
+#define IDC_MOOD_EMOJI 1039
+#define IDC_MOOD_TEXT 1041
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
-#define _APS_NEXT_RESOURCE_VALUE 104
+#define _APS_NEXT_RESOURCE_VALUE 126
#define _APS_NEXT_COMMAND_VALUE 40001
-#define _APS_NEXT_CONTROL_VALUE 1085
+#define _APS_NEXT_CONTROL_VALUE 1042
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
diff --git a/protocols/Teams/src/stdafx.h b/protocols/Teams/src/stdafx.h
index 9ef202a899..de676dace4 100644
--- a/protocols/Teams/src/stdafx.h
+++ b/protocols/Teams/src/stdafx.h
@@ -2,49 +2,102 @@
#define _COMMON_H_
#include <windows.h>
-#include <commctrl.h>
+
#include <malloc.h>
#include <time.h>
-
-#include <map>
-#include <regex>
-#include <vector>
#include <string>
-#include <algorithm>
+#include <vector>
+#include <regex>
+#include <map>
+#include <memory>
+#include <functional>
#include <newpluginapi.h>
-#include <m_chat_int.h>
-#include <m_contacts.h>
+
+#include <m_protoint.h>
+#include <m_protosvc.h>
+
#include <m_database.h>
#include <m_langpack.h>
-#include <m_message.h>
-#include <m_netlib.h>
+#include <m_clistint.h>
#include <m_options.h>
+#include <m_netlib.h>
#include <m_popup.h>
-#include <m_json.h>
-#include <m_avatars.h>
#include <m_icolib.h>
+#include <m_userinfo.h>
+#include <m_timezones.h>
+#include <m_contacts.h>
+#include <m_message.h>
+#include <m_avatars.h>
#include <m_skin.h>
-#include <m_clist.h>
+#include <m_chat_int.h>
#include <m_genmenu.h>
-#include <m_imgsrvc.h>
-#include <m_protocols.h>
-#include <m_protosvc.h>
-#include <m_protoint.h>
-#include <m_idle.h>
-#include <m_xstatus.h>
-#include <m_extraicons.h>
+#include <m_clc.h>
+#include <m_json.h>
#include <m_gui.h>
-#include <m_http.h>
-#include <m_system.h>
+#include <m_imgsrvc.h>
+#include <m_xml.h>
+#include <m_assocmgr.h>
+#include <m_file.h>
+
+extern char g_szMirVer[];
+extern HANDLE g_hCallEvent;
+
+struct MessageId
+{
+ ULONGLONG id;
+ HANDLE handle;
+};
#include "resource.h"
#include "version.h"
+#include "teams_menus.h"
+#include "teams_utils.h"
#define MODULENAME "Teams"
class CTeamsProto;
-#include "proto.h"
+/////////////////////////////////////////////////////////////////////////////////////////
+
+#define SKYPEWEB_CLIENTINFO_NAME "swx-skype.com"
+#define SKYPEWEB_CLIENTINFO_VERSION "908/1.85.0.29"
+
+enum SkypeHost
+{
+ HOST_API,
+ HOST_CONTACTS,
+ HOST_DEFAULT,
+ HOST_GRAPH,
+ HOST_LOGIN,
+ HOST_PEOPLE,
+ HOST_TEAMS,
+ HOST_OTHER
+};
+
+struct AsyncHttpRequest : public MTHttpRequest<CTeamsProto>
+{
+ SkypeHost m_host;
+ MCONTACT hContact = 0;
+
+ AsyncHttpRequest(int type, SkypeHost host, LPCSTR url = nullptr, MTHttpRequestHandler pFunc = nullptr);
+
+ void AddRegister(CTeamsProto *ppro);
+ void AddAuthentication(CTeamsProto *ppro);
+};
+
+#include "teams_proto.h"
+
+#include "requests/capabilities.h"
+#include "requests/chatrooms.h"
+#include "requests/contacts.h"
+#include "requests/history.h"
+#include "requests/poll.h"
+#include "requests/profile.h"
+#include "requests/search.h"
+#include "requests/status.h"
+#include "requests/subscriptions.h"
+
+#define POLLING_ERRORS_LIMIT 3
#endif //_COMMON_H_ \ No newline at end of file
diff --git a/protocols/Teams/src/teams_avatars.cpp b/protocols/Teams/src/teams_avatars.cpp
new file mode 100644
index 0000000000..6bbac21d04
--- /dev/null
+++ b/protocols/Teams/src/teams_avatars.cpp
@@ -0,0 +1,232 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+void CTeamsProto::GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen)
+{
+ CMStringW wszPath(GetAvatarPath());
+ wszPath += '\\';
+
+ const wchar_t *szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_JPEG));
+ CMStringA username(getId(hContact));
+ username.Replace("live:", "__live_");
+ username.Replace("facebook:", "__facebook_");
+ wszPath.AppendFormat(L"%S%s", username.c_str(), szFileType);
+
+ wcsncpy_s(pszDest, cbLen, wszPath, _TRUNCATE);
+}
+
+void CTeamsProto::ReloadAvatarInfo(MCONTACT hContact)
+{
+ if (hContact == NULL) {
+ ReportSelfAvatarChanged();
+ return;
+ }
+
+ PROTO_AVATAR_INFORMATION ai = { 0 };
+ ai.hContact = hContact;
+ SvcGetAvatarInfo(0, (LPARAM)&ai);
+}
+
+void CTeamsProto::SetAvatarUrl(MCONTACT hContact, const CMStringW &tszUrl)
+{
+ ptrW oldUrl(getWStringA(hContact, "AvatarUrl"));
+ if (oldUrl != NULL)
+ if (tszUrl == oldUrl)
+ return;
+
+ if (tszUrl.IsEmpty()) {
+ delSetting(hContact, "AvatarUrl");
+ ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, nullptr);
+ }
+ else {
+ setWString(hContact, "AvatarUrl", tszUrl);
+ setByte(hContact, "NeedNewAvatar", 1);
+
+ PROTO_AVATAR_INFORMATION ai = {};
+ ai.hContact = hContact;
+ GetAvatarFileName(ai.hContact, ai.filename, _countof(ai.filename));
+ ai.format = ProtoGetAvatarFormat(ai.filename);
+ ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai);
+ }
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// Avatar services for Miranda
+
+INT_PTR CTeamsProto::SvcGetAvatarCaps(WPARAM wParam, LPARAM lParam)
+{
+ switch (wParam) {
+ case AF_MAXSIZE:
+ ((POINT*)lParam)->x = 98;
+ ((POINT*)lParam)->y = 98;
+ break;
+
+ case AF_MAXFILESIZE:
+ return 32000;
+
+ case AF_PROPORTION:
+ return PIP_SQUARE;
+
+ case AF_FORMATSUPPORTED:
+ case AF_ENABLED:
+ case AF_DONTNEEDDELAYS:
+ case AF_FETCHIFPROTONOTVISIBLE:
+ case AF_FETCHIFCONTACTOFFLINE:
+ return 1;
+ }
+ return 0;
+}
+
+INT_PTR CTeamsProto::SvcGetAvatarInfo(WPARAM, LPARAM lParam)
+{
+ PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam;
+
+ pai->format = getByte(pai->hContact, "AvatarType", PA_FORMAT_JPEG);
+
+ wchar_t tszFileName[MAX_PATH];
+ GetAvatarFileName(pai->hContact, tszFileName, _countof(tszFileName));
+ wcsncpy(pai->filename, tszFileName, _countof(pai->filename));
+
+ if (::_waccess(pai->filename, 0) == 0 && !getBool(pai->hContact, "NeedNewAvatar", 0))
+ return GAIR_SUCCESS;
+
+ if (IsOnline())
+ if (ReceiveAvatar(pai->hContact))
+ return GAIR_WAITFOR;
+
+ debugLogA("No avatar");
+ return GAIR_NOAVATAR;
+}
+
+INT_PTR CTeamsProto::SvcGetMyAvatar(WPARAM wParam, LPARAM lParam)
+{
+ wchar_t path[MAX_PATH];
+ GetAvatarFileName(NULL, path, _countof(path));
+ wcsncpy((wchar_t *)wParam, path, (int)lParam);
+ return 0;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// Avatars' receiving
+
+struct GetAvatarRequest : public AsyncHttpRequest
+{
+ GetAvatarRequest(const char *url, MCONTACT hContact) :
+ AsyncHttpRequest(REQUEST_GET, HOST_OTHER, url, &CTeamsProto::OnReceiveAvatar)
+ {
+ flags |= NLHRF_REDIRECT;
+ pUserInfo = (void *)hContact;
+ }
+};
+
+void CTeamsProto::OnReceiveAvatar(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ if (response == nullptr || response->body.IsEmpty())
+ return;
+
+ MCONTACT hContact = (DWORD_PTR)pRequest->pUserInfo;
+ if (response->resultCode != 200)
+ return;
+
+ PROTO_AVATAR_INFORMATION ai = { 0 };
+ ai.format = ProtoGetBufferFormat(response->body);
+ setByte(hContact, "AvatarType", ai.format);
+ GetAvatarFileName(hContact, ai.filename, _countof(ai.filename));
+
+ FILE *out = _wfopen(ai.filename, L"wb");
+ if (out == nullptr) {
+ ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, &ai, 0);
+ return;
+ }
+
+ fwrite(response->body, 1, response->body.GetLength(), out);
+ fclose(out);
+ setByte(hContact, "NeedNewAvatar", 0);
+ ProtoBroadcastAck(hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, &ai, 0);
+}
+
+bool CTeamsProto::ReceiveAvatar(MCONTACT hContact)
+{
+ ptrA szUrl(getStringA(hContact, "AvatarUrl"));
+ if (!mir_strlen(szUrl))
+ return false;
+
+ PushRequest(new GetAvatarRequest(szUrl, hContact));
+ debugLogA("Requested to read an avatar from '%s'", szUrl.get());
+ return true;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// Setting my own avatar
+
+struct SetAvatarRequest : public AsyncHttpRequest
+{
+ SetAvatarRequest(const uint8_t *data, int dataSize, const char *szMime, CTeamsProto *ppro) :
+ AsyncHttpRequest(REQUEST_PUT, HOST_API, 0, &CTeamsProto::OnSentAvatar)
+ {
+ m_szUrl.AppendFormat("/users/%s/profile/avatar", ppro->m_szSkypename.MakeLower().c_str());
+
+ AddHeader("Content-Type", szMime);
+
+ m_szParam.Truncate(dataSize);
+ memcpy(m_szParam.GetBuffer(), data, dataSize);
+ }
+};
+
+void CTeamsProto::OnSentAvatar(MHttpResponse *response, AsyncHttpRequest*)
+{
+ SkypeReply root(response);
+ if (root.error())
+ return;
+}
+
+INT_PTR CTeamsProto::SvcSetMyAvatar(WPARAM, LPARAM lParam)
+{
+ wchar_t *path = (wchar_t*)lParam;
+ wchar_t avatarPath[MAX_PATH];
+ GetAvatarFileName(NULL, avatarPath, _countof(avatarPath));
+ if (path != nullptr) {
+ if (CopyFile(path, avatarPath, FALSE)) {
+ FILE *hFile = _wfopen(path, L"rb");
+ if (hFile) {
+ fseek(hFile, 0, SEEK_END);
+ size_t length = ftell(hFile);
+ if (length != -1) {
+ rewind(hFile);
+
+ mir_ptr<uint8_t> data((uint8_t*)mir_alloc(length));
+
+ if (data != NULL && fread(data, sizeof(uint8_t), length, hFile) == length) {
+ const char *szMime = FreeImage_GetFIFMimeType(FreeImage_GetFIFFromFilenameU(path));
+
+ PushRequest(new SetAvatarRequest(data, (int)length, szMime, this));
+ fclose(hFile);
+ return 0;
+ }
+ }
+ fclose(hFile);
+ }
+ }
+ return -1;
+ }
+ else if (IsFileExists(avatarPath))
+ DeleteFile(avatarPath);
+
+ return 0;
+}
diff --git a/protocols/Teams/src/teams_chatrooms.cpp b/protocols/Teams/src/teams_chatrooms.cpp
new file mode 100644
index 0000000000..dcb2451911
--- /dev/null
+++ b/protocols/Teams/src/teams_chatrooms.cpp
@@ -0,0 +1,605 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+void CTeamsProto::InitGroupChatModule()
+{
+ GCREGISTER gcr = {};
+ gcr.dwFlags = GC_DATABASE | GC_PERSISTENT;
+ gcr.iMaxText = 0;
+ gcr.ptszDispName = m_tszUserName;
+ gcr.pszModule = m_szModuleName;
+ Chat_Register(&gcr);
+
+ HookProtoEvent(ME_GC_EVENT, &CTeamsProto::OnGroupChatEventHook);
+ HookProtoEvent(ME_GC_BUILDMENU, &CTeamsProto::OnGroupChatMenuHook);
+
+ CreateProtoService(PS_JOINCHAT, &CTeamsProto::OnJoinChatRoom);
+ CreateProtoService(PS_LEAVECHAT, &CTeamsProto::OnLeaveChatRoom);
+}
+
+SESSION_INFO* CTeamsProto::StartChatRoom(const wchar_t *tid, const wchar_t *tname, const char *pszVersion)
+{
+ // Create the group chat session
+ SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, tid, tname);
+ if (!si)
+ return nullptr;
+
+ bool bFetchInfo = si->arUsers.getCount() == 0;
+ if (pszVersion) {
+ CMStringA oldVersion(getMStringA(si->hContact, "Version"));
+ if (oldVersion != pszVersion)
+ bFetchInfo = true;
+ }
+
+ if (bFetchInfo) {
+ // Create user statuses
+ Chat_AddGroup(si, TranslateT("Admin"));
+ Chat_AddGroup(si, TranslateT("User"));
+
+ PushRequest(new GetChatInfoRequest(tid));
+ }
+
+ // Finish initialization
+ Chat_Control(si, (getBool("HideChats", 1) ? WINDOW_HIDDEN : SESSION_INITDONE));
+ Chat_Control(si, SESSION_ONLINE);
+ return si;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// Group chat invitation dialog
+
+class CSkypeInviteDlg : public CTeamsDlgBase
+{
+ CCtrlCombo m_combo;
+
+public:
+ MCONTACT m_hContact = 0;
+
+ CSkypeInviteDlg(CTeamsProto *proto) :
+ CTeamsDlgBase(proto, IDD_GC_INVITE),
+ m_combo(this, IDC_CONTACT)
+ {}
+
+ bool OnInitDialog() override
+ {
+ for (auto &hContact : m_proto->AccContacts())
+ if (!m_proto->isChatRoom(hContact))
+ m_combo.AddString(Clist_GetContactDisplayName(hContact), hContact);
+ return true;
+ }
+
+ bool OnApply() override
+ {
+ m_hContact = m_combo.GetCurData();
+ return true;
+ }
+};
+
+int CTeamsProto::OnGroupChatEventHook(WPARAM, LPARAM lParam)
+{
+ GCHOOK *gch = (GCHOOK*)lParam;
+ if (!gch)
+ return 0;
+
+ auto *si = gch->si;
+ if (mir_strcmp(si->pszModule, m_szModuleName) != 0)
+ return 0;
+
+ T2Utf chat_id(si->ptszID), user_id(gch->ptszUID);
+
+ switch (gch->iType) {
+ case GC_USER_MESSAGE:
+ SendChatMessage(si, gch->ptszText);
+ break;
+
+ case GC_USER_PRIVMESS:
+ {
+ MCONTACT hContact = FindContact(user_id);
+ if (hContact == NULL) {
+ hContact = AddContact(user_id, T2Utf(gch->ptszNick), true);
+ setWord(hContact, "Status", ID_STATUS_ONLINE);
+ Contact::Hide(hContact);
+ }
+ CallService(MS_MSG_SENDMESSAGEW, hContact, 0);
+ }
+ break;
+
+ case GC_USER_LOGMENU:
+ switch (gch->dwData) {
+ case 10:
+ {
+ CSkypeInviteDlg dlg(this);
+ if (dlg.DoModal())
+ if (dlg.m_hContact != NULL)
+ PushRequest(new InviteUserToChatRequest(chat_id, getId(dlg.m_hContact), "User"));
+ }
+ break;
+
+ case 20:
+ OnLeaveChatRoom(si->hContact, NULL);
+ break;
+
+ case 30:
+ CMStringW newTopic = ChangeTopicForm();
+ if (!newTopic.IsEmpty())
+ PushRequest(new SetChatPropertiesRequest(chat_id, "topic", T2Utf(newTopic.GetBuffer())));
+ break;
+ }
+ break;
+
+ case GC_USER_NICKLISTMENU:
+ switch (gch->dwData) {
+ case 10:
+ KickChatUser(chat_id, user_id);
+ break;
+ case 30:
+ PushRequest(new InviteUserToChatRequest(chat_id, user_id, "Admin"));
+ break;
+ case 40:
+ PushRequest(new InviteUserToChatRequest(chat_id, user_id, "User"));
+ break;
+ case 50:
+ ptrW tnick_old(GetChatContactNick(si, gch->ptszUID, gch->ptszText));
+
+ ENTER_STRING pForm = {};
+ pForm.type = ESF_COMBO;
+ pForm.caption = TranslateT("Enter new nickname");
+ pForm.szModuleName = m_szModuleName;
+ pForm.szDataPrefix = "renamenick_";
+
+ if (EnterString(&pForm)) {
+ if (si->hContact == NULL)
+ break; // This probably shouldn't happen, but if chat is NULL for some reason, do nothing
+
+ ptrW tnick_new(pForm.ptszResult);
+ bool reset = mir_wstrlen(tnick_new) == 0;
+ if (reset) {
+ // User fill blank name, which means we reset the custom nick
+ db_unset(si->hContact, "UsersNicks", user_id);
+ tnick_new = GetChatContactNick(si, gch->ptszUID, gch->ptszText);
+ }
+
+ if (!mir_wstrcmp(tnick_old, tnick_new))
+ break; // New nick is same, do nothing
+
+ GCEVENT gce = { si, GC_EVENT_NICK };
+ gce.dwFlags = GCEF_ADDTOLOG;
+ gce.pszNick.w = tnick_old;
+ gce.bIsMe = IsMe(user_id);
+ gce.pszUID.w = gch->ptszUID;
+ gce.pszText.w= tnick_new;
+ gce.time = time(0);
+ Chat_Event(&gce);
+
+ if (!reset)
+ db_set_ws(si->hContact, "UsersNicks", user_id, tnick_new);
+ }
+ break;
+ }
+ break;
+ }
+ return 1;
+}
+
+INT_PTR CTeamsProto::OnJoinChatRoom(WPARAM hContact, LPARAM)
+{
+ if (hContact) {
+ ptrW idT(getWStringA(hContact, SKYPE_SETTINGS_ID));
+ ptrW nameT(getWStringA(hContact, "Nick"));
+ StartChatRoom(idT, nameT != NULL ? nameT : idT);
+ }
+ return 0;
+}
+
+INT_PTR CTeamsProto::OnLeaveChatRoom(WPARAM hContact, LPARAM)
+{
+ if (!IsOnline())
+ return 1;
+
+ if (hContact && IDYES == MessageBox(nullptr, TranslateT("This chat is going to be destroyed forever with all its contents. This action cannot be undone. Are you sure?"), TranslateT("Warning"), MB_YESNO | MB_ICONQUESTION)) {
+ ptrW idT(getWStringA(hContact, SKYPE_SETTINGS_ID));
+ auto *si = Chat_Find(idT, m_szModuleName);
+ Chat_Control(si, SESSION_OFFLINE);
+ Chat_Terminate(si);
+
+ db_delete_contact(hContact, CDF_DEL_CONTACT);
+ }
+ return 0;
+}
+
+/* CHAT EVENT */
+
+bool CTeamsProto::OnChatEvent(const JSONNode &node)
+{
+ CMStringW wszChatId(UrlToSkypeId(node["conversationLink"].as_mstring()));
+ CMStringW szFromId(UrlToSkypeId(node["from"].as_mstring()));
+
+ CMStringW wszTopic(node["threadtopic"].as_mstring());
+ CMStringW wszContent(node["content"].as_mstring());
+
+ SESSION_INFO *si = Chat_Find(wszChatId, m_szModuleName);
+ if (si == nullptr) {
+ si = StartChatRoom(wszChatId, wszTopic);
+ if (si == nullptr) {
+ debugLogW(L"unable to create chat %s", wszChatId.c_str());
+ return true;
+ }
+ }
+
+ std::string messageType = node["messagetype"].as_string();
+ if (messageType == "ThreadActivity/AddMember") {
+ // <addmember><eventtime>1429186229164</eventtime><initiator>8:initiator</initiator><target>8:user</target></addmember>
+ TiXmlDocument doc;
+ if (!doc.Parse(T2Utf(wszContent))) {
+ if (auto *pRoot = doc.FirstChildElement("addmember")) {
+ auto *pszTarget = XmlGetChildText(pRoot, "target");
+ if (!AddChatContact(si, Utf2T(pszTarget), L"User")) {
+ OBJLIST<char> arIds(1);
+ arIds.insert(newStr(pszTarget));
+ PushRequest(new GetChatMembersRequest(arIds, si));
+ }
+ }
+ }
+ return true;
+ }
+
+ if (messageType == "ThreadActivity/DeleteMember") {
+ // <deletemember><eventtime>1429186229164</eventtime><initiator>8:initiator</initiator><target>8:user</target></deletemember>
+ TiXmlDocument doc;
+ if (!doc.Parse(T2Utf(wszContent))) {
+ if (auto *pRoot = doc.FirstChildElement("deletemember")) {
+ CMStringW target = Utf2T(XmlGetChildText(pRoot, "target"));
+ CMStringW initiator = Utf2T(XmlGetChildText(pRoot, "initiator"));
+ RemoveChatContact(si, target, initiator);
+ }
+ }
+ return true;
+ }
+
+ if (messageType == "ThreadActivity/TopicUpdate") {
+ // <topicupdate><eventtime>1429532702130</eventtime><initiator>8:user</initiator><value>test topic</value></topicupdate>
+ TiXmlDocument doc;
+ if (!doc.Parse(T2Utf(wszContent))) {
+ if (auto *pRoot = doc.FirstChildElement("topicupdate")) {
+ CMStringW initiator = Utf2T(XmlGetChildText(pRoot, "initiator"));
+ CMStringW value = Utf2T(XmlGetChildText(pRoot, "value"));
+ Chat_ChangeSessionName(si, value);
+
+ GCEVENT gce = { si, GC_EVENT_TOPIC };
+ gce.pszUID.w = initiator;
+ gce.pszNick.w = GetSkypeNick(initiator);
+ gce.pszText.w = wszTopic;
+ Chat_Event(&gce);
+ }
+ }
+ return true;
+ }
+
+ if (messageType == "ThreadActivity/RoleUpdate") {
+ // <roleupdate><eventtime>1429551258363</eventtime><initiator>8:user</initiator><target><id>8:user1</id><role>admin</role></target></roleupdate>
+ TiXmlDocument doc;
+ if (!doc.Parse(T2Utf(wszContent))) {
+ if (auto *pRoot = doc.FirstChildElement("roleupdate")) {
+ CMStringW initiator = Utf2T(UrlToSkypeId(XmlGetChildText(pRoot, "initiator")));
+
+ auto *pTarget = pRoot->FirstChildElement("target");
+ if (pTarget) {
+ CMStringW id = Utf2T(UrlToSkypeId(XmlGetChildText(pTarget, "id")));
+ const char *role = XmlGetChildText(pTarget, "role");
+
+ GCEVENT gce = { si, !mir_strcmpi(role, "Admin") ? GC_EVENT_ADDSTATUS : GC_EVENT_REMOVESTATUS };
+ gce.dwFlags = GCEF_ADDTOLOG;
+ gce.pszNick.w = id;
+ gce.pszUID.w = id;
+ gce.pszText.w = initiator;
+ gce.time = time(0);
+ gce.bIsMe = IsMe(T2Utf(id));
+ gce.pszStatus.w = TranslateT("Admin");
+ Chat_Event(&gce);
+ }
+ }
+ }
+ return true;
+ }
+
+ // some slack, let's drop it
+ if (messageType == "ThreadActivity/HistoryDisclosedUpdate" || messageType == "ThreadActivity/JoiningEnabledUpdate")
+ return true;
+
+ return false;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CTeamsProto::SendChatMessage(SESSION_INFO *si, const wchar_t *tszMessage)
+{
+ if (!IsOnline())
+ return;
+
+ CMStringA szMessage(ptrA(mir_utf8encodeW(tszMessage)));
+ szMessage.TrimRight();
+ bool bRich = AddBbcodes(szMessage);
+
+ CMStringA szUrl = "/users/ME/conversations/" + mir_urlEncode(T2Utf(si->ptszID)) + "/messages";
+ AsyncHttpRequest *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT, szUrl, &CTeamsProto::OnMessageSent);
+
+ JSONNode node;
+ node << INT64_PARAM("clientmessageid", getRandomId()) << CHAR_PARAM("messagetype", bRich ? "RichText" : "Text")
+ << CHAR_PARAM("contenttype", "text") << CHAR_PARAM("content", szMessage);
+ if (strncmp(szMessage, "/me ", 4) == 0)
+ node << INT_PARAM("skypeemoteoffset", 4);
+ pReq->m_szParam = node.write().c_str();
+
+ PushRequest(pReq);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CTeamsProto::OnGetChatMembers(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ SkypeReply reply(response);
+ if (reply.error())
+ return;
+
+ auto &root = reply.data();
+ auto *si = (SESSION_INFO *)pRequest->pUserInfo;
+
+ for (auto &it : root["profiles"]) {
+ CMStringW wszUserId(Utf2T(it.name()));
+ if (auto *pUser = g_chatApi.UM_FindUser(si, wszUserId)) {
+ auto &pProfile = it["profile"];
+ if (auto &pName = pProfile["displayName"])
+ replaceStrW(pUser->pszNick, pName.as_mstring());
+ }
+ }
+
+ if (g_chatApi.OnChangeNick)
+ g_chatApi.OnChangeNick(si);
+}
+
+void CTeamsProto::OnGetChatInfo(MHttpResponse *response, AsyncHttpRequest*)
+{
+ SkypeReply reply(response);
+ if (reply.error())
+ return;
+
+ auto &root = reply.data();
+ const JSONNode &properties = root["properties"];
+ if (!properties["capabilities"] || properties["capabilities"].empty())
+ return;
+
+ CMStringW wszChatId(UrlToSkypeId(root["messages"].as_mstring()));
+ auto *si = Chat_Find(wszChatId, m_szModuleName);
+ if (si == nullptr)
+ return;
+
+ setString(si->hContact, "Version", root["version"].as_string().c_str());
+
+ OBJLIST<char> arIds(1);
+ for (auto &member : root["members"]) {
+ CMStringW username(UrlToSkypeId(member["userLink"].as_mstring()));
+ CMStringW role = member["role"].as_mstring();
+ if (!AddChatContact(si, username, role, true))
+ arIds.insert(newStr(mir_u2a(username)));
+ }
+
+ if (arIds.getCount())
+ PushRequest(new GetChatMembersRequest(arIds, si));
+
+ PushRequest(new GetHistoryRequest(si->hContact, T2Utf(si->ptszID), 100, 0, true));
+}
+
+wchar_t* CTeamsProto::GetChatContactNick(SESSION_INFO *si, const wchar_t *id, const wchar_t *name, bool *isQualified)
+{
+ if (isQualified)
+ *isQualified = true;
+
+ // Check if there's a user with this id in a chat
+ if (auto *pUser = g_chatApi.UM_FindUser(si, id))
+ return mir_wstrdup(pUser->pszNick);
+
+ // Check if there is custom nick for this chat contact
+ if (auto *tname = db_get_wsa(si->hContact, "UsersNicks", T2Utf(id)))
+ return tname;
+
+ // Check if we have this contact in database
+ if (IsMe(id)) {
+ // Return my nick
+ if (auto *tname = getWStringA("Nick"))
+ return tname;
+ }
+ else {
+ MCONTACT hContact = FindContact(id);
+ if (hContact != NULL) {
+ // Primarily return custom name
+ if (auto *tname = db_get_wsa(hContact, "CList", "MyHandle"))
+ return tname;
+
+ // If not exists, then user nick
+ if (auto *tname = getWStringA(hContact, "Nick"))
+ return tname;
+ }
+ }
+
+ if (isQualified)
+ *isQualified = false;
+
+ // Return default value as nick - given name or user id
+ if (name != nullptr)
+ return mir_wstrdup(name);
+ return mir_wstrdup(GetSkypeNick(id));
+}
+
+bool CTeamsProto::AddChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *role, bool isChange)
+{
+ bool isQualified;
+ ptrW szNick(GetChatContactNick(si, id, 0, &isQualified));
+
+ GCEVENT gce = { si, GC_EVENT_JOIN };
+ gce.dwFlags = GCEF_ADDTOLOG;
+ gce.pszNick.w = szNick;
+ gce.pszUID.w = id;
+ gce.time = !isChange ? time(0) : NULL;
+ gce.bIsMe = IsMe(id);
+ gce.pszStatus.w = TranslateW(role);
+ Chat_Event(&gce);
+
+ return isQualified;
+}
+
+void CTeamsProto::RemoveChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *initiator)
+{
+ if (IsMe(id))
+ return;
+
+ ptrW szNick(GetChatContactNick(si, id));
+ ptrW szInitiator(GetChatContactNick(si, initiator));
+
+ GCEVENT gce = { si, GC_EVENT_KICK };
+ gce.dwFlags = GCEF_ADDTOLOG;
+ gce.pszNick.w = szNick;
+ gce.pszUID.w = id;
+ gce.time = time(0);
+ gce.bIsMe = IsMe(id);
+ gce.pszStatus.w = szInitiator;
+ Chat_Event(&gce);
+}
+
+void CTeamsProto::KickChatUser(const char *chatId, const char *userId)
+{
+ PushRequest(new AsyncHttpRequest(REQUEST_DELETE, HOST_DEFAULT, "/threads/" + mir_urlEncode(chatId) + "/members/" + mir_urlEncode(userId)));
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// Group chat creation dialog
+
+class CSkypeGCCreateDlg : public CTeamsDlgBase
+{
+ CCtrlClc m_clc;
+
+public:
+ LIST<char> m_ContactsList;
+
+ CSkypeGCCreateDlg(CTeamsProto *proto) :
+ CTeamsDlgBase(proto, IDD_GC_CREATE),
+ m_clc(this, IDC_CLIST),
+ m_ContactsList(1)
+ {
+ m_clc.OnListRebuilt = Callback(this, &CSkypeGCCreateDlg::FilterList);
+ }
+
+ ~CSkypeGCCreateDlg()
+ {
+ CTeamsProto::FreeList(m_ContactsList);
+ m_ContactsList.destroy();
+ }
+
+ bool OnInitDialog() override
+ {
+ SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE,
+ GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE);
+ m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0);
+
+ ResetListOptions(&m_clc);
+ return true;
+ }
+
+ bool OnApply() override
+ {
+ for (auto &hContact : m_proto->AccContacts()) {
+ if (!m_proto->isChatRoom(hContact))
+ if (HANDLE hItem = m_clc.FindContact(hContact))
+ if (m_clc.GetCheck(hItem))
+ m_ContactsList.insert(m_proto->getId(hContact).Detach());
+ }
+
+ m_ContactsList.insert(m_proto->m_szSkypename.GetBuffer());
+ return true;
+ }
+
+ void FilterList(CCtrlClc *)
+ {
+ for (auto &hContact : Contacts()) {
+ char *proto = Proto_GetBaseAccountName(hContact);
+ if (mir_strcmp(proto, m_proto->m_szModuleName) || m_proto->isChatRoom(hContact))
+ if (HANDLE hItem = m_clc.FindContact(hContact))
+ m_clc.DeleteItem(hItem);
+ }
+ }
+
+ void ResetListOptions(CCtrlClc *)
+ {
+ m_clc.SetHideEmptyGroups(true);
+ m_clc.SetHideOfflineRoot(true);
+ }
+};
+
+INT_PTR CTeamsProto::SvcCreateChat(WPARAM, LPARAM)
+{
+ if (IsOnline()) {
+ CSkypeGCCreateDlg dlg(this);
+ if (dlg.DoModal()) {
+ PushRequest(new CreateChatroomRequest(dlg.m_ContactsList, this));
+ return 0;
+ }
+ }
+ return 1;
+}
+
+/* Menus */
+
+int CTeamsProto::OnGroupChatMenuHook(WPARAM, LPARAM lParam)
+{
+ GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam;
+ if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) return 0;
+
+ if (gcmi->Type == MENU_ON_LOG) {
+ static const struct gc_item Items[] =
+ {
+ { LPGENW("&Invite user..."), 10, MENU_ITEM, FALSE },
+ { LPGENW("&Leave chat session"), 20, MENU_ITEM, FALSE },
+ { LPGENW("&Change topic..."), 30, MENU_ITEM, FALSE }
+ };
+ Chat_AddMenuItems(gcmi->hMenu, _countof(Items), Items, &g_plugin);
+ }
+ else if (gcmi->Type == MENU_ON_NICKLIST) {
+ static const struct gc_item Items[] =
+ {
+ { LPGENW("Kick &user"), 10, MENU_ITEM },
+ { nullptr, 0, MENU_SEPARATOR },
+ { LPGENW("Set &role"), 20, MENU_NEWPOPUP },
+ { LPGENW("&Admin"), 30, MENU_POPUPITEM },
+ { LPGENW("&User"), 40, MENU_POPUPITEM },
+ { LPGENW("Change nick..."), 50, MENU_ITEM },
+ };
+ Chat_AddMenuItems(gcmi->hMenu, _countof(Items), Items, &g_plugin);
+ }
+
+ return 0;
+}
+
+CMStringW CTeamsProto::ChangeTopicForm()
+{
+ CMStringW caption(FORMAT, L"[%s] %s", _A2T(m_szModuleName).get(), TranslateT("Enter new chatroom topic"));
+ ENTER_STRING pForm = {};
+ pForm.type = ESF_MULTILINE;
+ pForm.caption = caption;
+ pForm.szModuleName = m_szModuleName;
+ return (!EnterString(&pForm)) ? CMStringW() : CMStringW(ptrW(pForm.ptszResult));
+}
diff --git a/protocols/Teams/src/teams_contacts.cpp b/protocols/Teams/src/teams_contacts.cpp
new file mode 100644
index 0000000000..fa4577ecfc
--- /dev/null
+++ b/protocols/Teams/src/teams_contacts.cpp
@@ -0,0 +1,290 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+uint16_t CTeamsProto::GetContactStatus(MCONTACT hContact)
+{
+ return getWord(hContact, "Status", ID_STATUS_OFFLINE);
+}
+
+void CTeamsProto::SetContactStatus(MCONTACT hContact, uint16_t status)
+{
+ uint16_t oldStatus = GetContactStatus(hContact);
+ if (oldStatus != status) {
+ setWord(hContact, "Status", status);
+ if (status == ID_STATUS_OFFLINE)
+ delSetting(hContact, "MirVer");
+ }
+}
+
+void CTeamsProto::SetChatStatus(MCONTACT hContact, int iStatus)
+{
+ ptrW tszChatID(getWStringA(hContact, SKYPE_SETTINGS_ID));
+ if (tszChatID != NULL)
+ Chat_Control(Chat_Find(tszChatID, m_szModuleName), (iStatus == ID_STATUS_OFFLINE) ? SESSION_OFFLINE : SESSION_ONLINE);
+}
+
+MCONTACT CTeamsProto::GetContactFromAuthEvent(MEVENT hEvent)
+{
+ uint32_t body[3];
+ DBEVENTINFO dbei = {};
+ dbei.cbBlob = sizeof(uint32_t) * 2;
+ dbei.pBlob = (char *)&body;
+
+ if (db_event_get(hEvent, &dbei))
+ return INVALID_CONTACT_ID;
+
+ if (dbei.eventType != EVENTTYPE_AUTHREQUEST)
+ return INVALID_CONTACT_ID;
+
+ if (mir_strcmp(dbei.szModule, m_szModuleName) != 0)
+ return INVALID_CONTACT_ID;
+ return DbGetAuthEventContact(&dbei);
+}
+
+MCONTACT CTeamsProto::FindContact(const char *skypeId)
+{
+ for (auto &hContact : AccContacts())
+ if (!mir_strcmpi(skypeId, ptrA(getUStringA(hContact, SKYPE_SETTINGS_ID))))
+ return hContact;
+
+ return 0;
+}
+
+MCONTACT CTeamsProto::FindContact(const wchar_t *skypeId)
+{
+ for (auto &hContact : AccContacts())
+ if (!mir_wstrcmpi(skypeId, getMStringW(hContact, SKYPE_SETTINGS_ID)))
+ return hContact;
+
+ return 0;
+}
+
+MCONTACT CTeamsProto::AddContact(const char *skypeId, const char *nick, bool isTemporary)
+{
+ MCONTACT hContact = FindContact(skypeId);
+ if (hContact)
+ return hContact;
+
+ hContact = db_add_contact();
+ Proto_AddToContact(hContact, m_szModuleName);
+
+ setString(hContact, SKYPE_SETTINGS_ID, skypeId);
+ setUString(hContact, "Nick", (nick) ? nick : GetSkypeNick(skypeId));
+
+ if (m_wstrCListGroup) {
+ Clist_GroupCreate(0, m_wstrCListGroup);
+ Clist_SetGroup(hContact, m_wstrCListGroup);
+ }
+
+ setByte(hContact, "Auth", 1);
+ setByte(hContact, "Grant", 1);
+
+ if (isTemporary)
+ Contact::RemoveFromList(hContact);
+ return hContact;
+}
+
+void CTeamsProto::LoadContactsAuth(MHttpResponse *response, AsyncHttpRequest*)
+{
+ SkypeReply reply(response);
+ if (reply.error())
+ return;
+
+ auto &root = reply.data();
+ for (auto &item : root["invite_list"]) {
+ std::string skypeId = item["mri"].as_string();
+ std::string reason = item["greeting"].as_string();
+
+ time_t eventTime = 0;
+ for (auto &it : item["invites"])
+ eventTime = IsoToUnixTime(it["time"].as_string());
+
+ std::string displayName = item["displayname"].as_string();
+ const char *szNick = (displayName.empty()) ? nullptr : displayName.c_str();
+
+ MCONTACT hContact = AddContact(skypeId.c_str(), szNick);
+ time_t lastEventTime = getDword(hContact, "LastAuthRequestTime");
+ if (lastEventTime && lastEventTime >= eventTime)
+ continue;
+
+ setUString(hContact, "Nick", displayName.c_str());
+
+ setDword(hContact, "LastAuthRequestTime", eventTime);
+ delSetting(hContact, "Auth");
+
+ DB::AUTH_BLOB blob(hContact, displayName.c_str(), nullptr, nullptr, skypeId.c_str(), reason.c_str());
+
+ DB::EventInfo dbei;
+ dbei.iTimestamp = time(0);
+ dbei.cbBlob = blob.size();
+ dbei.pBlob = blob;
+ ProtoChainRecv(hContact, PSR_AUTH, 0, (LPARAM)&dbei);
+ }
+}
+
+void CTeamsProto::LoadContactList(MHttpResponse *response, AsyncHttpRequest*)
+{
+ SkypeReply reply(response);
+ if (reply.error())
+ return;
+
+ auto &root = reply.data();
+ for (auto &item : root["contacts"]) {
+ CMStringA szSkypeId = item["person_id"].as_mstring();
+ if (!IsPossibleUserType(szSkypeId))
+ continue;
+
+ MCONTACT hContact = AddContact(szSkypeId, nullptr);
+ if (szSkypeId == "28:e7a9407c-2467-4a04-9546-70081f4ea80d")
+ m_hMyContact = hContact;
+
+ std::string displayName = item["display_name"].as_string();
+ if (!displayName.empty()) {
+ if (m_hMyContact == hContact) {
+ displayName = getMStringU("Nick");
+ displayName += " ";
+ displayName += TranslateU("(You)");
+
+ setWord(hContact, "Status", ID_STATUS_ONLINE);
+ }
+ setUString(hContact, "Nick", displayName.c_str());
+ }
+
+ if (item["authorized"].as_bool()) {
+ delSetting(hContact, "Auth");
+ delSetting(hContact, "Grant");
+ }
+ else setByte(hContact, "Grant", 1);
+
+ if (item["blocked"].as_bool())
+ setByte(hContact, "IsBlocked", 1);
+ else
+ delSetting(hContact, "IsBlocked");
+
+ ptrW wszGroup(Clist_GetGroup(hContact));
+ if (wszGroup == nullptr && m_wstrCListGroup) {
+ Clist_GroupCreate(0, m_wstrCListGroup);
+ Clist_SetGroup(hContact, m_wstrCListGroup);
+ }
+
+ auto &profile = item["profile"];
+ SetString(hContact, "Homepage", profile["website"]);
+
+ auto wstr = profile["birthday"].as_mstring();
+ if (!wstr.IsEmpty() ) {
+ int nYear, nMonth, nDay;
+ if (swscanf(wstr, L"%d-%d-%d", &nYear, &nMonth, &nDay) == 3)
+ Contact::SetBirthday(hContact, nDay, nMonth, nYear);
+ }
+
+ wstr = profile["gender"].as_mstring();
+ if (wstr == "male")
+ setByte(hContact, "Gender", 'M');
+ else if (wstr == "female")
+ setByte(hContact, "Gender", 'F');
+
+ auto &name = profile["name"];
+ SetString(hContact, "FirstName", name["first"]);
+ SetString(hContact, "LastName", name["surname"]);
+
+ if (auto &pMood = profile["mood"])
+ db_set_ws(hContact, "CList", "StatusMsg", RemoveHtml(pMood.as_mstring()));
+
+ SetAvatarUrl(hContact, profile["avatar_url"].as_mstring());
+ ReloadAvatarInfo(hContact);
+
+ for (auto &phone : profile["phones"]) {
+ CMStringW number = phone["number"].as_mstring();
+
+ auto wszType = phone["type"].as_mstring();
+ if (wszType == L"mobile")
+ setWString(hContact, "Cellular", number);
+ else if (wszType == L"phone")
+ setWString(hContact, "Phone", number);
+ }
+ }
+
+ PushRequest(new GetContactsAuthRequest());
+}
+
+INT_PTR CTeamsProto::OnRequestAuth(WPARAM hContact, LPARAM)
+{
+ if (hContact == INVALID_CONTACT_ID)
+ return 1;
+
+ PushRequest(new AddContactRequest(getId(hContact)));
+ return 0;
+}
+
+INT_PTR CTeamsProto::OnGrantAuth(WPARAM hContact, LPARAM)
+{
+ if (hContact == INVALID_CONTACT_ID)
+ return 1;
+
+ PushRequest(new AuthAcceptRequest(getId(hContact)));
+ return 0;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+bool CTeamsProto::OnContactDeleted(MCONTACT hContact, uint32_t flags)
+{
+ if (IsOnline() && hContact && (flags & CDF_DEL_CONTACT)) {
+ CMStringA szId(getId(hContact));
+ if (isChatRoom(hContact))
+ KickChatUser(szId, m_szOwnSkypeId);
+ else
+ PushRequest(new AsyncHttpRequest(REQUEST_DELETE, HOST_CONTACTS, "/users/SELF/contacts/" + mir_urlEncode(szId)));
+ }
+ return true;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+INT_PTR CTeamsProto::BlockContact(WPARAM hContact, LPARAM)
+{
+ if (!IsOnline()) return 1;
+
+ if (IDYES == MessageBox(NULL, TranslateT("Are you sure?"), TranslateT("Warning"), MB_YESNO | MB_ICONQUESTION))
+ PushRequest(new BlockContactRequest(this, hContact));
+ return 0;
+}
+
+void CTeamsProto::OnBlockContact(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ MCONTACT hContact = (DWORD_PTR)pRequest->pUserInfo;
+ if (response != nullptr)
+ Contact::Hide(hContact);
+}
+
+INT_PTR CTeamsProto::UnblockContact(WPARAM hContact, LPARAM)
+{
+ PushRequest(new UnblockContactRequest(this, hContact));
+ return 0;
+}
+
+void CTeamsProto::OnUnblockContact(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ if (response == nullptr)
+ return;
+
+ MCONTACT hContact = (DWORD_PTR)pRequest->pUserInfo;
+ Contact::Hide(hContact, false);
+ delSetting(hContact, "IsBlocked");
+}
diff --git a/protocols/Teams/src/teams_endpoint.cpp b/protocols/Teams/src/teams_endpoint.cpp
new file mode 100644
index 0000000000..d0431cd3b2
--- /dev/null
+++ b/protocols/Teams/src/teams_endpoint.cpp
@@ -0,0 +1,227 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+void CTeamsProto::CheckConvert()
+{
+ m_szSkypename = getMStringA(SKYPE_SETTINGS_ID);
+ if (m_szSkypename.IsEmpty()) {
+ m_szSkypename = getMStringA(SKYPE_SETTINGS_LOGIN);
+ if (!m_szSkypename.IsEmpty()) { // old settings format, need to update all settings
+ m_szSkypename.Insert(0, "8:");
+ setString(SKYPE_SETTINGS_ID, m_szSkypename);
+
+ for (auto &hContact : AccContacts()) {
+ CMStringA id(ptrA(getUStringA(hContact, "Skypename")));
+ if (!id.IsEmpty())
+ setString(hContact, SKYPE_SETTINGS_ID, (isChatRoom(hContact)) ? "19:" + id : "8:" + id);
+
+ ptrW wszNick(getWStringA(hContact, "Nick"));
+ if (wszNick == nullptr)
+ setUString(hContact, "Nick", id);
+
+ delSetting(hContact, "Skypename");
+ }
+ }
+ }
+}
+
+void CTeamsProto::ProcessTimer()
+{
+ if (!IsOnline())
+ return;
+
+ PushRequest(new GetContactListRequest());
+ SendPresence();
+}
+
+void CTeamsProto::SendCreateEndpoint()
+{
+ auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT, "/users/ME/endpoints", &CTeamsProto::OnEndpointCreated);
+ pReq->m_szParam = "{\"endpointFeatures\":\"Agent,Presence2015,MessageProperties,CustomUserProperties,Casts,ModernBots,AutoIdleForWebApi,secureThreads,notificationStream,InviteFree,SupportsReadReceipts,ued\"}";
+ pReq->AddHeader("Origin", "https://web.skype.com");
+ pReq->AddHeader("Referer", "https://web.skype.com/");
+ pReq->AddAuthentication(this);
+
+ PushRequest(pReq);
+}
+
+void CTeamsProto::OnEndpointCreated(MHttpResponse *response, AsyncHttpRequest*)
+{
+ if (IsStatusConnecting(m_iStatus))
+ m_iStatus++;
+
+ if (response == nullptr) {
+ debugLogA(__FUNCTION__ ": failed to get create endpoint");
+ ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ SetStatus(ID_STATUS_OFFLINE);
+ return;
+ }
+
+ switch (response->resultCode) {
+ case 200:
+ case 201: // okay, endpoint created
+ break;
+
+ case 301:
+ case 302: // redirect to the closest data center
+ if (auto *hdr = response->FindHeader("Location")) {
+ CMStringA szUrl(hdr+8);
+ int iEnd = szUrl.Find('/');
+ // g_plugin.szDefaultServer = (iEnd != -1) ? szUrl.Left(iEnd) : szUrl;
+ }
+ SendCreateEndpoint();
+ return;
+
+ case 401: // unauthorized
+ default:
+ delSetting("TokenExpiresIn");
+ ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ SetStatus(ID_STATUS_OFFLINE);
+ return;
+ }
+
+ // Succeeded, decode the answer
+ int oldStatus = m_iStatus;
+ m_iStatus = m_iDesiredStatus;
+ ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus);
+
+ if (auto *hdr = response->FindHeader("Set-RegistrationToken")) {
+ CMStringA szValue = hdr;
+ int iStart = 0;
+ while (true) {
+ CMStringA szToken = szValue.Tokenize(";", iStart).Trim();
+ if (iStart == -1)
+ break;
+
+ int iStart2 = 0;
+ CMStringA name = szToken.Tokenize("=", iStart2);
+ CMStringA val = szToken.Mid(iStart2);
+
+ if (name == "registrationToken")
+ m_szToken = val.Detach();
+ else if (name == "endpointId")
+ m_szId = val.Detach();
+ }
+ }
+
+ if (m_szId && m_hPollingThread == nullptr)
+ ForkThread(&CTeamsProto::PollingThread);
+
+ PushRequest(new CreateSubscriptionsRequest());
+}
+
+void CTeamsProto::OnEndpointDeleted(MHttpResponse *, AsyncHttpRequest *)
+{
+ m_szId = nullptr;
+ m_szToken = nullptr;
+}
+
+void CTeamsProto::OnSubscriptionsCreated(MHttpResponse *response, AsyncHttpRequest*)
+{
+ if (response == nullptr) {
+ debugLogA(__FUNCTION__ ": failed to create subscription");
+ ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ SetStatus(ID_STATUS_OFFLINE);
+ return;
+ }
+
+ SendPresence();
+}
+
+void CTeamsProto::SendPresence()
+{
+ ptrA epname;
+
+ if (!m_bUseHostnameAsPlace && m_wstrPlace && *m_wstrPlace)
+ epname = mir_utf8encodeW(m_wstrPlace);
+ else {
+ wchar_t compName[MAX_COMPUTERNAME_LENGTH + 1];
+ DWORD size = _countof(compName);
+ GetComputerName(compName, &size);
+ epname = mir_utf8encodeW(compName);
+ }
+
+ PushRequest(new SendCapabilitiesRequest(epname, this));
+}
+
+void CTeamsProto::OnCapabilitiesSended(MHttpResponse *response, AsyncHttpRequest*)
+{
+ if (response == nullptr || response->body.IsEmpty()) {
+ ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ SetStatus(ID_STATUS_OFFLINE);
+ return;
+ }
+
+ PushRequest(new SetStatusRequest(MirandaToSkypeStatus(m_iDesiredStatus)));
+
+ LIST<char> skypenames(1);
+ for (auto &hContact : AccContacts())
+ if (!isChatRoom(hContact))
+ skypenames.insert(getId(hContact).Detach());
+
+ PushRequest(new CreateContactsSubscriptionRequest(skypenames));
+ FreeList(skypenames);
+ skypenames.destroy();
+
+ ReceiveAvatar(0);
+ PushRequest(new GetContactListRequest());
+ PushRequest(new SyncConversations());
+
+ JSONNode root = JSONNode::parse(response->body);
+ if (root)
+ m_szOwnSkypeId = UrlToSkypeId(root["selfLink"].as_string().c_str()).Detach();
+
+ PushRequest(new GetProfileRequest(this, 0));
+}
+
+void CTeamsProto::OnStatusChanged(MHttpResponse *response, AsyncHttpRequest*)
+{
+ if (response == nullptr || response->body.IsEmpty()) {
+ debugLogA(__FUNCTION__ ": failed to change status");
+ ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ SetStatus(ID_STATUS_OFFLINE);
+ return;
+ }
+
+ JSONNode json = JSONNode::parse(response->body);
+ if (!json) {
+ debugLogA(__FUNCTION__ ": failed to change status");
+ ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ SetStatus(ID_STATUS_OFFLINE);
+ return;
+ }
+
+ const JSONNode &nStatus = json["status"];
+ if (!nStatus) {
+ debugLogA(__FUNCTION__ ": result contains no valid status to switch to");
+ return;
+ }
+
+ int iNewStatus = SkypeToMirandaStatus(nStatus.as_string().c_str());
+ if (iNewStatus == ID_STATUS_OFFLINE) {
+ debugLogA(__FUNCTION__ ": failed to change status");
+ ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, 1001);
+ SetStatus(ID_STATUS_OFFLINE);
+ return;
+ }
+
+ int oldStatus = m_iStatus;
+ m_iStatus = m_iDesiredStatus = iNewStatus;
+ ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus);
+}
diff --git a/protocols/Teams/src/teams_files.cpp b/protocols/Teams/src/teams_files.cpp
new file mode 100644
index 0000000000..a6e7d07087
--- /dev/null
+++ b/protocols/Teams/src/teams_files.cpp
@@ -0,0 +1,297 @@
+#include "stdafx.h"
+
+////////////////////////////////////////////////////////////////////////////////////////
+// File receiving
+
+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 CTeamsProto::ReceiveFileThread(void *param)
+{
+ auto *ofd = (OFDTHREAD *)param;
+
+ DB::EventInfo dbei(ofd->hDbEvent);
+ if (IsOnline() && dbei && !strcmp(dbei.szModule, m_szModuleName) && dbei.eventType == EVENTTYPE_FILE) {
+ DB::FILE_BLOB blob(dbei);
+
+ if (ofd->bCopy) {
+ ofd->wszPath = Utf2T(blob.getUrl()).get();
+ ofd->pCallback->Invoke(*ofd);
+ }
+ else {
+ CMStringA szCookie, szUrl;
+ szCookie.AppendFormat("skypetoken_asm=%s", m_szSkypeToken.c_str());
+
+ auto &json = dbei.getJson();
+ auto skft = json["skft"].as_string();
+ {
+ const char *preview;
+ if (skft == "Picture.1")
+ preview = "imgpsh_mobile_save_anim";
+ else if (skft == "Video.1")
+ preview = "video";
+ else
+ preview = "original";
+
+ MHttpRequest nlhr(REQUEST_GET);
+ nlhr.flags = NLHRF_HTTP11 | NLHRF_NOUSERAGENT;
+ nlhr.m_szUrl = blob.getUrl();
+ nlhr.m_szUrl.AppendFormat("/views/%s/status", preview);
+ nlhr.AddHeader("Accept", "*/*");
+ nlhr.AddHeader("Accept-Encoding", "gzip, deflate");
+ nlhr.AddHeader("Cookie", szCookie);
+ NLHR_PTR response(Netlib_HttpTransaction(m_hNetlibUser, &nlhr));
+ if (response) {
+ SkypeReply reply(response);
+ if (!reply.error()) {
+ auto &root = reply.data();
+ if (root["content_state"].as_string() == "ready")
+ szUrl = root["view_location"].as_string().c_str();
+ }
+ }
+ }
+
+ if (!szUrl.IsEmpty()) {
+ MHttpRequest nlhr(REQUEST_GET);
+ nlhr.flags = NLHRF_HTTP11 | NLHRF_NOUSERAGENT;
+ nlhr.m_szUrl = blob.getUrl();
+ if (skft == "Picture.1")
+ nlhr.m_szUrl += "/views/imgpsh_fullsize_anim";
+ else if (skft == "Video.1")
+ nlhr.m_szUrl += "/views/video";
+ else
+ nlhr.m_szUrl += "/views/original";
+
+ nlhr.AddHeader("Accept", "*/*");
+ nlhr.AddHeader("Accept-Encoding", "gzip, deflate");
+ nlhr.AddHeader("Cookie", szCookie);
+
+ NLHR_PTR reply(Netlib_DownloadFile(m_hNetlibUser, &nlhr, ofd->wszPath, DownloadCallack, ofd));
+ if (reply && reply->resultCode == 200) {
+ 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();
+ }
+ }
+ }
+ }
+
+ delete ofd;
+}
+
+INT_PTR CTeamsProto::SvcOfflineFile(WPARAM param, LPARAM)
+{
+ ForkThread(&CTeamsProto::ReceiveFileThread, (void *)param);
+ return 0;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////
+// File sending
+
+#define FILETRANSFER_FAILED(fup) { ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_FAILED, (HANDLE)fup); delete fup; fup = nullptr;}
+
+void CTeamsProto::SendFile(CFileUploadParam *fup)
+{
+ auto *pwszFileName = &fup->arFileName[0];
+ if (!IsOnline() || _waccess(pwszFileName, 0)) {
+ FILETRANSFER_FAILED(fup);
+ return;
+ }
+
+ if (auto *pBitmap = FreeImage_LoadU(FreeImage_GetFIFFromFilenameU(pwszFileName), pwszFileName)) {
+ fup->isPicture = true;
+ fup->width = FreeImage_GetWidth(pBitmap);
+ fup->height = FreeImage_GetHeight(pBitmap);
+ FreeImage_Unload(pBitmap);
+ }
+ else fup->isPicture = false;
+
+ ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_CONNECTING, (HANDLE)fup);
+
+ // create upload slot
+ auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://api.asm.skype.com/v1/objects", &CTeamsProto::OnASMObjectCreated);
+ pReq->flags &= (~NLHRF_DUMPASTEXT);
+ pReq->pUserInfo = fup;
+
+ pReq->AddHeader("Authorization", CMStringA(FORMAT, "skype_token %s", m_szSkypeToken.c_str()));
+ pReq->AddHeader("Content-Type", "application/json");
+ pReq->AddHeader("X-Client-Version", "0/0.0.0.0");
+
+ CMStringA szContact(getId(fup->hContact));
+ T2Utf uszFileName(&fup->arFileName[0]);
+ const char *szFileName = strrchr(uszFileName.get() + 1, '\\');
+
+ JSONNode node;
+ if (fup->isPicture)
+ node << CHAR_PARAM("type", "pish/image");
+ else
+ node << CHAR_PARAM("type", "sharing/file");
+
+ JSONNode jPermission(JSON_ARRAY); jPermission.set_name(szContact.c_str()); jPermission << CHAR_PARAM("", "read");
+ JSONNode jPermissions; jPermissions.set_name("permissions"); jPermissions << jPermission;
+ node << CHAR_PARAM("filename", szFileName) << jPermissions;
+ pReq->m_szParam = node.write().c_str();
+ PushRequest(pReq);
+}
+
+void CTeamsProto::OnASMObjectCreated(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ auto *fup = (CFileUploadParam*)pRequest->pUserInfo;
+ if (response == nullptr || response->body.IsEmpty()) {
+LBL_Error:
+ FILETRANSFER_FAILED(fup);
+ return;
+ }
+
+ if (response->resultCode != 200 && response->resultCode != 201) {
+ debugLogA("Object creation failed with error code %d", response->resultCode);
+ goto LBL_Error;
+ }
+
+ JSONNode node = JSONNode::parse(response->body);
+ std::string strObjectId = node["id"].as_string();
+ if (strObjectId.empty()) {
+ debugLogA("Invalid server response (empty object id)");
+ goto LBL_Error;
+ }
+
+ fup->uid = mir_strdup(strObjectId.c_str());
+ FILE *pFile = _wfopen(&fup->arFileName[0], L"rb");
+ if (pFile == nullptr)
+ goto LBL_Error;
+
+ fseek(pFile, 0, SEEK_END);
+ long lFileLen = ftell(pFile);
+ if (lFileLen < 1) {
+ fclose(pFile);
+ goto LBL_Error;
+ }
+
+ fseek(pFile, 0, SEEK_SET);
+
+ mir_ptr<uint8_t> pData((uint8_t*)mir_alloc(lFileLen));
+ long lBytes = (long)fread(pData, sizeof(uint8_t), lFileLen, pFile);
+ fclose(pFile);
+
+ if (lBytes != lFileLen)
+ goto LBL_Error;
+
+ fup->size = lBytes;
+ ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, (HANDLE)fup);
+
+ // upload file to the previously created slot
+ auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_OTHER, 0, &CTeamsProto::OnASMObjectUploaded);
+ pReq->m_szUrl.Format("https://api.asm.skype.com/v1/objects/%s/content/%s",
+ strObjectId.c_str(), fup->isPicture ? "imgpsh" : "original");
+ pReq->pUserInfo = fup;
+
+ pReq->AddHeader("Authorization", CMStringA(FORMAT, "skype_token %s", m_szSkypeToken.c_str()));
+ pReq->AddHeader("Content-Type", fup->isPicture ? "application" : "application/octet-stream");
+
+ pReq->m_szParam.Truncate(lBytes);
+ memcpy(pReq->m_szParam.GetBuffer(), pData, lBytes);
+ PushRequest(pReq);
+}
+
+void CTeamsProto::OnASMObjectUploaded(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ auto *fup = (CFileUploadParam *)pRequest->pUserInfo;
+ if (response == nullptr) {
+ FILETRANSFER_FAILED(fup);
+ return;
+ }
+
+ wchar_t *tszFile = wcsrchr(&fup->arFileName[0], L'\\') + 1;
+
+ TiXmlDocument doc;
+ auto *pRoot = doc.NewElement("URIObject");
+ doc.InsertEndChild(pRoot);
+
+ pRoot->SetAttribute("doc_id", fup->uid.get());
+ pRoot->SetAttribute("uri", CMStringA(FORMAT, "https://api.asm.skype.com/v1/objects/%s", fup->uid.get()));
+
+ // is that a picture?
+ CMStringA href;
+ if (fup->isPicture) {
+ pRoot->SetAttribute("type", "Picture.1");
+ pRoot->SetAttribute("url_thumbnail", CMStringA(FORMAT, "https://api.asm.skype.com/v1/objects/%s/views/imgt1_anim", fup->uid.get()));
+ pRoot->SetAttribute("width", fup->width);
+ pRoot->SetAttribute("height", fup->height);
+ pRoot->SetText("To view this shared photo, go to:");
+
+ href.Format("https://login.skype.com/login/sso?go=xmmfallback?pic=%s", fup->uid.get());
+ }
+ else {
+ pRoot->SetAttribute("type", "File.1");
+ pRoot->SetAttribute("url_thumbnail", CMStringA(FORMAT, "https://api.asm.skype.com/v1/objects/%s/views/original", fup->uid.get()));
+ pRoot->SetText("To view this file, go to:");
+
+ href.Format("https://login.skype.com/login/sso?go=webclient.xmm&docid=%s", fup->uid.get());
+ }
+
+ auto *xmlA = doc.NewElement("a"); xmlA->SetText(href);
+ xmlA->SetAttribute("href", href);
+ pRoot->InsertEndChild(xmlA);
+
+ auto *xmlOrigName = doc.NewElement("OriginalName"); xmlOrigName->SetAttribute("v", tszFile); pRoot->InsertEndChild(xmlOrigName);
+ auto *xmlSize = doc.NewElement("FileSize"); xmlSize->SetAttribute("v", (int)fup->size); pRoot->InsertEndChild(xmlSize);
+
+ if (fup->isPicture) {
+ auto xmlMeta = doc.NewElement("meta");
+ xmlMeta->SetAttribute("type", "photo"); xmlMeta->SetAttribute("originalName", tszFile);
+ pRoot->InsertEndChild(xmlMeta);
+ }
+
+ tinyxml2::XMLPrinter printer(0, true);
+ doc.Print(&printer);
+
+ // create a new file transfer event using previously filled slot
+ auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT);
+ pReq->m_szUrl.AppendFormat("/users/ME/conversations/%s/messages", mir_urlEncode(getId(fup->hContact)).c_str());
+ pReq->hContact = fup->hContact;
+
+ JSONNode ref(JSON_ARRAY); ref.set_name("amsreferences"); ref << CHAR_PARAM("", fup->uid);
+
+ JSONNode node;
+ if (fup->isPicture)
+ node << CHAR_PARAM("messagetype", "RichText/UriObject");
+ else
+ node << CHAR_PARAM("messagetype", "RichText/Media_GenericFile");
+
+ node << INT64_PARAM("clientmessageid", getRandomId()) << CHAR_PARAM("contenttype", "text") << CHAR_PARAM("content", printer.CStr()) << ref;
+ pReq->m_szParam = node.write().c_str();
+
+ PushRequest(pReq);
+
+ // if that's last file in the queue, finish file transfer, or proceed with the next file
+ if (fup->arFileName.getCount() == 1) {
+ ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, (HANDLE)fup);
+ delete fup;
+ }
+ else {
+ fup->arFileName.remove(int(0));
+ ProtoBroadcastAck(fup->hContact, ACKTYPE_FILE, ACKRESULT_NEXTFILE, (HANDLE)fup);
+ SendFile(fup);
+ }
+}
+
+HANDLE CTeamsProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles)
+{
+ if (!IsOnline())
+ return INVALID_HANDLE_VALUE;
+
+ CFileUploadParam *fup = new CFileUploadParam(hContact, ppszFiles, szDescription);
+ SendFile(fup);
+ return fup;
+}
diff --git a/protocols/Teams/src/teams_history.cpp b/protocols/Teams/src/teams_history.cpp
new file mode 100644
index 0000000000..eb6a9ca39f
--- /dev/null
+++ b/protocols/Teams/src/teams_history.cpp
@@ -0,0 +1,151 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+/* HISTORY SYNC */
+
+void CTeamsProto::OnGetServerHistory(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ SkypeReply reply(response);
+ if (reply.error())
+ return;
+
+ auto *pOrig = (GetHistoryRequest *)pRequest;
+ auto &root = reply.data();
+ const JSONNode &metadata = root["_metadata"];
+
+ int totalCount = metadata["totalCount"].as_int();
+ std::string syncState = metadata["syncState"].as_string();
+
+ bool bOperative = pRequest->pUserInfo != 0;
+ bool bSetLastTime = false;
+
+ int64_t lastMsgTime = 0; // max timestamp on this page
+
+ auto &conv = root["messages"];
+ for (auto it = conv.rbegin(); it != conv.rend(); ++it) {
+ auto &message = *it;
+ CMStringA szId = message["id"].as_mstring();
+ int64_t id = _atoi64(szId);
+ if (id > lastMsgTime) {
+ bSetLastTime = true;
+ lastMsgTime = id;
+ }
+
+ int iUserType;
+ CMStringA szMessageId(getMessageId(message));
+ CMStringA szChatId = UrlToSkypeId(message["conversationLink"].as_mstring(), &iUserType);
+ CMStringA szFrom = UrlToSkypeId(message["from"].as_mstring());
+
+ MCONTACT hContact = FindContact(szChatId);
+
+ DB::EventInfo dbei(db_event_getById(m_szModuleName, szMessageId));
+ dbei.hContact = hContact;
+ dbei.szModule = m_szModuleName;
+ dbei.szId = szMessageId;
+ dbei.bSent = IsMe(szFrom);
+ dbei.bMsec = dbei.bUtf = true;
+ dbei.iTimestamp = _wtoi64(message["id"].as_mstring());
+
+ if (iUserType == 19) {
+ dbei.szUserId = szFrom;
+
+ CMStringA szType(message["messagetype"].as_mstring());
+ if (szType.Left(15) == "ThreadActivity/")
+ continue;
+ }
+
+ if (!bOperative && !dbei.getEvent())
+ dbei.bRead = true;
+
+ if (ParseMessage(message, dbei)) {
+ if (dbei)
+ db_event_edit(dbei.getEvent(), &dbei, true);
+ else
+ db_event_add(hContact, &dbei);
+ }
+ }
+
+ if (bSetLastTime && lastMsgTime > getLastTime(pOrig->hContact))
+ setLastTime(pOrig->hContact, lastMsgTime);
+
+ if (totalCount >= 99 || conv.size() >= 99)
+ PushRequest(new GetHistoryRequest(pOrig->hContact, pOrig->m_who, 100, lastMsgTime, pRequest->pUserInfo != 0));
+}
+
+INT_PTR CTeamsProto::SvcLoadHistory(WPARAM hContact, LPARAM)
+{
+ PushRequest(new GetHistoryRequest(hContact, getId(hContact), 100, 0, false));
+ return 0;
+}
+
+void CTeamsProto::OnSyncConversations(MHttpResponse *response, AsyncHttpRequest*)
+{
+ SkypeReply reply(response);
+ if (reply.error())
+ return;
+
+ auto &root = reply.data();
+ const JSONNode &metadata = root["_metadata"];
+ const JSONNode &conversations = root["conversations"].as_array();
+
+ // int totalCount = metadata["totalCount"].as_int();
+ std::string syncState = metadata["syncState"].as_string();
+
+ for (auto &it: conversations) {
+ const JSONNode &lastMessage = it["lastMessage"];
+ if (!lastMessage)
+ continue;
+
+ int iUserType;
+ std::string strConversationLink = lastMessage["conversationLink"].as_string();
+ CMStringA szSkypename = UrlToSkypeId(strConversationLink.c_str(), &iUserType);
+ switch (iUserType) {
+ case 19:
+ {
+ auto &props = it["threadProperties"];
+ if (!props["lastleaveat"])
+ StartChatRoom(it["id"].as_mstring(), props["topic"].as_mstring(), props["version"].as_string().c_str());
+ }
+ __fallthrough;
+
+ case 8:
+ case 2:
+ int64_t id = _atoi64(lastMessage["id"].as_string().c_str());
+
+ MCONTACT hContact = FindContact(szSkypename);
+ if (hContact != NULL) {
+ auto lastMsgTime = getLastTime(hContact);
+ if (lastMsgTime && lastMsgTime < id && m_bAutoHistorySync)
+ PushRequest(new GetHistoryRequest(hContact, szSkypename, 100, lastMsgTime, true));
+ }
+ }
+ }
+
+ m_bHistorySynced = true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+
+INT_PTR CTeamsProto::SvcEmptyHistory(WPARAM hContact, LPARAM flags)
+{
+ if (flags & CDF_DEL_HISTORY) {
+ PushRequest(new EmptyHistoryRequest(getId(hContact)));
+ }
+ return 0;
+}
diff --git a/protocols/Teams/src/teams_http.cpp b/protocols/Teams/src/teams_http.cpp
index ecce58fa92..e32ae4c3cb 100644
--- a/protocols/Teams/src/teams_http.cpp
+++ b/protocols/Teams/src/teams_http.cpp
@@ -17,12 +17,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "stdafx.h"
-AsyncHttpRequest::AsyncHttpRequest(int type, HostType host, LPCSTR url, MTHttpRequestHandler pFunc) :
+AsyncHttpRequest::AsyncHttpRequest(int type, SkypeHost host, LPCSTR url, MTHttpRequestHandler pFunc) :
m_host(host)
{
switch (host) {
+ case HOST_API: m_szUrl = "api.skype.com"; break;
+ case HOST_PEOPLE: m_szUrl = "people.skype.com/v2"; break;
+ case HOST_CONTACTS: m_szUrl = "edge.skype.com/pcs/contacts/v2"; break;
+ case HOST_GRAPH: m_szUrl = "skypegraph.skype.com"; break;
case HOST_LOGIN: m_szUrl = "login.microsoftonline.com"; break;
case HOST_TEAMS: m_szUrl = "teams.live.com"; break;
+
+ case HOST_DEFAULT:
+ AddHeader("MS-IC3-Product", "Sfl");
+ m_szUrl = "msgapi.teams.live.com/v1";
+ break;
}
AddHeader("User-Agent", NETLIB_USER_AGENT);
@@ -33,17 +42,16 @@ AsyncHttpRequest::AsyncHttpRequest(int type, HostType host, LPCSTR url, MTHttpRe
flags = NLHRF_HTTP11 | NLHRF_SSL | NLHRF_DUMPASTEXT;
requestType = type;
}
-/*
+
void AsyncHttpRequest::AddAuthentication(CTeamsProto *ppro)
{
- AddHeader("Authentication", CMStringA("skypetoken=") + ppro->m_szApiToken);
+ AddHeader("Authentication", CMStringA("skypetoken=") + ppro->m_szSkypeToken);
}
void AsyncHttpRequest::AddRegister(CTeamsProto *ppro)
{
AddHeader("RegistrationToken", CMStringA("registrationToken=") + ppro->m_szToken);
}
-*/
/////////////////////////////////////////////////////////////////////////////////////////
@@ -103,6 +111,31 @@ MHttpResponse* CTeamsProto::DoSend(AsyncHttpRequest *pReq)
}
}
+ switch (pReq->m_host) {
+ case HOST_API:
+ case HOST_PEOPLE:
+ case HOST_CONTACTS:
+ if (m_szSkypeToken)
+ pReq->AddHeader("X-Skypetoken", m_szSkypeToken);
+
+ pReq->AddHeader("Accept", "application/json");
+ pReq->AddHeader("Origin", "https://web.skype.com");
+ pReq->AddHeader("Referer", "https://web.skype.com/");
+ break;
+
+ case HOST_GRAPH:
+ if (m_szSkypeToken)
+ pReq->AddHeader("X-Skypetoken", m_szSkypeToken);
+ pReq->AddHeader("Accept", "application/json");
+ break;
+
+ case HOST_DEFAULT:
+ if (m_szToken)
+ pReq->AddRegister(this);
+ pReq->AddHeader("Accept", "application/json, text/javascript");
+ break;
+ }
+
debugLogA("Send request to %s", pReq->m_szUrl.c_str());
return Netlib_HttpTransaction(m_hNetlibUser, pReq);
diff --git a/protocols/Teams/src/teams_login.cpp b/protocols/Teams/src/teams_login.cpp
index 90302a3461..72324355ac 100644
--- a/protocols/Teams/src/teams_login.cpp
+++ b/protocols/Teams/src/teams_login.cpp
@@ -39,11 +39,15 @@ void CTeamsProto::LoggedIn()
int oldStatus = m_iStatus;
m_iStatus = m_iDesiredStatus;
ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)oldStatus, m_iStatus);
+
+ m_impl.m_heartBeat.StartSafe(600 * 1000);
+
+ SendCreateEndpoint();
}
/////////////////////////////////////////////////////////////////////////////////////////
-void CTeamsProto::OnReceiveDevicePoll(MHttpResponse *response, AsyncHttpRequest*)
+void CTeamsProto::OnReceiveDevicePoll(MHttpResponse *response, AsyncHttpRequest *)
{
JsonReply reply(response);
if (!reply) {
@@ -81,9 +85,9 @@ void CTeamsProto::LoginPoll()
/////////////////////////////////////////////////////////////////////////////////////////
const wchar_t wszLoginMessage[] =
- LPGENW("To login into Teams you need to open '%S' in a browser and select your Teams account there.") L"\r\n\r\n"
- LPGENW("Enter the following code then: %s.") L"\r\n\r\n"
- LPGENW("Click Proceed to copy that code to clipboard and launch a browser");
+LPGENW("To login into Teams you need to open '%S' in a browser and select your Teams account there.") L"\r\n\r\n"
+LPGENW("Enter the following code then: %s.") L"\r\n\r\n"
+LPGENW("Click Proceed to copy that code to clipboard and launch a browser");
class CDeviceCodeDlg : public CTeamsDlgBase
{
@@ -120,7 +124,7 @@ static void CALLBACK LaunchDialog(void *param)
(new CDeviceCodeDlg((CTeamsProto *)param))->Show();
}
-void CTeamsProto::OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest*)
+void CTeamsProto::OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest *)
{
JsonReply reply(response);
if (!reply) {
@@ -142,7 +146,7 @@ void CTeamsProto::OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest
/////////////////////////////////////////////////////////////////////////////////////////
-void CTeamsProto::OnReceiveSkypeToken(MHttpResponse *response, AsyncHttpRequest*)
+void CTeamsProto::OnReceiveSkypeToken(MHttpResponse *response, AsyncHttpRequest *)
{
JsonReply reply(response);
if (!reply) {
@@ -156,7 +160,7 @@ void CTeamsProto::OnReceiveSkypeToken(MHttpResponse *response, AsyncHttpRequest*
LoggedIn();
}
-void CTeamsProto::OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest*)
+void CTeamsProto::OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest *)
{
JsonReply reply(response);
if (!reply) {
@@ -167,8 +171,6 @@ void CTeamsProto::OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest
auto &root = reply.data();
m_szAccessToken = root["access_token"].as_mstring();
setWString("RefreshToken", root["refresh_token"].as_mstring());
-
- LoggedIn();
}
void CTeamsProto::OnRefreshSkypeToken(MHttpResponse *response, AsyncHttpRequest *)
@@ -187,7 +189,7 @@ void CTeamsProto::OnRefreshSkypeToken(MHttpResponse *response, AsyncHttpRequest
PushRequest(pReq);
}
-void CTeamsProto::OnRefreshSubstrate(MHttpResponse *response, AsyncHttpRequest*)
+void CTeamsProto::OnRefreshSubstrate(MHttpResponse *response, AsyncHttpRequest *)
{
JsonReply reply(response);
if (!reply) {
diff --git a/protocols/Teams/src/teams_menus.cpp b/protocols/Teams/src/teams_menus.cpp
new file mode 100644
index 0000000000..9238c33946
--- /dev/null
+++ b/protocols/Teams/src/teams_menus.cpp
@@ -0,0 +1,99 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+HGENMENU CTeamsProto::ContactMenuItems[CMI_MAX];
+
+int CTeamsProto::OnPrebuildContactMenu(WPARAM hContact, LPARAM)
+{
+ if (!hContact)
+ return 0;
+
+ if (m_iStatus < ID_STATUS_ONLINE)
+ return 0;
+
+ if (isChatRoom(hContact))
+ return 0;
+
+ bool isCtrlPressed = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
+ bool isAuthNeed = getByte(hContact, "Auth", 0) > 0;
+ bool isGrantNeed = getByte(hContact, "Grant", 0) > 0;
+ bool isBlocked = getBool(hContact, "IsBlocked", false);
+
+ Menu_ShowItem(GetMenuItem(PROTO_MENU_REQ_AUTH), isCtrlPressed || isAuthNeed);
+ Menu_ShowItem(GetMenuItem(PROTO_MENU_GRANT_AUTH), isCtrlPressed || isGrantNeed);
+
+ Menu_ShowItem(ContactMenuItems[CMI_BLOCK], true);
+ Menu_ShowItem(ContactMenuItems[CMI_UNBLOCK], isCtrlPressed || isBlocked);
+ return 0;
+}
+
+int CTeamsProto::PrebuildContactMenu(WPARAM hContact, LPARAM lParam)
+{
+ for (auto &it : ContactMenuItems)
+ Menu_ShowItem(it, false);
+ CTeamsProto *proto = CMPlugin::getInstance(hContact);
+ return proto ? proto->OnPrebuildContactMenu(hContact, lParam) : 0;
+}
+
+void CTeamsProto::InitMenus()
+{
+ HookEvent(ME_CLIST_PREBUILDCONTACTMENU, &CTeamsProto::PrebuildContactMenu);
+
+ CMenuItem mi(&g_plugin);
+ mi.flags = CMIF_UNICODE;
+
+ mi.pszService = MODULENAME "/BlockContact";
+ mi.name.w = LPGENW("Block contact");
+ mi.position = CMI_POSITION + CMI_BLOCK;
+ mi.hIcolibItem = g_plugin.getIconHandle(IDI_BLOCKUSER);
+ SET_UID(mi, 0xc6169b8f, 0x53ab, 0x4242, 0xbe, 0x90, 0xe2, 0x4a, 0xa5, 0x73, 0x88, 0x32);
+ ContactMenuItems[CMI_BLOCK] = Menu_AddContactMenuItem(&mi);
+ CreateServiceFunction(mi.pszService, GlobalService<&CTeamsProto::BlockContact>);
+
+ mi.pszService = MODULENAME "/UnblockContact";
+ mi.name.w = LPGENW("Unblock contact");
+ mi.position = CMI_POSITION + CMI_UNBLOCK;
+ mi.hIcolibItem = g_plugin.getIconHandle(IDI_UNBLOCKUSER);
+ SET_UID(mi, 0x88542f43, 0x7448, 0x48d0, 0x81, 0xa3, 0x26, 0x0, 0x4f, 0x37, 0xee, 0xe0);
+ ContactMenuItems[CMI_UNBLOCK] = Menu_AddContactMenuItem(&mi);
+ CreateServiceFunction(mi.pszService, GlobalService<&CTeamsProto::UnblockContact>);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// Protocol's menu in the status bar
+
+void CTeamsProto::OnBuildProtoMenu()
+{
+ CMenuItem mi(&g_plugin);
+ mi.root = Menu_GetProtocolRoot(this);
+
+ mi.pszService = "/CreateNewChat";
+ CreateProtoService(mi.pszService, &CTeamsProto::SvcCreateChat);
+ mi.name.a = LPGEN("Create new chat");
+ mi.position = 200000;
+ mi.hIcolibItem = g_plugin.getIconHandle(IDI_CONFERENCE);
+ Menu_AddProtoMenuItem(&mi, m_szModuleName);
+
+ mi.pszService = "/SetMood";
+ CreateProtoService(mi.pszService, &CTeamsProto::SvcSetMood);
+ mi.name.a = LPGEN("Set own mood");
+ mi.position++;
+ mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_USERONLINE);
+ Menu_AddProtoMenuItem(&mi, m_szModuleName);
+}
diff --git a/protocols/Teams/src/teams_menus.h b/protocols/Teams/src/teams_menus.h
new file mode 100644
index 0000000000..04b87c6e83
--- /dev/null
+++ b/protocols/Teams/src/teams_menus.h
@@ -0,0 +1,30 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _SKYPE_MENUS_H_
+#define _SKYPE_MENUS_H_
+
+#define CMI_POSITION -201001000
+
+enum
+{
+ CMI_BLOCK,
+ CMI_UNBLOCK,
+ CMI_MAX // this item shall be the last one
+};
+
+#endif //_SKYPE_MENUS_H_ \ No newline at end of file
diff --git a/protocols/Teams/src/teams_messages.cpp b/protocols/Teams/src/teams_messages.cpp
new file mode 100644
index 0000000000..e55b83816c
--- /dev/null
+++ b/protocols/Teams/src/teams_messages.cpp
@@ -0,0 +1,326 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// MESSAGE SENDING
+
+void CTeamsProto::OnMessageSent(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ MCONTACT hContact = pRequest->hContact;
+ if (Contact::IsGroupChat(hContact))
+ return;
+
+ if (response != nullptr) {
+ if (response->resultCode != 201) {
+ std::string strError = Translate("Unknown error!");
+
+ if (!response->body.IsEmpty()) {
+ JSONNode jRoot = JSONNode::parse(response->body);
+ const JSONNode &jErr = jRoot["errorCode"];
+ if (jErr)
+ strError = jErr.as_string();
+ }
+
+ ProtoBroadcastAck(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, pRequest->pUserInfo, _A2T(strError.c_str()));
+ }
+ }
+ else ProtoBroadcastAck(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, pRequest->pUserInfo, (LPARAM)TranslateT("Network error!"));
+}
+
+// outcoming message flow
+
+int CTeamsProto::SendServerMsg(MCONTACT hContact, const char *szMessage, int64_t existingMsgId)
+{
+ if (!IsOnline())
+ return -1;
+
+ CMStringA str(szMessage);
+ bool bRich = AddBbcodes(str);
+ m_iMessageId++;
+
+ CMStringA szUrl = "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/messages";
+ if (existingMsgId)
+ szUrl.AppendFormat("/%lld", existingMsgId);
+
+ AsyncHttpRequest *pReq = new AsyncHttpRequest(existingMsgId ? REQUEST_PUT : REQUEST_POST, HOST_DEFAULT, szUrl, &CTeamsProto::OnMessageSent);
+ pReq->hContact = hContact;
+ pReq->pUserInfo = (HANDLE)m_iMessageId;
+
+ JSONNode node;
+ node << CHAR_PARAM("messagetype", bRich ? "RichText" : "Text") << CHAR_PARAM("contenttype", "text");
+ if (strncmp(str, "/me ", 4) == 0)
+ node << CHAR_PARAM("content", m_szSkypename + " " + str);
+ else
+ node << CHAR_PARAM("content", str);
+
+ if (!existingMsgId) {
+ int64_t iRandomId = getRandomId();
+ node << INT64_PARAM("clientmessageid", iRandomId);
+
+ mir_cslock lck(m_lckOutMessagesList);
+ m_OutMessages.insert(new COwnMessage(m_iMessageId, iRandomId));
+ }
+ pReq->m_szParam = node.write().c_str();
+
+ PushRequest(pReq);
+
+ return m_iMessageId;
+}
+
+// preparing message/action to be written into db
+int CTeamsProto::OnPreCreateMessage(WPARAM, LPARAM lParam)
+{
+ MessageWindowEvent *evt = (MessageWindowEvent*)lParam;
+ if (mir_strcmp(Proto_GetBaseAccountName(evt->hContact), m_szModuleName))
+ return 0;
+
+ int64_t msgId = (evt->dbei->szId) ? _atoi64(evt->dbei->szId) : -1;
+ for (auto &it : m_OutMessages)
+ if (it->hClientMessageId == msgId) {
+ evt->dbei->bMsec = true;
+ evt->dbei->iTimestamp = it->iTimestamp;
+ }
+
+ return 0;
+}
+
+/* MESSAGE EVENT */
+
+bool CTeamsProto::ParseMessage(const JSONNode &node, DB::EventInfo &dbei)
+{
+ auto &pContent = node["content"];
+ if (!pContent) {
+LBL_Deleted:
+ if (dbei)
+ db_event_delete(dbei.getEvent());
+ return false;
+ }
+
+ CMStringW wszContent = pContent.as_mstring();
+ if (wszContent.IsEmpty())
+ goto LBL_Deleted;
+
+ std::string strMessageType = node["messagetype"].as_string();
+ if (strMessageType == "RichText/Media_GenericFile" || strMessageType == "RichText/Media_Video" || strMessageType == "RichText/UriObject" ) {
+ ProcessFileRecv(dbei.hContact, node["content"].as_string().c_str(), dbei);
+ return false;
+ }
+ if (strMessageType == "RichText/Contacts") {
+ ProcessContactRecv(dbei.hContact, node["content"].as_string().c_str(), dbei);
+ return false;
+ }
+
+ if (strMessageType == "Text" || strMessageType == "RichText") {
+ if (dbei.bSent && dbei.szId) {
+ for (auto &it: m_OutMessages) {
+ if (it->hClientMessageId == _atoi64(dbei.szId)) {
+ it->iTimestamp = dbei.iTimestamp;
+
+ ProtoBroadcastAck(dbei.hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)it->hMessage, (LPARAM)dbei.szId);
+
+ mir_cslock lck(m_lckOutMessagesList);
+ m_OutMessages.removeItem(&it);
+ return false;
+ }
+ }
+ }
+
+ if (strMessageType == "RichText")
+ wszContent = RemoveHtml(wszContent);
+
+ dbei.eventType = EVENTTYPE_MESSAGE;
+ }
+ else if (strMessageType == "RichText/Media_Album")
+ return false;
+
+ replaceStr(dbei.pBlob, mir_utf8encodeW(wszContent));
+ dbei.cbBlob = (uint32_t)mir_strlen(dbei.pBlob);
+ return true;
+}
+
+void CTeamsProto::ProcessNewMessage(const JSONNode &node)
+{
+ int iUserType;
+ UrlToSkypeId(node["conversationLink"].as_string().c_str(), &iUserType);
+
+ int64_t timestamp = _wtoi64(node["id"].as_mstring());
+ CMStringA szMessageId(getMessageId(node));
+ CMStringA szConversationName(UrlToSkypeId(node["conversationLink"].as_string().c_str()));
+ CMStringA szFromSkypename(UrlToSkypeId(node["from"].as_mstring()));
+
+ if (iUserType == 19)
+ if (OnChatEvent(node))
+ return;
+
+ MCONTACT hContact = AddContact(szConversationName, nullptr, true);
+
+ if (m_bHistorySynced && timestamp > getLastTime(hContact))
+ setLastTime(hContact, timestamp);
+
+ std::string strMessageType = node["messagetype"].as_string();
+ if (strMessageType == "Control/Typing") {
+ CallService(MS_PROTO_CONTACTISTYPING, hContact, 30);
+ return;
+ }
+ if (strMessageType == "Control/ClearTyping") {
+ CallService(MS_PROTO_CONTACTISTYPING, hContact, PROTOTYPE_CONTACTTYPING_OFF);
+ return;
+ }
+
+ DB::EventInfo dbei(db_event_getById(m_szModuleName, szMessageId));
+ dbei.hContact = hContact;
+ dbei.iTimestamp = timestamp;
+ dbei.szId = szMessageId;
+ dbei.bUtf = dbei.bMsec = true;
+ dbei.bSent = IsMe(szFromSkypename);
+ if (iUserType == 19)
+ dbei.szUserId = szFromSkypename;
+
+ if (ParseMessage(node, dbei)) {
+ if (dbei)
+ db_event_edit(dbei.getEvent(), &dbei, true);
+ else
+ ProtoChainRecvMsg(hContact, dbei);
+ }
+}
+
+void CTeamsProto::OnMarkRead(MCONTACT hContact, MEVENT hDbEvent)
+{
+ if (IsOnline()) {
+ DB::EventInfo dbei(hDbEvent, false);
+ if (dbei && dbei.szId) {
+ auto *pReq = new AsyncHttpRequest(REQUEST_PUT, HOST_DEFAULT, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/properties?name=consumptionhorizon");
+ auto msgTimestamp = _atoi64(dbei.szId);
+
+ JSONNode node(JSON_NODE);
+ node << CHAR_PARAM("consumptionhorizon", CMStringA(::FORMAT, "%lld;%lld;%lld", msgTimestamp, msgTimestamp, msgTimestamp));
+ pReq->m_szParam = node.write().c_str();
+
+ PushRequest(pReq);
+ }
+ }
+}
+
+void CTeamsProto::OnReceiveOfflineFile(DB::EventInfo &dbei, DB::FILE_BLOB &blob)
+{
+ if (auto *ft = (CSkypeTransfer *)blob.getUserInfo()) {
+ blob.setUrl(ft->url);
+ blob.setSize(ft->iFileSize);
+
+ auto &json = dbei.setJson();
+ json << CHAR_PARAM("skft", ft->fileType);
+ if (ft->iHeight != -1)
+ json << INT_PARAM("h", ft->iHeight);
+ if (ft->iWidth != -1)
+ json << INT_PARAM("w", ft->iWidth);
+ delete ft;
+ }
+}
+
+void CTeamsProto::ProcessFileRecv(MCONTACT hContact, const char *szContent, DB::EventInfo &dbei)
+{
+ TiXmlDocument doc;
+ if (0 != doc.Parse(szContent))
+ return;
+
+ auto *xmlRoot = doc.FirstChildElement("URIObject");
+ if (xmlRoot == nullptr)
+ return;
+
+ CSkypeTransfer *ft = new CSkypeTransfer;
+ if (auto *str = xmlRoot->Attribute("doc_id"))
+ ft->docId = str;
+ if (auto *str = xmlRoot->Attribute("uri"))
+ ft->url = str;
+ ft->iWidth = xmlRoot->IntAttribute("width", -1);
+ ft->iHeight = xmlRoot->IntAttribute("heighr", -1);
+ if (auto *str = xmlRoot->Attribute("type"))
+ ft->fileType = str;
+ if (auto *xml = xmlRoot->FirstChildElement("FileSize"))
+ if (auto *str = xml->Attribute("v"))
+ ft->iFileSize = atoi(str);
+ if (auto *xml = xmlRoot->FirstChildElement("OriginalName"))
+ if (auto *str = xml->Attribute("v"))
+ ft->fileName = str;
+
+ if (ft->url.IsEmpty() || ft->fileName.IsEmpty() || ft->iFileSize == 0) {
+ debugLogA("Missing file info: url=<%s> name=<%s> %d", ft->url.c_str(), ft->fileName.c_str(), ft->iFileSize);
+ delete ft;
+ return;
+ }
+
+ int idx = ft->fileType.Find('/');
+ if (idx != -1)
+ ft->fileType = ft->fileType.Left(idx);
+
+ // ordinary file
+ if (ft->fileType == "File.1" || ft->fileType == "Picture.1" || ft->fileType == "Video.1") {
+ MEVENT hEvent;
+ dbei.flags |= DBEF_TEMPORARY | DBEF_JSON;
+ if (dbei) {
+ DB::FILE_BLOB blob(dbei);
+ OnReceiveOfflineFile(dbei, blob);
+ blob.write(dbei);
+ db_event_edit(dbei.getEvent(), &dbei, true);
+ delete ft;
+ hEvent = dbei.getEvent();
+ }
+ else hEvent = ProtoChainRecvFile(hContact, DB::FILE_BLOB(ft, ft->fileName), dbei);
+ }
+ else debugLogA("Invalid or unsupported file type <%s> ignored", ft->fileType.c_str());
+}
+
+void CTeamsProto::ProcessContactRecv(MCONTACT hContact, const char *szContent, DB::EventInfo &dbei)
+{
+ TiXmlDocument doc;
+ if (0 != doc.Parse(szContent))
+ return;
+
+ auto *xmlNode = doc.FirstChildElement("contacts");
+ if (xmlNode == nullptr)
+ return;
+
+ int nCount = 0;
+ for (auto *it : TiXmlEnum(xmlNode)) {
+ UNREFERENCED_PARAMETER(it);
+ nCount++;
+ }
+
+ PROTOSEARCHRESULT **psr = (PROTOSEARCHRESULT**)mir_calloc(sizeof(PROTOSEARCHRESULT*) * nCount);
+
+ nCount = 0;
+ for (auto *xmlContact : TiXmlFilter(xmlNode, "c")) {
+ psr[nCount] = (PROTOSEARCHRESULT*)mir_calloc(sizeof(PROTOSEARCHRESULT));
+ psr[nCount]->cbSize = sizeof(psr);
+ psr[nCount]->id.a = mir_strdup(xmlContact->Attribute("s"));
+ nCount++;
+ }
+
+ if (nCount) {
+ dbei.pBlob = (char*)psr;
+ dbei.cbBlob = nCount;
+
+ ProtoChainRecv(hContact, PSR_CONTACTS, 0, (LPARAM)&dbei);
+ for (int i = 0; i < nCount; i++) {
+ mir_free(psr[i]->id.a);
+ mir_free(psr[i]);
+ }
+ }
+ mir_free(psr);
+}
diff --git a/protocols/Teams/src/teams_mood.cpp b/protocols/Teams/src/teams_mood.cpp
new file mode 100644
index 0000000000..66a2b15444
--- /dev/null
+++ b/protocols/Teams/src/teams_mood.cpp
@@ -0,0 +1,146 @@
+/*
+Copyright (C) 2012-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+struct
+{
+ const char *ss;
+ const wchar_t *defStatus;
+ int defIcon;
+}
+static moods[] =
+{
+ { "", LPGENW("None") },
+ { "", LPGENW("Custom emoji") },
+ { "brb", LPGENW("Be right back") },
+ { "burger", LPGENW("Out for lunch") },
+ { "wait", LPGENW("In meetings") },
+ { "learn", LPGENW("At school") },
+ { "movie", LPGENW("At the movies") },
+ { "plane", LPGENW("Traveling") },
+ { "party", LPGENW("Celebrating") },
+ { "car", LPGENW("Driving") },
+ { "skip", LPGENW("At the gym") },
+ { "wfh", LPGENW("Working from home") },
+};
+
+struct SetStatusMsgRequest : public AsyncHttpRequest
+{
+ SetStatusMsgRequest(CTeamsProto *ppro) :
+ AsyncHttpRequest(REQUEST_POST, HOST_API, "/users/self/profile/partial")
+ {
+ int m_iMood = ppro->m_iMood;
+ auto &pMood = moods[m_iMood];
+
+ JSONNode node, payload;
+ payload.set_name("payload");
+
+ CMStringW s1, s2;
+ switch (m_iMood) {
+ case 0: // none
+ s1 = ppro->m_wstrMoodMessage;
+ break;
+ case 1: // custom
+ s1.Format(L"(%x) %s", Utf16toUtf32(ppro->m_wstrMoodEmoji), (wchar_t *)ppro->m_wstrMoodMessage);
+ break;
+ default:
+ s1.Format(L"(%S) %s", pMood.ss, (wchar_t *)ppro->m_wstrMoodMessage);
+ break;
+ }
+ payload << WCHAR_PARAM("mood", s1);
+
+ if (m_iMood > 1)
+ s2.Format(L"<ss type=\"%S\">(%S)</ss>%s", pMood.ss, pMood.ss, (wchar_t*)ppro->m_wstrMoodMessage);
+ else if (m_iMood == 1) {
+ int code = Utf16toUtf32(ppro->m_wstrMoodEmoji);
+ s2.Format(L"<ss type=\"%x\">(%x)</ss>%s", code, code, (wchar_t *)ppro->m_wstrMoodMessage);
+ }
+
+ if (!s2.IsEmpty())
+ payload << WCHAR_PARAM("richMood", s2);
+
+ node << payload;
+ m_szParam = node.write().c_str();
+ }
+};
+
+int getMoodIndex(const char *pszMood)
+{
+ for (auto &it : moods)
+ if (!mir_strcmpi(it.ss, pszMood))
+ return int(&it - moods);
+
+ return -1;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// Mood dialog
+
+class CMoodDialog : public CTeamsDlgBase
+{
+ CCtrlEdit edtText, edtEmoji;
+ CCtrlCombo cmbMoods;
+
+public:
+ CMoodDialog(CTeamsProto *ppro) :
+ CTeamsDlgBase(ppro, IDD_MOOD),
+ edtText(this, IDC_MOOD_TEXT),
+ edtEmoji(this, IDC_MOOD_EMOJI),
+ cmbMoods(this, IDC_MOOD_COMBO)
+ {
+ CreateLink(edtText, ppro->m_wstrMoodMessage);
+ CreateLink(edtEmoji, ppro->m_wstrMoodEmoji);
+
+ cmbMoods.OnChange = Callback(this, &CMoodDialog::onChangeSel_Mood);
+ }
+
+ bool OnInitDialog() override
+ {
+ for (auto &it : moods)
+ cmbMoods.AddString(TranslateW(it.defStatus), int(&it - moods));
+ cmbMoods.SetCurSel(m_proto->m_iMood);
+ onChangeSel_Mood(0);
+ return true;
+ }
+
+ bool OnApply() override
+ {
+ m_proto->m_iMood = cmbMoods.GetCurSel();
+
+ CMStringA szSetting(FORMAT, "Mood%d", (int)m_proto->m_iMood);
+ m_proto->setWString(szSetting, m_proto->m_wstrMoodMessage);
+
+ m_proto->PushRequest(new SetStatusMsgRequest(m_proto));
+ return true;
+ }
+
+ void onChangeSel_Mood(CCtrlCombo *)
+ {
+ int m_iMood = cmbMoods.GetCurSel();
+ edtEmoji.Enable(m_iMood == 1);
+
+ CMStringA szSetting(FORMAT, "Mood%d", m_iMood);
+ edtText.SetText(m_proto->getMStringW(szSetting));
+ }
+};
+
+INT_PTR CTeamsProto::SvcSetMood(WPARAM, LPARAM)
+{
+ CMoodDialog(this).DoModal();
+ return 0;
+}
diff --git a/protocols/Teams/src/teams_options.cpp b/protocols/Teams/src/teams_options.cpp
index 260368dfc5..e5334a0985 100644
--- a/protocols/Teams/src/teams_options.cpp
+++ b/protocols/Teams/src/teams_options.cpp
@@ -19,22 +19,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class COptionsMainDlg : public CTeamsDlgBase
{
- CCtrlEdit m_login, m_password, m_group;
+ CCtrlEdit m_login, m_password, m_group, m_place;
+ CCtrlCheck m_autosync, m_usehostname, m_usebb;
+ CCtrlHyperlink m_link;
public:
COptionsMainDlg(CTeamsProto *proto, int idDialog) :
CTeamsDlgBase(proto, idDialog),
m_login(this, IDC_LOGIN),
m_password(this, IDC_PASSWORD),
- m_group(this, IDC_GROUP)
+ m_group(this, IDC_GROUP),
+ m_place(this, IDC_PLACE),
+ m_autosync(this, IDC_AUTOSYNC),
+ m_usehostname(this, IDC_USEHOST),
+ m_usebb(this, IDC_BBCODES),
+ m_link(this, IDC_CHANGEPASS, "https://login.skype.com/recovery/password-change") // TODO : ...?username=%username%
{
CreateLink(m_group, proto->m_wstrCListGroup);
+ CreateLink(m_autosync, proto->m_bAutoHistorySync);
+ CreateLink(m_place, proto->m_wstrPlace);
+ CreateLink(m_usehostname, proto->m_bUseHostnameAsPlace);
+ CreateLink(m_usebb, proto->m_bUseBBCodes);
+ m_usehostname.OnChange = Callback(this, &COptionsMainDlg::OnUsehostnameCheck);
}
bool OnInitDialog() override
{
- m_login.SetTextA(ptrA(m_proto->getStringA("Login")));
+ m_login.SetTextA(ptrA(m_proto->getStringA(SKYPE_SETTINGS_ID)));
m_password.SetTextA(pass_ptrA(m_proto->getStringA("Password")));
+ m_place.Enable(!m_proto->m_bUseHostnameAsPlace);
m_login.SendMsg(EM_LIMITTEXT, 128, 0);
m_password.SendMsg(EM_LIMITTEXT, 128, 0);
m_group.SendMsg(EM_LIMITTEXT, 64, 0);
@@ -43,17 +56,27 @@ public:
bool OnApply() override
{
- m_proto->setString("Login", ptrA(m_login.GetTextA()));
- m_proto->setString("Password", ptrA(m_password.GetTextA()));
+ ptrA szNewSkypename(m_login.GetTextA()),
+ szOldSkypename(m_proto->getStringA(SKYPE_SETTINGS_ID));
+ pass_ptrA szNewPassword(m_password.GetTextA()),
+ szOldPassword(m_proto->getStringA("Password"));
+ if (mir_strcmpi(szNewSkypename, szOldSkypename) || mir_strcmp(szNewPassword, szOldPassword))
+ m_proto->delSetting("TokenExpiresIn");
+ m_proto->setString(SKYPE_SETTINGS_ID, szNewSkypename);
+ m_proto->setString("Password", szNewPassword);
ptrW group(m_group.GetText());
if (mir_wstrlen(group) > 0 && !Clist_GroupExists(group))
Clist_GroupCreate(0, group);
return true;
}
+
+ void OnUsehostnameCheck(CCtrlCheck *)
+ {
+ m_place.Enable(!m_usehostname.GetState());
+ }
};
/////////////////////////////////////////////////////////////////////////////////
-// module entry points
MWindow CTeamsProto::OnCreateAccMgrUI(MWindow hwndParent)
{
@@ -76,5 +99,3 @@ int CTeamsProto::OnOptionsInit(WPARAM wParam, LPARAM)
return 0;
}
-
-
diff --git a/protocols/Teams/src/teams_polling.cpp b/protocols/Teams/src/teams_polling.cpp
new file mode 100644
index 0000000000..d71d17e2e2
--- /dev/null
+++ b/protocols/Teams/src/teams_polling.cpp
@@ -0,0 +1,176 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+void CTeamsProto::PollingThread(void *)
+{
+ debugLogA(__FUNCTION__ ": entering");
+
+ m_iPollingId = -1;
+
+ while (true) {
+ if (m_isTerminated || m_szId == nullptr)
+ break;
+
+ std::unique_ptr<PollRequest> request(new PollRequest(this));
+ request->nlc = m_hPollingConn;
+ NLHR_PTR response(DoSend(request.get()));
+ if (m_isTerminated || m_szId == nullptr)
+ break;
+
+ if (response == nullptr || response->resultCode != 200) {
+ m_hPollingConn = nullptr;
+ continue;
+ }
+
+ m_hPollingConn = response->nlc;
+ if (!response->body.IsEmpty())
+ ParsePollData(response->body);
+ }
+
+ if (!m_isTerminated) {
+ debugLogA(__FUNCTION__ ": unexpected termination; switching protocol to offline");
+ SetStatus(ID_STATUS_OFFLINE);
+ }
+
+ m_hPollingConn = nullptr;
+ m_hPollingThread = nullptr;
+ debugLogA(__FUNCTION__ ": leaving");
+}
+
+void CTeamsProto::ParsePollData(const char *szData)
+{
+ debugLogA(__FUNCTION__);
+
+ JSONNode data = JSONNode::parse(szData);
+ if (!data)
+ return;
+
+ for (auto &message : data["eventMessages"]) {
+ int eventId = message["id"].as_int();
+ if (eventId > m_iPollingId)
+ m_iPollingId = eventId;
+
+ const JSONNode &resource = message["resource"];
+
+ std::string resourceType = message["resourceType"].as_string();
+ if (resourceType == "NewMessage")
+ ProcessNewMessage(resource);
+ else if (resourceType == "UserPresence")
+ ProcessUserPresence(resource);
+ else if (resourceType == "EndpointPresence")
+ ProcessEndpointPresence(resource);
+ else if (resourceType == "ConversationUpdate")
+ ProcessConversationUpdate(resource);
+ else if (resourceType == "ThreadUpdate")
+ ProcessThreadUpdate(resource);
+ }
+}
+
+void CTeamsProto::ProcessEndpointPresence(const JSONNode &node)
+{
+ debugLogA(__FUNCTION__);
+ std::string selfLink = node["selfLink"].as_string();
+ CMStringA skypename(UrlToSkypeId(selfLink.c_str()));
+
+ MCONTACT hContact = FindContact(skypename);
+ if (hContact == NULL)
+ return;
+
+ const JSONNode &publicInfo = node["publicInfo"];
+ const JSONNode &privateInfo = node["privateInfo"];
+ CMStringA MirVer;
+ if (publicInfo) {
+ std::string skypeNameVersion = publicInfo["skypeNameVersion"].as_string();
+ std::string version = publicInfo["version"].as_string();
+ std::string typ = publicInfo["typ"].as_string();
+ int iTyp = atoi(typ.c_str());
+ switch (iTyp) {
+ case 0:
+ case 1:
+ MirVer.Append("Skype (Web) " + ParseUrl(version.c_str(), "/"));
+ break;
+ case 10:
+ MirVer.Append("Skype (XBOX) " + ParseUrl(skypeNameVersion.c_str(), "/"));
+ break;
+ case 17:
+ MirVer.Append("Skype (Android) " + ParseUrl(skypeNameVersion.c_str(), "/"));
+ break;
+ case 16:
+ MirVer.Append("Skype (iOS) " + ParseUrl(skypeNameVersion.c_str(), "/"));
+ break;
+ case 12:
+ MirVer.Append("Skype (WinRT) " + ParseUrl(skypeNameVersion.c_str(), "/"));
+ break;
+ case 15:
+ MirVer.Append("Skype (WP) " + ParseUrl(skypeNameVersion.c_str(), "/"));
+ break;
+ case 13:
+ MirVer.Append("Skype (OSX) " + ParseUrl(skypeNameVersion.c_str(), "/"));
+ break;
+ case 11:
+ MirVer.Append("Skype (Windows) " + ParseUrl(skypeNameVersion.c_str(), "/"));
+ break;
+ case 14:
+ MirVer.Append("Skype (Linux) " + ParseUrl(skypeNameVersion.c_str(), "/"));
+ break;
+ case 125:
+ MirVer.AppendFormat("Miranda NG Skype %s", version.c_str());
+ break;
+ default:
+ MirVer.Append("Skype (Unknown)");
+ }
+ }
+
+ if (privateInfo != NULL) {
+ std::string epname = privateInfo["epname"].as_string();
+ if (!epname.empty())
+ MirVer.AppendFormat(" [%s]", epname.c_str());
+ }
+
+ setString(hContact, "MirVer", MirVer);
+}
+
+void CTeamsProto::ProcessUserPresence(const JSONNode &node)
+{
+ debugLogA(__FUNCTION__);
+
+ std::string selfLink = node["selfLink"].as_string();
+ std::string status = node["status"].as_string();
+ CMStringA skypename = UrlToSkypeId(selfLink.c_str());
+
+ if (!skypename.IsEmpty()) {
+ if (IsMe(skypename)) {
+ int iNewStatus = SkypeToMirandaStatus(status.c_str());
+ if (iNewStatus == ID_STATUS_OFFLINE) return;
+ int old_status = m_iStatus;
+ m_iDesiredStatus = iNewStatus;
+ m_iStatus = iNewStatus;
+ if (old_status != iNewStatus)
+ ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, iNewStatus);
+ }
+ else {
+ MCONTACT hContact = FindContact(skypename);
+ if (hContact != NULL)
+ SetContactStatus(hContact, SkypeToMirandaStatus(status.c_str()));
+ }
+ }
+}
+
+void CTeamsProto::ProcessConversationUpdate(const JSONNode &) {}
+void CTeamsProto::ProcessThreadUpdate(const JSONNode &) {}
diff --git a/protocols/Teams/src/teams_popups.cpp b/protocols/Teams/src/teams_popups.cpp
new file mode 100644
index 0000000000..efac8c26f9
--- /dev/null
+++ b/protocols/Teams/src/teams_popups.cpp
@@ -0,0 +1,83 @@
+#include "stdafx.h"
+
+void CTeamsProto::InitPopups()
+{
+ wchar_t desc[256];
+ char name[256];
+
+ POPUPCLASS ppc = {};
+ ppc.flags = PCF_UNICODE;
+ ppc.pszName = name;
+ ppc.pszDescription.w = desc;
+
+ mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Notifications"));
+ mir_snprintf(name, "%s_%s", m_szModuleName, "Notification");
+ ppc.hIcon = g_plugin.getIcon(IDI_NOTIFY);
+ ppc.iSeconds = 5;
+ m_PopupClasses.insert(Popup_RegisterClass(&ppc));
+
+ mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Errors"));
+ mir_snprintf(name, "%s_%s", m_szModuleName, "Error");
+ ppc.hIcon = g_plugin.getIcon(IDI_ERRORICON);
+ ppc.iSeconds = -1;
+ m_PopupClasses.insert(Popup_RegisterClass(&ppc));
+
+ mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Calls"));
+ mir_snprintf(name, "%s_%s", m_szModuleName, "Call");
+ ppc.hIcon = g_plugin.getIcon(IDI_CALL);
+ ppc.iSeconds = 30;
+ ppc.PluginWindowProc = PopupDlgProcCall;
+ m_PopupClasses.insert(Popup_RegisterClass(&ppc));
+}
+
+void CTeamsProto::UninitPopups()
+{
+ for (auto &it : m_PopupClasses)
+ Popup_UnregisterClass(it);
+}
+
+void CTeamsProto::ShowNotification(const wchar_t *caption, const wchar_t *message, MCONTACT hContact, int type)
+{
+ if (Miranda_IsTerminated())
+ return;
+
+ CMStringA className(FORMAT, "%s_", m_szModuleName);
+
+ switch (type) {
+ case 1:
+ className.Append("Error");
+ break;
+
+ default:
+ className.Append("Notification");
+ break;
+ }
+
+ POPUPDATACLASS ppd = {};
+ ppd.szTitle.w = caption;
+ ppd.szText.w = message;
+ ppd.pszClassName = className.GetBuffer();
+ ppd.hContact = hContact;
+ Popup_AddClass(&ppd);
+}
+
+void CTeamsProto::ShowNotification(const wchar_t *message, MCONTACT hContact)
+{
+ ShowNotification(_T(MODULENAME), message, hContact);
+}
+
+LRESULT CTeamsProto::PopupDlgProcCall(HWND hPopup, UINT uMsg, WPARAM wParam, LPARAM lParam)
+{
+ switch (uMsg) {
+ case WM_CONTEXTMENU:
+ PUDeletePopup(hPopup);
+ CallService(MODULENAME "/IncomingCallPP", 0, PUGetContact(hPopup));
+ break;
+ case WM_COMMAND:
+ PUDeletePopup(hPopup);
+ CallService(MODULENAME"/IncomingCallPP", 1, PUGetContact(hPopup));
+ break;
+ }
+
+ return DefWindowProc(hPopup, uMsg, wParam, lParam);
+}
diff --git a/protocols/Teams/src/teams_profile.cpp b/protocols/Teams/src/teams_profile.cpp
new file mode 100644
index 0000000000..a26d88b1fe
--- /dev/null
+++ b/protocols/Teams/src/teams_profile.cpp
@@ -0,0 +1,147 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+void CTeamsProto::UpdateProfileDisplayName(const JSONNode &root, MCONTACT hContact)
+{
+ ptrW firstname(getWStringA(hContact, "FirstName"));
+ ptrW lastname(getWStringA(hContact, "LastName"));
+ if (firstname) {
+ CMStringW nick(firstname);
+ if (lastname)
+ nick.AppendFormat(L" %s", lastname.get());
+ setWString(hContact, "Nick", nick);
+ }
+ else if (lastname)
+ setWString(hContact, "Nick", lastname);
+ else {
+ const JSONNode &node = root["displayname"];
+ SetString(hContact, "Nick", (node) ? node : root["username"]);
+ }
+}
+
+void CTeamsProto::UpdateProfileGender(const JSONNode &root, MCONTACT hContact)
+{
+ CMStringW gender = root["gender"].as_mstring();
+ if (!gender.IsEmpty() && gender != "null")
+ setByte(hContact, "Gender", (uint8_t)(_wtoi(gender) == 1 ? 'M' : 'F'));
+ else
+ delSetting(hContact, "Gender");
+}
+
+void CTeamsProto::UpdateProfileBirthday(const JSONNode &root, MCONTACT hContact)
+{
+ CMStringW birthday = root["birthday"].as_mstring();
+ if (!birthday.IsEmpty() && birthday != "null") {
+ int d, m, y;
+ if (3 == swscanf(birthday.GetBuffer(), L"%d-%d-%d", &y, &m, &d)) {
+ Contact::SetBirthday(hContact, d, m, y);
+ return;
+ }
+ }
+
+ delSetting(hContact, "BirthYear");
+ delSetting(hContact, "BirthDay");
+ delSetting(hContact, "BirthMonth");
+}
+
+void CTeamsProto::UpdateProfileCountry(const JSONNode &root, MCONTACT hContact)
+{
+ std::string isocode = root["country"].as_string();
+ if (!isocode.empty() && isocode != "null") {
+ char *country = (char *)CallService(MS_UTILS_GETCOUNTRYBYISOCODE, (WPARAM)isocode.c_str(), 0);
+ setString(hContact, "Country", country);
+ }
+ else delSetting(hContact, "Country");
+}
+
+void CTeamsProto::UpdateProfileEmails(const JSONNode &root, MCONTACT hContact)
+{
+ const JSONNode &node = root["emails"];
+ if (node) {
+ const JSONNode &items = node.as_array();
+ for (int i = 0; i < min(items.size(), 3); i++) {
+ const JSONNode &item = items.at(i);
+ if (!item)
+ break;
+
+ CMStringA name(FORMAT, "e-mail%d", i);
+ setWString(hContact, name, item.as_mstring());
+ }
+ }
+ else {
+ delSetting(hContact, "e-mail0");
+ delSetting(hContact, "e-mail1");
+ delSetting(hContact, "e-mail2");
+ }
+}
+
+void CTeamsProto::UpdateProfileAvatar(const JSONNode &root, MCONTACT hContact)
+{
+ CMStringW szUrl = root["avatarUrl"].as_mstring();
+ if (!szUrl.IsEmpty() && szUrl != "null") {
+ SetAvatarUrl(hContact, szUrl);
+ ReloadAvatarInfo(hContact);
+ }
+}
+
+//{"firstname":"Echo \/ Sound Test Service", "lastname" : null, "birthday" : null, "gender" : null, "country" : null, "city" : null, "language" : null, "homepage" : null, "about" : null, "province" : null, "jobtitle" : null, "emails" : [], "phoneMobile" : null, "phoneHome" : null, "phoneOffice" : null, "mood" : null, "richMood" : null, "avatarUrl" : null, "username" : "echo123"}
+void CTeamsProto::LoadProfile(MHttpResponse *response, AsyncHttpRequest *pRequest)
+{
+ MCONTACT hContact = (DWORD_PTR)pRequest->pUserInfo;
+
+ SkypeReply reply(response);
+ if (reply.error()) {
+ ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, 0);
+ return;
+ }
+
+ auto &root = reply.data();
+ std::string username = root["username"].as_string();
+ if (username.empty()) {
+ ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, 0);
+ return;
+ }
+
+ if (m_szSkypename != username.c_str())
+ m_szMyname = username.c_str();
+ else
+ m_szMyname = m_szSkypename;
+
+ SetString(hContact, "City", root["city"]);
+ SetString(hContact, "About", root["about"]);
+ SetString(hContact, "Phone", root["phone"]);
+ SetString(hContact, "State", root["province"]);
+ SetString(hContact, "Cellular", root["phoneMobile"]);
+ SetString(hContact, "Homepage", root["homepage"]);
+ SetString(hContact, "LastName", root["lastname"]);
+ SetString(hContact, "FirstName", root["firstname"]);
+ SetString(hContact, "CompanyPhone", root["phoneOffice"]);
+
+ if (auto &pMood = root["richMood"])
+ RemoveHtml(pMood.as_mstring(), true); // this call extracts only emoji / mood id
+
+ UpdateProfileDisplayName(root, hContact);
+ UpdateProfileGender(root, hContact);
+ UpdateProfileBirthday(root, hContact);
+ UpdateProfileCountry(root, hContact);
+ UpdateProfileEmails(root, hContact);
+ UpdateProfileAvatar(root, hContact);
+
+ ProtoBroadcastAck(hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, 0);
+}
diff --git a/protocols/Teams/src/teams_proto.cpp b/protocols/Teams/src/teams_proto.cpp
index 30a942fd99..c2dabdfc1c 100644
--- a/protocols/Teams/src/teams_proto.cpp
+++ b/protocols/Teams/src/teams_proto.cpp
@@ -4,20 +4,86 @@ CTeamsProto::CTeamsProto(const char *protoName, const wchar_t *userName) :
PROTO<CTeamsProto>(protoName, userName),
m_impl(*this),
m_requests(10),
- m_wstrCListGroup(this, "DefaultGroup", L"Teams")
+ m_PopupClasses(1),
+ m_OutMessages(3, PtrKeySortT),
+ m_bAutoHistorySync(this, "AutoSync", true),
+ m_bUseHostnameAsPlace(this, "UseHostName", true),
+ m_bUseBBCodes(this, "UseBBCodes", true),
+ m_wstrCListGroup(this, SKYPE_SETTINGS_GROUP, L"Skype"),
+ m_wstrPlace(this, "Place", L""),
+ m_iMood(this, "Mood", 0),
+ m_wstrMoodEmoji(this, "MoodEmoji", L""),
+ m_wstrMoodMessage(this, "XStatusMsg", L"")
{
- HookProtoEvent(ME_OPT_INITIALISE, &CTeamsProto::OnOptionsInit);
-
// network
NETLIBUSER nlu = {};
nlu.flags = NUF_OUTGOING | NUF_INCOMING | NUF_HTTPCONNS | NUF_UNICODE;
nlu.szDescriptiveName.w = m_tszUserName;
nlu.szSettingsModule = m_szModuleName;
m_hNetlibUser = Netlib_RegisterUser(&nlu);
+
+ CreateProtoService(PS_GETAVATARINFO, &CTeamsProto::SvcGetAvatarInfo);
+ CreateProtoService(PS_GETAVATARCAPS, &CTeamsProto::SvcGetAvatarCaps);
+ CreateProtoService(PS_GETMYAVATAR, &CTeamsProto::SvcGetMyAvatar);
+ CreateProtoService(PS_SETMYAVATAR, &CTeamsProto::SvcSetMyAvatar);
+
+ CreateProtoService(PS_OFFLINEFILE, &CTeamsProto::SvcOfflineFile);
+
+ CreateProtoService(PS_MENU_REQAUTH, &CTeamsProto::OnRequestAuth);
+ CreateProtoService(PS_MENU_GRANTAUTH, &CTeamsProto::OnGrantAuth);
+
+ CreateProtoService(PS_MENU_LOADHISTORY, &CTeamsProto::SvcLoadHistory);
+ CreateProtoService(PS_EMPTY_SRV_HISTORY, &CTeamsProto::SvcEmptyHistory);
+
+ HookProtoEvent(ME_OPT_INITIALISE, &CTeamsProto::OnOptionsInit);
+
+ CreateDirectoryTreeW(GetAvatarPath());
+
+ // sounds
+ g_plugin.addSound("skype_inc_call", L"SkypeWeb", LPGENW("Incoming call"));
+ g_plugin.addSound("skype_call_canceled", L"SkypeWeb", LPGENW("Incoming call canceled"));
+
+ CheckConvert();
+ InitGroupChatModule();
}
CTeamsProto::~CTeamsProto()
{
+ UninitPopups();
+}
+
+void CTeamsProto::OnEventDeleted(MCONTACT hContact, MEVENT hDbEvent, int flags)
+{
+ if (!hContact || !(flags & CDF_DEL_HISTORY))
+ return;
+
+ DB::EventInfo dbei(hDbEvent, false);
+ if (dbei.szId) {
+ auto *pReq = new AsyncHttpRequest(REQUEST_DELETE, HOST_DEFAULT, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/messages/" + dbei.szId);
+ pReq->AddAuthentication(this);
+ pReq->AddHeader("Origin", "https://web.skype.com");
+ pReq->AddHeader("Referer", "https://web.skype.com/");
+ PushRequest(pReq);
+ }
+}
+
+void CTeamsProto::OnEventEdited(MCONTACT hContact, MEVENT, const DBEVENTINFO &dbei)
+{
+ SendServerMsg(hContact, dbei.pBlob, dbei.iTimestamp);
+}
+
+void CTeamsProto::OnModulesLoaded()
+{
+ setAllContactStatuses(ID_STATUS_OFFLINE, false);
+
+ HookProtoEvent(ME_MSG_PRECREATEEVENT, &CTeamsProto::OnPreCreateMessage);
+
+ InitPopups();
+}
+
+void CTeamsProto::OnShutdown()
+{
+ StopQueue();
}
INT_PTR CTeamsProto::GetCaps(int type, MCONTACT)
@@ -37,6 +103,88 @@ INT_PTR CTeamsProto::GetCaps(int type, MCONTACT)
return 0;
}
+/////////////////////////////////////////////////////////////////////////////////////////
+
+MCONTACT CTeamsProto::AddToList(int, PROTOSEARCHRESULT *psr)
+{
+ debugLogA(__FUNCTION__);
+
+ if (psr->id.a == nullptr)
+ return NULL;
+
+ MCONTACT hContact;
+ if (psr->flags & PSR_UNICODE)
+ hContact = AddContact(T2Utf(psr->id.w), T2Utf(psr->nick.w));
+ else
+ hContact = AddContact(psr->id.a, psr->nick.a);
+
+ return hContact;
+}
+
+MCONTACT CTeamsProto::AddToListByEvent(int, int, MEVENT hDbEvent)
+{
+ debugLogA(__FUNCTION__);
+
+ DB::EventInfo dbei(hDbEvent);
+ if (!dbei)
+ return NULL;
+ if (mir_strcmp(dbei.szModule, m_szModuleName))
+ return NULL;
+ if (dbei.eventType != EVENTTYPE_AUTHREQUEST)
+ return NULL;
+
+ DB::AUTH_BLOB blob(dbei.pBlob);
+ return AddContact(blob.get_email(), blob.get_nick());
+}
+
+int CTeamsProto::Authorize(MEVENT hDbEvent)
+{
+ MCONTACT hContact = GetContactFromAuthEvent(hDbEvent);
+ if (hContact == INVALID_CONTACT_ID)
+ return 1;
+
+ PushRequest(new AuthAcceptRequest(getId(hContact)));
+ return 0;
+}
+
+int CTeamsProto::AuthDeny(MEVENT hDbEvent, const wchar_t *)
+{
+ MCONTACT hContact = GetContactFromAuthEvent(hDbEvent);
+ if (hContact == INVALID_CONTACT_ID)
+ return 1;
+
+ PushRequest(new AuthDeclineRequest(getId(hContact)));
+ return 0;
+}
+
+int CTeamsProto::AuthRecv(MCONTACT, DB::EventInfo &dbei)
+{
+ return Proto_AuthRecv(m_szModuleName, dbei);
+}
+
+int CTeamsProto::AuthRequest(MCONTACT hContact, const wchar_t *szMessage)
+{
+ if (hContact == INVALID_CONTACT_ID)
+ return 1;
+
+ PushRequest(new AddContactRequest(getId(hContact), T2Utf(szMessage)));
+ return 0;
+}
+
+int CTeamsProto::GetInfo(MCONTACT hContact, int)
+{
+ if (isChatRoom(hContact))
+ return 1;
+
+ PushRequest(new GetProfileRequest(this, hContact));
+ return 0;
+}
+
+int CTeamsProto::SendMsg(MCONTACT hContact, MEVENT, const char *szMessage)
+{
+ return SendServerMsg(hContact, szMessage);
+}
+
int CTeamsProto::SetStatus(int iNewStatus)
{
if (iNewStatus == m_iDesiredStatus)
@@ -66,7 +214,50 @@ int CTeamsProto::SetStatus(int iNewStatus)
if (m_iStatus == ID_STATUS_OFFLINE)
Login();
- // else
- // PushRequest(new SetStatusRequest(MirandaToSkypeStatus(m_iDesiredStatus)));
+ else
+ PushRequest(new SetStatusRequest(MirandaToSkypeStatus(m_iDesiredStatus)));
+ return 0;
+}
+
+int CTeamsProto::UserIsTyping(MCONTACT hContact, int iState)
+{
+ auto *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT, "/users/ME/conversations/" + mir_urlEncode(getId(hContact)) + "/messages");
+
+ JSONNode node;
+ node << INT64_PARAM("clientmessageid", getRandomId()) << CHAR_PARAM("contenttype", "text") << CHAR_PARAM("content", "")
+ << CHAR_PARAM("messagetype", (iState == PROTOTYPE_SELFTYPING_ON) ? "Control/Typing" : "Control/ClearTyping");
+ pReq->m_szParam = node.write().c_str();
+
+ PushRequest(pReq);
+ return 0;
+}
+
+int CTeamsProto::RecvContacts(MCONTACT hContact, DB::EventInfo &dbei)
+{
+ PROTOSEARCHRESULT **isrList = (PROTOSEARCHRESULT **)dbei.pBlob;
+
+ int nCount = dbei.cbBlob;
+
+ uint32_t cbBlob = 0;
+ for (int i = 0; i < nCount; i++)
+ cbBlob += int(/*mir_wstrlen(isrList[i]->nick.w)*/0 + 2 + mir_wstrlen(isrList[i]->id.w));
+
+ char *pBlob = (char *)mir_calloc(cbBlob);
+ char *pCurBlob = pBlob;
+
+ for (int i = 0; i < nCount; i++) {
+ pCurBlob += mir_strlen(pCurBlob) + 1;
+
+ mir_strcpy(pCurBlob, _T2A(isrList[i]->id.w));
+ pCurBlob += mir_strlen(pCurBlob) + 1;
+ }
+
+ dbei.szModule = m_szModuleName;
+ dbei.eventType = EVENTTYPE_CONTACTS;
+ dbei.cbBlob = cbBlob;
+ dbei.pBlob = pBlob;
+ db_event_add(hContact, &dbei);
+
+ mir_free(pBlob);
return 0;
}
diff --git a/protocols/Teams/src/teams_proto.h b/protocols/Teams/src/teams_proto.h
new file mode 100644
index 0000000000..9066d56079
--- /dev/null
+++ b/protocols/Teams/src/teams_proto.h
@@ -0,0 +1,388 @@
+#define TEAMS_CLIENT_ID "8ec6bc83-69c8-4392-8f08-b3c986009232"
+
+#define SKYPE_SETTINGS_ID "SkypeId"
+#define SKYPE_SETTINGS_LOGIN "Skypename"
+#define SKYPE_SETTINGS_USERTYPE "UserType"
+#define SKYPE_SETTINGS_PASSWORD "Password"
+#define SKYPE_SETTINGS_GROUP "DefaultGroup"
+
+struct COwnMessage
+{
+ COwnMessage(int _1, int64_t _2) :
+ hMessage(_1),
+ hClientMessageId(_2)
+ {}
+
+ int hMessage;
+ int64_t hClientMessageId, iTimestamp = -1;
+};
+
+struct CSkypeTransfer
+{
+ CMStringA docId, fileName, fileType, url;
+ int iFileSize = 0, iWidth = -1, iHeight = -1;
+};
+
+class CTeamsProto : public PROTO<CTeamsProto>
+{
+ friend class COptionsMainDlg;
+ friend class CDeviceCodeDlg;
+
+ friend class CSkypeOptionsMain;
+ friend class CSkypeGCCreateDlg;
+ friend class CSkypeInviteDlg;
+ friend class CMoodDialog;
+ friend class CDeviceCodeDlg;
+
+ class CTeamsProtoImpl
+ {
+ friend class CTeamsProto;
+ CTeamsProto &m_proto;
+
+ CTimer m_heartBeat, m_loginPoll;
+ void OnHeartBeat(CTimer *)
+ {
+ m_proto.ProcessTimer();
+ }
+ void OnLoginPoll(CTimer *)
+ {
+ m_proto.LoginPoll();
+ }
+
+ CTeamsProtoImpl(CTeamsProto &pro) :
+ m_proto(pro),
+ m_heartBeat(Miranda_GetSystemWindow(), UINT_PTR(this) + 1),
+ m_loginPoll(Miranda_GetSystemWindow(), UINT_PTR(this) + 2)
+ {
+ m_heartBeat.OnEvent = Callback(this, &CTeamsProtoImpl::OnHeartBeat);
+ m_loginPoll.OnEvent = Callback(this, &CTeamsProtoImpl::OnLoginPoll);
+ }
+ } m_impl;
+
+ // http queue
+ bool m_isTerminated = true;
+ mir_cs m_requestQueueLock;
+ LIST<AsyncHttpRequest> m_requests;
+ MEventHandle m_hRequestQueueEvent;
+ HANDLE m_hRequestQueueThread;
+ CMStringA m_szAccessToken, m_szSubstrateToken;
+
+ void __cdecl WorkerThread(void *);
+
+ void StartQueue();
+ void StopQueue();
+
+ MHttpResponse* DoSend(AsyncHttpRequest *request);
+
+ void Execute(AsyncHttpRequest *request);
+ void PushRequest(AsyncHttpRequest *request);
+
+ // login
+ CMStringW m_wszUserCode;
+ CMStringA m_szDeviceCode, m_szDeviceCookie, m_szVerificationUrl;
+ time_t m_iLoginExpires;
+
+ void Login();
+ void LoggedIn();
+ void LoginPoll();
+ void LoginError();
+
+ void SendCreateEndpoint();
+ void SendPresence();
+
+ void OauthRefreshServices();
+ void RefreshToken(const char *pszScope, AsyncHttpRequest::MTHttpRequestHandler pFunc);
+
+ void OnReceiveSkypeToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnReceiveDevicePoll(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnReceiveDeviceToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnRefreshAccessToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnRefreshSkypeToken(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnRefreshSubstrate(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+public:
+ // constructor
+ CTeamsProto(const char *protoName, const wchar_t *userName);
+ ~CTeamsProto();
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // Virtual functions
+
+ MCONTACT AddToList(int flags, PROTOSEARCHRESULT* psr) override;
+ MCONTACT AddToListByEvent(int flags, int iContact, MEVENT hDbEvent) override;
+ int AuthRequest(MCONTACT hContact, const wchar_t* szMessage) override;
+ int Authorize(MEVENT hDbEvent) override;
+ int AuthDeny(MEVENT hDbEvent, const wchar_t* szReason) override;
+ int AuthRecv(MCONTACT hContact, DB::EventInfo &dbei) override;
+ INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override;
+ int GetInfo(MCONTACT hContact, int infoType) override;
+ HANDLE SearchBasic(const wchar_t* id) override;
+ int SendMsg(MCONTACT hContact, MEVENT hReplyEvent, const char* msg) override;
+ int SetStatus(int iNewStatus) override;
+ int UserIsTyping(MCONTACT hContact, int type) override;
+ int RecvContacts(MCONTACT hContact, DB::EventInfo &dbei) override;
+ HANDLE SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override;
+
+ void OnBuildProtoMenu(void) override;
+ bool OnContactDeleted(MCONTACT, uint32_t flags) override;
+ MWindow OnCreateAccMgrUI(MWindow) override;
+ void OnEventEdited(MCONTACT hContact, MEVENT hDbEvent, const DBEVENTINFO &dbei) override;
+ void OnEventDeleted(MCONTACT hContact, MEVENT hDbEvent, int flags) override;
+ void OnMarkRead(MCONTACT, MEVENT) override;
+ void OnModulesLoaded() override;
+ void OnReceiveOfflineFile(DB::EventInfo &dbei, DB::FILE_BLOB &blob) override;
+ void OnShutdown() override;
+
+ // menus
+ static void InitMenus();
+
+ // popups
+ void InitPopups();
+ void UninitPopups();
+
+ // search
+ void __cdecl SearchBasicThread(void *param);
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // services
+
+ static INT_PTR __cdecl SvcEventGetIcon(WPARAM, LPARAM);
+ static INT_PTR __cdecl SvcGetEventText(WPARAM, LPARAM);
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // settings
+
+ CMOption<bool> m_bAutoHistorySync;
+ CMOption<bool> m_bUseBBCodes;
+
+ CMOption<bool> m_bUseHostnameAsPlace;
+ CMOption<wchar_t*> m_wstrPlace;
+
+ CMOption<wchar_t*> m_wstrCListGroup;
+
+ CMOption<uint8_t> m_iMood;
+ CMOption<wchar_t*> m_wstrMoodMessage, m_wstrMoodEmoji;
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // other data
+
+ int m_iPollingId, m_iMessageId = 1;
+ ptrA m_szToken, m_szId, m_szOwnSkypeId;
+ CMStringA m_szSkypename, m_szMyname, m_szSkypeToken;
+ MCONTACT m_hMyContact;
+
+ __forceinline CMStringA getId(MCONTACT hContact) {
+ return getMStringA(hContact, SKYPE_SETTINGS_ID);
+ }
+
+ void OnSearch(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ // login
+ void OnSubscriptionsCreated(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnCapabilitiesSended(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnStatusChanged(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void OnEndpointCreated(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnEndpointDeleted(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ // oauth
+ void OnOAuthStart(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnOAuthConfirm(MHttpResponse* response, AsyncHttpRequest* pRequest);
+ void OnOAuthAuthorize(MHttpResponse* response, AsyncHttpRequest* pRequest);
+ void OnOAuthEnd(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void OnASMObjectCreated(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnASMObjectUploaded(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void LoadContactsAuth(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void LoadContactList(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void OnBlockContact(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnUnblockContact(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void OnReceiveAvatar(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnSentAvatar(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void OnMessageSent(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void OnGetServerHistory(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnSyncConversations(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void OnGetChatInfo(MHttpResponse *response, AsyncHttpRequest *pRequest);
+ void OnGetChatMembers(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ void CheckConvert(void);
+ bool CheckOauth(const char *szResponse);
+
+ void LoadProfile(MHttpResponse *response, AsyncHttpRequest *pRequest);
+
+ static INT_PTR __cdecl GlobalParseSkypeUriService(WPARAM, LPARAM lParam);
+private:
+ bool m_bHistorySynced;
+
+ static std::map<std::wstring, std::wstring> languages;
+
+ LIST<void> m_PopupClasses;
+ OBJLIST<COwnMessage> m_OutMessages;
+
+ // locks
+ mir_cs m_lckOutMessagesList;
+ mir_cs messageSyncLock;
+ mir_cs m_StatusLock;
+
+ HANDLE m_hPollingThread;
+ HNETLIBCONN m_hPollingConn;
+
+ // avatars
+ void SetAvatarUrl(MCONTACT hContact, const CMStringW &tszUrl);
+ bool ReceiveAvatar(MCONTACT hContact);
+ void ReloadAvatarInfo(MCONTACT hContact);
+ void GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen);
+
+ INT_PTR __cdecl SvcGetAvatarInfo(WPARAM, LPARAM);
+ INT_PTR __cdecl SvcGetAvatarCaps(WPARAM, LPARAM);
+ INT_PTR __cdecl SvcGetMyAvatar(WPARAM, LPARAM);
+ INT_PTR __cdecl SvcSetMyAvatar(WPARAM, LPARAM);
+
+ // menus
+ static HGENMENU ContactMenuItems[CMI_MAX];
+ int OnPrebuildContactMenu(WPARAM hContact, LPARAM);
+ static int PrebuildContactMenu(WPARAM hContact, LPARAM lParam);
+
+ // options
+ int __cdecl OnOptionsInit(WPARAM wParam, LPARAM lParam);
+
+ // profile
+ void UpdateProfileDisplayName(const JSONNode &root, MCONTACT hContact = NULL);
+ void UpdateProfileGender(const JSONNode &root, MCONTACT hContact = NULL);
+ void UpdateProfileBirthday(const JSONNode &root, MCONTACT hContact = NULL);
+ void UpdateProfileCountry(const JSONNode &node, MCONTACT hContact = NULL);
+ void UpdateProfileEmails(const JSONNode &root, MCONTACT hContact = NULL);
+ void UpdateProfileAvatar(const JSONNode &root, MCONTACT hContact = NULL);
+
+ // contacts
+ uint16_t GetContactStatus(MCONTACT hContact);
+ void SetContactStatus(MCONTACT hContact, uint16_t status);
+
+ MCONTACT FindContact(const char *skypeId);
+ MCONTACT FindContact(const wchar_t *skypeId);
+
+ MCONTACT AddContact(const char *skypename, const char *nick, bool isTemporary = false);
+
+ MCONTACT GetContactFromAuthEvent(MEVENT hEvent);
+
+ // files
+ void SendFile(CFileUploadParam *fup);
+
+ void __cdecl ReceiveFileThread(void *param);
+
+ INT_PTR __cdecl SvcOfflineFile(WPARAM, LPARAM);
+
+ // messages
+ std::map<ULONGLONG, HANDLE> m_mpOutMessagesIds;
+
+ int SendServerMsg(MCONTACT hContact, const char *szMessage, int64_t iMessageId = 0);
+
+ int __cdecl OnPreCreateMessage(WPARAM, LPARAM lParam);
+
+ void ProcessContactRecv(MCONTACT hContact, const char *szContent, DB::EventInfo &dbei);
+ void ProcessFileRecv(MCONTACT hContact, const char *szContent, DB::EventInfo &dbei);
+
+ // chats
+ void InitGroupChatModule();
+
+ int __cdecl OnGroupChatEventHook(WPARAM, LPARAM lParam);
+ int __cdecl OnGroupChatMenuHook(WPARAM, LPARAM lParam);
+ INT_PTR __cdecl OnJoinChatRoom(WPARAM hContact, LPARAM);
+ INT_PTR __cdecl OnLeaveChatRoom(WPARAM hContact, LPARAM);
+
+ SESSION_INFO* StartChatRoom(const wchar_t *tid, const wchar_t *tname, const char *pszVersion = nullptr);
+
+ bool OnChatEvent(const JSONNode &node);
+ wchar_t* GetChatContactNick(SESSION_INFO *si, const wchar_t *id, const wchar_t *name = nullptr, bool *isQualified = nullptr);
+
+ bool AddChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *role, bool isChange = false);
+ void RemoveChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *initiator = L"");
+ void SendChatMessage(SESSION_INFO *si, const wchar_t *tszMessage);
+
+ void KickChatUser(const char *chatId, const char *userId);
+
+ void SetChatStatus(MCONTACT hContact, int iStatus);
+
+ // polling
+ void __cdecl PollingThread(void*);
+
+ bool ParseMessage(const JSONNode &node, DB::EventInfo &dbei);
+ void ParsePollData(const char*);
+
+ void ProcessNewMessage(const JSONNode &node);
+ void ProcessUserPresence(const JSONNode &node);
+ void ProcessThreadUpdate(const JSONNode &node);
+ void ProcessEndpointPresence(const JSONNode &node);
+ void ProcessConversationUpdate(const JSONNode &node);
+
+ // utils
+ template <typename T>
+ __inline static void FreeList(const LIST<T> &lst)
+ {
+ for (auto &it : lst)
+ mir_free(it);
+ }
+
+ __forceinline bool IsOnline() const
+ { return (m_iStatus > ID_STATUS_OFFLINE);
+ }
+
+ bool IsMe(const wchar_t *str);
+ bool IsMe(const char *str);
+
+ int64_t getLastTime(MCONTACT);
+ void setLastTime(MCONTACT, int64_t);
+
+ CMStringW RemoveHtml(const CMStringW &src, bool bCheckSS = false);
+
+ static time_t IsoToUnixTime(const std::string &stamp);
+
+ static int SkypeToMirandaStatus(const char *status);
+ static const char *MirandaToSkypeStatus(int status);
+
+ void ShowNotification(const wchar_t *message, MCONTACT hContact = NULL);
+ void ShowNotification(const wchar_t *caption, const wchar_t *message, MCONTACT hContact = NULL, int type = 0);
+ static bool IsFileExists(std::wstring path);
+
+ static LRESULT CALLBACK PopupDlgProcCall(HWND hPopup, UINT uMsg, WPARAM wParam, LPARAM lParam);
+
+ void ProcessTimer();
+
+ void SetString(MCONTACT hContact, const char *pszSetting, const JSONNode &node);
+
+ CMStringW ChangeTopicForm();
+
+ // services
+ INT_PTR __cdecl BlockContact(WPARAM hContact, LPARAM);
+ INT_PTR __cdecl UnblockContact(WPARAM hContact, LPARAM);
+ INT_PTR __cdecl OnRequestAuth(WPARAM hContact, LPARAM);
+ INT_PTR __cdecl OnGrantAuth(WPARAM hContact, LPARAM);
+ INT_PTR __cdecl SvcLoadHistory(WPARAM hContact, LPARAM);
+ INT_PTR __cdecl SvcEmptyHistory(WPARAM hContact, LPARAM);
+ INT_PTR __cdecl SvcCreateChat(WPARAM, LPARAM);
+ INT_PTR __cdecl SvcSetMood(WPARAM, LPARAM);
+ INT_PTR __cdecl ParseSkypeUriService(WPARAM, LPARAM lParam);
+
+ template<INT_PTR(__cdecl CTeamsProto::*Service)(WPARAM, LPARAM)>
+ static INT_PTR __cdecl GlobalService(WPARAM wParam, LPARAM lParam)
+ {
+ auto *proto = CMPlugin::getInstance((MCONTACT)wParam);
+ return proto ? (proto->*Service)(wParam, lParam) : 0;
+ }
+};
+
+typedef CProtoDlgBase<CTeamsProto> CTeamsDlgBase;
+
+struct CMPlugin : public ACCPROTOPLUGIN<CTeamsProto>
+{
+ CMPlugin();
+
+ int Load() override;
+ int Unload() override;
+};
diff --git a/protocols/Teams/src/teams_search.cpp b/protocols/Teams/src/teams_search.cpp
new file mode 100644
index 0000000000..a88daaae47
--- /dev/null
+++ b/protocols/Teams/src/teams_search.cpp
@@ -0,0 +1,62 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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"
+
+HANDLE CTeamsProto::SearchBasic(const wchar_t *id)
+{
+ ForkThread(&CTeamsProto::SearchBasicThread, (void *)id);
+ return (HANDLE)1;
+}
+
+void CTeamsProto::SearchBasicThread(void *id)
+{
+ debugLogA("CTeamsProto::OnSearchBasicThread");
+ if (IsOnline())
+ PushRequest(new GetSearchRequest(T2Utf((wchar_t *)id)));
+}
+
+void CTeamsProto::OnSearch(MHttpResponse *response, AsyncHttpRequest*)
+{
+ debugLogA(__FUNCTION__);
+
+ SkypeReply reply(response);
+ if (reply.error()) {
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)1, 0);
+ return;
+ }
+
+ auto &root = reply.data();
+ const JSONNode &items = root["results"].as_array();
+ for (auto &it : items) {
+ const JSONNode &item = it["nodeProfileData"];
+
+ std::string skypeId = item["skypeId"].as_string();
+ if (UrlToSkypeId(skypeId.c_str()).IsEmpty())
+ skypeId = "8:" + skypeId;
+
+ std::string name = item["name"].as_string();
+
+ PROTOSEARCHRESULT psr = { sizeof(psr) };
+ psr.flags = PSR_UTF8;
+ psr.id.a = const_cast<char *>(skypeId.c_str());
+ psr.nick.a = const_cast<char *>(name.c_str());
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)1, (LPARAM)&psr);
+ }
+
+ ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)1, 0);
+}
diff --git a/protocols/Teams/src/teams_utils.cpp b/protocols/Teams/src/teams_utils.cpp
new file mode 100644
index 0000000000..59beeaa894
--- /dev/null
+++ b/protocols/Teams/src/teams_utils.cpp
@@ -0,0 +1,885 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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 warning(disable:4566)
+
+time_t CTeamsProto::IsoToUnixTime(const std::string &stamp)
+{
+ char date[9];
+ int i, y;
+
+ if (stamp.empty())
+ return 0;
+
+ char *p = NEWSTR_ALLOCA(stamp.c_str());
+
+ // skip '-' chars
+ int si = 0, sj = 0;
+ while (true) {
+ if (p[si] == '-')
+ si++;
+ else if (!(p[sj++] = p[si++]))
+ break;
+ }
+
+ // Get the date part
+ for (i = 0; *p != '\0' && i < 8 && isdigit(*p); p++, i++)
+ date[i] = *p;
+
+ // Parse year
+ if (i == 6) {
+ // 2-digit year (1970-2069)
+ y = (date[0] - '0') * 10 + (date[1] - '0');
+ if (y < 70) y += 100;
+ }
+ else if (i == 8) {
+ // 4-digit year
+ y = (date[0] - '0') * 1000 + (date[1] - '0') * 100 + (date[2] - '0') * 10 + date[3] - '0';
+ y -= 1900;
+ }
+ else return 0;
+
+ struct tm timestamp;
+ timestamp.tm_year = y;
+
+ // Parse month
+ timestamp.tm_mon = (date[i - 4] - '0') * 10 + date[i - 3] - '0' - 1;
+
+ // Parse date
+ timestamp.tm_mday = (date[i - 2] - '0') * 10 + date[i - 1] - '0';
+
+ // Skip any date/time delimiter
+ for (; *p != '\0' && !isdigit(*p); p++);
+
+ // Parse time
+ if (sscanf(p, "%d:%d:%d", &timestamp.tm_hour, &timestamp.tm_min, &timestamp.tm_sec) != 3)
+ return (time_t)0;
+
+ timestamp.tm_isdst = 0; // DST is already present in _timezone below
+ time_t t = mktime(&timestamp);
+
+ _tzset();
+ t -= _timezone;
+ return (t >= 0) ? t : 0;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+
+struct HtmlEntity
+{
+ const char *entity;
+ const char *symbol;
+};
+
+const HtmlEntity htmlEntities[] =
+{
+ { "AElig", "\u00C6" },
+ { "Aacute", "\u00C1" },
+ { "Acirc", "\u00C2" },
+ { "Agrave", "\u00C0" },
+ { "Alpha", "\u0391" },
+ { "Aring", "\u00C5" },
+ { "Atilde", "\u00C3" },
+ { "Auml", "\u00C4" },
+ { "Beta", "\u0392" },
+ { "Ccedil", "\u00C7" },
+ { "Chi", "\u03A7" },
+ { "Dagger", "‡" },
+ { "Delta", "\u0394" },
+ { "ETH", "\u00D0" },
+ { "Eacute", "\u00C9" },
+ { "Ecirc", "\u00CA" },
+ { "Egrave", "\u00C8" },
+ { "Epsilon", "\u0395" },
+ { "Eta", "\u0397" },
+ { "Euml", "\u00CB" },
+ { "Gamma", "\u0393" },
+ { "Iacute", "\u00CD" },
+ { "Icirc", "\u00CE" },
+ { "Igrave", "\u00CC" },
+ { "Iota", "\u0399" },
+ { "Iuml", "\u00CF" },
+ { "Kappa", "\u039A" },
+ { "Lambda", "\u039B" },
+ { "Mu", "\u039C" },
+ { "Ntilde", "\u00D1" },
+ { "Nu", "\u039D" },
+ { "OElig", "\u0152" },
+ { "Oacute", "\u00D3" },
+ { "Ocirc", "\u00D4" },
+ { "Ograve", "\u00D2" },
+ { "Omega", "\u03A9" },
+ { "Omicron", "\u039F" },
+ { "Oslash", "\u00D8" },
+ { "Otilde", "\u00D5" },
+ { "Ouml", "\u00D6" },
+ { "Phi", "\u03A6" },
+ { "Pi", "\u03A0" },
+ { "Prime", "\u2033" },
+ { "Psi", "\u03A8" },
+ { "Rho", "\u03A1" },
+ { "Scaron", "Š" },
+ { "Sigma", "Σ" },
+ { "THORN", "Þ" },
+ { "Tau", "Τ" },
+ { "Theta", "Θ" },
+ { "Uacute", "Ú" },
+ { "Ucirc", "Û" },
+ { "Ugrave", "Ù" },
+ { "Upsilon", "Υ" },
+ { "Uuml", "Ü" },
+ { "Xi", "Ξ" },
+ { "Yacute", "Ý" },
+ { "Yuml", "Ÿ" },
+ { "Zeta", "Ζ" },
+ { "aacute", "á" },
+ { "acirc", "â" },
+ { "acute", "´" },
+ { "aelig", "æ" },
+ { "agrave", "à" },
+ { "alefsym", "ℵ" },
+ { "alpha", "α" },
+ { "amp", "&" },
+ { "and", "∧" },
+ { "ang", "∠" },
+ { "apos", "'" },
+ { "aring", "å" },
+ { "asymp", "≈" },
+ { "atilde", "ã" },
+ { "auml", "ä" },
+ { "bdquo", "„" },
+ { "beta", "β" },
+ { "brvbar", "¦" },
+ { "bull", "•" },
+ { "cap", "∩" },
+ { "ccedil", "ç" },
+ { "cedil", "¸" },
+ { "cent", "¢" },
+ { "chi", "χ" },
+ { "circ", "ˆ" },
+ { "clubs", "♣" },
+ { "cong", "≅" },
+ { "copy", "©" },
+ { "crarr", "↵" },
+ { "cup", "∪" },
+ { "curren", "¤" },
+ { "dArr", "⇓" },
+ { "dagger", "†" },
+ { "darr", "↓" },
+ { "deg", "°" },
+ { "delta", "δ" },
+ { "diams", "♦" },
+ { "divide", "÷" },
+ { "eacute", "é" },
+ { "ecirc", "ê" },
+ { "egrave", "è" },
+ { "empty", "∅" },
+ { "emsp", " " },
+ { "ensp", " " },
+ { "epsilon", "ε" },
+ { "equiv", "≡" },
+ { "eta", "η" },
+ { "eth", "ð" },
+ { "euml", "ë" },
+ { "euro", "€" },
+ { "exist", "∃" },
+ { "fnof", "ƒ" },
+ { "forall", "∀" },
+ { "frac12", "½" },
+ { "frac14", "¼" },
+ { "frac34", "¾" },
+ { "frasl", "⁄" },
+ { "gamma", "γ" },
+ { "ge", "≥" },
+ { "gt", ">" },
+ { "hArr", "⇔" },
+ { "harr", "↔" },
+ { "hearts", "♥" },
+ { "hellip", "…" },
+ { "iacute", "í" },
+ { "icirc", "î" },
+ { "iexcl", "¡" },
+ { "igrave", "ì" },
+ { "image", "ℑ" },
+ { "infin", "∞" },
+ { "int", "∫" },
+ { "iota", "ι" },
+ { "iquest", "¿" },
+ { "isin", "∈" },
+ { "iuml", "ï" },
+ { "kappa", "κ" },
+ { "lArr", "⇐" },
+ { "lambda", "λ" },
+ { "lang", "〈" },
+ { "laquo", "«" },
+ { "larr", "←" },
+ { "lceil", "⌈" },
+ { "ldquo", "“" },
+ { "le", "≤" },
+ { "lfloor", "⌊" },
+ { "lowast", "∗" },
+ { "loz", "◊" },
+ { "lrm", "\xE2\x80\x8E" },
+ { "lsaquo", "‹" },
+ { "lsquo", "‘" },
+ { "lt", "<" },
+ { "macr", "¯" },
+ { "mdash", "—" },
+ { "micro", "µ" },
+ { "middot", "·" },
+ { "minus", "−" },
+ { "mu", "μ" },
+ { "nabla", "∇" },
+ { "nbsp", " " },
+ { "ndash", "–" },
+ { "ne", "≠" },
+ { "ni", "∋" },
+ { "not", "¬" },
+ { "notin", "∉" },
+ { "nsub", "⊄" },
+ { "ntilde", "ñ" },
+ { "nu", "ν" },
+ { "oacute", "ó" },
+ { "ocirc", "ô" },
+ { "oelig", "œ" },
+ { "ograve", "ò" },
+ { "oline", "‾" },
+ { "omega", "ω" },
+ { "omicron", "ο" },
+ { "oplus", "⊕" },
+ { "or", "∨" },
+ { "ordf", "ª" },
+ { "ordm", "º" },
+ { "oslash", "ø" },
+ { "otilde", "õ" },
+ { "otimes", "⊗" },
+ { "ouml", "ö" },
+ { "para", "¶" },
+ { "part", "∂" },
+ { "permil", "‰" },
+ { "perp", "⊥" },
+ { "phi", "φ" },
+ { "pi", "π" },
+ { "piv", "ϖ" },
+ { "plusmn", "±" },
+ { "pound", "£" },
+ { "prime", "′" },
+ { "prod", "∏" },
+ { "prop", "∝" },
+ { "psi", "ψ" },
+ { "quot", "\"" },
+ { "rArr", "⇒" },
+ { "radic", "√" },
+ { "rang", "〉" },
+ { "raquo", "»" },
+ { "rarr", "→" },
+ { "rceil", "⌉" },
+ { "rdquo", "”" },
+ { "real", "ℜ" },
+ { "reg", "®" },
+ { "rfloor", "⌋" },
+ { "rho", "ρ" },
+ { "rlm", "\xE2\x80\x8F" },
+ { "rsaquo", "›" },
+ { "rsquo", "’" },
+ { "sbquo", "‚" },
+ { "scaron", "š" },
+ { "sdot", "⋅" },
+ { "sect", "§" },
+ { "shy", "\xC2\xAD" },
+ { "sigma", "σ" },
+ { "sigmaf", "ς" },
+ { "sim", "∼" },
+ { "spades", "♠" },
+ { "sub", "⊂" },
+ { "sube", "⊆" },
+ { "sum", "∑" },
+ { "sup", "⊃" },
+ { "sup1", "¹" },
+ { "sup2", "²" },
+ { "sup3", "³" },
+ { "supe", "⊇" },
+ { "szlig", "ß" },
+ { "tau", "τ" },
+ { "there4", "∴" },
+ { "theta", "θ" },
+ { "thetasym", "ϑ" },
+ { "thinsp", " " },
+ { "thorn", "þ" },
+ { "tilde", "˜" },
+ { "times", "×" },
+ { "trade", "™" },
+ { "uArr", "⇑" },
+ { "uacute", "ú" },
+ { "uarr", "↑" },
+ { "ucirc", "û" },
+ { "ugrave", "ù" },
+ { "uml", "¨" },
+ { "upsih", "ϒ" },
+ { "upsilon", "υ" },
+ { "uuml", "ü" },
+ { "weierp", "℘" },
+ { "xi", "ξ" },
+ { "yacute", "ý" },
+ { "yen", "¥" },
+ { "yuml", "ÿ" },
+ { "zeta", "ζ" },
+ { "zwj", "\xE2\x80\x8D" },
+ { "zwnj", "\xE2\x80\x8C" }
+};
+
+static CMStringW getAttrText(const wchar_t *pwszText, const wchar_t *pwszAttrName)
+{
+ if (auto *p1 = mir_wstrstri(pwszText, pwszAttrName)) {
+ p1 += wcslen(pwszAttrName);
+ if (p1[0] == '=' && p1[1] == '\"') {
+ p1 += 2;
+ if (auto *p2 = wcschr(p1, '\"'))
+ return CMStringW(p1, p2 - p1);
+ }
+ }
+ return L"";
+}
+
+CMStringW CTeamsProto::RemoveHtml(const CMStringW &data, bool bCheckSS)
+{
+ bool inSS = false;
+ CMStringW new_string;
+
+ for (int i = 0; i < data.GetLength(); i++) {
+ wchar_t c = data[i];
+ if (c == '<') {
+ if (bCheckSS && !wcsncmp(data.c_str() + i + 1, L"ss ", 3))
+ inSS = true;
+ else if (!wcsncmp(data.c_str() + i + 1, L"/ss>", 4)) {
+ CMStringW wszStatusMsg = data.Mid(i + 5);
+ wszStatusMsg.Trim();
+ m_wstrMoodMessage = wszStatusMsg;
+ inSS = false;
+ }
+ else if (m_bUseBBCodes) {
+ bool bEnable = true;
+ auto *p = data.c_str() + i + 1;
+ if (*p == '/') {
+ bEnable = false;
+ p++;
+ }
+
+ if (!wcsncmp(p, L"b>", 2))
+ new_string.Append(bEnable ? L"[b]" : L"[/b]");
+ else if (!wcsncmp(p, L"i>", 2))
+ new_string.Append(bEnable ? L"[i]" : L"[/i]");
+ else if (!wcsncmp(p, L"u>", 2))
+ new_string.Append(bEnable ? L"[u]" : L"[/u]");
+ else if (!wcsncmp(p, L"s>", 2))
+ new_string.Append(bEnable ? L"[s]" : L"[/s]");
+ else if (!wcsncmp(p, L"pre ", 4))
+ new_string.Append(L"[code]");
+ else if (!wcsncmp(p, L"pre>", 4))
+ new_string.Append(L"[/code]");
+ else if (!wcsncmp(p, L"legacyquote>", 12)) {
+ if (bEnable)
+ i = data.Find(L"legacyquote>", i+13);
+ }
+ else if (!wcsncmp(p, L"quote ", 6)) {
+ CMStringW author(getAttrText(p, L"authorname"));
+ CMStringW timestamp(getAttrText(p, L"timestamp"));
+
+ wchar_t wszTime[100];
+ TimeZone_PrintTimeStamp(0, _wtoi(timestamp), L"D t", wszTime, _countof(wszTime), 0);
+
+ new_string.AppendFormat(L"[quote]%s %s %s: \r\n", wszTime, author.c_str(), TranslateT("wrote"));
+ }
+ else if (!wcsncmp(p, L"quote>", 6)) {
+ new_string.Append(L"[/quote]");
+ }
+ }
+
+ i = data.Find('>', i);
+ if (i == -1)
+ break;
+
+ continue;
+ }
+
+ // special character
+ if (c == '&') {
+ int begin = i;
+ i = data.Find(';', i);
+ if (i == -1)
+ i = begin;
+ else {
+ CMStringW entity = data.Mid(begin + 1, i - begin - 1);
+
+ bool found = false;
+ if (entity.GetLength() > 1 && entity[0] == '#') {
+ // Numeric replacement
+ bool hex = false;
+ if (entity[1] == 'x') {
+ hex = true;
+ entity.Delete(0, 2);
+ }
+ else entity.Delete(0, 1);
+
+ if (!entity.IsEmpty()) {
+ found = true;
+ errno = 0;
+ unsigned long value = wcstoul(entity, nullptr, hex ? 16 : 10);
+ if (errno != 0) { // error with conversion in strtoul, ignore the result
+ found = false;
+ }
+ else if (value <= 127) { // U+0000 .. U+007F
+ new_string += (char)value;
+ }
+ else if (value >= 128 && value <= 2047) { // U+0080 .. U+07FF
+ new_string += (char)(192 + (value / 64));
+ new_string += (char)(128 + (value % 64));
+ }
+ else if (value >= 2048 && value <= 65535) { // U+0800 .. U+FFFF
+ new_string += (char)(224 + (value / 4096));
+ new_string += (char)(128 + ((value / 64) % 64));
+ new_string += (char)(128 + (value % 64));
+ }
+ else {
+ new_string += (char)((value >> 24) & 0xFF);
+ new_string += (char)((value >> 16) & 0xFF);
+ new_string += (char)((value >> 8) & 0xFF);
+ new_string += (char)((value) & 0xFF);
+ }
+ }
+ }
+ else {
+ // Keyword replacement
+ CMStringA tmp = entity;
+ for (auto &it : htmlEntities) {
+ if (!mir_strcmpi(tmp, it.entity)) {
+ new_string += it.symbol;
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (found)
+ continue;
+ else
+ i = begin;
+ }
+ }
+
+ if (c == '(' && inSS) {
+ int iEnd = data.Find(')', i);
+ if (iEnd != -1) {
+ CMStringW ss(data.Mid(i + 1, iEnd - i - 1));
+ uint32_t code = getMoodIndex(T2Utf(ss));
+ if (code != -1)
+ m_iMood = code;
+ else if (1 == swscanf(ss, L"%x_", &code)) {
+ Utf32toUtf16(code, new_string);
+ m_wstrMoodEmoji = new_string;
+ }
+
+ i = iEnd;
+ continue;
+ }
+ }
+
+ new_string.AppendChar(c);
+ }
+
+ return new_string;
+}
+
+bool AddBbcodes(CMStringA &str)
+{
+ bool bUsed = false;
+ CMStringA ret;
+
+ for (const char *p = str; *p; p++) {
+ if (*p == '[') {
+ p++;
+ if (!strncmp(p, "b]", 2)) {
+ p++;
+ ret.Append("<b _pre=\"*\" _post=\"*\">");
+ bUsed = true;
+ }
+ else if (!strncmp(p, "/b]", 3)) {
+ p += 2;
+ ret.Append("</b>");
+ bUsed = true;
+ }
+ else if (!strncmp(p, "i]", 2)) {
+ p++;
+ ret.Append("<i _pre=\"_\" _post=\"_\">");
+ bUsed = true;
+ }
+ else if (!strncmp(p, "/i]", 3)) {
+ p += 2;
+ ret.Append("</i>");
+ bUsed = true;
+ }
+ else if (!strncmp(p, "s]", 2)) {
+ p++;
+ ret.Append("<s _pre=\"~\" _post=\"~\">");
+ bUsed = true;
+ }
+ else if (!strncmp(p, "/s]", 3)) {
+ p += 2;
+ ret.Append("</s>");
+ bUsed = true;
+ }
+ else if (!strncmp(p, "code]", 5)) {
+ p += 4;
+ ret.Append("<pre _pre=\"```\" _post=\"```\">");
+ bUsed = true;
+ }
+ else if (!strncmp(p, "/code]", 6)) {
+ p += 5;
+ ret.Append("</pre>");
+ bUsed = true;
+ }
+ }
+ else ret.AppendChar(*p);
+ }
+
+ if (bUsed)
+ str = ret;
+ return bUsed;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+
+void Utf32toUtf16(uint32_t c, CMStringW &dest)
+{
+ if (c < 0x10000)
+ dest.AppendChar(c);
+ else {
+ unsigned int t = c - 0x10000;
+ dest.AppendChar((((t << 12) >> 22) + 0xD800));
+ dest.AppendChar((((t << 22) >> 22) + 0xDC00));
+ }
+}
+
+bool is_surrogate(wchar_t uc) { return (uc - 0xd800u) < 2048u; }
+bool is_high_surrogate(wchar_t uc) { return (uc & 0xfffffc00) == 0xd800; }
+bool is_low_surrogate(wchar_t uc) { return (uc & 0xfffffc00) == 0xdc00; }
+
+uint32_t Utf16toUtf32(const wchar_t *str)
+{
+ if (!is_surrogate(str[0]))
+ return str[0];
+
+ if (is_high_surrogate(str[0]) && is_low_surrogate(str[1]))
+ return (str[0] << 10) + str[1] - 0x35fdc00;
+
+ return 0;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+
+const char* CTeamsProto::MirandaToSkypeStatus(int status)
+{
+ switch (status) {
+ case ID_STATUS_AWAY:
+ return "Away";
+
+ case ID_STATUS_DND:
+ return "Busy";
+
+ case ID_STATUS_IDLE:
+ return "Idle";
+
+ case ID_STATUS_INVISIBLE:
+ return "Hidden";
+ }
+ return "Online";
+}
+
+int CTeamsProto::SkypeToMirandaStatus(const char *status)
+{
+ if (!mir_strcmpi(status, "Online"))
+ return ID_STATUS_ONLINE;
+ else if (!mir_strcmpi(status, "Hidden"))
+ return ID_STATUS_INVISIBLE;
+ else if (!mir_strcmpi(status, "Away"))
+ return ID_STATUS_AWAY;
+ else if (!mir_strcmpi(status, "Idle"))
+ return ID_STATUS_AWAY;
+ else if (!mir_strcmpi(status, "Busy"))
+ return ID_STATUS_DND;
+ else
+ return ID_STATUS_OFFLINE;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+
+bool CTeamsProto::IsMe(const wchar_t *str)
+{
+ return (!mir_wstrcmpi(str, Utf2T(m_szMyname)) || !mir_wstrcmp(str, Utf2T(m_szOwnSkypeId)));
+}
+
+bool CTeamsProto::IsMe(const char *str)
+{
+ return (!mir_strcmpi(str, m_szMyname) || !mir_strcmp(str, m_szOwnSkypeId));
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+
+int64_t CTeamsProto::getLastTime(MCONTACT hContact)
+{
+ ptrA szLastTime(getStringA(hContact, "LastMsgTime"));
+ return (szLastTime) ? _atoi64(szLastTime) : 0;
+}
+
+void CTeamsProto::setLastTime(MCONTACT hContact, int64_t iValue)
+{
+ char buf[100];
+ _i64toa(iValue, buf, 10);
+ setString(hContact, "LastMsgTime", buf);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////
+
+bool CTeamsProto::IsFileExists(std::wstring path)
+{
+ return _waccess(path.c_str(), 0) == 0;
+}
+
+int64_t getRandomId()
+{
+ int64_t ret;
+ Utils_GetRandom(&ret, sizeof(ret));
+ if (ret < 0)
+ ret = -ret;
+ return ret;
+}
+
+CMStringA getMessageId(const JSONNode &node)
+{
+ CMStringA ret(node["skypeeditedid"].as_mstring());
+ if (ret.IsEmpty())
+ ret = node["clientmessageid"].as_mstring();
+ return ret;
+}
+
+const char* GetSkypeNick(const char *szSkypeId)
+{
+ if (auto *p = strchr(szSkypeId, ':'))
+ return p + 1;
+ return szSkypeId;
+}
+
+const wchar_t* GetSkypeNick(const wchar_t *szSkypeId)
+{
+ if (auto *p = wcsrchr(szSkypeId, ':'))
+ return p + 1;
+ return szSkypeId;
+}
+
+void CTeamsProto::SetString(MCONTACT hContact, const char *pszSetting, const JSONNode &node)
+{
+ CMStringW str = node.as_mstring();
+ if (str.IsEmpty() || str == L"null")
+ delSetting(hContact, pszSetting);
+ else
+ setWString(hContact, pszSetting, str);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// url parsing
+
+CMStringA ParseUrl(const char *url, const char *token)
+{
+ if (url == nullptr)
+ return CMStringA();
+
+ auto *start = strstr(url, token);
+ if (start == nullptr)
+ return CMStringA();
+
+ auto *end = strchr(++start, '/');
+ if (end == nullptr)
+ return CMStringA(start);
+ return CMStringA(start, end - start);
+}
+
+CMStringW ParseUrl(const wchar_t *url, const wchar_t *token)
+{
+ if (url == nullptr)
+ return CMStringW();
+
+ auto *start = wcsstr(url, token);
+ if (start == nullptr)
+ return CMStringW();
+
+ auto *end = wcschr(++start, '/');
+ if (end == nullptr)
+ return CMStringW(start);
+ return CMStringW(start, end - start);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+static int possibleTypes[] = { 1, 2, 4, 8, 19, 28 };
+
+bool IsPossibleUserType(const char *pszUserId)
+{
+ int iType = atoi(pszUserId);
+
+ // we don't process 28 and other shit
+ for (auto &it : possibleTypes)
+ if (it == iType)
+ return true;
+
+ return false;
+}
+
+CMStringA UrlToSkypeId(const char *url, int *pUserType)
+{
+ int userType = -1;
+ CMStringA szResult;
+
+ if (url != nullptr) {
+ for (auto &it : possibleTypes) {
+ char tmp[10];
+ sprintf_s(tmp, "/%d:", it);
+ if (strstr(url, tmp)) {
+ userType = it;
+ szResult = ParseUrl(url, tmp);
+ break;
+ }
+ }
+ }
+
+ if (pUserType)
+ *pUserType = userType;
+
+ return szResult;
+}
+
+CMStringW UrlToSkypeId(const wchar_t *url, int *pUserType)
+{
+ int userType = -1;
+ CMStringW szResult;
+
+ if (url != nullptr) {
+ for (auto &it : possibleTypes) {
+ wchar_t tmp[10];
+ swprintf_s(tmp, L"/%d:", it);
+ if (wcsstr(url, tmp)) {
+ userType = it;
+ szResult = ParseUrl(url, tmp);
+ break;
+ }
+ }
+ }
+
+ if (pUserType)
+ *pUserType = userType;
+
+ return szResult;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+INT_PTR CTeamsProto::ParseSkypeUriService(WPARAM, LPARAM lParam)
+{
+ wchar_t *arg = (wchar_t *)lParam;
+ if (arg == nullptr)
+ return 1;
+
+ // skip leading prefix
+ wchar_t szUri[1024];
+ wcsncpy_s(szUri, arg, _TRUNCATE);
+ wchar_t *szJid = wcschr(szUri, ':');
+ if (szJid == nullptr)
+ return 1;
+
+ // empty jid?
+ if (!*szJid)
+ return 1;
+
+ // command code
+ wchar_t *szCommand = szJid;
+ szCommand = wcschr(szCommand, '?');
+ if (szCommand)
+ *(szCommand++) = 0;
+
+ // parameters
+ wchar_t *szSecondParam = szCommand ? wcschr(szCommand, '&') : nullptr;
+ if (szSecondParam)
+ *(szSecondParam++) = 0;
+
+ // no command or message command
+ if (!szCommand || !mir_wstrcmpi(szCommand, L"chat")) {
+ if (szSecondParam) {
+ wchar_t *szChatId = wcsstr(szSecondParam, L"id=");
+ if (szChatId) {
+ szChatId += 5;
+ StartChatRoom(szChatId, szChatId);
+ return 0;
+ }
+ }
+ MCONTACT hContact = AddContact(_T2A(szJid), nullptr, true);
+ CallService(MS_MSG_SENDMESSAGE, (WPARAM)hContact, NULL);
+ return 0;
+ }
+
+ if (!mir_wstrcmpi(szCommand, L"call")) {
+ MCONTACT hContact = AddContact(_T2A(szJid), nullptr, true);
+ NotifyEventHooks(g_hCallEvent, (WPARAM)hContact, (LPARAM)0);
+ return 0;
+ }
+
+ if (!mir_wstrcmpi(szCommand, L"userinfo"))
+ return 0;
+
+ if (!mir_wstrcmpi(szCommand, L"add")) {
+ MCONTACT hContact = FindContact(_T2A(szJid));
+ if (hContact == NULL) {
+ PROTOSEARCHRESULT psr = { 0 };
+ psr.cbSize = sizeof(psr);
+ psr.id.w = mir_wstrdup(szJid);
+ psr.nick.w = mir_wstrdup(szJid);
+ psr.flags = PSR_UNICODE;
+ Contact::AddBySearch(m_szModuleName, &psr);
+ }
+ return 0;
+ }
+
+ if (!mir_wstrcmpi(szCommand, L"sendfile")) {
+ MCONTACT hContact = AddContact(_T2A(szJid), nullptr, true);
+ File::Send(hContact);
+ return 1;
+ }
+
+ if (!mir_wstrcmpi(szCommand, L"voicemail"))
+ return 1;
+
+ return 1;
+}
+
+INT_PTR CTeamsProto::GlobalParseSkypeUriService(WPARAM wParam, LPARAM lParam)
+{
+ for (auto &it : CMPlugin::g_arInstances)
+ if (it->IsOnline())
+ return it->ParseSkypeUriService(wParam, lParam);
+
+ return 1;
+}
diff --git a/protocols/Teams/src/teams_utils.h b/protocols/Teams/src/teams_utils.h
new file mode 100644
index 0000000000..f7d205da61
--- /dev/null
+++ b/protocols/Teams/src/teams_utils.h
@@ -0,0 +1,73 @@
+/*
+Copyright (c) 2015-25 Miranda NG team (https://miranda-ng.org)
+
+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 version 2
+of the License.
+
+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/>.
+*/
+
+#ifndef _UTILS_H_
+#define _UTILS_H_
+
+void Utf32toUtf16(uint32_t c, CMStringW &dest);
+uint32_t Utf16toUtf32(const wchar_t *str);
+
+const char* GetSkypeNick(const char *pszSkypeId);
+const wchar_t* GetSkypeNick(const wchar_t *szSkypeId);
+
+CMStringA ParseUrl(const char *url, const char *token);
+
+bool AddBbcodes(CMStringA &str);
+
+bool IsPossibleUserType(const char *pszUserId);
+
+CMStringA UrlToSkypeId(const char *url, int *pUserType = nullptr);
+CMStringW UrlToSkypeId(const wchar_t *url, int *pUserType = nullptr);
+
+int getMoodIndex(const char *pszMood);
+
+int64_t getRandomId();
+CMStringA getMessageId(const JSONNode &node);
+
+struct CFileUploadParam : public MZeroedObject
+{
+ OBJLIST<wchar_t> arFileName;
+ ptrW tszDesc;
+ ptrA atr;
+ ptrA fname;
+ ptrA uid;
+ long size;
+ int width, height;
+ MCONTACT hContact;
+ bool isPicture;
+
+ CFileUploadParam(MCONTACT _hContact, wchar_t **_files, const wchar_t* _desc) :
+ arFileName(1),
+ hContact(_hContact),
+ tszDesc(mir_wstrdup(_desc))
+ {
+ for (auto p = _files; *p != 0; p++)
+ arFileName.insert(newStrW(*p));
+ }
+};
+
+struct SkypeReply : public JsonReply
+{
+ SkypeReply(MHttpResponse *response) :
+ JsonReply(response)
+ {
+ if (m_root)
+ m_errorCode = (*m_root)["status"]["code"].as_int();
+ }
+};
+
+#endif //_UTILS_H_