diff options
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", ×tamp.tm_hour, ×tamp.tm_min, ×tamp.tm_sec) != 3) + return (time_t)0; + + timestamp.tm_isdst = 0; // DST is already present in _timezone below + time_t t = mktime(×tamp); + + _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_ |
