From 1979fd80424d16b2e489f9b57d01d9c7811d25a2 Mon Sep 17 00:00:00 2001 From: dartraiden Date: Mon, 2 Jan 2023 21:10:29 +0300 Subject: Update copyrights --- protocols/CloudFile/src/stdafx.cxx | 38 +- protocols/CloudFile/src/version.h | 2 +- protocols/ConnectionNotify/src/stdafx.cxx | 34 +- protocols/CurrencyRates/src/stdafx.cxx | 36 +- protocols/Dummy/src/dummy.h | 2 +- protocols/Dummy/src/dummy_options.cpp | 2 +- protocols/Dummy/src/dummy_proto.cpp | 376 ++-- protocols/Dummy/src/dummy_proto.h | 106 +- protocols/Dummy/src/main.cpp | 2 +- protocols/Dummy/src/stdafx.cxx | 36 +- protocols/Dummy/src/stdafx.h | 120 +- protocols/Dummy/src/version.h | 2 +- protocols/Facebook/src/avatars.cpp | 262 +-- protocols/Facebook/src/db.h | 88 +- protocols/Facebook/src/dialogs.cpp | 156 +- protocols/Facebook/src/dialogs.h | 2 +- protocols/Facebook/src/groupchats.cpp | 510 ++--- protocols/Facebook/src/http.cpp | 370 ++-- protocols/Facebook/src/main.cpp | 144 +- protocols/Facebook/src/mqtt.cpp | 688 +++--- protocols/Facebook/src/mqtt.h | 278 +-- protocols/Facebook/src/options.cpp | 168 +- protocols/Facebook/src/proto.cpp | 604 ++--- protocols/Facebook/src/proto.h | 1114 +++++----- protocols/Facebook/src/server.cpp | 2102 +++++++++--------- protocols/Facebook/src/stdafx.cxx | 36 +- protocols/Facebook/src/stdafx.h | 134 +- protocols/Facebook/src/thrift.cpp | 616 +++--- protocols/Facebook/src/version.h | 2 +- protocols/Gadu-Gadu/src/stdafx.cxx | 2 +- protocols/Gadu-Gadu/src/userinfo.cpp | 656 +++--- protocols/GmailNotifier/src/stdafx.cxx | 34 +- protocols/ICQ-WIM/src/groupchats.cpp | 584 ++--- protocols/ICQ-WIM/src/http.cpp | 770 +++---- protocols/ICQ-WIM/src/ignore.cpp | 164 +- protocols/ICQ-WIM/src/main.cpp | 204 +- protocols/ICQ-WIM/src/mra.cpp | 282 +-- protocols/ICQ-WIM/src/options.cpp | 788 +++---- protocols/ICQ-WIM/src/poll.cpp | 818 +++---- protocols/ICQ-WIM/src/proto.cpp | 1356 ++++++------ protocols/ICQ-WIM/src/proto.h | 988 ++++----- protocols/ICQ-WIM/src/server.cpp | 2440 ++++++++++----------- protocols/ICQ-WIM/src/stdafx.cxx | 36 +- protocols/ICQ-WIM/src/stdafx.h | 218 +- protocols/ICQ-WIM/src/userinfo.cpp | 128 +- protocols/ICQ-WIM/src/utils.cpp | 792 +++---- protocols/ICQ-WIM/src/version.h | 2 +- protocols/ICQCorp/src/stdafx.cxx | 36 +- protocols/ICQCorp/src/version.h | 2 +- protocols/IRCG/src/stdafx.cxx | 2 +- protocols/IRCG/src/version.h | 2 +- protocols/JabberG/src/jabber.cpp | 2 +- protocols/JabberG/src/jabber_adhoc.cpp | 2 +- protocols/JabberG/src/jabber_agent.cpp | 2 +- protocols/JabberG/src/jabber_api.cpp | 2 +- protocols/JabberG/src/jabber_archive.cpp | 2 +- protocols/JabberG/src/jabber_bookmarks.cpp | 2 +- protocols/JabberG/src/jabber_byte.cpp | 2 +- protocols/JabberG/src/jabber_byte.h | 2 +- protocols/JabberG/src/jabber_caps.cpp | 2 +- protocols/JabberG/src/jabber_caps.h | 2 +- protocols/JabberG/src/jabber_captcha.cpp | 2 +- protocols/JabberG/src/jabber_chat.cpp | 2 +- protocols/JabberG/src/jabber_console.cpp | 2 +- protocols/JabberG/src/jabber_disco.cpp | 2 +- protocols/JabberG/src/jabber_disco.h | 2 +- protocols/JabberG/src/jabber_events.cpp | 2 +- protocols/JabberG/src/jabber_file.cpp | 2 +- protocols/JabberG/src/jabber_form.cpp | 2 +- protocols/JabberG/src/jabber_ft.cpp | 2 +- protocols/JabberG/src/jabber_groupchat.cpp | 2 +- protocols/JabberG/src/jabber_ibb.cpp | 2 +- protocols/JabberG/src/jabber_ibb.h | 2 +- protocols/JabberG/src/jabber_icolib.cpp | 2 +- protocols/JabberG/src/jabber_icolib.h | 2 +- protocols/JabberG/src/jabber_iq.cpp | 2 +- protocols/JabberG/src/jabber_iq.h | 2 +- protocols/JabberG/src/jabber_iq_handlers.cpp | 2 +- protocols/JabberG/src/jabber_iqid.cpp | 2 +- protocols/JabberG/src/jabber_iqid_muc.cpp | 2 +- protocols/JabberG/src/jabber_libstr.cpp | 2 +- protocols/JabberG/src/jabber_list.cpp | 2 +- protocols/JabberG/src/jabber_list.h | 2 +- protocols/JabberG/src/jabber_mam.cpp | 320 +-- protocols/JabberG/src/jabber_menu.cpp | 2 +- protocols/JabberG/src/jabber_message_handlers.cpp | 2 +- protocols/JabberG/src/jabber_message_manager.cpp | 2 +- protocols/JabberG/src/jabber_message_manager.h | 2 +- protocols/JabberG/src/jabber_misc.cpp | 2 +- protocols/JabberG/src/jabber_notes.cpp | 2 +- protocols/JabberG/src/jabber_notes.h | 2 +- protocols/JabberG/src/jabber_omemo.cpp | 2 +- protocols/JabberG/src/jabber_omemo.h | 2 +- protocols/JabberG/src/jabber_opt.cpp | 2 +- protocols/JabberG/src/jabber_password.cpp | 2 +- protocols/JabberG/src/jabber_presence_manager.cpp | 2 +- protocols/JabberG/src/jabber_presence_manager.h | 2 +- protocols/JabberG/src/jabber_privacy.cpp | 2 +- protocols/JabberG/src/jabber_privacy.h | 2 +- protocols/JabberG/src/jabber_proto.cpp | 2 +- protocols/JabberG/src/jabber_proto.h | 2 +- protocols/JabberG/src/jabber_rc.cpp | 2 +- protocols/JabberG/src/jabber_rc.h | 2 +- protocols/JabberG/src/jabber_roster.cpp | 1102 +++++----- protocols/JabberG/src/jabber_search.cpp | 1530 ++++++------- protocols/JabberG/src/jabber_search.h | 2 +- protocols/JabberG/src/jabber_secur.cpp | 2 +- protocols/JabberG/src/jabber_secur.h | 2 +- protocols/JabberG/src/jabber_send_manager.cpp | 2 +- protocols/JabberG/src/jabber_send_manager.h | 2 +- protocols/JabberG/src/jabber_strm_mgmt.cpp | 2 +- protocols/JabberG/src/jabber_strm_mgmt.h | 2 +- protocols/JabberG/src/jabber_svc.cpp | 2 +- protocols/JabberG/src/jabber_thread.cpp | 2 +- protocols/JabberG/src/jabber_treelist.cpp | 2 +- protocols/JabberG/src/jabber_userinfo.cpp | 1824 +++++++-------- protocols/JabberG/src/jabber_util.cpp | 2 +- protocols/JabberG/src/jabber_vcard.cpp | 2 +- protocols/JabberG/src/jabber_xml.cpp | 2 +- protocols/JabberG/src/jabber_xml.h | 2 +- protocols/JabberG/src/jabber_xstatus.cpp | 2 +- protocols/JabberG/src/jabber_xstatus.h | 2 +- protocols/JabberG/src/jabber_zstream.cpp | 2 +- protocols/JabberG/src/stdafx.cxx | 2 +- protocols/JabberG/src/stdafx.h | 2 +- protocols/JabberG/src/version.h | 2 +- protocols/LotusNotify/src/stdafx.cxx | 34 +- protocols/MinecraftDynmap/src/chat.cpp | 368 ++-- protocols/MinecraftDynmap/src/communication.cpp | 884 ++++---- protocols/MinecraftDynmap/src/constants.h | 92 +- protocols/MinecraftDynmap/src/dialogs.cpp | 196 +- protocols/MinecraftDynmap/src/dialogs.h | 54 +- protocols/MinecraftDynmap/src/main.cpp | 180 +- protocols/MinecraftDynmap/src/proto.cpp | 360 +-- protocols/MinecraftDynmap/src/proto.h | 272 +-- protocols/MinecraftDynmap/src/stdafx.cxx | 34 +- protocols/MinecraftDynmap/src/stdafx.h | 140 +- protocols/MinecraftDynmap/src/utils.h | 126 +- protocols/MinecraftDynmap/src/version.h | 2 +- protocols/NewsAggregator/Src/stdafx.cxx | 34 +- protocols/NewsAggregator/Src/version.h | 2 +- protocols/Non-IM Contact/src/stdafx.cxx | 34 +- protocols/Omegle/src/chat.cpp | 2 +- protocols/Omegle/src/client.h | 2 +- protocols/Omegle/src/communication.cpp | 1524 ++++++------- protocols/Omegle/src/connection.cpp | 2 +- protocols/Omegle/src/constants.h | 2 +- protocols/Omegle/src/db.h | 2 +- protocols/Omegle/src/dialogs.cpp | 2 +- protocols/Omegle/src/dialogs.h | 2 +- protocols/Omegle/src/http.cpp | 2 +- protocols/Omegle/src/http.h | 2 +- protocols/Omegle/src/main.cpp | 2 +- protocols/Omegle/src/messages.cpp | 148 +- protocols/Omegle/src/proto.cpp | 2 +- protocols/Omegle/src/proto.h | 2 +- protocols/Omegle/src/stdafx.cxx | 2 +- protocols/Omegle/src/stdafx.h | 2 +- protocols/Omegle/src/theme.cpp | 2 +- protocols/Omegle/src/theme.h | 2 +- protocols/Omegle/src/version.h | 2 +- protocols/Sametime/src/version.h | 2 +- protocols/SkypeWeb/src/main.cpp | 2 +- protocols/SkypeWeb/src/request_queue.cpp | 2 +- protocols/SkypeWeb/src/requests/avatars.h | 2 +- protocols/SkypeWeb/src/requests/capabilities.h | 2 +- protocols/SkypeWeb/src/requests/chatrooms.h | 2 +- protocols/SkypeWeb/src/requests/contacts.h | 2 +- protocols/SkypeWeb/src/requests/endpoint.h | 2 +- protocols/SkypeWeb/src/requests/history.h | 2 +- protocols/SkypeWeb/src/requests/login.h | 2 +- protocols/SkypeWeb/src/requests/messages.h | 2 +- protocols/SkypeWeb/src/requests/oauth.h | 148 +- protocols/SkypeWeb/src/requests/poll.h | 2 +- protocols/SkypeWeb/src/requests/profile.h | 2 +- protocols/SkypeWeb/src/requests/search.h | 2 +- protocols/SkypeWeb/src/requests/status.h | 2 +- protocols/SkypeWeb/src/requests/subscriptions.h | 2 +- protocols/SkypeWeb/src/skype_avatars.cpp | 2 +- protocols/SkypeWeb/src/skype_chatrooms.cpp | 2 +- protocols/SkypeWeb/src/skype_contacts.cpp | 2 +- protocols/SkypeWeb/src/skype_db.cpp | 238 +- protocols/SkypeWeb/src/skype_db.h | 2 +- protocols/SkypeWeb/src/skype_events.cpp | 2 +- protocols/SkypeWeb/src/skype_history_sync.cpp | 2 +- protocols/SkypeWeb/src/skype_icons.cpp | 2 +- protocols/SkypeWeb/src/skype_login.cpp | 2 +- protocols/SkypeWeb/src/skype_menus.cpp | 2 +- protocols/SkypeWeb/src/skype_menus.h | 2 +- protocols/SkypeWeb/src/skype_messages.cpp | 2 +- protocols/SkypeWeb/src/skype_oauth.cpp | 330 +-- protocols/SkypeWeb/src/skype_options.cpp | 2 +- protocols/SkypeWeb/src/skype_polling.cpp | 2 +- protocols/SkypeWeb/src/skype_profile.cpp | 2 +- protocols/SkypeWeb/src/skype_proto.cpp | 686 +++--- protocols/SkypeWeb/src/skype_proto.h | 2 +- protocols/SkypeWeb/src/skype_search.cpp | 2 +- protocols/SkypeWeb/src/skype_trouter.cpp | 2 +- protocols/SkypeWeb/src/skype_utils.cpp | 1358 ++++++------ protocols/SkypeWeb/src/skype_utils.h | 2 +- protocols/SkypeWeb/src/stdafx.cxx | 2 +- protocols/SkypeWeb/src/stdafx.h | 2 +- protocols/SkypeWeb/src/version.h | 2 +- protocols/Steam/src/stdafx.cxx | 2 +- protocols/Steam/src/version.h | 2 +- protocols/Telegram/src/auth.cpp | 2 +- protocols/Telegram/src/options.cpp | 2 +- protocols/Telegram/src/server.cpp | 2 +- protocols/Telegram/src/stdafx.cxx | 2 +- protocols/Telegram/src/utils.cpp | 2 +- protocols/Telegram/src/version.h | 2 +- protocols/Tox/src/stdafx.cxx | 2 +- protocols/Tox/src/version.h | 2 +- protocols/Twitter/src/StringUtil.cpp | 2 +- protocols/Twitter/src/chat.cpp | 2 +- protocols/Twitter/src/connection.cpp | 2 +- protocols/Twitter/src/contacts.cpp | 2 +- protocols/Twitter/src/http.h | 2 +- protocols/Twitter/src/main.cpp | 2 +- protocols/Twitter/src/oauth.cpp | 2 +- protocols/Twitter/src/proto.cpp | 2 +- protocols/Twitter/src/proto.h | 2 +- protocols/Twitter/src/stdafx.cxx | 2 +- protocols/Twitter/src/theme.cpp | 2 +- protocols/Twitter/src/theme.h | 2 +- protocols/Twitter/src/twitter.cpp | 2 +- protocols/Twitter/src/ui.cpp | 2 +- protocols/Twitter/src/ui.h | 2 +- protocols/Twitter/src/utility.cpp | 2 +- protocols/Twitter/src/utility.h | 2 +- protocols/Twitter/src/version.h | 2 +- protocols/VKontakte/src/main.cpp | 2 +- protocols/VKontakte/src/misc.cpp | 2 +- protocols/VKontakte/src/stdafx.cxx | 2 +- protocols/VKontakte/src/stdafx.h | 2 +- protocols/VKontakte/src/version.h | 2 +- protocols/VKontakte/src/vk.h | 2 +- protocols/VKontakte/src/vk_avatars.cpp | 2 +- protocols/VKontakte/src/vk_captcha.cpp | 2 +- protocols/VKontakte/src/vk_chats.cpp | 2 +- protocols/VKontakte/src/vk_dialogs.cpp | 2 +- protocols/VKontakte/src/vk_dialogs.h | 2 +- protocols/VKontakte/src/vk_feed.cpp | 2 +- protocols/VKontakte/src/vk_files.cpp | 2 +- protocols/VKontakte/src/vk_history.cpp | 2 +- protocols/VKontakte/src/vk_messages.cpp | 2 +- protocols/VKontakte/src/vk_options.cpp | 2 +- protocols/VKontakte/src/vk_options.h | 2 +- protocols/VKontakte/src/vk_pollserver.cpp | 2 +- protocols/VKontakte/src/vk_proto.cpp | 2 +- protocols/VKontakte/src/vk_proto.h | 2 +- protocols/VKontakte/src/vk_queue.cpp | 2 +- protocols/VKontakte/src/vk_search.cpp | 2 +- protocols/VKontakte/src/vk_status.cpp | 2 +- protocols/VKontakte/src/vk_struct.cpp | 2 +- protocols/VKontakte/src/vk_struct.h | 2 +- protocols/VKontakte/src/vk_thread.cpp | 2 +- protocols/VKontakte/src/vk_wallpost.cpp | 2 +- protocols/VKontakte/src/vkjs.js | 2 +- protocols/Weather/src/stdafx.cxx | 34 +- protocols/Weather/src/stdafx.h | 1094 ++++----- protocols/Weather/src/version.h | 2 +- protocols/WebView/src/main.cpp | 548 ++--- protocols/WebView/src/stdafx.cxx | 34 +- protocols/WhatsApp/src/appsync.cpp | 2 +- protocols/WhatsApp/src/avatars.cpp | 338 +-- protocols/WhatsApp/src/chats.cpp | 2 +- protocols/WhatsApp/src/crypt.cpp | 354 +-- protocols/WhatsApp/src/db.h | 86 +- protocols/WhatsApp/src/dicts.h | 388 ++-- protocols/WhatsApp/src/iq.cpp | 1140 +++++----- protocols/WhatsApp/src/main.cpp | 142 +- protocols/WhatsApp/src/message.cpp | 1004 ++++----- protocols/WhatsApp/src/noise.cpp | 402 ++-- protocols/WhatsApp/src/options.cpp | 190 +- protocols/WhatsApp/src/proto.cpp | 2 +- protocols/WhatsApp/src/proto.h | 2 +- protocols/WhatsApp/src/qrcode.cpp | 274 +-- protocols/WhatsApp/src/server.cpp | 2 +- protocols/WhatsApp/src/signal.cpp | 1576 ++++++------- protocols/WhatsApp/src/stdafx.cxx | 16 +- protocols/WhatsApp/src/stdafx.h | 144 +- protocols/WhatsApp/src/utils.cpp | 1178 +++++----- protocols/WhatsApp/src/utils.h | 534 ++--- protocols/WhatsApp/src/version.h | 2 +- protocols/WhatsApp/src/wanode.cpp | 1216 +++++----- protocols/YAMN/src/stdafx.cxx | 34 +- 287 files changed, 20728 insertions(+), 20728 deletions(-) (limited to 'protocols') diff --git a/protocols/CloudFile/src/stdafx.cxx b/protocols/CloudFile/src/stdafx.cxx index 090527701a..3f8c5724b4 100644 --- a/protocols/CloudFile/src/stdafx.cxx +++ b/protocols/CloudFile/src/stdafx.cxx @@ -1,20 +1,20 @@ -/* -Copyright (C) 2012-22 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 . -*/ - -#include "stdafx.h" - +/* +Copyright (C) 2012-23 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 . +*/ + +#include "stdafx.h" + ULONG FileTransferParam::hFileProcess = 1; \ No newline at end of file diff --git a/protocols/CloudFile/src/version.h b/protocols/CloudFile/src/version.h index a06ae42f39..f1891030a5 100644 --- a/protocols/CloudFile/src/version.h +++ b/protocols/CloudFile/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Allows you to transfer files via cloud services." #define __AUTHOR "Miranda NG team" #define __AUTHORWEB "https://miranda-ng.org/p/CloudFile" -#define __COPYRIGHT "© 2017-22 Miranda NG team" +#define __COPYRIGHT "© 2017-23 Miranda NG team" diff --git a/protocols/ConnectionNotify/src/stdafx.cxx b/protocols/ConnectionNotify/src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/ConnectionNotify/src/stdafx.cxx +++ b/protocols/ConnectionNotify/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/CurrencyRates/src/stdafx.cxx b/protocols/CurrencyRates/src/stdafx.cxx index d265a4c02e..8c570f6949 100644 --- a/protocols/CurrencyRates/src/stdafx.cxx +++ b/protocols/CurrencyRates/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - -#include "stdafx.h" +/* +Copyright (C) 2012-23 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 . +*/ + +#include "stdafx.h" diff --git a/protocols/Dummy/src/dummy.h b/protocols/Dummy/src/dummy.h index 14b4e4ea44..12071af068 100644 --- a/protocols/Dummy/src/dummy.h +++ b/protocols/Dummy/src/dummy.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2014-17 Robert Pösel, 2017-22 Miranda NG team +Copyright (c) 2014-17 Robert Pösel, 2017-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/Dummy/src/dummy_options.cpp b/protocols/Dummy/src/dummy_options.cpp index 4369bfe9df..3ceecc0225 100644 --- a/protocols/Dummy/src/dummy_options.cpp +++ b/protocols/Dummy/src/dummy_options.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2014-17 Robert Pösel, 2017-22 Miranda NG team +Copyright (c) 2014-17 Robert Pösel, 2017-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/Dummy/src/dummy_proto.cpp b/protocols/Dummy/src/dummy_proto.cpp index fe075aab59..7ec308eb3e 100644 --- a/protocols/Dummy/src/dummy_proto.cpp +++ b/protocols/Dummy/src/dummy_proto.cpp @@ -1,188 +1,188 @@ -/* -Copyright (c) 2014-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation 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 . -*/ - -#include "stdafx.h" - -const ttemplate templates[DUMMY_PROTO_COUNT] = -{ - { LPGEN("Custom"), "", "" }, - { "AIM", "SN", LPGEN("Screen name") }, - { "Discord", "id", LPGEN("Discord ID") }, - { "EmLAN", "Nick", LPGEN("User name") }, - { "Facebook", "ID", LPGEN("Facebook ID") }, - { "GG", "UIN", LPGEN("Gadu-Gadu number") }, - { "ICQ", "UIN", LPGEN("User ID") }, - { "ICQCorp", "UIN", LPGEN("ICQ number") }, - { "IRC", "Nick", LPGEN("Nickname") }, - { "Jabber", "jid", LPGEN("JID") }, - { "MinecraftDynmap", "Nick", LPGEN("Visible name") }, - { "MRA", "e-mail", LPGEN("E-mail address") }, - { "MSN", "wlid", LPGEN("Live ID") }, - { "Omegle", "nick", LPGEN("Visible name") }, - { "Sametime", "stid", LPGEN("ID") }, - { "Skype (SkypeKit)", "sid", LPGEN("Skype name") }, - { "Skype (Classic)", "Username", LPGEN("Skype name") }, - { "Skype (Web)", "Username", LPGEN("Skype name") }, - { "Steam", "SteamID", LPGEN("Steam ID") }, - { "Tlen", "jid", LPGEN("Tlen login") }, - { "Tox", "ToxID", LPGEN("Tox ID") }, - { "Twitter", "Username", LPGEN("Username") }, - { "VK", "ID", LPGEN("VKontakte ID") }, - { "WhatsApp", "ID", LPGEN("WhatsApp ID") }, - { "XFire", "Username", LPGEN("Username") }, - { "Yahoo", "yahoo_id", LPGEN("ID") }, -}; - -void CDummyProto::SearchIdAckThread(void *targ) -{ - PROTOSEARCHRESULT psr = { 0 }; - psr.cbSize = sizeof(psr); - psr.flags = PSR_UNICODE; - psr.id.w = (wchar_t*)targ; - ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_DATA, targ, (LPARAM)&psr); - - ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, targ, 0); - - mir_free(targ); -} - -static int sttCompareProtocols(const CDummyProto *p1, const CDummyProto *p2) -{ - return mir_wstrcmp(p1->m_tszUserName, p2->m_tszUserName); -} - -CDummyProto::CDummyProto(const char *szModuleName, const wchar_t *ptszUserName) : - PROTO(szModuleName, ptszUserName) -{ - CreateProtoService(PS_CREATEACCMGRUI, &CDummyProto::SvcCreateAccMgrUI); - - msgid = 0; - - int id = getTemplateId(); - ptrA setting(id > 0 ? mir_strdup(templates[id].setting) : getStringA(DUMMY_ID_SETTING)); - if (setting != NULL) { - strncpy_s(uniqueIdText, setting, _TRUNCATE); - Proto_SetUniqueId(m_szModuleName, uniqueIdText); - } - else uniqueIdText[0] = '\0'; - - uniqueIdSetting[0] = '\0'; -} - -CDummyProto::~CDummyProto() -{ -} - -////////////////////////////////////////////////////////////////////////////// - -int CDummyProto::getTemplateId() -{ - int id = this->getByte(DUMMY_ID_TEMPLATE, -1); - if (id >= 0 && id < _countof(templates)) - return id; - - CMStringA szProto(getMStringA("AM_BaseProto")); - for (auto &it : templates) - if (!stricmp(it.name, szProto)) - return int(&it - templates); - - return 0; -} - -INT_PTR CDummyProto::GetCaps(int type, MCONTACT) -{ - switch(type) { - case PFLAGNUM_1: - return PF1_IM | PF1_BASICSEARCH | PF1_ADDSEARCHRES; - - case PFLAGNUM_2: - return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_FREECHAT; - - case PFLAGNUM_3: - return 0; - - case PFLAGNUM_4: - return PF4_AVATARS | PF4_NOAUTHDENYREASON | PF4_NOCUSTOMAUTH; - - case PFLAGNUM_5: - return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_FREECHAT; - - case PFLAG_MAXLENOFMESSAGE: - return 0; - - case PFLAG_UNIQUEIDTEXT: - if (uniqueIdSetting[0] == '\0') { - int id = getTemplateId(); - ptrW setting(id > 0 ? mir_a2u(Translate(templates[id].text)) : getWStringA(DUMMY_ID_TEXT)); - if (setting != NULL) - wcsncpy_s(uniqueIdSetting, setting, _TRUNCATE); - } - return (INT_PTR)uniqueIdSetting; - } - return 0; -} - -////////////////////////////////////////////////////////////////////////////// - -int CDummyProto::SendMsg(MCONTACT hContact, int, const char *msg) -{ - std::string message = msg; - unsigned int id = InterlockedIncrement(&this->msgid); - - if (getByte(DUMMY_KEY_ALLOW_SENDING, 0)) - ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)id); - else - ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)id, (LPARAM)TranslateT("This Dummy account has disabled sending messages. Enable it in account options.")); - return id; -} - -int CDummyProto::SetStatus(int) -{ - return 0; -} - -HANDLE CDummyProto::SearchBasic(const wchar_t* id) -{ - if (uniqueIdSetting[0] == '\0') - return nullptr; - - wchar_t *tid = mir_wstrdup(id); - ForkThread(&CDummyProto::SearchIdAckThread, tid); - return tid; -} - -MCONTACT CDummyProto::AddToList(int flags, PROTOSEARCHRESULT* psr) -{ - if (psr->id.w == nullptr) - return NULL; - - MCONTACT hContact = db_add_contact(); - Proto_AddToContact(hContact, m_szModuleName); - - if (flags & PALF_TEMPORARY) { - Contact::Hide(hContact); - Contact::RemoveFromList(hContact); - } - else if (!Contact::OnList(hContact)) { - Contact::Hide(hContact, false); - Contact::PutOnList(hContact); - } - setWString(hContact, _T2A(uniqueIdSetting), psr->id.w); - setWString(hContact, "Nick", psr->id.w); - - return hContact; -} +/* +Copyright (c) 2014-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation 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 . +*/ + +#include "stdafx.h" + +const ttemplate templates[DUMMY_PROTO_COUNT] = +{ + { LPGEN("Custom"), "", "" }, + { "AIM", "SN", LPGEN("Screen name") }, + { "Discord", "id", LPGEN("Discord ID") }, + { "EmLAN", "Nick", LPGEN("User name") }, + { "Facebook", "ID", LPGEN("Facebook ID") }, + { "GG", "UIN", LPGEN("Gadu-Gadu number") }, + { "ICQ", "UIN", LPGEN("User ID") }, + { "ICQCorp", "UIN", LPGEN("ICQ number") }, + { "IRC", "Nick", LPGEN("Nickname") }, + { "Jabber", "jid", LPGEN("JID") }, + { "MinecraftDynmap", "Nick", LPGEN("Visible name") }, + { "MRA", "e-mail", LPGEN("E-mail address") }, + { "MSN", "wlid", LPGEN("Live ID") }, + { "Omegle", "nick", LPGEN("Visible name") }, + { "Sametime", "stid", LPGEN("ID") }, + { "Skype (SkypeKit)", "sid", LPGEN("Skype name") }, + { "Skype (Classic)", "Username", LPGEN("Skype name") }, + { "Skype (Web)", "Username", LPGEN("Skype name") }, + { "Steam", "SteamID", LPGEN("Steam ID") }, + { "Tlen", "jid", LPGEN("Tlen login") }, + { "Tox", "ToxID", LPGEN("Tox ID") }, + { "Twitter", "Username", LPGEN("Username") }, + { "VK", "ID", LPGEN("VKontakte ID") }, + { "WhatsApp", "ID", LPGEN("WhatsApp ID") }, + { "XFire", "Username", LPGEN("Username") }, + { "Yahoo", "yahoo_id", LPGEN("ID") }, +}; + +void CDummyProto::SearchIdAckThread(void *targ) +{ + PROTOSEARCHRESULT psr = { 0 }; + psr.cbSize = sizeof(psr); + psr.flags = PSR_UNICODE; + psr.id.w = (wchar_t*)targ; + ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_DATA, targ, (LPARAM)&psr); + + ProtoBroadcastAck(NULL, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, targ, 0); + + mir_free(targ); +} + +static int sttCompareProtocols(const CDummyProto *p1, const CDummyProto *p2) +{ + return mir_wstrcmp(p1->m_tszUserName, p2->m_tszUserName); +} + +CDummyProto::CDummyProto(const char *szModuleName, const wchar_t *ptszUserName) : + PROTO(szModuleName, ptszUserName) +{ + CreateProtoService(PS_CREATEACCMGRUI, &CDummyProto::SvcCreateAccMgrUI); + + msgid = 0; + + int id = getTemplateId(); + ptrA setting(id > 0 ? mir_strdup(templates[id].setting) : getStringA(DUMMY_ID_SETTING)); + if (setting != NULL) { + strncpy_s(uniqueIdText, setting, _TRUNCATE); + Proto_SetUniqueId(m_szModuleName, uniqueIdText); + } + else uniqueIdText[0] = '\0'; + + uniqueIdSetting[0] = '\0'; +} + +CDummyProto::~CDummyProto() +{ +} + +////////////////////////////////////////////////////////////////////////////// + +int CDummyProto::getTemplateId() +{ + int id = this->getByte(DUMMY_ID_TEMPLATE, -1); + if (id >= 0 && id < _countof(templates)) + return id; + + CMStringA szProto(getMStringA("AM_BaseProto")); + for (auto &it : templates) + if (!stricmp(it.name, szProto)) + return int(&it - templates); + + return 0; +} + +INT_PTR CDummyProto::GetCaps(int type, MCONTACT) +{ + switch(type) { + case PFLAGNUM_1: + return PF1_IM | PF1_BASICSEARCH | PF1_ADDSEARCHRES; + + case PFLAGNUM_2: + return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_FREECHAT; + + case PFLAGNUM_3: + return 0; + + case PFLAGNUM_4: + return PF4_AVATARS | PF4_NOAUTHDENYREASON | PF4_NOCUSTOMAUTH; + + case PFLAGNUM_5: + return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_FREECHAT; + + case PFLAG_MAXLENOFMESSAGE: + return 0; + + case PFLAG_UNIQUEIDTEXT: + if (uniqueIdSetting[0] == '\0') { + int id = getTemplateId(); + ptrW setting(id > 0 ? mir_a2u(Translate(templates[id].text)) : getWStringA(DUMMY_ID_TEXT)); + if (setting != NULL) + wcsncpy_s(uniqueIdSetting, setting, _TRUNCATE); + } + return (INT_PTR)uniqueIdSetting; + } + return 0; +} + +////////////////////////////////////////////////////////////////////////////// + +int CDummyProto::SendMsg(MCONTACT hContact, int, const char *msg) +{ + std::string message = msg; + unsigned int id = InterlockedIncrement(&this->msgid); + + if (getByte(DUMMY_KEY_ALLOW_SENDING, 0)) + ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)id); + else + ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)id, (LPARAM)TranslateT("This Dummy account has disabled sending messages. Enable it in account options.")); + return id; +} + +int CDummyProto::SetStatus(int) +{ + return 0; +} + +HANDLE CDummyProto::SearchBasic(const wchar_t* id) +{ + if (uniqueIdSetting[0] == '\0') + return nullptr; + + wchar_t *tid = mir_wstrdup(id); + ForkThread(&CDummyProto::SearchIdAckThread, tid); + return tid; +} + +MCONTACT CDummyProto::AddToList(int flags, PROTOSEARCHRESULT* psr) +{ + if (psr->id.w == nullptr) + return NULL; + + MCONTACT hContact = db_add_contact(); + Proto_AddToContact(hContact, m_szModuleName); + + if (flags & PALF_TEMPORARY) { + Contact::Hide(hContact); + Contact::RemoveFromList(hContact); + } + else if (!Contact::OnList(hContact)) { + Contact::Hide(hContact, false); + Contact::PutOnList(hContact); + } + setWString(hContact, _T2A(uniqueIdSetting), psr->id.w); + setWString(hContact, "Nick", psr->id.w); + + return hContact; +} diff --git a/protocols/Dummy/src/dummy_proto.h b/protocols/Dummy/src/dummy_proto.h index b5a902cbc6..ffe2d4b9fc 100644 --- a/protocols/Dummy/src/dummy_proto.h +++ b/protocols/Dummy/src/dummy_proto.h @@ -1,53 +1,53 @@ -/* -Copyright (c) 2014-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation 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 . -*/ - -#pragma once - -struct CDummyProto; - -struct CDummyProto : public PROTO -{ - CDummyProto(const char*, const wchar_t*); - ~CDummyProto(); - - //==================================================================================== - // PROTO_INTERFACE - //==================================================================================== - - INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; - - int SendMsg(MCONTACT hContact, int flags, const char* msg) override; - - int SetStatus(int iNewStatus) override; - - HANDLE SearchBasic(const wchar_t* id) override; - - MCONTACT AddToList(int flags, PROTOSEARCHRESULT* psr) override; - - //==== Services ====================================================================== - - INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); - - void __cdecl SearchIdAckThread(void*); - - char uniqueIdText[100]; - wchar_t uniqueIdSetting[100]; - - int getTemplateId(); - - volatile unsigned int msgid; -}; +/* +Copyright (c) 2014-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation 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 . +*/ + +#pragma once + +struct CDummyProto; + +struct CDummyProto : public PROTO +{ + CDummyProto(const char*, const wchar_t*); + ~CDummyProto(); + + //==================================================================================== + // PROTO_INTERFACE + //==================================================================================== + + INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; + + int SendMsg(MCONTACT hContact, int flags, const char* msg) override; + + int SetStatus(int iNewStatus) override; + + HANDLE SearchBasic(const wchar_t* id) override; + + MCONTACT AddToList(int flags, PROTOSEARCHRESULT* psr) override; + + //==== Services ====================================================================== + + INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); + + void __cdecl SearchIdAckThread(void*); + + char uniqueIdText[100]; + wchar_t uniqueIdSetting[100]; + + int getTemplateId(); + + volatile unsigned int msgid; +}; diff --git a/protocols/Dummy/src/main.cpp b/protocols/Dummy/src/main.cpp index acb41872b4..950b4d953c 100644 --- a/protocols/Dummy/src/main.cpp +++ b/protocols/Dummy/src/main.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2014-17 Robert Pösel, 2017-22 Miranda NG team +Copyright (c) 2014-17 Robert Pösel, 2017-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/Dummy/src/stdafx.cxx b/protocols/Dummy/src/stdafx.cxx index 4f38935a64..23ad62d26a 100644 --- a/protocols/Dummy/src/stdafx.cxx +++ b/protocols/Dummy/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (c) 2014-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation 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 . -*/ - -#include "stdafx.h" +/* +Copyright (c) 2014-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation 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 . +*/ + +#include "stdafx.h" diff --git a/protocols/Dummy/src/stdafx.h b/protocols/Dummy/src/stdafx.h index ca2b5e2f57..881c816a4f 100644 --- a/protocols/Dummy/src/stdafx.h +++ b/protocols/Dummy/src/stdafx.h @@ -1,60 +1,60 @@ -/* -Copyright (c) 2014-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation 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 . -*/ - -#pragma once - -#include -#include -#include - -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "resource.h" -#include "dummy.h" -#include "dummy_proto.h" +/* +Copyright (c) 2014-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation 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 . +*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "resource.h" +#include "dummy.h" +#include "dummy_proto.h" diff --git a/protocols/Dummy/src/version.h b/protocols/Dummy/src/version.h index 83a6d2faa9..89cf1ae1c4 100644 --- a/protocols/Dummy/src/version.h +++ b/protocols/Dummy/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Dummy protocol for Miranda NG. Could be used for holding contacts and history from deprecated protocols or for creating virtual contacts." #define __AUTHOR "Robert Pösel" #define __AUTHORWEB "https://miranda-ng.org/p/Dummy" -#define __COPYRIGHT "© 2014-17 Robert Pösel, 2017-22 Miranda NG team" +#define __COPYRIGHT "© 2014-17 Robert Pösel, 2017-23 Miranda NG team" diff --git a/protocols/Facebook/src/avatars.cpp b/protocols/Facebook/src/avatars.cpp index 0ced4b1b25..0e73a9241b 100644 --- a/protocols/Facebook/src/avatars.cpp +++ b/protocols/Facebook/src/avatars.cpp @@ -1,131 +1,131 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -void FacebookProto::GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName) -{ - wchar_t wszPath[MAX_PATH]; - mir_snwprintf(wszPath, MAX_PATH, L"%s\\%S", VARSW(L"%miranda_avatarcache%"), m_szModuleName); - CreateDirectoryTreeW(wszPath); - - CMStringW id(getMStringW(hContact, DBKEY_ID)); - mir_snwprintf(pwszFileName, MAX_PATH, L"%s\\%s.jpg", wszPath, id.c_str()); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void __cdecl FacebookProto::AvatarsUpdate(void *) -{ - NETLIBHTTPREQUEST req = {}; - req.cbSize = sizeof(req); - req.flags = NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; - req.requestType = REQUEST_GET; - - CMStringA szParams((m_bUseBigAvatars) ? "type=large" : "type=normal"); - szParams.AppendFormat("&access_token=%s", m_szAuthToken.c_str()); - - for (auto &cc : AccContacts()) { - if (Miranda_IsTerminated()) - break; - - if (!getByte(cc, "UpdateNeeded")) - continue; - - delSetting(cc, "UpdateNeeded"); - - CMStringA szUrl(FORMAT, "https://graph.facebook.com/%s/picture?%s", getMStringA(cc, DBKEY_ID).c_str(), szParams.c_str()); - req.szUrl = szUrl.GetBuffer(); - - NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(m_hNetlibUser, &req); - if (pReply == nullptr) { - debugLogA("Failed to retrieve avatar from url: %s", szUrl.c_str()); - continue; - } - - PROTO_AVATAR_INFORMATION ai; - ai.hContact = cc; - ai.format = PA_FORMAT_UNKNOWN; - GetAvatarFilename(cc, ai.filename); - - bool bSuccess = false; - if (pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { - if (auto *pszHdr = Netlib_GetHeader(pReply, "Content-Type")) - ai.format = ProtoGetAvatarFormatByMimeType(pszHdr); - - if (ai.format != PA_FORMAT_UNKNOWN) { - FILE *fout = _wfopen(ai.filename, L"wb"); - if (fout) { - fwrite(pReply->pData, 1, pReply->dataLength, fout); - fclose(fout); - bSuccess = true; - } - else debugLogA("Error saving avatar to file %S", ai.filename); - } - else debugLogA("unknown avatar mime type"); - } - else debugLogA("Error %d reading avatar from url: %s", pReply->resultCode, szUrl.c_str()); - - ProtoBroadcastAck(cc, ACKTYPE_AVATAR, bSuccess ? ACKRESULT_SUCCESS : ACKRESULT_FAILED, (HANDLE)&ai); - - Netlib_FreeHttpRequest(pReply); - } -} - -INT_PTR FacebookProto::GetAvatarInfo(WPARAM flags, LPARAM lParam) -{ - PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam; - GetAvatarFilename(pai->hContact, pai->filename); - - bool bFileExist = _waccess(pai->filename, 0) == 0; - - // if we still need to load an avatar - if ((flags & GAIF_FORCE) || !bFileExist) { - setByte(pai->hContact, "UpdateNeeded", 1); - ForkThread(&FacebookProto::AvatarsUpdate); - return GAIR_WAITFOR; - } - - return (bFileExist) ? GAIR_SUCCESS : GAIR_NOAVATAR; -} - -INT_PTR FacebookProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) -{ - int res = 0; - - switch (wParam) { - case AF_MAXSIZE: - ((POINT *)lParam)->x = ((POINT *)lParam)->y = 128; - break; - - case AF_FORMATSUPPORTED: - res = lParam == PA_FORMAT_PNG || lParam == PA_FORMAT_GIF || lParam == PA_FORMAT_JPEG; - break; - - case AF_ENABLED: - case AF_FETCHIFPROTONOTVISIBLE: - case AF_FETCHIFCONTACTOFFLINE: - return 1; - } - - return res; -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +void FacebookProto::GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName) +{ + wchar_t wszPath[MAX_PATH]; + mir_snwprintf(wszPath, MAX_PATH, L"%s\\%S", VARSW(L"%miranda_avatarcache%"), m_szModuleName); + CreateDirectoryTreeW(wszPath); + + CMStringW id(getMStringW(hContact, DBKEY_ID)); + mir_snwprintf(pwszFileName, MAX_PATH, L"%s\\%s.jpg", wszPath, id.c_str()); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void __cdecl FacebookProto::AvatarsUpdate(void *) +{ + NETLIBHTTPREQUEST req = {}; + req.cbSize = sizeof(req); + req.flags = NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; + req.requestType = REQUEST_GET; + + CMStringA szParams((m_bUseBigAvatars) ? "type=large" : "type=normal"); + szParams.AppendFormat("&access_token=%s", m_szAuthToken.c_str()); + + for (auto &cc : AccContacts()) { + if (Miranda_IsTerminated()) + break; + + if (!getByte(cc, "UpdateNeeded")) + continue; + + delSetting(cc, "UpdateNeeded"); + + CMStringA szUrl(FORMAT, "https://graph.facebook.com/%s/picture?%s", getMStringA(cc, DBKEY_ID).c_str(), szParams.c_str()); + req.szUrl = szUrl.GetBuffer(); + + NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(m_hNetlibUser, &req); + if (pReply == nullptr) { + debugLogA("Failed to retrieve avatar from url: %s", szUrl.c_str()); + continue; + } + + PROTO_AVATAR_INFORMATION ai; + ai.hContact = cc; + ai.format = PA_FORMAT_UNKNOWN; + GetAvatarFilename(cc, ai.filename); + + bool bSuccess = false; + if (pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { + if (auto *pszHdr = Netlib_GetHeader(pReply, "Content-Type")) + ai.format = ProtoGetAvatarFormatByMimeType(pszHdr); + + if (ai.format != PA_FORMAT_UNKNOWN) { + FILE *fout = _wfopen(ai.filename, L"wb"); + if (fout) { + fwrite(pReply->pData, 1, pReply->dataLength, fout); + fclose(fout); + bSuccess = true; + } + else debugLogA("Error saving avatar to file %S", ai.filename); + } + else debugLogA("unknown avatar mime type"); + } + else debugLogA("Error %d reading avatar from url: %s", pReply->resultCode, szUrl.c_str()); + + ProtoBroadcastAck(cc, ACKTYPE_AVATAR, bSuccess ? ACKRESULT_SUCCESS : ACKRESULT_FAILED, (HANDLE)&ai); + + Netlib_FreeHttpRequest(pReply); + } +} + +INT_PTR FacebookProto::GetAvatarInfo(WPARAM flags, LPARAM lParam) +{ + PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION *)lParam; + GetAvatarFilename(pai->hContact, pai->filename); + + bool bFileExist = _waccess(pai->filename, 0) == 0; + + // if we still need to load an avatar + if ((flags & GAIF_FORCE) || !bFileExist) { + setByte(pai->hContact, "UpdateNeeded", 1); + ForkThread(&FacebookProto::AvatarsUpdate); + return GAIR_WAITFOR; + } + + return (bFileExist) ? GAIR_SUCCESS : GAIR_NOAVATAR; +} + +INT_PTR FacebookProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + int res = 0; + + switch (wParam) { + case AF_MAXSIZE: + ((POINT *)lParam)->x = ((POINT *)lParam)->y = 128; + break; + + case AF_FORMATSUPPORTED: + res = lParam == PA_FORMAT_PNG || lParam == PA_FORMAT_GIF || lParam == PA_FORMAT_JPEG; + break; + + case AF_ENABLED: + case AF_FETCHIFPROTONOTVISIBLE: + case AF_FETCHIFCONTACTOFFLINE: + return 1; + } + + return res; +} diff --git a/protocols/Facebook/src/db.h b/protocols/Facebook/src/db.h index eb31b19ef0..8095cfad65 100644 --- a/protocols/Facebook/src/db.h +++ b/protocols/Facebook/src/db.h @@ -1,44 +1,44 @@ -/* - -Facebook plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -#define MODULENAME "Facebook" - -// Contact DB keys -#define DBKEY_LOGIN "Email" -#define DBKEY_ID "ID" -#define DBKEY_SID "SID" -#define DBKEY_NICK "Nick" -#define DBKEY_PASS "Password" -#define DBKEY_CLIENT_ID "ClientID" -#define DBKEY_DEVICE_ID "DeviceID" -#define DBKEY_AVATAR "Avatar" -#define DBKEY_CONTACT_TYPE "ContactType" -#define DBKEY_TOKEN "Token" -#define DBKEY_SYNC_TOKEN "SyncToken" - -// Account DB keys -#define DBKEY_SET_MIRANDA_STATUS "SetMirandaStatus" - -// Hidden account DB keys (can't be changed through GUI) -#define DBKEY_LOCALE "Locale" // [HIDDEN] - (string) en_US, cs_CZ, etc. (requires restart to apply) +/* + +Facebook plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +#define MODULENAME "Facebook" + +// Contact DB keys +#define DBKEY_LOGIN "Email" +#define DBKEY_ID "ID" +#define DBKEY_SID "SID" +#define DBKEY_NICK "Nick" +#define DBKEY_PASS "Password" +#define DBKEY_CLIENT_ID "ClientID" +#define DBKEY_DEVICE_ID "DeviceID" +#define DBKEY_AVATAR "Avatar" +#define DBKEY_CONTACT_TYPE "ContactType" +#define DBKEY_TOKEN "Token" +#define DBKEY_SYNC_TOKEN "SyncToken" + +// Account DB keys +#define DBKEY_SET_MIRANDA_STATUS "SetMirandaStatus" + +// Hidden account DB keys (can't be changed through GUI) +#define DBKEY_LOCALE "Locale" // [HIDDEN] - (string) en_US, cs_CZ, etc. (requires restart to apply) diff --git a/protocols/Facebook/src/dialogs.cpp b/protocols/Facebook/src/dialogs.cpp index 4e9e215b86..1938c94448 100644 --- a/protocols/Facebook/src/dialogs.cpp +++ b/protocols/Facebook/src/dialogs.cpp @@ -1,78 +1,78 @@ -/* - -Facebook plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -INT_PTR CALLBACK FBAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) -{ - FacebookProto *proto = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); - - switch (message) { - - case WM_INITDIALOG: - { - TranslateDialogDefault(hwnd); - - proto = reinterpret_cast(lparam); - SetWindowLongPtr(hwnd, GWLP_USERDATA, lparam); - - ptrA login(db_get_sa(0, proto->ModuleName(), DBKEY_LOGIN)); - if (login != nullptr) - SetDlgItemTextA(hwnd, IDC_UN, login); - - ptrA password(db_get_sa(0, proto->ModuleName(), DBKEY_PASS)); - if (password != nullptr) - SetDlgItemTextA(hwnd, IDC_PW, password); - - //if (!proto->isOffline()) { - // SendDlgItemMessage(hwnd, IDC_UN, EM_SETREADONLY, 1, 0); - // SendDlgItemMessage(hwnd, IDC_PW, EM_SETREADONLY, 1, 0); - //} - return TRUE; - } - case WM_COMMAND: - if (HIWORD(wparam) == EN_CHANGE && reinterpret_cast(lparam) == GetFocus()) { - switch (LOWORD(wparam)) { - case IDC_UN: - case IDC_PW: - SendMessage(GetParent(hwnd), PSM_CHANGED, 0, 0); - } - } - break; - - case WM_NOTIFY: - if (reinterpret_cast(lparam)->code == PSN_APPLY) { - char str[128]; - - GetDlgItemTextA(hwnd, IDC_UN, str, _countof(str)); - db_set_s(0, proto->ModuleName(), DBKEY_LOGIN, str); - - GetDlgItemTextA(hwnd, IDC_PW, str, _countof(str)); - db_set_s(0, proto->ModuleName(), DBKEY_PASS, str); - return TRUE; - } - break; - - } - - return FALSE; -} +/* + +Facebook plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +INT_PTR CALLBACK FBAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) +{ + FacebookProto *proto = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + switch (message) { + + case WM_INITDIALOG: + { + TranslateDialogDefault(hwnd); + + proto = reinterpret_cast(lparam); + SetWindowLongPtr(hwnd, GWLP_USERDATA, lparam); + + ptrA login(db_get_sa(0, proto->ModuleName(), DBKEY_LOGIN)); + if (login != nullptr) + SetDlgItemTextA(hwnd, IDC_UN, login); + + ptrA password(db_get_sa(0, proto->ModuleName(), DBKEY_PASS)); + if (password != nullptr) + SetDlgItemTextA(hwnd, IDC_PW, password); + + //if (!proto->isOffline()) { + // SendDlgItemMessage(hwnd, IDC_UN, EM_SETREADONLY, 1, 0); + // SendDlgItemMessage(hwnd, IDC_PW, EM_SETREADONLY, 1, 0); + //} + return TRUE; + } + case WM_COMMAND: + if (HIWORD(wparam) == EN_CHANGE && reinterpret_cast(lparam) == GetFocus()) { + switch (LOWORD(wparam)) { + case IDC_UN: + case IDC_PW: + SendMessage(GetParent(hwnd), PSM_CHANGED, 0, 0); + } + } + break; + + case WM_NOTIFY: + if (reinterpret_cast(lparam)->code == PSN_APPLY) { + char str[128]; + + GetDlgItemTextA(hwnd, IDC_UN, str, _countof(str)); + db_set_s(0, proto->ModuleName(), DBKEY_LOGIN, str); + + GetDlgItemTextA(hwnd, IDC_PW, str, _countof(str)); + db_set_s(0, proto->ModuleName(), DBKEY_PASS, str); + return TRUE; + } + break; + + } + + return FALSE; +} diff --git a/protocols/Facebook/src/dialogs.h b/protocols/Facebook/src/dialogs.h index 23d821c697..5dcaeb667b 100644 --- a/protocols/Facebook/src/dialogs.h +++ b/protocols/Facebook/src/dialogs.h @@ -3,7 +3,7 @@ Facebook plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2009-11 Michal Zelinka, 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Facebook/src/groupchats.cpp b/protocols/Facebook/src/groupchats.cpp index 92266e3e2d..b43e05696a 100644 --- a/protocols/Facebook/src/groupchats.cpp +++ b/protocols/Facebook/src/groupchats.cpp @@ -1,255 +1,255 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// -// Invitation dialog - -class CGroupchatInviteDlg : public CFBDlgBase -{ - CCtrlClc m_clc; - SESSION_INFO *m_si; - - 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(1); - m_clc.SetHideOfflineRoot(1); - m_clc.SetOfflineModes(PF2_NONE); - } - -public: - CGroupchatInviteDlg(FacebookProto *ppro, SESSION_INFO *si) : - CFBDlgBase(ppro, IDD_GROUPCHAT_INVITE), - m_si(si), - m_clc(this, IDC_CLIST) - { - m_clc.OnNewContact = - m_clc.OnListRebuilt = Callback(this, &CGroupchatInviteDlg::FilterList); - m_clc.OnOptionsChanged = Callback(this, &CGroupchatInviteDlg::ResetListOptions); - } - - bool OnInitDialog() override - { - SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, - GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_SHOWHIDDEN | CLS_HIDEOFFLINE | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE | CLS_GROUPCHECKBOXES); - m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); - - ResetListOptions(&m_clc); - FilterList(&m_clc); - return true; - } - - bool OnApply() override - { - JSONNode list(JSON_ARRAY); - - for (auto &hContact : m_proto->AccContacts()) { - if (m_proto->isChatRoom(hContact)) - continue; - - if (HANDLE hItem = m_clc.FindContact(hContact)) { - if (m_clc.GetCheck(hItem)) { - JSONNode user; user << CHAR_PARAM("type", "id") << CHAR_PARAM("id", m_proto->getMStringA(hContact, DBKEY_ID)); - list << user; - } - } - } - - auto *pReq = m_proto->CreateRequest(FB_API_URL_PARTS, "addMembers", "POST"); - pReq << CHAR_PARAM("to", list.write().c_str()) << WCHAR_PARAM("id", CMStringW(FORMAT, L"t_%s", m_si->ptszID)); - pReq->CalcSig(); - - JsonReply reply(m_proto->ExecuteRequest(pReq)); - return true; - } -}; - -void FacebookProto::Chat_InviteUser(SESSION_INFO *si) -{ - CGroupchatInviteDlg dlg(this, si); - if (si->pDlg) - dlg.SetParent(((CDlgBase *)si->pDlg)->GetHwnd()); - dlg.DoModal(); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Group chats - -enum ChatMenuItems -{ - IDM_INVITE = 10, IDM_LEAVE, - - IDM_KICK = 20 -}; - -static gc_item sttLogListItems[] = -{ - { LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM }, - { nullptr, 0, MENU_SEPARATOR }, - { LPGENW("&Leave/destroy chat"), IDM_LEAVE, MENU_ITEM }, -}; - -static gc_item sttNickListItems[] = -{ - { LPGENW("&Kick user"), IDM_KICK, MENU_ITEM }, -}; - -int FacebookProto::GroupchatMenuHook(WPARAM, LPARAM lParam) -{ - GCMENUITEMS *gcmi = (GCMENUITEMS *)lParam; - if (gcmi == nullptr) - return 0; - - if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) - return 0; - - if (SESSION_INFO *si = g_chatApi.SM_FindSession(gcmi->pszID, gcmi->pszModule)) { - if (gcmi->Type == MENU_ON_LOG) - Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); - if (gcmi->Type == MENU_ON_NICKLIST) - Chat_AddMenuItems(gcmi->hMenu, _countof(sttNickListItems), sttNickListItems, &g_plugin); - } - - return 0; -} - -int FacebookProto::GroupchatEventHook(WPARAM, LPARAM lParam) -{ - GCHOOK *gch = (GCHOOK *)lParam; - if (gch == nullptr) - return 0; - - if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) - return 0; - - SESSION_INFO *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule); - if (si == nullptr) - return 1; - - switch (gch->iType) { - case GC_USER_MESSAGE: - rtrimw(gch->ptszText); - if (!mir_wstrlen(gch->ptszText)) - break; - - if (m_bOnline) { - wchar_t *wszText = NEWWSTR_ALLOCA(gch->ptszText); - Chat_UnescapeTags(wszText); - - int mid = SendMsg(si->hContact, 0, T2Utf(wszText)); - - mir_cslock lck(m_csOwnMessages); - for (auto &msg : arOwnMessages) - if (msg->reqId == mid) - msg->wszText = wszText; - } - break; - - case GC_USER_PRIVMESS: - Chat_SendPrivateMessage(gch); - break; - - case GC_USER_LOGMENU: - Chat_ProcessLogMenu(si, gch); - break; - - case GC_USER_NICKLISTMENU: - Chat_ProcessNickMenu(si, gch); - break; - } - - return 1; -} - -void FacebookProto::Chat_ProcessLogMenu(SESSION_INFO *si, GCHOOK *gch) -{ - switch (gch->dwData) { - case IDM_INVITE: - Chat_InviteUser(si); - break; - - case IDM_LEAVE: - Chat_Leave(si); - break; - } -} - -void FacebookProto::Chat_ProcessNickMenu(SESSION_INFO *si, GCHOOK *gch) -{ - switch (gch->dwData) { - case IDM_KICK: - Chat_KickUser(si, gch->ptszUID); - break; - } -} - -void FacebookProto::Chat_SendPrivateMessage(GCHOOK *gch) -{ - auto *pUser = FindUser(_wtoi64(gch->ptszUID)); - if (pUser == nullptr) { - pUser = AddContact(gch->ptszUID, true); - setWString(pUser->hContact, "Nick", gch->ptszNick); - db_set_b(pUser->hContact, "CList", "Hidden", 1); - db_set_dw(pUser->hContact, "Ignore", "Mask1", 0); - } - - CallService(MS_MSG_SENDMESSAGE, pUser->hContact, 0); -} - -int FacebookProto::Chat_KickUser(SESSION_INFO *si, const wchar_t *pwszUid) -{ - auto *pReq = CreateRequest(FB_API_URL_PARTS, "removeMembers", "DELETE"); - pReq << WCHAR_PARAM("id", CMStringW(FORMAT, L"t_%s", si->ptszID)); - if (pwszUid != nullptr) { - JSONNode list(JSON_ARRAY); - JSONNode user; user << CHAR_PARAM("type", "id") << WCHAR_PARAM("id", pwszUid); - list << user; - pReq << CHAR_PARAM("to", list.write().c_str()); - } - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - return reply.error(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static void __cdecl DestroyRoomThread(SESSION_INFO *si) -{ - ::Sleep(100); - Chat_Terminate(si->pszModule, si->ptszID, true); -} - -void FacebookProto::Chat_Leave(SESSION_INFO *si) -{ - if (Chat_KickUser(si, nullptr) == 0) - mir_forkThread(DestroyRoomThread, si); -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// +// Invitation dialog + +class CGroupchatInviteDlg : public CFBDlgBase +{ + CCtrlClc m_clc; + SESSION_INFO *m_si; + + 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(1); + m_clc.SetHideOfflineRoot(1); + m_clc.SetOfflineModes(PF2_NONE); + } + +public: + CGroupchatInviteDlg(FacebookProto *ppro, SESSION_INFO *si) : + CFBDlgBase(ppro, IDD_GROUPCHAT_INVITE), + m_si(si), + m_clc(this, IDC_CLIST) + { + m_clc.OnNewContact = + m_clc.OnListRebuilt = Callback(this, &CGroupchatInviteDlg::FilterList); + m_clc.OnOptionsChanged = Callback(this, &CGroupchatInviteDlg::ResetListOptions); + } + + bool OnInitDialog() override + { + SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, + GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_SHOWHIDDEN | CLS_HIDEOFFLINE | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE | CLS_GROUPCHECKBOXES); + m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); + + ResetListOptions(&m_clc); + FilterList(&m_clc); + return true; + } + + bool OnApply() override + { + JSONNode list(JSON_ARRAY); + + for (auto &hContact : m_proto->AccContacts()) { + if (m_proto->isChatRoom(hContact)) + continue; + + if (HANDLE hItem = m_clc.FindContact(hContact)) { + if (m_clc.GetCheck(hItem)) { + JSONNode user; user << CHAR_PARAM("type", "id") << CHAR_PARAM("id", m_proto->getMStringA(hContact, DBKEY_ID)); + list << user; + } + } + } + + auto *pReq = m_proto->CreateRequest(FB_API_URL_PARTS, "addMembers", "POST"); + pReq << CHAR_PARAM("to", list.write().c_str()) << WCHAR_PARAM("id", CMStringW(FORMAT, L"t_%s", m_si->ptszID)); + pReq->CalcSig(); + + JsonReply reply(m_proto->ExecuteRequest(pReq)); + return true; + } +}; + +void FacebookProto::Chat_InviteUser(SESSION_INFO *si) +{ + CGroupchatInviteDlg dlg(this, si); + if (si->pDlg) + dlg.SetParent(((CDlgBase *)si->pDlg)->GetHwnd()); + dlg.DoModal(); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Group chats + +enum ChatMenuItems +{ + IDM_INVITE = 10, IDM_LEAVE, + + IDM_KICK = 20 +}; + +static gc_item sttLogListItems[] = +{ + { LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM }, + { nullptr, 0, MENU_SEPARATOR }, + { LPGENW("&Leave/destroy chat"), IDM_LEAVE, MENU_ITEM }, +}; + +static gc_item sttNickListItems[] = +{ + { LPGENW("&Kick user"), IDM_KICK, MENU_ITEM }, +}; + +int FacebookProto::GroupchatMenuHook(WPARAM, LPARAM lParam) +{ + GCMENUITEMS *gcmi = (GCMENUITEMS *)lParam; + if (gcmi == nullptr) + return 0; + + if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) + return 0; + + if (SESSION_INFO *si = g_chatApi.SM_FindSession(gcmi->pszID, gcmi->pszModule)) { + if (gcmi->Type == MENU_ON_LOG) + Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); + if (gcmi->Type == MENU_ON_NICKLIST) + Chat_AddMenuItems(gcmi->hMenu, _countof(sttNickListItems), sttNickListItems, &g_plugin); + } + + return 0; +} + +int FacebookProto::GroupchatEventHook(WPARAM, LPARAM lParam) +{ + GCHOOK *gch = (GCHOOK *)lParam; + if (gch == nullptr) + return 0; + + if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) + return 0; + + SESSION_INFO *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule); + if (si == nullptr) + return 1; + + switch (gch->iType) { + case GC_USER_MESSAGE: + rtrimw(gch->ptszText); + if (!mir_wstrlen(gch->ptszText)) + break; + + if (m_bOnline) { + wchar_t *wszText = NEWWSTR_ALLOCA(gch->ptszText); + Chat_UnescapeTags(wszText); + + int mid = SendMsg(si->hContact, 0, T2Utf(wszText)); + + mir_cslock lck(m_csOwnMessages); + for (auto &msg : arOwnMessages) + if (msg->reqId == mid) + msg->wszText = wszText; + } + break; + + case GC_USER_PRIVMESS: + Chat_SendPrivateMessage(gch); + break; + + case GC_USER_LOGMENU: + Chat_ProcessLogMenu(si, gch); + break; + + case GC_USER_NICKLISTMENU: + Chat_ProcessNickMenu(si, gch); + break; + } + + return 1; +} + +void FacebookProto::Chat_ProcessLogMenu(SESSION_INFO *si, GCHOOK *gch) +{ + switch (gch->dwData) { + case IDM_INVITE: + Chat_InviteUser(si); + break; + + case IDM_LEAVE: + Chat_Leave(si); + break; + } +} + +void FacebookProto::Chat_ProcessNickMenu(SESSION_INFO *si, GCHOOK *gch) +{ + switch (gch->dwData) { + case IDM_KICK: + Chat_KickUser(si, gch->ptszUID); + break; + } +} + +void FacebookProto::Chat_SendPrivateMessage(GCHOOK *gch) +{ + auto *pUser = FindUser(_wtoi64(gch->ptszUID)); + if (pUser == nullptr) { + pUser = AddContact(gch->ptszUID, true); + setWString(pUser->hContact, "Nick", gch->ptszNick); + db_set_b(pUser->hContact, "CList", "Hidden", 1); + db_set_dw(pUser->hContact, "Ignore", "Mask1", 0); + } + + CallService(MS_MSG_SENDMESSAGE, pUser->hContact, 0); +} + +int FacebookProto::Chat_KickUser(SESSION_INFO *si, const wchar_t *pwszUid) +{ + auto *pReq = CreateRequest(FB_API_URL_PARTS, "removeMembers", "DELETE"); + pReq << WCHAR_PARAM("id", CMStringW(FORMAT, L"t_%s", si->ptszID)); + if (pwszUid != nullptr) { + JSONNode list(JSON_ARRAY); + JSONNode user; user << CHAR_PARAM("type", "id") << WCHAR_PARAM("id", pwszUid); + list << user; + pReq << CHAR_PARAM("to", list.write().c_str()); + } + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + return reply.error(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static void __cdecl DestroyRoomThread(SESSION_INFO *si) +{ + ::Sleep(100); + Chat_Terminate(si->pszModule, si->ptszID, true); +} + +void FacebookProto::Chat_Leave(SESSION_INFO *si) +{ + if (Chat_KickUser(si, nullptr) == 0) + mir_forkThread(DestroyRoomThread, si); +} diff --git a/protocols/Facebook/src/http.cpp b/protocols/Facebook/src/http.cpp index 7bc6e5082c..6f520c0486 100644 --- a/protocols/Facebook/src/http.cpp +++ b/protocols/Facebook/src/http.cpp @@ -1,185 +1,185 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -static int CompareParams(const AsyncHttpRequest::Param *p1, const AsyncHttpRequest::Param *p2) -{ - return strcmp(p1->key, p2->key); -} - -AsyncHttpRequest::AsyncHttpRequest() : - params(5, CompareParams) -{ -} - -void AsyncHttpRequest::CalcSig() -{ - CMStringA buf; - for (auto &it : params) - buf.AppendFormat("%s=%s", it->key.c_str(), it->val.c_str()); - - buf.Append(FB_API_SECRET); - - char szHash[33]; - uint8_t digest[16]; - mir_md5_hash((uint8_t*)buf.c_str(), buf.GetLength(), digest); - bin2hex(digest, sizeof(digest), szHash); - this << CHAR_PARAM("sig", szHash); - - for (auto &it : params) { - if (!m_szParam.IsEmpty()) - m_szParam.AppendChar('&'); - m_szParam.AppendFormat("%s=%s", it->key.c_str(), mir_urlEncode(it->val.c_str()).c_str()); - } -} - -AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const CHAR_PARAM ¶m) -{ - pReq->params.insert(new AsyncHttpRequest::Param(param.szName, param.szValue)); - return pReq; -} - -AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const INT_PARAM ¶m) -{ - char value[40]; - itoa(param.iValue, value, 10); - pReq->params.insert(new AsyncHttpRequest::Param(param.szName, value)); - return pReq; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = (*m_root)["error_code"].as_int(); -} - -JsonReply::~JsonReply() -{ - json_delete(m_root); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -AsyncHttpRequest* FacebookProto::CreateRequest(const char *szUrl, const char *szName, const char *szMethod) -{ - AsyncHttpRequest *pReq = new AsyncHttpRequest(); - pReq->m_szUrl = szUrl; - pReq->requestType = REQUEST_POST; - pReq << CHAR_PARAM("api_key", FB_API_KEY) - << CHAR_PARAM("device_id", m_szDeviceID) - << CHAR_PARAM("fb_api_req_friendly_name", szName) - << CHAR_PARAM("format", "json") - << CHAR_PARAM("method", szMethod); - - CMStringA szLocale = getMStringA(DBKEY_LOCALE); - if (szLocale.IsEmpty()) - szLocale = "en"; - pReq << CHAR_PARAM("locale", szLocale); - - if (!m_szAuthToken.IsEmpty()) { - pReq->flags |= NLHRF_NODUMPHEADERS; - pReq->AddHeader("Authorization", "OAuth " + m_szAuthToken); - } - - pReq->AddHeader("User-Agent", FB_API_AGENT); - pReq->AddHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); - return pReq; -} - -AsyncHttpRequest* FacebookProto::CreateRequestGQL(int64_t query_id) { - const char* szName; - - switch (query_id) { - case FB_API_QUERY_CONTACT: - szName = "UsersQuery"; - break; - case FB_API_QUERY_CONTACTS: - szName = "FetchContactsFullQuery"; - break; - case FB_API_QUERY_CONTACTS_AFTER: - szName = "FetchContactsFullWithAfterQuery"; - break; - case FB_API_QUERY_CONTACTS_DELTA: - szName = "FetchContactsDeltaQuery"; - break; - case FB_API_QUERY_STICKER: - szName = "FetchStickersWithPreviewsQuery"; - break; - case FB_API_QUERY_THREAD: - szName = "ThreadQuery"; - break; - case FB_API_QUERY_SEQ_ID: - case FB_API_QUERY_THREADS: - szName = "ThreadListQuery"; - break; - case FB_API_QUERY_XMA: - szName = "XMAQuery"; - break; - default: - return nullptr; - } - - AsyncHttpRequest* pReq = CreateRequest(FB_API_URL_GQL, szName, "get"); - pReq << INT64_PARAM("query_id", query_id); - return pReq; -} - -NETLIBHTTPREQUEST* FacebookProto::ExecuteRequest(AsyncHttpRequest *pReq) -{ - CMStringA str; - - pReq->flags |= NLHRF_HTTP11; - pReq->szUrl = pReq->m_szUrl.GetBuffer(); - if (!pReq->m_szParam.IsEmpty()) { - if (pReq->requestType == REQUEST_GET) { - str.Format("%s?%s", pReq->m_szUrl.c_str(), pReq->m_szParam.c_str()); - pReq->szUrl = str.GetBuffer(); - } - else { - pReq->dataLength = pReq->m_szParam.GetLength(); - pReq->pData = mir_strdup(pReq->m_szParam); - } - } - - debugLogA("Executing request:\n%s", pReq->szUrl); - - NETLIBHTTPREQUEST *reply = Netlib_HttpTransaction(m_hNetlibUser, pReq); - delete pReq; - return reply; -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +static int CompareParams(const AsyncHttpRequest::Param *p1, const AsyncHttpRequest::Param *p2) +{ + return strcmp(p1->key, p2->key); +} + +AsyncHttpRequest::AsyncHttpRequest() : + params(5, CompareParams) +{ +} + +void AsyncHttpRequest::CalcSig() +{ + CMStringA buf; + for (auto &it : params) + buf.AppendFormat("%s=%s", it->key.c_str(), it->val.c_str()); + + buf.Append(FB_API_SECRET); + + char szHash[33]; + uint8_t digest[16]; + mir_md5_hash((uint8_t*)buf.c_str(), buf.GetLength(), digest); + bin2hex(digest, sizeof(digest), szHash); + this << CHAR_PARAM("sig", szHash); + + for (auto &it : params) { + if (!m_szParam.IsEmpty()) + m_szParam.AppendChar('&'); + m_szParam.AppendFormat("%s=%s", it->key.c_str(), mir_urlEncode(it->val.c_str()).c_str()); + } +} + +AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const CHAR_PARAM ¶m) +{ + pReq->params.insert(new AsyncHttpRequest::Param(param.szName, param.szValue)); + return pReq; +} + +AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const INT_PARAM ¶m) +{ + char value[40]; + itoa(param.iValue, value, 10); + pReq->params.insert(new AsyncHttpRequest::Param(param.szName, value)); + return pReq; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = (*m_root)["error_code"].as_int(); +} + +JsonReply::~JsonReply() +{ + json_delete(m_root); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +AsyncHttpRequest* FacebookProto::CreateRequest(const char *szUrl, const char *szName, const char *szMethod) +{ + AsyncHttpRequest *pReq = new AsyncHttpRequest(); + pReq->m_szUrl = szUrl; + pReq->requestType = REQUEST_POST; + pReq << CHAR_PARAM("api_key", FB_API_KEY) + << CHAR_PARAM("device_id", m_szDeviceID) + << CHAR_PARAM("fb_api_req_friendly_name", szName) + << CHAR_PARAM("format", "json") + << CHAR_PARAM("method", szMethod); + + CMStringA szLocale = getMStringA(DBKEY_LOCALE); + if (szLocale.IsEmpty()) + szLocale = "en"; + pReq << CHAR_PARAM("locale", szLocale); + + if (!m_szAuthToken.IsEmpty()) { + pReq->flags |= NLHRF_NODUMPHEADERS; + pReq->AddHeader("Authorization", "OAuth " + m_szAuthToken); + } + + pReq->AddHeader("User-Agent", FB_API_AGENT); + pReq->AddHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8"); + return pReq; +} + +AsyncHttpRequest* FacebookProto::CreateRequestGQL(int64_t query_id) { + const char* szName; + + switch (query_id) { + case FB_API_QUERY_CONTACT: + szName = "UsersQuery"; + break; + case FB_API_QUERY_CONTACTS: + szName = "FetchContactsFullQuery"; + break; + case FB_API_QUERY_CONTACTS_AFTER: + szName = "FetchContactsFullWithAfterQuery"; + break; + case FB_API_QUERY_CONTACTS_DELTA: + szName = "FetchContactsDeltaQuery"; + break; + case FB_API_QUERY_STICKER: + szName = "FetchStickersWithPreviewsQuery"; + break; + case FB_API_QUERY_THREAD: + szName = "ThreadQuery"; + break; + case FB_API_QUERY_SEQ_ID: + case FB_API_QUERY_THREADS: + szName = "ThreadListQuery"; + break; + case FB_API_QUERY_XMA: + szName = "XMAQuery"; + break; + default: + return nullptr; + } + + AsyncHttpRequest* pReq = CreateRequest(FB_API_URL_GQL, szName, "get"); + pReq << INT64_PARAM("query_id", query_id); + return pReq; +} + +NETLIBHTTPREQUEST* FacebookProto::ExecuteRequest(AsyncHttpRequest *pReq) +{ + CMStringA str; + + pReq->flags |= NLHRF_HTTP11; + pReq->szUrl = pReq->m_szUrl.GetBuffer(); + if (!pReq->m_szParam.IsEmpty()) { + if (pReq->requestType == REQUEST_GET) { + str.Format("%s?%s", pReq->m_szUrl.c_str(), pReq->m_szParam.c_str()); + pReq->szUrl = str.GetBuffer(); + } + else { + pReq->dataLength = pReq->m_szParam.GetLength(); + pReq->pData = mir_strdup(pReq->m_szParam); + } + } + + debugLogA("Executing request:\n%s", pReq->szUrl); + + NETLIBHTTPREQUEST *reply = Netlib_HttpTransaction(m_hNetlibUser, pReq); + delete pReq; + return reply; +} diff --git a/protocols/Facebook/src/main.cpp b/protocols/Facebook/src/main.cpp index 9e308016fc..2b9215e2ed 100644 --- a/protocols/Facebook/src/main.cpp +++ b/protocols/Facebook/src/main.cpp @@ -1,72 +1,72 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -#pragma comment(lib, "Rpcrt4.lib") - -CMPlugin g_plugin; - -bool g_bMessageState; - -///////////////////////////////////////////////////////////////////////////////////////// - -PLUGININFOEX pluginInfoEx = { - sizeof(PLUGININFOEX), - __PLUGIN_NAME, - PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), - __DESCRIPTION, - __AUTHOR, - __COPYRIGHT, - __AUTHORWEB, - UNICODE_AWARE, - // {86033E58-A1E3-43AD-AE8E-305E15E72A91} - { 0xee0543fb, 0x711d, 0x4ac8, { 0xb6, 0xc0, 0x1d, 0xda, 0x48, 0x38, 0x10, 0x7e }} -}; - -CMPlugin::CMPlugin() : - ACCPROTOPLUGIN(MODULENAME, pluginInfoEx) -{ - SetUniqueId(DBKEY_ID); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Interface information - -extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; - -///////////////////////////////////////////////////////////////////////////////////////// -// Load - -static int OnModuleLoaded(WPARAM, LPARAM) -{ - g_bMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE) != 0; - return 0; -} - -int CMPlugin::Load() -{ - HookEvent(ME_SYSTEM_MODULELOAD, OnModuleLoaded); - HookEvent(ME_SYSTEM_MODULEUNLOAD, OnModuleLoaded); - HookEvent(ME_SYSTEM_MODULESLOADED, OnModuleLoaded); - - // Initialize random generator (used only as fallback in utils) - return 0; -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +#pragma comment(lib, "Rpcrt4.lib") + +CMPlugin g_plugin; + +bool g_bMessageState; + +///////////////////////////////////////////////////////////////////////////////////////// + +PLUGININFOEX pluginInfoEx = { + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + // {86033E58-A1E3-43AD-AE8E-305E15E72A91} + { 0xee0543fb, 0x711d, 0x4ac8, { 0xb6, 0xc0, 0x1d, 0xda, 0x48, 0x38, 0x10, 0x7e }} +}; + +CMPlugin::CMPlugin() : + ACCPROTOPLUGIN(MODULENAME, pluginInfoEx) +{ + SetUniqueId(DBKEY_ID); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Interface information + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// +// Load + +static int OnModuleLoaded(WPARAM, LPARAM) +{ + g_bMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE) != 0; + return 0; +} + +int CMPlugin::Load() +{ + HookEvent(ME_SYSTEM_MODULELOAD, OnModuleLoaded); + HookEvent(ME_SYSTEM_MODULEUNLOAD, OnModuleLoaded); + HookEvent(ME_SYSTEM_MODULESLOADED, OnModuleLoaded); + + // Initialize random generator (used only as fallback in utils) + return 0; +} diff --git a/protocols/Facebook/src/mqtt.cpp b/protocols/Facebook/src/mqtt.cpp index 7c2eb5ef5f..b7fbe0a3bd 100644 --- a/protocols/Facebook/src/mqtt.cpp +++ b/protocols/Facebook/src/mqtt.cpp @@ -1,344 +1,344 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -uint8_t *FacebookProto::doZip(size_t cbData, const void *pData, size_t &cbRes) -{ - size_t dataSize = cbData + 100; - uint8_t *pRes = (uint8_t *)mir_alloc(dataSize); - - z_stream zStreamOut = {}; - deflateInit(&zStreamOut, Z_BEST_COMPRESSION); - zStreamOut.avail_in = (unsigned)cbData; - zStreamOut.next_in = (uint8_t *)pData; - zStreamOut.avail_out = (unsigned)dataSize; - zStreamOut.next_out = (uint8_t *)pRes; - deflate(&zStreamOut, Z_FINISH); - deflateEnd(&zStreamOut); - - cbRes = dataSize - zStreamOut.avail_out; - return pRes; -} - -uint8_t *FacebookProto::doUnzip(size_t cbData, const void *pData, size_t &cbRes) -{ - size_t dataSize = cbData * 10; - uint8_t *pRes = (uint8_t *)mir_alloc(dataSize); - - z_stream zStreamOut = {}; - inflateInit(&zStreamOut); - zStreamOut.avail_in = (unsigned)cbData; - zStreamOut.next_in = (uint8_t *)pData; - zStreamOut.avail_out = (unsigned)dataSize; - zStreamOut.next_out = (uint8_t *)pRes; - int rc = inflate(&zStreamOut, Z_FINISH); - inflateEnd(&zStreamOut); - - switch (rc) { - case Z_OK: - case Z_STREAM_END: - cbRes = dataSize - zStreamOut.avail_out; - return pRes; - } - - mir_free(pRes); - cbRes = 0; - return nullptr; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// MqttMessage class members - -MqttMessage::MqttMessage() : - m_leadingByte(0) -{ -} - -MqttMessage::MqttMessage(FbMqttMessageType type, uint8_t flags) -{ - m_leadingByte = ((type & 0x0F) << 4) | (flags & 0x0F); -} - -char* MqttMessage::readStr(const uint8_t *&pData) const -{ - u_short len = ntohs(*(u_short *)pData); pData += sizeof(u_short); - if (len == 0) - return nullptr; - - char *res = (char*)mir_alloc(len + 1); - memcpy(res, pData, len); - res[len] = 0; - pData += len; - return res; -} - -void MqttMessage::writeStr(const char *str) -{ - size_t len = mir_strlen(str); - writeInt16((uint16_t)len); - writeBuf(str, len); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// MQTT functions - -bool FacebookProto::MqttParse(const MqttMessage &payload) -{ - auto *pData = (const uint8_t *)payload.data(), *pBeg = pData; - int flags = payload.getFlags(); - uint16_t mid; - - switch (payload.getType()) { - case FB_MQTT_MESSAGE_TYPE_CONNACK: - if (pData[1] != 0) { // connection failed; - int iErrorCode = ntohs(*(u_short *)pData); - debugLogA("Login failed with error %d", iErrorCode); - - if (iErrorCode == 4) { // invalid login/password - delSetting(DBKEY_TOKEN); - m_szAuthToken.Empty(); - ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, 0, LOGINERR_WRONGPASSWORD); - } - else ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, 0, LOGINERR_WRONGPROTOCOL); - return false; - } - - OnLoggedIn(); - break; - - case FB_MQTT_MESSAGE_TYPE_PUBREL: - mid = ntohs(*(u_short *)pData); - pData += 2; - { - MqttMessage reply(FB_MQTT_MESSAGE_TYPE_PUBCOMP); - reply.writeInt16(mid); - MqttSend(reply); - } - break; - - case FB_MQTT_MESSAGE_TYPE_PUBLISH: - char *str = payload.readStr(pData); - - if ((flags & FB_MQTT_MESSAGE_FLAG_QOS1) || (flags & FB_MQTT_MESSAGE_FLAG_QOS2)) { - mid = ntohs(*(u_short *)pData); - pData += 2; - - MqttMessage reply((flags & FB_MQTT_MESSAGE_FLAG_QOS1) ? FB_MQTT_MESSAGE_TYPE_PUBACK : FB_MQTT_MESSAGE_TYPE_PUBREC); - reply.writeInt16(mid); - MqttSend(reply); - } - - OnPublish(str, pData, payload.size() - (pData - pBeg)); - mir_free(str); - break; - } - - return true; -} - -bool FacebookProto::MqttRead(MqttMessage &payload) -{ - uint8_t b; - int res = Netlib_Recv(m_mqttConn, (char *)&b, sizeof(b), MSG_NODUMP); - if (res != 1) - return false; - - payload.m_leadingByte = b; - - uint32_t m = 1, remainingBytes = 0; - do { - if ((res = Netlib_Recv(m_mqttConn, (char *)&b, sizeof(b), MSG_NODUMP)) != 1) - return false; - - remainingBytes += (b & 0x7F) * m; - m *= 128; - } while ((b & 0x80) != 0); - - debugLogA("Received message of type=%d, flags=%x, body length=%d", payload.getType(), payload.getFlags(), remainingBytes); - - if (remainingBytes != 0) { - while (remainingBytes > 0) { - uint8_t buf[1024]; - int size = min(remainingBytes, sizeof(buf)); - if ((res = Netlib_Recv(m_mqttConn, (char *)buf, size)) <= 0) - return false; - - payload.writeBuf(buf, res); - remainingBytes -= res; - } - } - - return true; -} - -void FacebookProto::MqttSend(const MqttMessage &payload) -{ - FbThrift msg; - msg << payload.m_leadingByte; - msg.writeIntV(payload.size()); - msg.writeBuf(payload.data(), payload.size()); - Netlib_Send(m_mqttConn, (char*)msg.data(), (unsigned)msg.size()); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// creates initial MQTT will and sends initialization packet - -void FacebookProto::MqttLogin() -{ - uint8_t zeroByte = 0; - Utils_GetRandom(&m_iMqttId, sizeof(m_iMqttId) / 2); - - FbThrift thrift; - thrift.writeField(FB_THRIFT_TYPE_STRING); // Client identifier - thrift << m_szClientID; - - thrift.writeField(FB_THRIFT_TYPE_STRUCT, 4, 1); - - thrift.writeField(FB_THRIFT_TYPE_I64); // User identifier - thrift.writeInt64(m_uid); - - thrift.writeField(FB_THRIFT_TYPE_STRING); // User agent - thrift << FB_API_MQTT_AGENT; - - thrift.writeField(FB_THRIFT_TYPE_I64); - thrift.writeInt64(23); - thrift.writeField(FB_THRIFT_TYPE_I64); - thrift.writeInt64(26); - thrift.writeField(FB_THRIFT_TYPE_I32); - thrift.writeInt32(1); - - thrift.writeBool(true); - thrift.writeBool(!m_bLoginInvisible); // visibility - - thrift.writeField(FB_THRIFT_TYPE_STRING); // device id - thrift << m_szDeviceID; - - thrift.writeBool(true); - thrift.writeField(FB_THRIFT_TYPE_I32); - thrift.writeInt32(1); - thrift.writeField(FB_THRIFT_TYPE_I32); - thrift.writeInt32(0); - thrift.writeField(FB_THRIFT_TYPE_I64); - thrift.writeInt64(m_iMqttId); - - thrift.writeField(FB_THRIFT_TYPE_LIST, 14, 12); - thrift.writeList(FB_THRIFT_TYPE_I32, 0); - thrift << zeroByte; - - thrift.writeField(FB_THRIFT_TYPE_STRING); - thrift << m_szAuthToken << zeroByte; - - size_t dataSize; - mir_ptr pData(doZip(thrift.size(), thrift.data(), dataSize)); - - uint8_t protocolVersion = 3; - uint8_t flags = FB_MQTT_CONNECT_FLAG_USER | FB_MQTT_CONNECT_FLAG_PASS | FB_MQTT_CONNECT_FLAG_CLR | FB_MQTT_CONNECT_FLAG_QOS1; - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_CONNECT); - payload.writeStr("MQTToT"); - payload << protocolVersion << flags; - payload.writeInt16(60); // timeout - payload.writeBuf(pData, dataSize); - MqttSend(payload); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// various MQTT send commands - -void FacebookProto::MqttPing() -{ - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_PINGREQ, FB_MQTT_MESSAGE_FLAG_QOS1); - MqttSend(payload); -} - -void FacebookProto::MqttPublish(const char *topic, const JSONNode &value) -{ - auto str = value.write(); - debugLogA("Publish: <%s> -> <%s>", topic, str.c_str()); - - size_t dataSize; - mir_ptr pData(doZip(str.length(), str.c_str(), dataSize)); - - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_PUBLISH, FB_MQTT_MESSAGE_FLAG_QOS1); - payload.writeStr(topic); - payload.writeInt16(++m_mid); - payload.writeBuf(pData, dataSize); - MqttSend(payload); -} - -void FacebookProto::MqttSubscribe(const char *topic, ...) -{ - uint8_t zeroByte = 0; - - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_SUBSCRIBE, FB_MQTT_MESSAGE_FLAG_QOS1); - payload.writeInt16(++m_mid); - payload.writeStr(topic); - payload << zeroByte; - - va_list ap; - va_start(ap, topic); - while ((topic = va_arg(ap, const char *)) != nullptr) { - payload.writeStr(topic); - payload << zeroByte; - } - va_end(ap); - - MqttSend(payload); -} - -void FacebookProto::MqttUnsubscribe(const char *topic, ...) -{ - MqttMessage payload(FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE, FB_MQTT_MESSAGE_FLAG_QOS1); - payload.writeInt16(++m_mid); - payload.writeStr(topic); - - va_list ap; - va_start(ap, topic); - while ((topic = va_arg(ap, const char *)) != nullptr) - payload.writeStr(topic); - va_end(ap); - - MqttSend(payload); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// MQTT queue - -void FacebookProto::MqttQueueConnect() -{ - JSONNode query; - query << INT_PARAM("delta_batch_size", 125) << INT_PARAM("max_deltas_able_to_process", 1000) << INT_PARAM("sync_api_version", 3) << CHAR_PARAM("encoding", "JSON"); - if (m_szSyncToken.IsEmpty()) { - JSONNode hashes; hashes.set_name("graphql_query_hashes"); hashes << CHAR_PARAM("xma_query_id", __STRINGIFY(FB_API_QUERY_XMA)); - - JSONNode xma; xma.set_name(__STRINGIFY(FB_API_QUERY_XMA)); xma << CHAR_PARAM("xma_id", ""); - JSONNode hql; hql.set_name("graphql_query_params"); hql << xma; - - JSONNode params; params.set_name("queue_params"); - params << CHAR_PARAM("buzz_on_deltas_enabled", "false") << hashes << hql; - - query << INT64_PARAM("initial_titan_sequence_id", m_sid) << CHAR_PARAM("device_id", m_szDeviceID) << INT64_PARAM("entity_fbid", m_uid) << params; - MqttPublish("/messenger_sync_create_queue", query); - } - else { - query << INT64_PARAM("last_seq_id", m_sid) << CHAR_PARAM("sync_token", m_szSyncToken); - MqttPublish("/messenger_sync_get_diffs", query); - } -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +uint8_t *FacebookProto::doZip(size_t cbData, const void *pData, size_t &cbRes) +{ + size_t dataSize = cbData + 100; + uint8_t *pRes = (uint8_t *)mir_alloc(dataSize); + + z_stream zStreamOut = {}; + deflateInit(&zStreamOut, Z_BEST_COMPRESSION); + zStreamOut.avail_in = (unsigned)cbData; + zStreamOut.next_in = (uint8_t *)pData; + zStreamOut.avail_out = (unsigned)dataSize; + zStreamOut.next_out = (uint8_t *)pRes; + deflate(&zStreamOut, Z_FINISH); + deflateEnd(&zStreamOut); + + cbRes = dataSize - zStreamOut.avail_out; + return pRes; +} + +uint8_t *FacebookProto::doUnzip(size_t cbData, const void *pData, size_t &cbRes) +{ + size_t dataSize = cbData * 10; + uint8_t *pRes = (uint8_t *)mir_alloc(dataSize); + + z_stream zStreamOut = {}; + inflateInit(&zStreamOut); + zStreamOut.avail_in = (unsigned)cbData; + zStreamOut.next_in = (uint8_t *)pData; + zStreamOut.avail_out = (unsigned)dataSize; + zStreamOut.next_out = (uint8_t *)pRes; + int rc = inflate(&zStreamOut, Z_FINISH); + inflateEnd(&zStreamOut); + + switch (rc) { + case Z_OK: + case Z_STREAM_END: + cbRes = dataSize - zStreamOut.avail_out; + return pRes; + } + + mir_free(pRes); + cbRes = 0; + return nullptr; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// MqttMessage class members + +MqttMessage::MqttMessage() : + m_leadingByte(0) +{ +} + +MqttMessage::MqttMessage(FbMqttMessageType type, uint8_t flags) +{ + m_leadingByte = ((type & 0x0F) << 4) | (flags & 0x0F); +} + +char* MqttMessage::readStr(const uint8_t *&pData) const +{ + u_short len = ntohs(*(u_short *)pData); pData += sizeof(u_short); + if (len == 0) + return nullptr; + + char *res = (char*)mir_alloc(len + 1); + memcpy(res, pData, len); + res[len] = 0; + pData += len; + return res; +} + +void MqttMessage::writeStr(const char *str) +{ + size_t len = mir_strlen(str); + writeInt16((uint16_t)len); + writeBuf(str, len); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// MQTT functions + +bool FacebookProto::MqttParse(const MqttMessage &payload) +{ + auto *pData = (const uint8_t *)payload.data(), *pBeg = pData; + int flags = payload.getFlags(); + uint16_t mid; + + switch (payload.getType()) { + case FB_MQTT_MESSAGE_TYPE_CONNACK: + if (pData[1] != 0) { // connection failed; + int iErrorCode = ntohs(*(u_short *)pData); + debugLogA("Login failed with error %d", iErrorCode); + + if (iErrorCode == 4) { // invalid login/password + delSetting(DBKEY_TOKEN); + m_szAuthToken.Empty(); + ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, 0, LOGINERR_WRONGPASSWORD); + } + else ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, 0, LOGINERR_WRONGPROTOCOL); + return false; + } + + OnLoggedIn(); + break; + + case FB_MQTT_MESSAGE_TYPE_PUBREL: + mid = ntohs(*(u_short *)pData); + pData += 2; + { + MqttMessage reply(FB_MQTT_MESSAGE_TYPE_PUBCOMP); + reply.writeInt16(mid); + MqttSend(reply); + } + break; + + case FB_MQTT_MESSAGE_TYPE_PUBLISH: + char *str = payload.readStr(pData); + + if ((flags & FB_MQTT_MESSAGE_FLAG_QOS1) || (flags & FB_MQTT_MESSAGE_FLAG_QOS2)) { + mid = ntohs(*(u_short *)pData); + pData += 2; + + MqttMessage reply((flags & FB_MQTT_MESSAGE_FLAG_QOS1) ? FB_MQTT_MESSAGE_TYPE_PUBACK : FB_MQTT_MESSAGE_TYPE_PUBREC); + reply.writeInt16(mid); + MqttSend(reply); + } + + OnPublish(str, pData, payload.size() - (pData - pBeg)); + mir_free(str); + break; + } + + return true; +} + +bool FacebookProto::MqttRead(MqttMessage &payload) +{ + uint8_t b; + int res = Netlib_Recv(m_mqttConn, (char *)&b, sizeof(b), MSG_NODUMP); + if (res != 1) + return false; + + payload.m_leadingByte = b; + + uint32_t m = 1, remainingBytes = 0; + do { + if ((res = Netlib_Recv(m_mqttConn, (char *)&b, sizeof(b), MSG_NODUMP)) != 1) + return false; + + remainingBytes += (b & 0x7F) * m; + m *= 128; + } while ((b & 0x80) != 0); + + debugLogA("Received message of type=%d, flags=%x, body length=%d", payload.getType(), payload.getFlags(), remainingBytes); + + if (remainingBytes != 0) { + while (remainingBytes > 0) { + uint8_t buf[1024]; + int size = min(remainingBytes, sizeof(buf)); + if ((res = Netlib_Recv(m_mqttConn, (char *)buf, size)) <= 0) + return false; + + payload.writeBuf(buf, res); + remainingBytes -= res; + } + } + + return true; +} + +void FacebookProto::MqttSend(const MqttMessage &payload) +{ + FbThrift msg; + msg << payload.m_leadingByte; + msg.writeIntV(payload.size()); + msg.writeBuf(payload.data(), payload.size()); + Netlib_Send(m_mqttConn, (char*)msg.data(), (unsigned)msg.size()); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// creates initial MQTT will and sends initialization packet + +void FacebookProto::MqttLogin() +{ + uint8_t zeroByte = 0; + Utils_GetRandom(&m_iMqttId, sizeof(m_iMqttId) / 2); + + FbThrift thrift; + thrift.writeField(FB_THRIFT_TYPE_STRING); // Client identifier + thrift << m_szClientID; + + thrift.writeField(FB_THRIFT_TYPE_STRUCT, 4, 1); + + thrift.writeField(FB_THRIFT_TYPE_I64); // User identifier + thrift.writeInt64(m_uid); + + thrift.writeField(FB_THRIFT_TYPE_STRING); // User agent + thrift << FB_API_MQTT_AGENT; + + thrift.writeField(FB_THRIFT_TYPE_I64); + thrift.writeInt64(23); + thrift.writeField(FB_THRIFT_TYPE_I64); + thrift.writeInt64(26); + thrift.writeField(FB_THRIFT_TYPE_I32); + thrift.writeInt32(1); + + thrift.writeBool(true); + thrift.writeBool(!m_bLoginInvisible); // visibility + + thrift.writeField(FB_THRIFT_TYPE_STRING); // device id + thrift << m_szDeviceID; + + thrift.writeBool(true); + thrift.writeField(FB_THRIFT_TYPE_I32); + thrift.writeInt32(1); + thrift.writeField(FB_THRIFT_TYPE_I32); + thrift.writeInt32(0); + thrift.writeField(FB_THRIFT_TYPE_I64); + thrift.writeInt64(m_iMqttId); + + thrift.writeField(FB_THRIFT_TYPE_LIST, 14, 12); + thrift.writeList(FB_THRIFT_TYPE_I32, 0); + thrift << zeroByte; + + thrift.writeField(FB_THRIFT_TYPE_STRING); + thrift << m_szAuthToken << zeroByte; + + size_t dataSize; + mir_ptr pData(doZip(thrift.size(), thrift.data(), dataSize)); + + uint8_t protocolVersion = 3; + uint8_t flags = FB_MQTT_CONNECT_FLAG_USER | FB_MQTT_CONNECT_FLAG_PASS | FB_MQTT_CONNECT_FLAG_CLR | FB_MQTT_CONNECT_FLAG_QOS1; + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_CONNECT); + payload.writeStr("MQTToT"); + payload << protocolVersion << flags; + payload.writeInt16(60); // timeout + payload.writeBuf(pData, dataSize); + MqttSend(payload); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// various MQTT send commands + +void FacebookProto::MqttPing() +{ + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_PINGREQ, FB_MQTT_MESSAGE_FLAG_QOS1); + MqttSend(payload); +} + +void FacebookProto::MqttPublish(const char *topic, const JSONNode &value) +{ + auto str = value.write(); + debugLogA("Publish: <%s> -> <%s>", topic, str.c_str()); + + size_t dataSize; + mir_ptr pData(doZip(str.length(), str.c_str(), dataSize)); + + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_PUBLISH, FB_MQTT_MESSAGE_FLAG_QOS1); + payload.writeStr(topic); + payload.writeInt16(++m_mid); + payload.writeBuf(pData, dataSize); + MqttSend(payload); +} + +void FacebookProto::MqttSubscribe(const char *topic, ...) +{ + uint8_t zeroByte = 0; + + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_SUBSCRIBE, FB_MQTT_MESSAGE_FLAG_QOS1); + payload.writeInt16(++m_mid); + payload.writeStr(topic); + payload << zeroByte; + + va_list ap; + va_start(ap, topic); + while ((topic = va_arg(ap, const char *)) != nullptr) { + payload.writeStr(topic); + payload << zeroByte; + } + va_end(ap); + + MqttSend(payload); +} + +void FacebookProto::MqttUnsubscribe(const char *topic, ...) +{ + MqttMessage payload(FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE, FB_MQTT_MESSAGE_FLAG_QOS1); + payload.writeInt16(++m_mid); + payload.writeStr(topic); + + va_list ap; + va_start(ap, topic); + while ((topic = va_arg(ap, const char *)) != nullptr) + payload.writeStr(topic); + va_end(ap); + + MqttSend(payload); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// MQTT queue + +void FacebookProto::MqttQueueConnect() +{ + JSONNode query; + query << INT_PARAM("delta_batch_size", 125) << INT_PARAM("max_deltas_able_to_process", 1000) << INT_PARAM("sync_api_version", 3) << CHAR_PARAM("encoding", "JSON"); + if (m_szSyncToken.IsEmpty()) { + JSONNode hashes; hashes.set_name("graphql_query_hashes"); hashes << CHAR_PARAM("xma_query_id", __STRINGIFY(FB_API_QUERY_XMA)); + + JSONNode xma; xma.set_name(__STRINGIFY(FB_API_QUERY_XMA)); xma << CHAR_PARAM("xma_id", ""); + JSONNode hql; hql.set_name("graphql_query_params"); hql << xma; + + JSONNode params; params.set_name("queue_params"); + params << CHAR_PARAM("buzz_on_deltas_enabled", "false") << hashes << hql; + + query << INT64_PARAM("initial_titan_sequence_id", m_sid) << CHAR_PARAM("device_id", m_szDeviceID) << INT64_PARAM("entity_fbid", m_uid) << params; + MqttPublish("/messenger_sync_create_queue", query); + } + else { + query << INT64_PARAM("last_seq_id", m_sid) << CHAR_PARAM("sync_token", m_szSyncToken); + MqttPublish("/messenger_sync_get_diffs", query); + } +} diff --git a/protocols/Facebook/src/mqtt.h b/protocols/Facebook/src/mqtt.h index 08849fbc4c..8e1b4964c7 100644 --- a/protocols/Facebook/src/mqtt.h +++ b/protocols/Facebook/src/mqtt.h @@ -1,139 +1,139 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -#define FACEBOOK_ORCA_AGENT FB_API_MQTT_AGENT - -#define FB_THRIFT_TYPE_STOP 0 -#define FB_THRIFT_TYPE_VOID 1 -#define FB_THRIFT_TYPE_BOOL 2 -#define FB_THRIFT_TYPE_BYTE 3 -#define FB_THRIFT_TYPE_DOUBLE 4 -#define FB_THRIFT_TYPE_I16 6 -#define FB_THRIFT_TYPE_I32 8 -#define FB_THRIFT_TYPE_I64 10 -#define FB_THRIFT_TYPE_STRING 11 -#define FB_THRIFT_TYPE_STRUCT 12 -#define FB_THRIFT_TYPE_MAP 13 -#define FB_THRIFT_TYPE_SET 14 -#define FB_THRIFT_TYPE_LIST 15 - -enum FbMqttMessageType -{ - FB_MQTT_MESSAGE_TYPE_CONNECT = 1, - FB_MQTT_MESSAGE_TYPE_CONNACK = 2, - FB_MQTT_MESSAGE_TYPE_PUBLISH = 3, - FB_MQTT_MESSAGE_TYPE_PUBACK = 4, - FB_MQTT_MESSAGE_TYPE_PUBREC = 5, - FB_MQTT_MESSAGE_TYPE_PUBREL = 6, - FB_MQTT_MESSAGE_TYPE_PUBCOMP = 7, - FB_MQTT_MESSAGE_TYPE_SUBSCRIBE = 8, - FB_MQTT_MESSAGE_TYPE_SUBACK = 9, - FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE = 10, - FB_MQTT_MESSAGE_TYPE_UNSUBACK = 11, - FB_MQTT_MESSAGE_TYPE_PINGREQ = 12, - FB_MQTT_MESSAGE_TYPE_PINGRESP = 13, - FB_MQTT_MESSAGE_TYPE_DISCONNECT = 14 -}; - -class FbThrift -{ - MBinBuffer m_buf; - -public: - __forceinline void* data() const { return m_buf.data(); } - __forceinline size_t size() const { return m_buf.length(); } - - __forceinline void reset(size_t cbLen, void *pData) - { m_buf.assign(pData, cbLen); - } - - FbThrift& operator<<(uint8_t); - FbThrift& operator<<(const char *); - - void writeBool(bool value); - void writeBuf(const void *pData, size_t cbLen); - void writeInt16(uint16_t value); - void writeInt32(int32_t value); - void writeInt64(int64_t value); - void writeIntV(uint64_t value); - void writeField(int type); - void writeField(int type, int id, int lastid); - void writeList(int iType, int size); -}; - -class FbThriftReader : public FbThrift -{ - bool m_lastBool = false, m_lastBval = false; - size_t offset = 0; - - uint8_t decodeType(int type); - -public: - __forceinline CMStringA rest() const { return CMStringA((char*)data() + offset, int(size() - offset)); } - - bool isStop(); - bool readBool(bool &bVal); - bool readByte(uint8_t &val); - bool readField(uint8_t &type, uint16_t &id); - bool readInt16(uint16_t &val); - bool readInt32(uint32_t &val); - bool readInt64(uint64_t &val); - bool readIntV(uint64_t &val); - bool readList(uint8_t &type, uint32_t &size); - bool readStr(char *&val); // val must be freed via mir_free() -}; - -class MqttMessage : public FbThrift -{ - friend class FacebookProto; - - uint8_t m_leadingByte; - -public: - MqttMessage(); - MqttMessage(FbMqttMessageType type, uint8_t flags = 0); - - __forceinline int getType() const - { - return m_leadingByte >> 4; - } - - __forceinline int getFlags() const - { return m_leadingByte & 0x0F; - } - - char* readStr(const uint8_t *&pData) const; - void writeStr(const char *str); -}; - -#define FB_MQTT_CONNECT_FLAG_CLR 0x0002 -#define FB_MQTT_CONNECT_FLAG_WILL 0x0004 -#define FB_MQTT_CONNECT_FLAG_QOS0 0x0000 -#define FB_MQTT_CONNECT_FLAG_QOS1 0x0008 -#define FB_MQTT_CONNECT_FLAG_QOS2 0x0010 -#define FB_MQTT_CONNECT_FLAG_RET 0x0020 -#define FB_MQTT_CONNECT_FLAG_PASS 0x0040 -#define FB_MQTT_CONNECT_FLAG_USER 0x0080 - -#define FB_MQTT_MESSAGE_FLAG_QOS0 0x0000 -#define FB_MQTT_MESSAGE_FLAG_QOS1 0x0002 -#define FB_MQTT_MESSAGE_FLAG_QOS2 0x0004 +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +#define FACEBOOK_ORCA_AGENT FB_API_MQTT_AGENT + +#define FB_THRIFT_TYPE_STOP 0 +#define FB_THRIFT_TYPE_VOID 1 +#define FB_THRIFT_TYPE_BOOL 2 +#define FB_THRIFT_TYPE_BYTE 3 +#define FB_THRIFT_TYPE_DOUBLE 4 +#define FB_THRIFT_TYPE_I16 6 +#define FB_THRIFT_TYPE_I32 8 +#define FB_THRIFT_TYPE_I64 10 +#define FB_THRIFT_TYPE_STRING 11 +#define FB_THRIFT_TYPE_STRUCT 12 +#define FB_THRIFT_TYPE_MAP 13 +#define FB_THRIFT_TYPE_SET 14 +#define FB_THRIFT_TYPE_LIST 15 + +enum FbMqttMessageType +{ + FB_MQTT_MESSAGE_TYPE_CONNECT = 1, + FB_MQTT_MESSAGE_TYPE_CONNACK = 2, + FB_MQTT_MESSAGE_TYPE_PUBLISH = 3, + FB_MQTT_MESSAGE_TYPE_PUBACK = 4, + FB_MQTT_MESSAGE_TYPE_PUBREC = 5, + FB_MQTT_MESSAGE_TYPE_PUBREL = 6, + FB_MQTT_MESSAGE_TYPE_PUBCOMP = 7, + FB_MQTT_MESSAGE_TYPE_SUBSCRIBE = 8, + FB_MQTT_MESSAGE_TYPE_SUBACK = 9, + FB_MQTT_MESSAGE_TYPE_UNSUBSCRIBE = 10, + FB_MQTT_MESSAGE_TYPE_UNSUBACK = 11, + FB_MQTT_MESSAGE_TYPE_PINGREQ = 12, + FB_MQTT_MESSAGE_TYPE_PINGRESP = 13, + FB_MQTT_MESSAGE_TYPE_DISCONNECT = 14 +}; + +class FbThrift +{ + MBinBuffer m_buf; + +public: + __forceinline void* data() const { return m_buf.data(); } + __forceinline size_t size() const { return m_buf.length(); } + + __forceinline void reset(size_t cbLen, void *pData) + { m_buf.assign(pData, cbLen); + } + + FbThrift& operator<<(uint8_t); + FbThrift& operator<<(const char *); + + void writeBool(bool value); + void writeBuf(const void *pData, size_t cbLen); + void writeInt16(uint16_t value); + void writeInt32(int32_t value); + void writeInt64(int64_t value); + void writeIntV(uint64_t value); + void writeField(int type); + void writeField(int type, int id, int lastid); + void writeList(int iType, int size); +}; + +class FbThriftReader : public FbThrift +{ + bool m_lastBool = false, m_lastBval = false; + size_t offset = 0; + + uint8_t decodeType(int type); + +public: + __forceinline CMStringA rest() const { return CMStringA((char*)data() + offset, int(size() - offset)); } + + bool isStop(); + bool readBool(bool &bVal); + bool readByte(uint8_t &val); + bool readField(uint8_t &type, uint16_t &id); + bool readInt16(uint16_t &val); + bool readInt32(uint32_t &val); + bool readInt64(uint64_t &val); + bool readIntV(uint64_t &val); + bool readList(uint8_t &type, uint32_t &size); + bool readStr(char *&val); // val must be freed via mir_free() +}; + +class MqttMessage : public FbThrift +{ + friend class FacebookProto; + + uint8_t m_leadingByte; + +public: + MqttMessage(); + MqttMessage(FbMqttMessageType type, uint8_t flags = 0); + + __forceinline int getType() const + { + return m_leadingByte >> 4; + } + + __forceinline int getFlags() const + { return m_leadingByte & 0x0F; + } + + char* readStr(const uint8_t *&pData) const; + void writeStr(const char *str); +}; + +#define FB_MQTT_CONNECT_FLAG_CLR 0x0002 +#define FB_MQTT_CONNECT_FLAG_WILL 0x0004 +#define FB_MQTT_CONNECT_FLAG_QOS0 0x0000 +#define FB_MQTT_CONNECT_FLAG_QOS1 0x0008 +#define FB_MQTT_CONNECT_FLAG_QOS2 0x0010 +#define FB_MQTT_CONNECT_FLAG_RET 0x0020 +#define FB_MQTT_CONNECT_FLAG_PASS 0x0040 +#define FB_MQTT_CONNECT_FLAG_USER 0x0080 + +#define FB_MQTT_MESSAGE_FLAG_QOS0 0x0000 +#define FB_MQTT_MESSAGE_FLAG_QOS1 0x0002 +#define FB_MQTT_MESSAGE_FLAG_QOS2 0x0004 diff --git a/protocols/Facebook/src/options.cpp b/protocols/Facebook/src/options.cpp index 0d8c1eb38a..2ed98b1dfc 100644 --- a/protocols/Facebook/src/options.cpp +++ b/protocols/Facebook/src/options.cpp @@ -1,85 +1,85 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -class CFacebookAccOptsDlg : public CFBDlgBase -{ - CCtrlEdit edtGroup; - CCtrlCheck chkEnableChats, chkHideChats, chkKeepUnread, chkLoginInvis, chkLoadAll; - CCtrlHyperlink linkMainPage; - -public: - CFacebookAccOptsDlg(FacebookProto *pThis) : - CFBDlgBase(pThis, IDD_OPTIONS), - edtGroup(this, IDC_GROUP), - chkLoadAll(this, IDC_LOADALL), - chkHideChats(this, IDC_HIDECHATS), - chkKeepUnread(this, IDC_KEEP_UNREAD), - chkLoginInvis(this, IDC_INVIS_LOGIN), - chkEnableChats(this, IDC_ENABLECHATS), - linkMainPage(this, IDC_NEWACCOUNTLINK, "https://www.facebook.com") - { - CreateLink(edtGroup, pThis->m_wszDefaultGroup); - CreateLink(chkLoadAll, pThis->m_bLoadAll); - CreateLink(chkHideChats, pThis->m_bHideGroupchats); - CreateLink(chkKeepUnread, pThis->m_bKeepUnread); - CreateLink(chkLoginInvis, pThis->m_bLoginInvisible); - CreateLink(chkEnableChats, pThis->m_bUseGroupchats); - } - - bool OnInitDialog() override - { - ptrA login(m_proto->getStringA(DBKEY_LOGIN)); - if (login != nullptr) - SetDlgItemTextA(m_hwnd, IDC_UN, login); - - ptrA password(m_proto->getStringA(DBKEY_PASS)); - if (password != nullptr) - SetDlgItemTextA(m_hwnd, IDC_PW, password); - return true; - } - - bool OnApply() override - { - char str[128]; - - GetDlgItemTextA(m_hwnd, IDC_UN, str, _countof(str)); - m_proto->setString(DBKEY_LOGIN, str); - - GetDlgItemTextA(m_hwnd, IDC_PW, str, _countof(str)); - m_proto->setString(DBKEY_PASS, str); - return true; - } -}; - -int FacebookProto::OnOptionsInit(WPARAM wParam, LPARAM) -{ - OPTIONSDIALOGPAGE odp = {}; - odp.position = -790000000; - odp.szTitle.w = m_tszUserName; - odp.szGroup.w = LPGENW("Network"); - odp.flags = ODPF_UNICODE; - - odp.szTab.w = LPGENW("Account"); - odp.pDialog = new CFacebookAccOptsDlg(this); - g_plugin.addOptions(wParam, &odp); - return 0; +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +class CFacebookAccOptsDlg : public CFBDlgBase +{ + CCtrlEdit edtGroup; + CCtrlCheck chkEnableChats, chkHideChats, chkKeepUnread, chkLoginInvis, chkLoadAll; + CCtrlHyperlink linkMainPage; + +public: + CFacebookAccOptsDlg(FacebookProto *pThis) : + CFBDlgBase(pThis, IDD_OPTIONS), + edtGroup(this, IDC_GROUP), + chkLoadAll(this, IDC_LOADALL), + chkHideChats(this, IDC_HIDECHATS), + chkKeepUnread(this, IDC_KEEP_UNREAD), + chkLoginInvis(this, IDC_INVIS_LOGIN), + chkEnableChats(this, IDC_ENABLECHATS), + linkMainPage(this, IDC_NEWACCOUNTLINK, "https://www.facebook.com") + { + CreateLink(edtGroup, pThis->m_wszDefaultGroup); + CreateLink(chkLoadAll, pThis->m_bLoadAll); + CreateLink(chkHideChats, pThis->m_bHideGroupchats); + CreateLink(chkKeepUnread, pThis->m_bKeepUnread); + CreateLink(chkLoginInvis, pThis->m_bLoginInvisible); + CreateLink(chkEnableChats, pThis->m_bUseGroupchats); + } + + bool OnInitDialog() override + { + ptrA login(m_proto->getStringA(DBKEY_LOGIN)); + if (login != nullptr) + SetDlgItemTextA(m_hwnd, IDC_UN, login); + + ptrA password(m_proto->getStringA(DBKEY_PASS)); + if (password != nullptr) + SetDlgItemTextA(m_hwnd, IDC_PW, password); + return true; + } + + bool OnApply() override + { + char str[128]; + + GetDlgItemTextA(m_hwnd, IDC_UN, str, _countof(str)); + m_proto->setString(DBKEY_LOGIN, str); + + GetDlgItemTextA(m_hwnd, IDC_PW, str, _countof(str)); + m_proto->setString(DBKEY_PASS, str); + return true; + } +}; + +int FacebookProto::OnOptionsInit(WPARAM wParam, LPARAM) +{ + OPTIONSDIALOGPAGE odp = {}; + odp.position = -790000000; + odp.szTitle.w = m_tszUserName; + odp.szGroup.w = LPGENW("Network"); + odp.flags = ODPF_UNICODE; + + odp.szTab.w = LPGENW("Account"); + odp.pDialog = new CFacebookAccOptsDlg(this); + g_plugin.addOptions(wParam, &odp); + return 0; } \ No newline at end of file diff --git a/protocols/Facebook/src/proto.cpp b/protocols/Facebook/src/proto.cpp index 7a1d8b1c93..db2d401e21 100644 --- a/protocols/Facebook/src/proto.cpp +++ b/protocols/Facebook/src/proto.cpp @@ -1,302 +1,302 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -static int CompareUsers(const FacebookUser *p1, const FacebookUser *p2) -{ - if (p1->id == p2->id) - return 0; - - return (p1->id < p2->id) ? -1 : 1; -} - -static int CompareMessages(const COwnMessage *p1, const COwnMessage *p2) -{ - if (p1->msgId == p2->msgId) - return 0; - - return (p1->msgId < p2->msgId) ? -1 : 1; -} - -FacebookProto::FacebookProto(const char *proto_name, const wchar_t *username) : - PROTO(proto_name, username), - m_impl(*this), - m_users(50, CompareUsers), - arOwnMessages(1, CompareMessages), - m_bLoadAll(this, "LoadAllContacts", false), - m_bKeepUnread(this, "KeepUnread", false), - m_bUseBigAvatars(this, "UseBigAvatars", true), - m_bUseGroupchats(this, "UseGroupChats", true), - m_bHideGroupchats(this, "HideGroupChats", true), - m_bLoginInvisible(this, "LoginInvisible", false), - m_wszDefaultGroup(this, "DefaultGroup", L"Facebook") -{ - // to upgrade previous settings - if (getByte("Compatibility") < 1) { - setByte("Compatibility", 1); - delSetting(DBKEY_DEVICE_ID); - } - - m_szDeviceID = getMStringA(DBKEY_DEVICE_ID); - if (m_szDeviceID.IsEmpty()) { - UUID deviceId; - UuidCreate(&deviceId); - RPC_CSTR szId; - UuidToStringA(&deviceId, &szId); - m_szDeviceID = szId; - setString(DBKEY_DEVICE_ID, m_szDeviceID); - RpcStringFreeA(&szId); - } - - m_szClientID = getMStringA(DBKEY_CLIENT_ID); - if (m_szClientID.IsEmpty()) { - for (int i = 0; i < 20; i++) { - uint32_t dwRandon; - Utils_GetRandom(&dwRandon, sizeof(dwRandon)); - int c = dwRandon % 62; - if (c >= 0 && c < 26) - c += 'a'; - else if (c >= 26 && c < 52) - c += 'A' - 26; - else if (c >= 52 && c < 62) - c += '0' - 52; - m_szClientID.AppendChar(c); - } - setString(DBKEY_CLIENT_ID, m_szClientID); - } - - m_uid = _atoi64(getMStringA(DBKEY_ID)); - m_sid = _atoi64(getMStringA(DBKEY_SID)); - m_szSyncToken = getMStringA(DBKEY_SYNC_TOKEN); - - // Create standard network connection - NETLIBUSER nlu = {}; - nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; - nlu.szSettingsModule = m_szModuleName; - nlu.szDescriptiveName.w = m_tszUserName; - m_hNetlibUser = Netlib_RegisterUser(&nlu); - - db_set_resident(m_szModuleName, "UpdateNeeded"); - - // Services - CreateProtoService(PS_CREATEACCMGRUI, &FacebookProto::SvcCreateAccMgrUI); - CreateProtoService(PS_GETAVATARINFO, &FacebookProto::GetAvatarInfo); - CreateProtoService(PS_GETAVATARCAPS, &FacebookProto::GetAvatarCaps); - - // Events - HookProtoEvent(ME_GC_EVENT, &FacebookProto::GroupchatEventHook); - HookProtoEvent(ME_GC_BUILDMENU, &FacebookProto::GroupchatMenuHook); - HookProtoEvent(ME_OPT_INITIALISE, &FacebookProto::OnOptionsInit); - HookProtoEvent(ME_DB_EVENT_MARKED_READ, &FacebookProto::OnMarkedRead); - - // Group chats - GCREGISTER gcr = {}; - gcr.dwFlags = GC_TYPNOTIF; - gcr.ptszDispName = m_tszUserName; - gcr.pszModule = m_szModuleName; - Chat_Register(&gcr); -} - -FacebookProto::~FacebookProto() -{ -} - -///////////////////////////////////////////////////////////////////////////////////////// -// protocol events - -void FacebookProto::OnContactAdded(MCONTACT hContact) -{ - __int64 userId = _atoi64(getMStringA(hContact, DBKEY_ID)); - if (userId && !FindUser(userId)) { - mir_cslock lck(m_csUsers); - m_users.insert(new FacebookUser(userId, hContact)); - } -} - -void FacebookProto::OnModulesLoaded() -{ - VARSW wszCache(L"%miranda_avatarcache%"); - - CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", wszCache.get(), m_szModuleName); - SMADD_CONT cont = { 2, m_szModuleName, wszPath }; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); - - wszPath.Format(L"%s\\%S\\Stickers\\*.webp", wszCache.get(), m_szModuleName); - cont.path = wszPath; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); - - // contacts cache - for (auto &cc : AccContacts()) { - CMStringA szId(getMStringA(cc, DBKEY_ID)); - if (!szId.IsEmpty()) - m_users.insert(new FacebookUser(_atoi64(szId), cc, isChatRoom(cc))); - } - - // Default group - Clist_GroupCreate(0, m_wszDefaultGroup); -} - -void FacebookProto::OnShutdown() -{ - if (m_mqttConn != nullptr) - Netlib_Shutdown(m_mqttConn); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MCONTACT FacebookProto::AddToList(int, PROTOSEARCHRESULT *psr) -{ - if (!mir_wstrlen(psr->id.w)) - return 0; - - if (auto *pUser = FindUser(_wtoi64(psr->id.w))) - return pUser->hContact; - - MCONTACT hContact = db_add_contact(); - setWString(hContact, DBKEY_ID, psr->id.w); - Proto_AddToContact(hContact, m_szModuleName); - return hContact; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR FacebookProto::GetCaps(int type, MCONTACT) -{ - switch (type) { - case PFLAGNUM_1: - { - DWORD_PTR flags = PF1_IM | PF1_CHAT | PF1_SERVERCLIST | PF1_AUTHREQ; - - if (getByte(DBKEY_SET_MIRANDA_STATUS)) - return flags |= PF1_MODEMSG; - else - return flags |= PF1_MODEMSGRECV; - } - - case PFLAGNUM_2: - return PF2_ONLINE | PF2_SHORTAWAY | PF2_INVISIBLE | PF2_IDLE; - - case PFLAGNUM_3: - if (getByte(DBKEY_SET_MIRANDA_STATUS)) - return PF2_ONLINE; // | PF2_SHORTAWAY; - else - return 0; - - case PFLAGNUM_4: - return PF4_NOCUSTOMAUTH | PF4_AVATARS | PF4_SUPPORTTYPING | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE | PF4_READNOTIFY; - - case PFLAG_MAXLENOFMESSAGE: - return FACEBOOK_MESSAGE_LIMIT; - - case PFLAG_UNIQUEIDTEXT: - return (INT_PTR) L"Facebook ID"; - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int FacebookProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) -{ - if (!m_bOnline) { - ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)1, (LPARAM)TranslateT("Protocol is offline or user isn't authorized yet")); - return 1; - } - - CMStringA userId(getMStringA(hContact, DBKEY_ID)); - - __int64 msgId; - Utils_GetRandom(&msgId, sizeof(msgId)); - msgId = abs(msgId); - - JSONNode root; root << CHAR_PARAM("body", pszSrc) << INT64_PARAM("msgid", msgId) << INT64_PARAM("sender_fbid", m_uid) << CHAR_PARAM("to", userId); - MqttPublish("/send_message2", root); - - mir_cslock lck(m_csOwnMessages); - arOwnMessages.insert(new COwnMessage(msgId, m_mid, hContact)); - return m_mid; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int FacebookProto::SetStatus(int iNewStatus) -{ - if (iNewStatus != ID_STATUS_OFFLINE && IsStatusConnecting(m_iStatus)) { - debugLogA("=== Status is already connecting, no change"); - return 0; - } - - // Routing statuses not supported by Facebook - switch (iNewStatus) { - case ID_STATUS_ONLINE: - case ID_STATUS_OFFLINE: - break; - - default: - iNewStatus = ID_STATUS_AWAY; - break; - } - - if (m_iStatus == iNewStatus) { - debugLogA("=== Statuses are same, no change"); - return 0; - } - - m_iDesiredStatus = iNewStatus; - - int iOldStatus = m_iStatus; - - // log off & free all resources - if (iNewStatus == ID_STATUS_OFFLINE) { - OnShutdown(); - - m_iStatus = ID_STATUS_OFFLINE; - } - else if (m_iStatus == ID_STATUS_OFFLINE) { // we gonna connect - debugLogA("*** Beginning SignOn process"); - - m_iStatus = ID_STATUS_CONNECTING; - - ForkThread(&FacebookProto::ServerThread); - } - else m_iStatus = iNewStatus; - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - return 0; -} - -////////////////////////////////////////////////////////////////////////////// - -int FacebookProto::UserIsTyping(MCONTACT hContact, int type) -{ - JSONNode root; root << INT_PARAM("state", type == PROTOTYPE_SELFTYPING_ON) << CHAR_PARAM("to", getMStringA(hContact, DBKEY_ID)); - MqttPublish("/typing", root); - return 0; -} - -////////////////////////////////////////////////////////////////////////////// -// Services - -INT_PTR FacebookProto::SvcCreateAccMgrUI(WPARAM, LPARAM lParam) -{ - return (INT_PTR) CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_FACEBOOKACCOUNT), - (HWND) lParam, FBAccountProc, (LPARAM) this); -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +static int CompareUsers(const FacebookUser *p1, const FacebookUser *p2) +{ + if (p1->id == p2->id) + return 0; + + return (p1->id < p2->id) ? -1 : 1; +} + +static int CompareMessages(const COwnMessage *p1, const COwnMessage *p2) +{ + if (p1->msgId == p2->msgId) + return 0; + + return (p1->msgId < p2->msgId) ? -1 : 1; +} + +FacebookProto::FacebookProto(const char *proto_name, const wchar_t *username) : + PROTO(proto_name, username), + m_impl(*this), + m_users(50, CompareUsers), + arOwnMessages(1, CompareMessages), + m_bLoadAll(this, "LoadAllContacts", false), + m_bKeepUnread(this, "KeepUnread", false), + m_bUseBigAvatars(this, "UseBigAvatars", true), + m_bUseGroupchats(this, "UseGroupChats", true), + m_bHideGroupchats(this, "HideGroupChats", true), + m_bLoginInvisible(this, "LoginInvisible", false), + m_wszDefaultGroup(this, "DefaultGroup", L"Facebook") +{ + // to upgrade previous settings + if (getByte("Compatibility") < 1) { + setByte("Compatibility", 1); + delSetting(DBKEY_DEVICE_ID); + } + + m_szDeviceID = getMStringA(DBKEY_DEVICE_ID); + if (m_szDeviceID.IsEmpty()) { + UUID deviceId; + UuidCreate(&deviceId); + RPC_CSTR szId; + UuidToStringA(&deviceId, &szId); + m_szDeviceID = szId; + setString(DBKEY_DEVICE_ID, m_szDeviceID); + RpcStringFreeA(&szId); + } + + m_szClientID = getMStringA(DBKEY_CLIENT_ID); + if (m_szClientID.IsEmpty()) { + for (int i = 0; i < 20; i++) { + uint32_t dwRandon; + Utils_GetRandom(&dwRandon, sizeof(dwRandon)); + int c = dwRandon % 62; + if (c >= 0 && c < 26) + c += 'a'; + else if (c >= 26 && c < 52) + c += 'A' - 26; + else if (c >= 52 && c < 62) + c += '0' - 52; + m_szClientID.AppendChar(c); + } + setString(DBKEY_CLIENT_ID, m_szClientID); + } + + m_uid = _atoi64(getMStringA(DBKEY_ID)); + m_sid = _atoi64(getMStringA(DBKEY_SID)); + m_szSyncToken = getMStringA(DBKEY_SYNC_TOKEN); + + // Create standard network connection + NETLIBUSER nlu = {}; + nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szSettingsModule = m_szModuleName; + nlu.szDescriptiveName.w = m_tszUserName; + m_hNetlibUser = Netlib_RegisterUser(&nlu); + + db_set_resident(m_szModuleName, "UpdateNeeded"); + + // Services + CreateProtoService(PS_CREATEACCMGRUI, &FacebookProto::SvcCreateAccMgrUI); + CreateProtoService(PS_GETAVATARINFO, &FacebookProto::GetAvatarInfo); + CreateProtoService(PS_GETAVATARCAPS, &FacebookProto::GetAvatarCaps); + + // Events + HookProtoEvent(ME_GC_EVENT, &FacebookProto::GroupchatEventHook); + HookProtoEvent(ME_GC_BUILDMENU, &FacebookProto::GroupchatMenuHook); + HookProtoEvent(ME_OPT_INITIALISE, &FacebookProto::OnOptionsInit); + HookProtoEvent(ME_DB_EVENT_MARKED_READ, &FacebookProto::OnMarkedRead); + + // Group chats + GCREGISTER gcr = {}; + gcr.dwFlags = GC_TYPNOTIF; + gcr.ptszDispName = m_tszUserName; + gcr.pszModule = m_szModuleName; + Chat_Register(&gcr); +} + +FacebookProto::~FacebookProto() +{ +} + +///////////////////////////////////////////////////////////////////////////////////////// +// protocol events + +void FacebookProto::OnContactAdded(MCONTACT hContact) +{ + __int64 userId = _atoi64(getMStringA(hContact, DBKEY_ID)); + if (userId && !FindUser(userId)) { + mir_cslock lck(m_csUsers); + m_users.insert(new FacebookUser(userId, hContact)); + } +} + +void FacebookProto::OnModulesLoaded() +{ + VARSW wszCache(L"%miranda_avatarcache%"); + + CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", wszCache.get(), m_szModuleName); + SMADD_CONT cont = { 2, m_szModuleName, wszPath }; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); + + wszPath.Format(L"%s\\%S\\Stickers\\*.webp", wszCache.get(), m_szModuleName); + cont.path = wszPath; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); + + // contacts cache + for (auto &cc : AccContacts()) { + CMStringA szId(getMStringA(cc, DBKEY_ID)); + if (!szId.IsEmpty()) + m_users.insert(new FacebookUser(_atoi64(szId), cc, isChatRoom(cc))); + } + + // Default group + Clist_GroupCreate(0, m_wszDefaultGroup); +} + +void FacebookProto::OnShutdown() +{ + if (m_mqttConn != nullptr) + Netlib_Shutdown(m_mqttConn); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MCONTACT FacebookProto::AddToList(int, PROTOSEARCHRESULT *psr) +{ + if (!mir_wstrlen(psr->id.w)) + return 0; + + if (auto *pUser = FindUser(_wtoi64(psr->id.w))) + return pUser->hContact; + + MCONTACT hContact = db_add_contact(); + setWString(hContact, DBKEY_ID, psr->id.w); + Proto_AddToContact(hContact, m_szModuleName); + return hContact; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR FacebookProto::GetCaps(int type, MCONTACT) +{ + switch (type) { + case PFLAGNUM_1: + { + DWORD_PTR flags = PF1_IM | PF1_CHAT | PF1_SERVERCLIST | PF1_AUTHREQ; + + if (getByte(DBKEY_SET_MIRANDA_STATUS)) + return flags |= PF1_MODEMSG; + else + return flags |= PF1_MODEMSGRECV; + } + + case PFLAGNUM_2: + return PF2_ONLINE | PF2_SHORTAWAY | PF2_INVISIBLE | PF2_IDLE; + + case PFLAGNUM_3: + if (getByte(DBKEY_SET_MIRANDA_STATUS)) + return PF2_ONLINE; // | PF2_SHORTAWAY; + else + return 0; + + case PFLAGNUM_4: + return PF4_NOCUSTOMAUTH | PF4_AVATARS | PF4_SUPPORTTYPING | PF4_NOAUTHDENYREASON | PF4_IMSENDOFFLINE | PF4_READNOTIFY; + + case PFLAG_MAXLENOFMESSAGE: + return FACEBOOK_MESSAGE_LIMIT; + + case PFLAG_UNIQUEIDTEXT: + return (INT_PTR) L"Facebook ID"; + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int FacebookProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) +{ + if (!m_bOnline) { + ProtoBroadcastAsync(hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)1, (LPARAM)TranslateT("Protocol is offline or user isn't authorized yet")); + return 1; + } + + CMStringA userId(getMStringA(hContact, DBKEY_ID)); + + __int64 msgId; + Utils_GetRandom(&msgId, sizeof(msgId)); + msgId = abs(msgId); + + JSONNode root; root << CHAR_PARAM("body", pszSrc) << INT64_PARAM("msgid", msgId) << INT64_PARAM("sender_fbid", m_uid) << CHAR_PARAM("to", userId); + MqttPublish("/send_message2", root); + + mir_cslock lck(m_csOwnMessages); + arOwnMessages.insert(new COwnMessage(msgId, m_mid, hContact)); + return m_mid; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int FacebookProto::SetStatus(int iNewStatus) +{ + if (iNewStatus != ID_STATUS_OFFLINE && IsStatusConnecting(m_iStatus)) { + debugLogA("=== Status is already connecting, no change"); + return 0; + } + + // Routing statuses not supported by Facebook + switch (iNewStatus) { + case ID_STATUS_ONLINE: + case ID_STATUS_OFFLINE: + break; + + default: + iNewStatus = ID_STATUS_AWAY; + break; + } + + if (m_iStatus == iNewStatus) { + debugLogA("=== Statuses are same, no change"); + return 0; + } + + m_iDesiredStatus = iNewStatus; + + int iOldStatus = m_iStatus; + + // log off & free all resources + if (iNewStatus == ID_STATUS_OFFLINE) { + OnShutdown(); + + m_iStatus = ID_STATUS_OFFLINE; + } + else if (m_iStatus == ID_STATUS_OFFLINE) { // we gonna connect + debugLogA("*** Beginning SignOn process"); + + m_iStatus = ID_STATUS_CONNECTING; + + ForkThread(&FacebookProto::ServerThread); + } + else m_iStatus = iNewStatus; + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + return 0; +} + +////////////////////////////////////////////////////////////////////////////// + +int FacebookProto::UserIsTyping(MCONTACT hContact, int type) +{ + JSONNode root; root << INT_PARAM("state", type == PROTOTYPE_SELFTYPING_ON) << CHAR_PARAM("to", getMStringA(hContact, DBKEY_ID)); + MqttPublish("/typing", root); + return 0; +} + +////////////////////////////////////////////////////////////////////////////// +// Services + +INT_PTR FacebookProto::SvcCreateAccMgrUI(WPARAM, LPARAM lParam) +{ + return (INT_PTR) CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_FACEBOOKACCOUNT), + (HWND) lParam, FBAccountProc, (LPARAM) this); +} diff --git a/protocols/Facebook/src/proto.h b/protocols/Facebook/src/proto.h index 14dc343d18..7fbeebd2cb 100644 --- a/protocols/Facebook/src/proto.h +++ b/protocols/Facebook/src/proto.h @@ -1,557 +1,557 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - - -/** - * FB_API_AHOST: - * - * The HTTP host for the Facebook API. - */ -#define FB_API_AHOST "https://api.facebook.com" - -/** - * FB_API_BHOST: - * - * The HTTP host for the Facebook BAPI. - */ -#define FB_API_BHOST "https://b-api.facebook.com" - -/** - * FB_API_GHOST: - * - * The HTTP host for the Facebook Graph API. - */ -#define FB_API_GHOST "https://graph.facebook.com" - -/** - * FB_API_WHOST: - * - * The HTTP host for the Facebook website. - */ -#define FB_API_WHOST "https://www.facebook.com" - -/** - * FB_API_FBRPC_PREFIX - * - * The fbrpc URL prefix used in links shared from the mobile app. - */ -#define FB_API_FBRPC_PREFIX "fbrpc://facebook/nativethirdparty" - -/** - * FB_API_KEY: - * - * The Facebook API key. - */ -#define FB_API_KEY "256002347743983" - -/** - * FB_API_SECRET: - * - * The Facebook API secret. - */ -#define FB_API_SECRET "374e60f8b9bb6b8cbb30f78030438895" - -/** - * FB_ORCA_AGENT - * - * The part of the user agent that looks like the official client, since the - * server started checking this. - */ - -#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/537.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" - -/** - * FB_API_AGENT: - * - * The HTTP User-Agent header. - */ -#define FB_API_AGENT "Facebook plugin / Purple / 0.9.6 " FB_ORCA_AGENT - -/** - * FB_API_MQTT_AGENT - * - * The client information string sent in the MQTT CONNECT message - */ - -#define FB_API_MQTT_AGENT FB_API_AGENT - -/** - * FB_API_URL_ATTACH: - * - * The URL for attachment URL requests. - */ -#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.getAttachment" -//#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.attachmentRedirect" - -/** - * FB_API_URL_AUTH: - * - * The URL for authentication requests. - */ -#define FB_API_URL_AUTH FB_API_BHOST "/method/auth.login" - -/** - * FB_API_URL_GQL: - * - * The URL for GraphQL requests. - */ -#define FB_API_URL_GQL FB_API_GHOST "/graphql" - -/** - * FB_API_URL_MESSAGES: - * - * The URL for linking message threads. - */ -#define FB_API_URL_MESSAGES FB_API_WHOST "/messages" - -/** - * FB_API_URL_PARTS: - * - * The URL for participant management requests. - */ -#define FB_API_URL_PARTS FB_API_GHOST "/participants" - -/** - * FB_API_URL_THREADS: - * - * The URL for thread management requests. - */ -#define FB_API_URL_THREADS FB_API_GHOST "/me/group_threads" - -/** - * FB_API_URL_TOPIC: - * - * The URL for thread topic requests. - */ -#define FB_API_URL_TOPIC FB_API_AHOST "/method/messaging.setthreadname" - -/** - * FB_API_QUERY_CONTACT: - * - * The query hash for the `UsersQuery`. - * - * Key mapping: - * 0: user_fbids - * 1: include_full_user_info - * 2: profile_pic_large_size - * 3: profile_pic_medium_size - * 4: profile_pic_small_size - */ -#define FB_API_QUERY_CONTACT 10153915107411729 - -/** - * FB_API_QUERY_CONTACTS: - * - * The query hash for the `FetchContactsFullQuery`. - * - * Key mapping: - * 0: profile_types - * 1: limit - * 2: big_img_size - * 3: huge_img_size - * 4: small_img_size - */ -#define FB_API_QUERY_CONTACTS 10154444360806729 - -/** - * FB_API_QUERY_CONTACTS_AFTER: - * - * The query hash for the `FetchContactsFullWithAfterQuery`. - * - * Key mapping: - * 0: profile_types - * 1: after - * 2: limit - * 3: big_img_size - * 4: huge_img_size - * 5: small_img_size - */ -#define FB_API_QUERY_CONTACTS_AFTER 10154444360816729 - - -/** - * FB_API_QUERY_CONTACTS_DELTA: - * - * The query hash for the `FetchContactsDeltaQuery`. - * - * Key mapping: - * 0: after - * 1: profile_types - * 2: limit - * 3: big_img_size - * 4: huge_img_size - * 5: small_img_size - */ -#define FB_API_QUERY_CONTACTS_DELTA 10154444360801729 - -/** - * FB_API_QUERY_STICKER: - * - * The query hash for the `FetchStickersWithPreviewsQuery`. - * - * Key mapping: - * 0: sticker_ids - * 1: media_type - * 2: preview_size - * 3: scaling_factor - * 4: animated_media_type - */ -#define FB_API_QUERY_STICKER 10152877994321729 - -/** - * FB_API_QUERY_THREAD: - * - * The query hash for the `ThreadQuery`. - * - * Key mapping: - * 0: thread_ids - * 1: verification_type - * 2: hash_key - * 3: small_preview_size - * 4: large_preview_size - * 5: item_count - * 6: event_count - * 7: full_screen_height - * 8: full_screen_width - * 9: medium_preview_size - * 10: fetch_users_separately - * 11: include_message_info - * 12: msg_count - * 13: include_full_user_info - * 14: profile_pic_large_size - * 15: profile_pic_medium_size - * 16: profile_pic_small_size - */ -#define FB_API_QUERY_THREAD 10153919752036729 - -/** - * FB_API_QUERY_THREADS: - * - * The query hash for the `ThreadListQuery`. - * - * Key mapping: - * 0: folder_tag (INBOX, PENDING, ARCHIVED, OTHER, UNREAD) - * 1: thread_count (result is sorted from newest to oldest when parameter is sent) - * 2: include_thread_info - * 3: verification_type - * 4: hash_key - * 5: small_preview_size - * 6: large_preview_size - * 7: item_count - * 8: event_count - * 9: full_screen_height - * 10: full_screen_width - * 11: medium_preview_size - * 12: fetch_users_separately - * 13: include_message_info - * 14: msg_count - * 15: UNKNOWN - * 16: profile_pic_large_size - * 17: profile_pic_medium_size - * 18: profile_pic_small_size - */ -#define FB_API_QUERY_THREADS 10153919752026729 - -/** - * FB_API_QUERY_SEQ_ID: - * - * A variant of ThreadListQuery with sequence ID - * - * TODO: parameters. - */ - -#define FB_API_QUERY_SEQ_ID 10155268192741729 - -/** - * FB_API_QUERY_XMA: - * - * The query hash for the `XMAQuery`. - * - * Key mapping: - * 0: xma_id - */ -#define FB_API_QUERY_XMA 10153919431161729 - -/** - * FB_API_CONTACTS_COUNT: - * - * The maximum amount of contacts to fetch in a single request. If this - * value is set too high, HTTP request will fail. This is due to the - * request data being too large. - */ -#define FB_API_CONTACTS_COUNT 500 - -#define FACEBOOK_MESSAGE_LIMIT 100000 - -class FacebookProto; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct AsyncHttpRequest : public MTHttpRequest -{ - struct Param - { - Param(const char *p1, const char *p2) : - key(p1), val(p2) - {} - - CMStringA key, val; - }; - OBJLIST params; - - AsyncHttpRequest(); - - void CalcSig(); -}; - -AsyncHttpRequest *operator<<(AsyncHttpRequest *, const CHAR_PARAM &); -AsyncHttpRequest *operator<<(AsyncHttpRequest *, const INT_PARAM &); - -class JsonReply -{ - JSONNode *m_root = nullptr; - int m_errorCode = 0; - -public: - JsonReply(NETLIBHTTPREQUEST *); - ~JsonReply(); - - __forceinline JSONNode &data() const { return *m_root; } - __forceinline int error() const { return m_errorCode; } -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct FacebookUser -{ - FacebookUser(__int64 _p1, MCONTACT _p2, bool _p3 = false, bool _p4 = false) : - id(_p1), - hContact(_p2), - bIsChat(_p3), - bIsChatInitialized(_p4) - {} - - __int64 id; - MCONTACT hContact; - bool bIsChat; - bool bIsChatInitialized; -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct COwnMessage -{ - __int64 msgId; - int reqId; - MCONTACT hContact; - CMStringW wszText; - - COwnMessage() : - msgId(0), - reqId(0), - hContact(0) - { - } - - COwnMessage(__int64 _id, int _reqId, MCONTACT _hContact) : - msgId(_id), - reqId(_reqId), - hContact(_hContact) - { - } -}; - -class FacebookProto : public PROTO -{ - friend class CGroupchatInviteDlg; - - class FacebookImpl - { - friend class FacebookProto; - - FacebookProto &m_proto; - CTimer m_heartBeat; - - void OnHeartBeat(CTimer *) - { - m_proto.MqttPing(); - } - - FacebookImpl(FacebookProto &pro) : - m_proto(pro), - m_heartBeat(Miranda_GetSystemWindow(), (UINT_PTR)this) - { - m_heartBeat.OnEvent = Callback(this, &FacebookImpl::OnHeartBeat); - } - } m_impl; - - uint8_t *doZip(size_t cbData, const void *pData, size_t &cbRes); - uint8_t *doUnzip(size_t cbData, const void *pData, size_t &cbRes); - - void ConnectionFailed(int iErrorCode = 0); - - AsyncHttpRequest *CreateRequest(const char *szUrl, const char *szName, const char *szMethod); - AsyncHttpRequest *CreateRequestGQL(int64_t id); - NETLIBHTTPREQUEST *ExecuteRequest(AsyncHttpRequest *pReq); - - // Avatars - void __cdecl AvatarsUpdate(void *); - void GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName); - - // Group chats - void Chat_InviteUser(SESSION_INFO *si); - int Chat_KickUser(SESSION_INFO *si, const wchar_t *pwszUid); - void Chat_Leave(SESSION_INFO *si); - void Chat_SendPrivateMessage(GCHOOK *gch); - void Chat_ProcessLogMenu(SESSION_INFO *si, GCHOOK *gch); - void Chat_ProcessNickMenu(SESSION_INFO *si, GCHOOK *gch); - - // MQTT - void MqttLogin(); - - void MqttPing(); - void MqttPublish(const char *topic, const JSONNode &value); - void MqttSubscribe(const char *topic, ...); - void MqttUnsubscribe(const char *topic, ...); - - bool MqttRead(MqttMessage &payload); - bool MqttParse(const MqttMessage &payload); - void MqttSend(const MqttMessage &payload); - - void MqttQueueConnect(); - - void OnPublish(const char *str, const uint8_t *payLoad, size_t cbLen); - void OnPublishMessage(FbThriftReader &rdr); - void OnPublishPresence(FbThriftReader &rdr); - void OnPublishUtn(FbThriftReader &rdr); - - HNETLIBCONN m_mqttConn; - __int64 m_iMqttId; - int16_t m_mid; // MQTT message id - - // internal data - CMStringA m_szDeviceID; // stored, GUID that identifies this miranda's account - CMStringA m_szClientID; // stored, random alphanumeric string of 20 chars - __int64 m_uid; // stored, Facebook user id - - CMStringA m_szSyncToken; // stored, sequence query token - __int64 m_sid; // stored, Facebook sequence id - - int m_iUnread; - bool m_bOnline; - bool m_QueueCreated; - - CMStringA m_szAuthToken; // calculated - - mir_cs m_csOwnMessages; - OBJLIST arOwnMessages; - bool ExtractOwnMessage(__int64 msgId, COwnMessage &res); - - mir_cs m_csUsers; - OBJLIST m_users; - - FacebookUser* FindUser(__int64 id); - - FacebookUser *UserFromJson(const JSONNode &root, CMStringW &wszId, bool &bIsChat); - - bool CheckOwnMessage(FacebookUser *pUser, __int64 offlineId, const char *pszMsgId); - void FetchAttach(const CMStringA &mid, __int64 fbid, CMStringA &szBody); - - void OnLoggedIn(); - void OnLoggedOut(); - - FacebookUser* RefreshThread(JSONNode &n); - FacebookUser* RefreshThread(CMStringW &wszId); - bool RefreshSid(); - int RefreshToken(); - void RefreshThreads(); - int RefreshContacts(); - - FacebookUser *AddContact(const CMStringW &wszId, bool bTemp = true); - - void __cdecl ServerThread(void *); - -public: - FacebookProto(const char *proto_name, const wchar_t *username); - ~FacebookProto(); - - inline const char *ModuleName() const - { - return m_szModuleName; - } - - void OnPublishPrivateMessage(const JSONNode &json); - void OnPublishReadReceipt(const JSONNode &json); - void OnPublishSentMessage(const JSONNode &json); - void OnPublishThreadName(const JSONNode &json); - void OnPublishChatJoin(const JSONNode &json); - void OnPublishChatLeave(const JSONNode &json); - - ////////////////////////////////////////////////////////////////////////////////////// - // options - - CMOption m_wszDefaultGroup; // clist group to store contacts - CMOption m_bUseBigAvatars; // use big or small avatars by default - CMOption m_bUseGroupchats; // do we need group chats at all? - CMOption m_bHideGroupchats; // do not open chat windows on creation - CMOption m_bLoginInvisible; // login in the invisible mode - CMOption m_bKeepUnread; // do not mark incoming messages as read - CMOption m_bLoadAll; // load all contacts, not only those who have ARE_FRIENDS status - - //////////////////////////////////////////////////////////////////////////////////////// - // PROTO_INTERFACE - - void OnContactAdded(MCONTACT) override; - void OnModulesLoaded() override; - void OnShutdown() override; - - MCONTACT AddToList(int flags, PROTOSEARCHRESULT *psr) override; - INT_PTR GetCaps(int type, MCONTACT hContact) override; - int SendMsg(MCONTACT hContact, int flags, const char *pszSrc) override; - int SetStatus(int iNewStatus) override; - int UserIsTyping(MCONTACT hContact, int type) override; - - //////////////////////////////////////////////////////////////////////////////////////// - // Events - - int __cdecl OnMarkedRead(WPARAM, LPARAM); - int __cdecl OnOptionsInit(WPARAM, LPARAM); - - int __cdecl GroupchatMenuHook(WPARAM, LPARAM); - int __cdecl GroupchatEventHook(WPARAM, LPARAM); - - //////////////////////////////////////////////////////////////////////////////////////// - // Services - - INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); - INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); - INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); -}; - -typedef CProtoDlgBase CFBDlgBase; - -struct CMPlugin : public ACCPROTOPLUGIN -{ - CMPlugin(); - - int Load() override; -}; +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + + +/** + * FB_API_AHOST: + * + * The HTTP host for the Facebook API. + */ +#define FB_API_AHOST "https://api.facebook.com" + +/** + * FB_API_BHOST: + * + * The HTTP host for the Facebook BAPI. + */ +#define FB_API_BHOST "https://b-api.facebook.com" + +/** + * FB_API_GHOST: + * + * The HTTP host for the Facebook Graph API. + */ +#define FB_API_GHOST "https://graph.facebook.com" + +/** + * FB_API_WHOST: + * + * The HTTP host for the Facebook website. + */ +#define FB_API_WHOST "https://www.facebook.com" + +/** + * FB_API_FBRPC_PREFIX + * + * The fbrpc URL prefix used in links shared from the mobile app. + */ +#define FB_API_FBRPC_PREFIX "fbrpc://facebook/nativethirdparty" + +/** + * FB_API_KEY: + * + * The Facebook API key. + */ +#define FB_API_KEY "256002347743983" + +/** + * FB_API_SECRET: + * + * The Facebook API secret. + */ +#define FB_API_SECRET "374e60f8b9bb6b8cbb30f78030438895" + +/** + * FB_ORCA_AGENT + * + * The part of the user agent that looks like the official client, since the + * server started checking this. + */ + +#define FB_ORCA_AGENT "[FBAN/Orca-Android;FBAV/537.0.0.31.101;FBPN/com.facebook.orca;FBLC/en_US;FBBV/52182662]" + +/** + * FB_API_AGENT: + * + * The HTTP User-Agent header. + */ +#define FB_API_AGENT "Facebook plugin / Purple / 0.9.6 " FB_ORCA_AGENT + +/** + * FB_API_MQTT_AGENT + * + * The client information string sent in the MQTT CONNECT message + */ + +#define FB_API_MQTT_AGENT FB_API_AGENT + +/** + * FB_API_URL_ATTACH: + * + * The URL for attachment URL requests. + */ +#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.getAttachment" +//#define FB_API_URL_ATTACH FB_API_AHOST "/method/messaging.attachmentRedirect" + +/** + * FB_API_URL_AUTH: + * + * The URL for authentication requests. + */ +#define FB_API_URL_AUTH FB_API_BHOST "/method/auth.login" + +/** + * FB_API_URL_GQL: + * + * The URL for GraphQL requests. + */ +#define FB_API_URL_GQL FB_API_GHOST "/graphql" + +/** + * FB_API_URL_MESSAGES: + * + * The URL for linking message threads. + */ +#define FB_API_URL_MESSAGES FB_API_WHOST "/messages" + +/** + * FB_API_URL_PARTS: + * + * The URL for participant management requests. + */ +#define FB_API_URL_PARTS FB_API_GHOST "/participants" + +/** + * FB_API_URL_THREADS: + * + * The URL for thread management requests. + */ +#define FB_API_URL_THREADS FB_API_GHOST "/me/group_threads" + +/** + * FB_API_URL_TOPIC: + * + * The URL for thread topic requests. + */ +#define FB_API_URL_TOPIC FB_API_AHOST "/method/messaging.setthreadname" + +/** + * FB_API_QUERY_CONTACT: + * + * The query hash for the `UsersQuery`. + * + * Key mapping: + * 0: user_fbids + * 1: include_full_user_info + * 2: profile_pic_large_size + * 3: profile_pic_medium_size + * 4: profile_pic_small_size + */ +#define FB_API_QUERY_CONTACT 10153915107411729 + +/** + * FB_API_QUERY_CONTACTS: + * + * The query hash for the `FetchContactsFullQuery`. + * + * Key mapping: + * 0: profile_types + * 1: limit + * 2: big_img_size + * 3: huge_img_size + * 4: small_img_size + */ +#define FB_API_QUERY_CONTACTS 10154444360806729 + +/** + * FB_API_QUERY_CONTACTS_AFTER: + * + * The query hash for the `FetchContactsFullWithAfterQuery`. + * + * Key mapping: + * 0: profile_types + * 1: after + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_AFTER 10154444360816729 + + +/** + * FB_API_QUERY_CONTACTS_DELTA: + * + * The query hash for the `FetchContactsDeltaQuery`. + * + * Key mapping: + * 0: after + * 1: profile_types + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + */ +#define FB_API_QUERY_CONTACTS_DELTA 10154444360801729 + +/** + * FB_API_QUERY_STICKER: + * + * The query hash for the `FetchStickersWithPreviewsQuery`. + * + * Key mapping: + * 0: sticker_ids + * 1: media_type + * 2: preview_size + * 3: scaling_factor + * 4: animated_media_type + */ +#define FB_API_QUERY_STICKER 10152877994321729 + +/** + * FB_API_QUERY_THREAD: + * + * The query hash for the `ThreadQuery`. + * + * Key mapping: + * 0: thread_ids + * 1: verification_type + * 2: hash_key + * 3: small_preview_size + * 4: large_preview_size + * 5: item_count + * 6: event_count + * 7: full_screen_height + * 8: full_screen_width + * 9: medium_preview_size + * 10: fetch_users_separately + * 11: include_message_info + * 12: msg_count + * 13: include_full_user_info + * 14: profile_pic_large_size + * 15: profile_pic_medium_size + * 16: profile_pic_small_size + */ +#define FB_API_QUERY_THREAD 10153919752036729 + +/** + * FB_API_QUERY_THREADS: + * + * The query hash for the `ThreadListQuery`. + * + * Key mapping: + * 0: folder_tag (INBOX, PENDING, ARCHIVED, OTHER, UNREAD) + * 1: thread_count (result is sorted from newest to oldest when parameter is sent) + * 2: include_thread_info + * 3: verification_type + * 4: hash_key + * 5: small_preview_size + * 6: large_preview_size + * 7: item_count + * 8: event_count + * 9: full_screen_height + * 10: full_screen_width + * 11: medium_preview_size + * 12: fetch_users_separately + * 13: include_message_info + * 14: msg_count + * 15: UNKNOWN + * 16: profile_pic_large_size + * 17: profile_pic_medium_size + * 18: profile_pic_small_size + */ +#define FB_API_QUERY_THREADS 10153919752026729 + +/** + * FB_API_QUERY_SEQ_ID: + * + * A variant of ThreadListQuery with sequence ID + * + * TODO: parameters. + */ + +#define FB_API_QUERY_SEQ_ID 10155268192741729 + +/** + * FB_API_QUERY_XMA: + * + * The query hash for the `XMAQuery`. + * + * Key mapping: + * 0: xma_id + */ +#define FB_API_QUERY_XMA 10153919431161729 + +/** + * FB_API_CONTACTS_COUNT: + * + * The maximum amount of contacts to fetch in a single request. If this + * value is set too high, HTTP request will fail. This is due to the + * request data being too large. + */ +#define FB_API_CONTACTS_COUNT 500 + +#define FACEBOOK_MESSAGE_LIMIT 100000 + +class FacebookProto; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct AsyncHttpRequest : public MTHttpRequest +{ + struct Param + { + Param(const char *p1, const char *p2) : + key(p1), val(p2) + {} + + CMStringA key, val; + }; + OBJLIST params; + + AsyncHttpRequest(); + + void CalcSig(); +}; + +AsyncHttpRequest *operator<<(AsyncHttpRequest *, const CHAR_PARAM &); +AsyncHttpRequest *operator<<(AsyncHttpRequest *, const INT_PARAM &); + +class JsonReply +{ + JSONNode *m_root = nullptr; + int m_errorCode = 0; + +public: + JsonReply(NETLIBHTTPREQUEST *); + ~JsonReply(); + + __forceinline JSONNode &data() const { return *m_root; } + __forceinline int error() const { return m_errorCode; } +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct FacebookUser +{ + FacebookUser(__int64 _p1, MCONTACT _p2, bool _p3 = false, bool _p4 = false) : + id(_p1), + hContact(_p2), + bIsChat(_p3), + bIsChatInitialized(_p4) + {} + + __int64 id; + MCONTACT hContact; + bool bIsChat; + bool bIsChatInitialized; +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct COwnMessage +{ + __int64 msgId; + int reqId; + MCONTACT hContact; + CMStringW wszText; + + COwnMessage() : + msgId(0), + reqId(0), + hContact(0) + { + } + + COwnMessage(__int64 _id, int _reqId, MCONTACT _hContact) : + msgId(_id), + reqId(_reqId), + hContact(_hContact) + { + } +}; + +class FacebookProto : public PROTO +{ + friend class CGroupchatInviteDlg; + + class FacebookImpl + { + friend class FacebookProto; + + FacebookProto &m_proto; + CTimer m_heartBeat; + + void OnHeartBeat(CTimer *) + { + m_proto.MqttPing(); + } + + FacebookImpl(FacebookProto &pro) : + m_proto(pro), + m_heartBeat(Miranda_GetSystemWindow(), (UINT_PTR)this) + { + m_heartBeat.OnEvent = Callback(this, &FacebookImpl::OnHeartBeat); + } + } m_impl; + + uint8_t *doZip(size_t cbData, const void *pData, size_t &cbRes); + uint8_t *doUnzip(size_t cbData, const void *pData, size_t &cbRes); + + void ConnectionFailed(int iErrorCode = 0); + + AsyncHttpRequest *CreateRequest(const char *szUrl, const char *szName, const char *szMethod); + AsyncHttpRequest *CreateRequestGQL(int64_t id); + NETLIBHTTPREQUEST *ExecuteRequest(AsyncHttpRequest *pReq); + + // Avatars + void __cdecl AvatarsUpdate(void *); + void GetAvatarFilename(MCONTACT hContact, wchar_t *pwszFileName); + + // Group chats + void Chat_InviteUser(SESSION_INFO *si); + int Chat_KickUser(SESSION_INFO *si, const wchar_t *pwszUid); + void Chat_Leave(SESSION_INFO *si); + void Chat_SendPrivateMessage(GCHOOK *gch); + void Chat_ProcessLogMenu(SESSION_INFO *si, GCHOOK *gch); + void Chat_ProcessNickMenu(SESSION_INFO *si, GCHOOK *gch); + + // MQTT + void MqttLogin(); + + void MqttPing(); + void MqttPublish(const char *topic, const JSONNode &value); + void MqttSubscribe(const char *topic, ...); + void MqttUnsubscribe(const char *topic, ...); + + bool MqttRead(MqttMessage &payload); + bool MqttParse(const MqttMessage &payload); + void MqttSend(const MqttMessage &payload); + + void MqttQueueConnect(); + + void OnPublish(const char *str, const uint8_t *payLoad, size_t cbLen); + void OnPublishMessage(FbThriftReader &rdr); + void OnPublishPresence(FbThriftReader &rdr); + void OnPublishUtn(FbThriftReader &rdr); + + HNETLIBCONN m_mqttConn; + __int64 m_iMqttId; + int16_t m_mid; // MQTT message id + + // internal data + CMStringA m_szDeviceID; // stored, GUID that identifies this miranda's account + CMStringA m_szClientID; // stored, random alphanumeric string of 20 chars + __int64 m_uid; // stored, Facebook user id + + CMStringA m_szSyncToken; // stored, sequence query token + __int64 m_sid; // stored, Facebook sequence id + + int m_iUnread; + bool m_bOnline; + bool m_QueueCreated; + + CMStringA m_szAuthToken; // calculated + + mir_cs m_csOwnMessages; + OBJLIST arOwnMessages; + bool ExtractOwnMessage(__int64 msgId, COwnMessage &res); + + mir_cs m_csUsers; + OBJLIST m_users; + + FacebookUser* FindUser(__int64 id); + + FacebookUser *UserFromJson(const JSONNode &root, CMStringW &wszId, bool &bIsChat); + + bool CheckOwnMessage(FacebookUser *pUser, __int64 offlineId, const char *pszMsgId); + void FetchAttach(const CMStringA &mid, __int64 fbid, CMStringA &szBody); + + void OnLoggedIn(); + void OnLoggedOut(); + + FacebookUser* RefreshThread(JSONNode &n); + FacebookUser* RefreshThread(CMStringW &wszId); + bool RefreshSid(); + int RefreshToken(); + void RefreshThreads(); + int RefreshContacts(); + + FacebookUser *AddContact(const CMStringW &wszId, bool bTemp = true); + + void __cdecl ServerThread(void *); + +public: + FacebookProto(const char *proto_name, const wchar_t *username); + ~FacebookProto(); + + inline const char *ModuleName() const + { + return m_szModuleName; + } + + void OnPublishPrivateMessage(const JSONNode &json); + void OnPublishReadReceipt(const JSONNode &json); + void OnPublishSentMessage(const JSONNode &json); + void OnPublishThreadName(const JSONNode &json); + void OnPublishChatJoin(const JSONNode &json); + void OnPublishChatLeave(const JSONNode &json); + + ////////////////////////////////////////////////////////////////////////////////////// + // options + + CMOption m_wszDefaultGroup; // clist group to store contacts + CMOption m_bUseBigAvatars; // use big or small avatars by default + CMOption m_bUseGroupchats; // do we need group chats at all? + CMOption m_bHideGroupchats; // do not open chat windows on creation + CMOption m_bLoginInvisible; // login in the invisible mode + CMOption m_bKeepUnread; // do not mark incoming messages as read + CMOption m_bLoadAll; // load all contacts, not only those who have ARE_FRIENDS status + + //////////////////////////////////////////////////////////////////////////////////////// + // PROTO_INTERFACE + + void OnContactAdded(MCONTACT) override; + void OnModulesLoaded() override; + void OnShutdown() override; + + MCONTACT AddToList(int flags, PROTOSEARCHRESULT *psr) override; + INT_PTR GetCaps(int type, MCONTACT hContact) override; + int SendMsg(MCONTACT hContact, int flags, const char *pszSrc) override; + int SetStatus(int iNewStatus) override; + int UserIsTyping(MCONTACT hContact, int type) override; + + //////////////////////////////////////////////////////////////////////////////////////// + // Events + + int __cdecl OnMarkedRead(WPARAM, LPARAM); + int __cdecl OnOptionsInit(WPARAM, LPARAM); + + int __cdecl GroupchatMenuHook(WPARAM, LPARAM); + int __cdecl GroupchatEventHook(WPARAM, LPARAM); + + //////////////////////////////////////////////////////////////////////////////////////// + // Services + + INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); + INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); +}; + +typedef CProtoDlgBase CFBDlgBase; + +struct CMPlugin : public ACCPROTOPLUGIN +{ + CMPlugin(); + + int Load() override; +}; diff --git a/protocols/Facebook/src/server.cpp b/protocols/Facebook/src/server.cpp index b35cc5461a..39ba67f83d 100644 --- a/protocols/Facebook/src/server.cpp +++ b/protocols/Facebook/src/server.cpp @@ -1,1051 +1,1051 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -void FacebookProto::ConnectionFailed(int iErrorCode) -{ - if (iErrorCode) { - POPUPDATAW popup; - popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); - wcscpy_s(popup.lpwzContactName, m_tszUserName); - mir_snwprintf(popup.lpwzText, TranslateT("Connection failed with error code %d"), iErrorCode); - PUAddPopupW(&popup); - } - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_FAILED, (HANDLE)m_iStatus, m_iDesiredStatus); - - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); - - OnShutdown(); -} - -bool FacebookProto::ExtractOwnMessage(__int64 msgId, COwnMessage &res) -{ - mir_cslock lck(m_csOwnMessages); - for (auto &it : arOwnMessages) - if (it->msgId == msgId) { - res = *it; - arOwnMessages.removeItem(&it); - return true; - } - - return false; -} - -void FacebookProto::OnLoggedIn() -{ - m_mid = 0; - - JSONNode root; root << BOOL_PARAM("foreground", true) << INT_PARAM("keepalive_timeout", 60); - MqttPublish("/foreground_state", root); - - MqttSubscribe("/inbox", "/mercury", "/messaging_events", "/orca_presence", "/orca_typing_notifications", "/pp", "/t_ms", "/t_p", "/t_rtc", "/webrtc", "/webrtc_response", 0); - MqttUnsubscribe("/orca_message_notifications", 0); - - // if sequence is not initialized, request SID from the server - if (m_sid == 0) { - if (!RefreshSid()) { - ConnectionFailed(); - return; - } - } - - // point of no return; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); - m_iStatus = m_iDesiredStatus; - m_bOnline = true; - m_impl.m_heartBeat.Start(60000); - - // connect message queue - MqttQueueConnect(); - - // request message threads if needed - if (m_bUseGroupchats) - RefreshThreads(); -} - -void FacebookProto::OnLoggedOut() -{ - m_impl.m_heartBeat.Stop(); - m_bOnline = false; - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - - setAllContactStatuses(ID_STATUS_OFFLINE, false); -} - -FacebookUser* FacebookProto::AddContact(const CMStringW &wszId, bool bTemp) -{ - MCONTACT hContact = db_add_contact(); - setWString(hContact, DBKEY_ID, wszId); - Proto_AddToContact(hContact, m_szModuleName); - Clist_SetGroup(hContact, m_wszDefaultGroup); - if (bTemp) - Contact::RemoveFromList(hContact); - - return FindUser(_wtoi64(wszId)); -} - -FacebookUser* FacebookProto::FindUser(__int64 id) -{ - mir_cslock lck(m_csUsers); - return m_users.find((FacebookUser *)&id); -} - -FacebookUser* FacebookProto::UserFromJson(const JSONNode &root, CMStringW &wszUserId, bool &bIsChat) -{ - bIsChat = false; - wszUserId = root["threadKey"]["otherUserFbId"].as_mstring(); - if (wszUserId.IsEmpty()) { - // if only thread id is present, it must be a group chat - wszUserId = root["threadKey"]["threadFbId"].as_mstring(); - bIsChat = true; - } - - auto *pUser = FindUser(_wtoi64(wszUserId)); - if (pUser == nullptr) { - debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); - return nullptr; - } - - if (pUser->bIsChat != bIsChat) { - debugLogA("Wrong chat user: %d vs %d for user %lld, ignored", pUser->bIsChat, bIsChat, pUser->id); - return nullptr; - } - - return pUser; -} - -int FacebookProto::RefreshContacts() -{ - CMStringA szCursor; - bool bNeedUpdate = false; - - while (true) { - JSONNode root; root << CHAR_PARAM("0", "user"); - - AsyncHttpRequest *pReq; - if (szCursor.IsEmpty()) { - pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS); - root << INT_PARAM("1", FB_API_CONTACTS_COUNT); - } - else { - pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS_AFTER); - root << CHAR_PARAM("1", szCursor) << INT_PARAM("2", FB_API_CONTACTS_COUNT); - } - pReq << CHAR_PARAM("query_params", root.write().c_str()); - pReq->flags |= NLHRF_NODUMPSEND; - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (int iErrorCode = reply.error()) - return iErrorCode; // unknown error - - bool bLoadAll = m_bLoadAll; - auto &data = reply.data()["viewer"]["messenger_contacts"]; - - for (auto &it : data["nodes"]) { - auto &n = it["represented_profile"]; - CMStringW wszId(n["id"].as_mstring()); - __int64 id = _wtoi64(wszId); - - MCONTACT hContact; - if (id != m_uid) { - bool bIsFriend = bLoadAll || n["friendship_status"].as_mstring() == L"ARE_FRIENDS"; - - auto *pUser = FindUser(id); - if (pUser == nullptr) { - if (!bIsFriend) - continue; - pUser = AddContact(wszId, false); - } - else if (!bIsFriend) - Contact::RemoveFromList(pUser->hContact); // adios! - - hContact = pUser->hContact; - } - else hContact = 0; - - if (auto &nName = it["structured_name"]) { - CMStringW wszName(nName["text"].as_mstring()); - setWString(hContact, DBKEY_NICK, wszName); - for (auto &nn : nName["parts"]) { - CMStringW wszPart(nn["part"].as_mstring()); - int offset = nn["offset"].as_int(), length = nn["length"].as_int(); - if (wszPart == L"first") - setWString(hContact, "FirstName", wszName.Mid(offset, length)); - else if (wszPart == L"last") - setWString(hContact, "LastName", wszName.Mid(offset, length)); - } - } - - if (auto &nBirth = n["birthdate"]) { - setDword(hContact, "BirthDay", nBirth["day"].as_int()); - setDword(hContact, "BirthMonth", nBirth["month"].as_int()); - } - - if (auto &nCity = n["current_city"]) - setWString(hContact, "City", nCity["name"].as_mstring()); - - if (auto &nAva = it[(m_bUseBigAvatars) ? "hugePictureUrl" : "bigPictureUrl"]) { - CMStringW wszOldUrl(getMStringW(hContact, DBKEY_AVATAR)), wszNewUrl(nAva["uri"].as_mstring()); - if (wszOldUrl != wszNewUrl) { - bNeedUpdate = true; - setByte(hContact, "UpdateNeeded", 1); - setWString(hContact, DBKEY_AVATAR, wszNewUrl); - } - } - } - - if (!data["page_info"]["has_next_page"].as_bool()) { - debugLogA("Got no next page, exiting", szCursor.c_str()); - break; - } - - szCursor = data["page_info"]["end_cursor"].as_mstring(); - debugLogA("Got cursor: %s", szCursor.c_str()); - } - - if (bNeedUpdate) - ForkThread(&FacebookProto::AvatarsUpdate); - return 0; -} - -bool FacebookProto::RefreshSid() -{ - auto *pReq = CreateRequestGQL(FB_API_QUERY_SEQ_ID); - JSONNode root; root << CHAR_PARAM("1", "0"); - pReq << CHAR_PARAM("query_params", root.write().c_str()); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (reply.error()) - return false; - - auto &n = reply.data()["viewer"]["message_threads"]; - CMStringW wszSid(n["sync_sequence_id"].as_mstring()); - setWString(DBKEY_SID, wszSid); - m_sid = _wtoi64(wszSid); - m_iUnread = n["unread_count"].as_int(); - return true; -} - -FacebookUser* FacebookProto::RefreshThread(JSONNode &n) -{ - if (!n["is_group_thread"].as_bool()) - return nullptr; - - CMStringW chatId(n["thread_key"]["thread_fbid"].as_mstring()); - CMStringW name(n["name"].as_mstring()); - if (name.IsEmpty()) { - for (auto &u : n["all_participants"]["nodes"]) { - auto &ur = u["messaging_actor"]; - CMStringW userId(ur["id"].as_mstring()); - if (_wtoi64(userId) == m_uid) - continue; - - if (!name.IsEmpty()) - name.Append(L", "); - name += ur["name"].as_mstring(); - } - - if (name.GetLength() > 128) { - name.Truncate(125); - name.Append(L"..."); - } - } - - auto *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, chatId, name); - if (si == nullptr) - return nullptr; - - setWString(si->hContact, DBKEY_ID, chatId); - Chat_AddGroup(si, TranslateT("Participant")); - - for (auto &u : n["all_participants"]["nodes"]) { - auto &ur = u["messaging_actor"]; - CMStringW userId(ur["id"].as_mstring()); - CMStringW userName(ur["name"].as_mstring()); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; - gce.pszID.w = chatId; - gce.pszUID.w = userId; - gce.pszNick.w = userName; - gce.bIsMe = _wtoi64(userId) == m_uid; - gce.time = time(0); - Chat_Event(&gce); - } - - Chat_Control(m_szModuleName, chatId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); - Chat_Control(m_szModuleName, chatId, SESSION_ONLINE); - - __int64 userId = _wtoi64(chatId); - auto *pUser = FindUser(userId); - - if (pUser == nullptr) { - mir_cslock lck(m_csUsers); - pUser = new FacebookUser(userId, si->hContact, true, true); - m_users.insert(pUser); - } - else { - pUser->hContact = si->hContact; - pUser->bIsChatInitialized = true; - } - - return pUser; -} - -FacebookUser* FacebookProto::RefreshThread(CMStringW &wszId) -{ - auto *pReq = CreateRequestGQL(FB_API_QUERY_THREAD); - pReq << WCHAR_PARAM("query_params", CMStringW(FORMAT, L"{\"0\":[\"%s\"], \"12\":0, \"13\":\"false\"}", wszId.c_str())); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (!reply.error()) { - auto &root = reply.data(); - for (auto &n : root) - return RefreshThread(n); - } - - return nullptr; -} - -void FacebookProto::RefreshThreads() -{ - int threadsLimit = 40; - - auto * pReq = CreateRequestGQL(FB_API_QUERY_THREADS); - JSONNode json; json << INT_PARAM("1", threadsLimit) << CHAR_PARAM("2", "true") << CHAR_PARAM("12", "false") << CHAR_PARAM("13", "false"); - pReq << CHAR_PARAM("query_params", json.write().c_str()); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (!reply.error()) { - auto &root = reply.data()["viewer"]["message_threads"]; - - for (auto &n : root["nodes"]) { - if (n["is_group_thread"].as_bool() && n["is_viewer_subscribed"].as_bool() && !n["has_viewer_archived"].as_bool()) - RefreshThread(n); - } - - // TODO: save timestamp of last message/action/... into DB - // TODO: lower threadsLimit to 10, load next pages if timestamp of last message is higher than timestamp in DB - } -} - -int FacebookProto::RefreshToken() -{ - auto *pReq = CreateRequest(FB_API_URL_AUTH, "authenticate", "auth.login"); - pReq->flags |= NLHRF_NODUMP; - pReq << CHAR_PARAM("email", getMStringA(DBKEY_LOGIN)); - pReq << CHAR_PARAM("password", getMStringA(DBKEY_PASS)); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (reply.error()) - return reply.error(); - - m_szAuthToken = reply.data()["access_token"].as_mstring(); - setString(DBKEY_TOKEN, m_szAuthToken); - - CMStringA m_szUid = reply.data()["uid"].as_mstring(); - setString(DBKEY_ID, m_szUid); - m_uid = _atoi64(m_szUid); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void FacebookProto::ServerThread(void *) -{ - m_QueueCreated = false; - -LBL_Begin: - m_szAuthToken = getMStringA(DBKEY_TOKEN); - if (m_szAuthToken.IsEmpty()) { - if (int iErrorCode = RefreshToken()) { - ConnectionFailed(iErrorCode); - return; - } - } - - int iErrorCode = RefreshContacts(); - if (iErrorCode != 0) { - if (iErrorCode == 401) { - delSetting(DBKEY_TOKEN); - goto LBL_Begin; - } - - ConnectionFailed(iErrorCode); - return; - } - - // connect to MQTT server - m_mqttConn = Netlib_OpenConnection(m_hNetlibUser, "mqtt.facebook.com", 443, 0, NLOCF_SSL); - if (m_mqttConn == nullptr) { - debugLogA("connection failed, exiting"); - ConnectionFailed(); - return; - } - - // send initial packet - MqttLogin(); - - while (!Miranda_IsTerminated()) { - NETLIBSELECT nls = {}; - nls.hReadConns[0] = m_mqttConn; - nls.dwTimeout = 1000; - int ret = Netlib_Select(&nls); - if (ret == SOCKET_ERROR) { - debugLogA("Netlib_Recv() failed, error=%d", WSAGetLastError()); - break; - } - - // no data, continue waiting - if (ret == 0) - continue; - - MqttMessage msg; - if (!MqttRead(msg)) { - debugLogA("MqttRead() failed"); - break; - } - - if (!MqttParse(msg)) { - debugLogA("MqttParse() failed"); - break; - } - } - - debugLogA("exiting ServerThread"); - - Netlib_CloseHandle(m_mqttConn); - m_mqttConn = nullptr; - - OnLoggedOut(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int FacebookProto::OnMarkedRead(WPARAM, LPARAM hDbEvent) -{ - MCONTACT hContact = db_event_getContact(hDbEvent); - if (!hContact) - return 0; - - // filter out only events of my protocol - const char *szProto = Proto_GetBaseAccountName(hContact); - if (mir_strcmp(szProto, m_szModuleName)) - return 0; - - if (m_bKeepUnread) - return 0; - - JSONNode root; root << BOOL_PARAM("state", true) << INT_PARAM("syncSeqId", m_sid) << CHAR_PARAM("mark", "read"); - if (isChatRoom(hContact)) - root << CHAR_PARAM("threadFbId", getMStringA(hContact, DBKEY_ID)); - else - root << CHAR_PARAM("otherUserFbId", getMStringA(hContact, DBKEY_ID)); - MqttPublish("/mark_thread", root); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void FacebookProto::OnPublish(const char *topic, const uint8_t *p, size_t cbLen) -{ - FbThriftReader rdr; - - // that might be a zipped buffer - if (cbLen >= 2) { - size_t dataSize; - void *pData = doUnzip(cbLen, p, dataSize); - if (pData != nullptr) { - debugLogA("UNZIP: %d bytes unzipped ok", dataSize); - Netlib_Dump(m_mqttConn, pData, dataSize, false, 0); - rdr.reset(dataSize, pData); - mir_free(pData); - } - } - - if (rdr.size() == 0) - rdr.reset(cbLen, (void*)p); - - if (!strcmp(topic, "/t_p")) - OnPublishPresence(rdr); - else if (!strcmp(topic, "/t_ms")) - OnPublishMessage(rdr); - else if (!strcmp(topic, "/orca_typing_notifications")) - OnPublishUtn(rdr); -} - -void FacebookProto::OnPublishPresence(FbThriftReader &rdr) -{ - char *str = nullptr; - rdr.readStr(str); - mir_free(str); - - bool bVal; - uint8_t fieldType; - uint16_t fieldId; - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_BOOL); - assert(fieldId == 1); - rdr.readBool(bVal); - - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_LIST); - assert(fieldId == 1); - - uint32_t size; - rdr.readList(fieldType, size); - assert(fieldType == FB_THRIFT_TYPE_STRUCT); - - debugLogA("Received list of presences: %d records", size); - for (uint32_t i = 0; i < size; i++) { - uint64_t userId, timestamp, voipBits; - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_I64); - assert(fieldId == 1); - rdr.readInt64(userId); - - uint32_t u32; - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_I32); - assert(fieldId == 1); - rdr.readInt32(u32); - - auto *pUser = FindUser(userId); - if (pUser == nullptr) - debugLogA("Skipping presence from unknown user %lld", userId); - else { - debugLogA("Presence from user %lld => %d", userId, u32); - setWord(pUser->hContact, "Status", (u32 != 0) ? ID_STATUS_ONLINE : ID_STATUS_OFFLINE); - } - - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_I64); - assert(fieldId == 1 || fieldId == 3 || fieldId == 4); - rdr.readInt64(timestamp); - - while (!rdr.isStop()) { - rdr.readField(fieldType, fieldId); - assert(fieldType == FB_THRIFT_TYPE_I64 || fieldType == FB_THRIFT_TYPE_I16 || fieldType == FB_THRIFT_TYPE_I32); - rdr.readIntV(voipBits); - } - - rdr.readByte(fieldType); - assert(fieldType == FB_THRIFT_TYPE_STOP); - } - - rdr.readByte(fieldType); - assert(fieldType == FB_THRIFT_TYPE_STOP); -} - -void FacebookProto::OnPublishUtn(FbThriftReader &rdr) -{ - JSONNode root = JSONNode::parse(rdr.rest()); - auto *pUser = FindUser(_wtoi64(root["sender_fbid"].as_mstring())); - if (pUser != nullptr) { - int length = (root["state"].as_int() == 0) ? PROTOTYPE_CONTACTTYPING_OFF : 60; - CallService(MS_PROTO_CONTACTISTYPING, pUser->hContact, length); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -struct -{ - const char *messageType; - void (FacebookProto:: *pFunc)(const JSONNode &); -} -static MsgHandlers[] = -{ - { "deltaNewMessage", &FacebookProto::OnPublishPrivateMessage }, - { "deltaThreadName", &FacebookProto::OnPublishThreadName }, - { "deltaSentMessage", &FacebookProto::OnPublishSentMessage }, - { "deltaReadReceipt", &FacebookProto::OnPublishReadReceipt }, - { "deltaParticipantsAddedToGroupThread", &FacebookProto::OnPublishChatJoin }, - { "deltaParticipantLeftGroupThread", &FacebookProto::OnPublishChatLeave }, -}; - -void FacebookProto::OnPublishMessage(FbThriftReader &rdr) -{ - uint8_t stop; - if (rdr.isStop()) - rdr.readByte(stop); - else { - uint8_t type; - uint16_t id; - rdr.readField(type, id); - _ASSERT(type == FB_THRIFT_TYPE_STRING); - _ASSERT(id == 1 || id == 2); - - char *szShit = nullptr; - rdr.readStr(szShit); - mir_free(szShit); - - rdr.readByte(stop); - } - - CMStringA szJson(rdr.rest()); - debugLogA("MS: <%s>", szJson.c_str()); - JSONNode root = JSONNode::parse(szJson); - - CMStringA errorCode = root["errorCode"].as_mstring(); - if (!errorCode.IsEmpty()) { - if (!m_QueueCreated && (errorCode == "ERROR_QUEUE_OVERFLOW" || errorCode == "ERROR_QUEUE_NOT_FOUND" || errorCode == "ERROR_QUEUE_LOST" || errorCode == "ERROR_QUEUE_EXCEEDS_MAX_DELTAS")) { - m_QueueCreated = true; // prevent queue creation request from being sent twice - delSetting(DBKEY_SYNC_TOKEN); m_szSyncToken.Empty(); - delSetting(DBKEY_SID); m_sid = 0; - if (!RefreshSid()) { - ConnectionFailed(); - return; - } - - MqttQueueConnect(); - } - } - - CMStringW str = root["lastIssuedSeqId"].as_mstring(); - if (!str.IsEmpty()) { - setWString(DBKEY_SID, str); - m_sid = _wtoi64(str); - } - - str = root["syncToken"].as_mstring(); - if (!str.IsEmpty()) { - m_szSyncToken = str; - setString(DBKEY_SYNC_TOKEN, m_szSyncToken); - return; - } - - for (auto &it : root["deltas"]) { - for (auto &handler : MsgHandlers) { - auto &json = it[handler.messageType]; - if (json) { - (this->*(handler.pFunc))(json); - break; - } - } - } -} - -// new message arrived -struct -{ - const char *szTag, *szClientVersion; -} -static facebookClients[] = -{ - { "source:titan:web", "Facebook (website)" }, - { "app_id:256002347743983", "Facebook (Facebook Messenger)" } -}; - -void FacebookProto::FetchAttach(const CMStringA &mid, __int64 fbid, CMStringA &szBody) -{ - for (int iAttempt = 0; iAttempt < 5; iAttempt++) { - auto *pReq = CreateRequest(FB_API_URL_ATTACH, "getAttachment", "messaging.getAttachment"); - pReq << CHAR_PARAM("mid", mid) << INT64_PARAM("aid", fbid); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - switch (reply.error()) { - case 0: - { - std::string uri = reply.data()["redirect_uri"].as_string(); - std::string type = reply.data()["content_type"].as_string(); - if (!uri.empty()) - szBody.AppendFormat("\r\n%s: %s", TranslateU(type.find("image/") != -1 ? "Picture attachment" : "File attachment"), uri.c_str()); - } - return; - - case 509: // attachment isn't ready, wait a bit and retry - ::Sleep(100); - continue; - - default: // shit happened, exiting - return; - } - } -} - -void FacebookProto::OnPublishPrivateMessage(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - if (!offlineId) { - debugLogA("We care about messages only, event skipped"); - return; - } - - bool bIsChat; - CMStringW wszUserId; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - - if (!bIsChat && pUser == nullptr) - pUser = AddContact(wszUserId, true); - else if (bIsChat && (pUser == nullptr || !pUser->bIsChatInitialized)) // chat room does not exists or is not initialized - pUser = RefreshThread(wszUserId); - - if (pUser == nullptr) { - debugLogA("User not found and adding failed, event skipped"); - return; - } - - for (auto &it : metadata["tags"]) { - auto *szTagName = it.name(); - for (auto &cli : facebookClients) { - if (!mir_strcmp(szTagName, cli.szTag)) { - setString(pUser->hContact, "MirVer", cli.szClientVersion); - break; - } - } - } - - CMStringA szId(metadata["messageId"].as_mstring()); - if (CheckOwnMessage(pUser, offlineId, szId)) { - debugLogA("own message <%s> skipped", szId.c_str()); - return; - } - - if (db_event_getById(m_szModuleName, szId)) { - debugLogA("this message <%s> was already stored, exiting", szId.c_str()); - return; - } - - // parse message body - CMStringA szBody(root["body"].as_string().c_str()); - if (szBody.IsEmpty()) - szBody = metadata["snippet"].as_string().c_str(); - - // parse stickers - CMStringA stickerId = root["stickerId"].as_mstring(); - if (!stickerId.IsEmpty()) { - if (ServiceExists(MS_SMILEYADD_LOADCONTACTSMILEYS)) { - CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); - CreateDirectoryTreeW(wszPath); - - bool bSuccess = false; - CMStringW wszFileName(FORMAT, L"%s\\STK{%S}.png", wszPath.c_str(), stickerId.c_str()); - uint32_t dwAttrib = GetFileAttributesW(wszFileName); - if (dwAttrib == INVALID_FILE_ATTRIBUTES) { - wszFileName.Format(L"%s\\STK{%S}.webp", wszPath.c_str(), stickerId.c_str()); - dwAttrib = GetFileAttributesW(wszFileName); - } - - // new sticker - if (dwAttrib == INVALID_FILE_ATTRIBUTES) { - auto *pReq = CreateRequestGQL(FB_API_QUERY_STICKER); - pReq << CHAR_PARAM("query_params", CMStringA(FORMAT, "{\"0\":[\"%s\"]}", stickerId.c_str())); - pReq->CalcSig(); - - JsonReply reply(ExecuteRequest(pReq)); - if (!reply.error()) { - for (auto &sticker : reply.data()) { - // std::string szUrl = sticker["animated_image"]["uri"].as_string(); - // if (szUrl.empty()) - // szUrl = sticker["thread_image"]["uri"].as_string(); - // else - // wszFileName.Format(L"%s\\STK{%S}.webp", wszPath.c_str(), stickerId.c_str()); - std::string szUrl = sticker["thread_image"]["uri"].as_string(); - - NETLIBHTTPREQUEST req = {}; - req.cbSize = sizeof(req); - req.flags = NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; - req.requestType = REQUEST_GET; - req.szUrl = (char*)szUrl.c_str(); - - NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(m_hNetlibUser, &req); - if (pReply != nullptr && pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { - bSuccess = true; - FILE *out = _wfopen(wszFileName, L"wb"); - fwrite(pReply->pData, 1, pReply->dataLength, out); - fclose(out); - } - } - } - } - else bSuccess = true; - - if (bSuccess) { - if (!szBody.IsEmpty()) - szBody += "\r\n"; - szBody += "STK{" + stickerId + "}"; - - SMADD_CONT cont = { 1, m_szModuleName, wszFileName }; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); - } - else szBody += TranslateU("Sticker received"); - } - else szBody += TranslateU("SmileyAdd plugin required to support stickers"); - } - - // parse attachments (links, files, ...) - for (auto &it : root["attachments"]) { - // madness... json inside json - CMStringA szJson(it["xmaGraphQL"].as_mstring()); - if (szJson.IsEmpty()) { - __int64 fbid = _wtoi64(it["fbid"].as_mstring()); - if (fbid == 0) { - debugLogA("Neither a GQL nor an inline attachment, nothing to do"); - continue; - } - - // inline attachment, request its description - FetchAttach(szId, fbid, szBody); - continue; - } - - JSONROOT nBody(szJson); - if (!nBody) - continue; - - const JSONNode &attach = (*nBody).at((json_index_t)0)["story_attachment"]; - szBody += "\r\n-----------------------------------"; - - CMStringA str = attach["url"].as_mstring(); - if (!str.IsEmpty()) { - if (str.Left(8) == "fbrpc://") { - int iStart = str.Find("target_url="); - if (iStart != 0) { - CMStringA tmp; - - iStart += 11; - int iEnd = str.Find("&", iStart); - if (iEnd != -1) - tmp = str.Mid(iStart, iEnd - iStart); - else - tmp = str.Right(iStart); - - mir_urlDecode(tmp.GetBuffer()); - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("URL"), tmp.c_str()); - } - } - else szBody.AppendFormat("\r\n\t%s: %s", TranslateU("URL"), str.c_str()); - } - - str = attach["title"].as_string().c_str(); - if (!str.IsEmpty()) - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Title"), str.c_str()); - - str = attach["source"]["text"].as_string().c_str(); - if (!str.IsEmpty()) - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Source"), str.c_str()); - - str = attach["description"]["text"].as_string().c_str(); - if (!str.IsEmpty()) - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Description"), str.c_str()); - - str = attach["media"]["playable_url"].as_string().c_str(); - if (!str.IsEmpty()) - szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Playable media"), str.c_str()); - } - - // if that's a group chat, send it to the room - CMStringW wszActorFbId(metadata["actorFbId"].as_mstring()); - __int64 actorFbId = _wtoi64(wszActorFbId); - - if (pUser->bIsChat) { - szBody.Replace("%", "%%"); - ptrW wszText(mir_utf8decodeW(szBody)); - - // TODO: GC_EVENT_JOIN for chat participants which are missing (for example added later during group chat) - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; - gce.pszID.w = wszUserId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszActorFbId; - gce.pszText.w = wszText; - gce.time = time(0); - gce.bIsMe = actorFbId == m_uid; - Chat_Event(&gce); - - debugLogA("New channel %lld message from %S: %s", pUser->id, gce.pszUID.w, gce.pszText.w); - } - else { // otherwise store a private message - PROTORECVEVENT pre = {}; - pre.timestamp = uint32_t(_wtoi64(metadata["timestamp"].as_mstring()) / 1000); - pre.szMessage = (char *)szBody.c_str(); - pre.szMsgId = (char *)szId.c_str(); - - if (m_uid == actorFbId) - pre.flags |= PREF_SENT; - - ProtoChainRecvMsg(pUser->hContact, &pre); - } -} - -// changing thread name -void FacebookProto::OnPublishThreadName(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - if (!offlineId) { - debugLogA("We care about messages only, event skipped"); - return; - } - - bool bIsChat; - CMStringW wszUserId; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - if (!bIsChat || pUser == nullptr) - return; - - CMStringW wszTitle = root["name"].as_mstring(); - if (!wszTitle.IsEmpty()) - setWString(pUser->hContact, DBKEY_NICK, wszTitle); - else - delSetting(pUser->hContact, DBKEY_NICK); -} - -// user joined chat -void FacebookProto::OnPublishChatJoin(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - if (!offlineId) { - debugLogA("We care about messages only, event skipped"); - return; - } - - bool bIsChat; - CMStringW wszUserId; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - if (!bIsChat || pUser == nullptr) - return; - - CMStringW wszText(metadata["adminText"].as_mstring()); - for (auto &it : root["addedParticipants"]) { - CMStringW wszNick(it["fullName"].as_mstring()), wszId(it["userFbId"].as_mstring()); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; - gce.pszID.w = wszUserId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszNick.w = wszNick; - gce.pszUID.w = wszId; - gce.pszText.w = wszText; - gce.time = time(0); - gce.bIsMe = _wtoi64(wszId) == m_uid; - Chat_Event(&gce); - } -} - -// user left chat -void FacebookProto::OnPublishChatLeave(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - if (!offlineId) { - debugLogA("We care about messages only, event skipped"); - return; - } - - bool bIsChat; - CMStringW wszUserId; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - if (!bIsChat || pUser == nullptr) - return; - - CMStringW wszText(metadata["adminText"].as_mstring()), wszId(root["leftParticipantFbId"].as_mstring()); - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_PART }; - gce.pszID.w = wszUserId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszId; - gce.pszText.w = wszText; - gce.time = time(0); - gce.bIsMe = _wtoi64(wszId) == m_uid; - Chat_Event(&gce); -} - -// read notification -void FacebookProto::OnPublishReadReceipt(const JSONNode &root) -{ - CMStringW wszUserId; - bool bIsChat; - auto *pUser = UserFromJson(root, wszUserId, bIsChat); - if (pUser == nullptr) { - debugLogA("Message from unknown contact %S, ignored", wszUserId.c_str()); - return; - } - - uint32_t timestamp = _wtoi64(root["watermarkTimestampMs"].as_mstring()); - for (MEVENT ev = db_event_firstUnread(pUser->hContact); ev != 0; ev = db_event_next(pUser->hContact, ev)) { - DBEVENTINFO dbei = {}; - if (db_event_get(ev, &dbei)) - continue; - - if (dbei.timestamp > timestamp) - break; - - if (!dbei.markedRead()) - db_event_markRead(pUser->hContact, ev); - } -} - -// my own message was sent -bool FacebookProto::CheckOwnMessage(FacebookUser *pUser, __int64 offlineId, const char *pszMsgId) -{ - COwnMessage tmp; - if (!ExtractOwnMessage(offlineId, tmp)) - return false; - - if (pUser->bIsChat) { - CMStringW wszId(FORMAT, L"%lld", m_uid); - tmp.wszText.Replace(L"%", L"%%"); - - wchar_t userId[100]; - _i64tow_s(pUser->id, userId, _countof(userId), 10); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; - gce.pszID.w = userId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszId; - gce.pszText.w = tmp.wszText; - gce.time = time(0); - gce.bIsMe = true; - Chat_Event(&gce); - } - else ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)tmp.reqId, (LPARAM)pszMsgId); - - return true; -} - -void FacebookProto::OnPublishSentMessage(const JSONNode &root) -{ - auto &metadata = root["messageMetadata"]; - - __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); - - CMStringW wszUserId; - bool bIsChat; - auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); - if (pUser == nullptr) { - debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); - return; - } - - std::string szMsgId(metadata["messageId"].as_string()); - CheckOwnMessage(pUser, offlineId, szMsgId.c_str()); -} +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +void FacebookProto::ConnectionFailed(int iErrorCode) +{ + if (iErrorCode) { + POPUPDATAW popup; + popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); + wcscpy_s(popup.lpwzContactName, m_tszUserName); + mir_snwprintf(popup.lpwzText, TranslateT("Connection failed with error code %d"), iErrorCode); + PUAddPopupW(&popup); + } + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_FAILED, (HANDLE)m_iStatus, m_iDesiredStatus); + + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); + + OnShutdown(); +} + +bool FacebookProto::ExtractOwnMessage(__int64 msgId, COwnMessage &res) +{ + mir_cslock lck(m_csOwnMessages); + for (auto &it : arOwnMessages) + if (it->msgId == msgId) { + res = *it; + arOwnMessages.removeItem(&it); + return true; + } + + return false; +} + +void FacebookProto::OnLoggedIn() +{ + m_mid = 0; + + JSONNode root; root << BOOL_PARAM("foreground", true) << INT_PARAM("keepalive_timeout", 60); + MqttPublish("/foreground_state", root); + + MqttSubscribe("/inbox", "/mercury", "/messaging_events", "/orca_presence", "/orca_typing_notifications", "/pp", "/t_ms", "/t_p", "/t_rtc", "/webrtc", "/webrtc_response", 0); + MqttUnsubscribe("/orca_message_notifications", 0); + + // if sequence is not initialized, request SID from the server + if (m_sid == 0) { + if (!RefreshSid()) { + ConnectionFailed(); + return; + } + } + + // point of no return; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, m_iDesiredStatus); + m_iStatus = m_iDesiredStatus; + m_bOnline = true; + m_impl.m_heartBeat.Start(60000); + + // connect message queue + MqttQueueConnect(); + + // request message threads if needed + if (m_bUseGroupchats) + RefreshThreads(); +} + +void FacebookProto::OnLoggedOut() +{ + m_impl.m_heartBeat.Stop(); + m_bOnline = false; + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + + setAllContactStatuses(ID_STATUS_OFFLINE, false); +} + +FacebookUser* FacebookProto::AddContact(const CMStringW &wszId, bool bTemp) +{ + MCONTACT hContact = db_add_contact(); + setWString(hContact, DBKEY_ID, wszId); + Proto_AddToContact(hContact, m_szModuleName); + Clist_SetGroup(hContact, m_wszDefaultGroup); + if (bTemp) + Contact::RemoveFromList(hContact); + + return FindUser(_wtoi64(wszId)); +} + +FacebookUser* FacebookProto::FindUser(__int64 id) +{ + mir_cslock lck(m_csUsers); + return m_users.find((FacebookUser *)&id); +} + +FacebookUser* FacebookProto::UserFromJson(const JSONNode &root, CMStringW &wszUserId, bool &bIsChat) +{ + bIsChat = false; + wszUserId = root["threadKey"]["otherUserFbId"].as_mstring(); + if (wszUserId.IsEmpty()) { + // if only thread id is present, it must be a group chat + wszUserId = root["threadKey"]["threadFbId"].as_mstring(); + bIsChat = true; + } + + auto *pUser = FindUser(_wtoi64(wszUserId)); + if (pUser == nullptr) { + debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); + return nullptr; + } + + if (pUser->bIsChat != bIsChat) { + debugLogA("Wrong chat user: %d vs %d for user %lld, ignored", pUser->bIsChat, bIsChat, pUser->id); + return nullptr; + } + + return pUser; +} + +int FacebookProto::RefreshContacts() +{ + CMStringA szCursor; + bool bNeedUpdate = false; + + while (true) { + JSONNode root; root << CHAR_PARAM("0", "user"); + + AsyncHttpRequest *pReq; + if (szCursor.IsEmpty()) { + pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS); + root << INT_PARAM("1", FB_API_CONTACTS_COUNT); + } + else { + pReq = CreateRequestGQL(FB_API_QUERY_CONTACTS_AFTER); + root << CHAR_PARAM("1", szCursor) << INT_PARAM("2", FB_API_CONTACTS_COUNT); + } + pReq << CHAR_PARAM("query_params", root.write().c_str()); + pReq->flags |= NLHRF_NODUMPSEND; + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (int iErrorCode = reply.error()) + return iErrorCode; // unknown error + + bool bLoadAll = m_bLoadAll; + auto &data = reply.data()["viewer"]["messenger_contacts"]; + + for (auto &it : data["nodes"]) { + auto &n = it["represented_profile"]; + CMStringW wszId(n["id"].as_mstring()); + __int64 id = _wtoi64(wszId); + + MCONTACT hContact; + if (id != m_uid) { + bool bIsFriend = bLoadAll || n["friendship_status"].as_mstring() == L"ARE_FRIENDS"; + + auto *pUser = FindUser(id); + if (pUser == nullptr) { + if (!bIsFriend) + continue; + pUser = AddContact(wszId, false); + } + else if (!bIsFriend) + Contact::RemoveFromList(pUser->hContact); // adios! + + hContact = pUser->hContact; + } + else hContact = 0; + + if (auto &nName = it["structured_name"]) { + CMStringW wszName(nName["text"].as_mstring()); + setWString(hContact, DBKEY_NICK, wszName); + for (auto &nn : nName["parts"]) { + CMStringW wszPart(nn["part"].as_mstring()); + int offset = nn["offset"].as_int(), length = nn["length"].as_int(); + if (wszPart == L"first") + setWString(hContact, "FirstName", wszName.Mid(offset, length)); + else if (wszPart == L"last") + setWString(hContact, "LastName", wszName.Mid(offset, length)); + } + } + + if (auto &nBirth = n["birthdate"]) { + setDword(hContact, "BirthDay", nBirth["day"].as_int()); + setDword(hContact, "BirthMonth", nBirth["month"].as_int()); + } + + if (auto &nCity = n["current_city"]) + setWString(hContact, "City", nCity["name"].as_mstring()); + + if (auto &nAva = it[(m_bUseBigAvatars) ? "hugePictureUrl" : "bigPictureUrl"]) { + CMStringW wszOldUrl(getMStringW(hContact, DBKEY_AVATAR)), wszNewUrl(nAva["uri"].as_mstring()); + if (wszOldUrl != wszNewUrl) { + bNeedUpdate = true; + setByte(hContact, "UpdateNeeded", 1); + setWString(hContact, DBKEY_AVATAR, wszNewUrl); + } + } + } + + if (!data["page_info"]["has_next_page"].as_bool()) { + debugLogA("Got no next page, exiting", szCursor.c_str()); + break; + } + + szCursor = data["page_info"]["end_cursor"].as_mstring(); + debugLogA("Got cursor: %s", szCursor.c_str()); + } + + if (bNeedUpdate) + ForkThread(&FacebookProto::AvatarsUpdate); + return 0; +} + +bool FacebookProto::RefreshSid() +{ + auto *pReq = CreateRequestGQL(FB_API_QUERY_SEQ_ID); + JSONNode root; root << CHAR_PARAM("1", "0"); + pReq << CHAR_PARAM("query_params", root.write().c_str()); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (reply.error()) + return false; + + auto &n = reply.data()["viewer"]["message_threads"]; + CMStringW wszSid(n["sync_sequence_id"].as_mstring()); + setWString(DBKEY_SID, wszSid); + m_sid = _wtoi64(wszSid); + m_iUnread = n["unread_count"].as_int(); + return true; +} + +FacebookUser* FacebookProto::RefreshThread(JSONNode &n) +{ + if (!n["is_group_thread"].as_bool()) + return nullptr; + + CMStringW chatId(n["thread_key"]["thread_fbid"].as_mstring()); + CMStringW name(n["name"].as_mstring()); + if (name.IsEmpty()) { + for (auto &u : n["all_participants"]["nodes"]) { + auto &ur = u["messaging_actor"]; + CMStringW userId(ur["id"].as_mstring()); + if (_wtoi64(userId) == m_uid) + continue; + + if (!name.IsEmpty()) + name.Append(L", "); + name += ur["name"].as_mstring(); + } + + if (name.GetLength() > 128) { + name.Truncate(125); + name.Append(L"..."); + } + } + + auto *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, chatId, name); + if (si == nullptr) + return nullptr; + + setWString(si->hContact, DBKEY_ID, chatId); + Chat_AddGroup(si, TranslateT("Participant")); + + for (auto &u : n["all_participants"]["nodes"]) { + auto &ur = u["messaging_actor"]; + CMStringW userId(ur["id"].as_mstring()); + CMStringW userName(ur["name"].as_mstring()); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; + gce.pszID.w = chatId; + gce.pszUID.w = userId; + gce.pszNick.w = userName; + gce.bIsMe = _wtoi64(userId) == m_uid; + gce.time = time(0); + Chat_Event(&gce); + } + + Chat_Control(m_szModuleName, chatId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); + Chat_Control(m_szModuleName, chatId, SESSION_ONLINE); + + __int64 userId = _wtoi64(chatId); + auto *pUser = FindUser(userId); + + if (pUser == nullptr) { + mir_cslock lck(m_csUsers); + pUser = new FacebookUser(userId, si->hContact, true, true); + m_users.insert(pUser); + } + else { + pUser->hContact = si->hContact; + pUser->bIsChatInitialized = true; + } + + return pUser; +} + +FacebookUser* FacebookProto::RefreshThread(CMStringW &wszId) +{ + auto *pReq = CreateRequestGQL(FB_API_QUERY_THREAD); + pReq << WCHAR_PARAM("query_params", CMStringW(FORMAT, L"{\"0\":[\"%s\"], \"12\":0, \"13\":\"false\"}", wszId.c_str())); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (!reply.error()) { + auto &root = reply.data(); + for (auto &n : root) + return RefreshThread(n); + } + + return nullptr; +} + +void FacebookProto::RefreshThreads() +{ + int threadsLimit = 40; + + auto * pReq = CreateRequestGQL(FB_API_QUERY_THREADS); + JSONNode json; json << INT_PARAM("1", threadsLimit) << CHAR_PARAM("2", "true") << CHAR_PARAM("12", "false") << CHAR_PARAM("13", "false"); + pReq << CHAR_PARAM("query_params", json.write().c_str()); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (!reply.error()) { + auto &root = reply.data()["viewer"]["message_threads"]; + + for (auto &n : root["nodes"]) { + if (n["is_group_thread"].as_bool() && n["is_viewer_subscribed"].as_bool() && !n["has_viewer_archived"].as_bool()) + RefreshThread(n); + } + + // TODO: save timestamp of last message/action/... into DB + // TODO: lower threadsLimit to 10, load next pages if timestamp of last message is higher than timestamp in DB + } +} + +int FacebookProto::RefreshToken() +{ + auto *pReq = CreateRequest(FB_API_URL_AUTH, "authenticate", "auth.login"); + pReq->flags |= NLHRF_NODUMP; + pReq << CHAR_PARAM("email", getMStringA(DBKEY_LOGIN)); + pReq << CHAR_PARAM("password", getMStringA(DBKEY_PASS)); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (reply.error()) + return reply.error(); + + m_szAuthToken = reply.data()["access_token"].as_mstring(); + setString(DBKEY_TOKEN, m_szAuthToken); + + CMStringA m_szUid = reply.data()["uid"].as_mstring(); + setString(DBKEY_ID, m_szUid); + m_uid = _atoi64(m_szUid); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void FacebookProto::ServerThread(void *) +{ + m_QueueCreated = false; + +LBL_Begin: + m_szAuthToken = getMStringA(DBKEY_TOKEN); + if (m_szAuthToken.IsEmpty()) { + if (int iErrorCode = RefreshToken()) { + ConnectionFailed(iErrorCode); + return; + } + } + + int iErrorCode = RefreshContacts(); + if (iErrorCode != 0) { + if (iErrorCode == 401) { + delSetting(DBKEY_TOKEN); + goto LBL_Begin; + } + + ConnectionFailed(iErrorCode); + return; + } + + // connect to MQTT server + m_mqttConn = Netlib_OpenConnection(m_hNetlibUser, "mqtt.facebook.com", 443, 0, NLOCF_SSL); + if (m_mqttConn == nullptr) { + debugLogA("connection failed, exiting"); + ConnectionFailed(); + return; + } + + // send initial packet + MqttLogin(); + + while (!Miranda_IsTerminated()) { + NETLIBSELECT nls = {}; + nls.hReadConns[0] = m_mqttConn; + nls.dwTimeout = 1000; + int ret = Netlib_Select(&nls); + if (ret == SOCKET_ERROR) { + debugLogA("Netlib_Recv() failed, error=%d", WSAGetLastError()); + break; + } + + // no data, continue waiting + if (ret == 0) + continue; + + MqttMessage msg; + if (!MqttRead(msg)) { + debugLogA("MqttRead() failed"); + break; + } + + if (!MqttParse(msg)) { + debugLogA("MqttParse() failed"); + break; + } + } + + debugLogA("exiting ServerThread"); + + Netlib_CloseHandle(m_mqttConn); + m_mqttConn = nullptr; + + OnLoggedOut(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int FacebookProto::OnMarkedRead(WPARAM, LPARAM hDbEvent) +{ + MCONTACT hContact = db_event_getContact(hDbEvent); + if (!hContact) + return 0; + + // filter out only events of my protocol + const char *szProto = Proto_GetBaseAccountName(hContact); + if (mir_strcmp(szProto, m_szModuleName)) + return 0; + + if (m_bKeepUnread) + return 0; + + JSONNode root; root << BOOL_PARAM("state", true) << INT_PARAM("syncSeqId", m_sid) << CHAR_PARAM("mark", "read"); + if (isChatRoom(hContact)) + root << CHAR_PARAM("threadFbId", getMStringA(hContact, DBKEY_ID)); + else + root << CHAR_PARAM("otherUserFbId", getMStringA(hContact, DBKEY_ID)); + MqttPublish("/mark_thread", root); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void FacebookProto::OnPublish(const char *topic, const uint8_t *p, size_t cbLen) +{ + FbThriftReader rdr; + + // that might be a zipped buffer + if (cbLen >= 2) { + size_t dataSize; + void *pData = doUnzip(cbLen, p, dataSize); + if (pData != nullptr) { + debugLogA("UNZIP: %d bytes unzipped ok", dataSize); + Netlib_Dump(m_mqttConn, pData, dataSize, false, 0); + rdr.reset(dataSize, pData); + mir_free(pData); + } + } + + if (rdr.size() == 0) + rdr.reset(cbLen, (void*)p); + + if (!strcmp(topic, "/t_p")) + OnPublishPresence(rdr); + else if (!strcmp(topic, "/t_ms")) + OnPublishMessage(rdr); + else if (!strcmp(topic, "/orca_typing_notifications")) + OnPublishUtn(rdr); +} + +void FacebookProto::OnPublishPresence(FbThriftReader &rdr) +{ + char *str = nullptr; + rdr.readStr(str); + mir_free(str); + + bool bVal; + uint8_t fieldType; + uint16_t fieldId; + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_BOOL); + assert(fieldId == 1); + rdr.readBool(bVal); + + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_LIST); + assert(fieldId == 1); + + uint32_t size; + rdr.readList(fieldType, size); + assert(fieldType == FB_THRIFT_TYPE_STRUCT); + + debugLogA("Received list of presences: %d records", size); + for (uint32_t i = 0; i < size; i++) { + uint64_t userId, timestamp, voipBits; + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_I64); + assert(fieldId == 1); + rdr.readInt64(userId); + + uint32_t u32; + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_I32); + assert(fieldId == 1); + rdr.readInt32(u32); + + auto *pUser = FindUser(userId); + if (pUser == nullptr) + debugLogA("Skipping presence from unknown user %lld", userId); + else { + debugLogA("Presence from user %lld => %d", userId, u32); + setWord(pUser->hContact, "Status", (u32 != 0) ? ID_STATUS_ONLINE : ID_STATUS_OFFLINE); + } + + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_I64); + assert(fieldId == 1 || fieldId == 3 || fieldId == 4); + rdr.readInt64(timestamp); + + while (!rdr.isStop()) { + rdr.readField(fieldType, fieldId); + assert(fieldType == FB_THRIFT_TYPE_I64 || fieldType == FB_THRIFT_TYPE_I16 || fieldType == FB_THRIFT_TYPE_I32); + rdr.readIntV(voipBits); + } + + rdr.readByte(fieldType); + assert(fieldType == FB_THRIFT_TYPE_STOP); + } + + rdr.readByte(fieldType); + assert(fieldType == FB_THRIFT_TYPE_STOP); +} + +void FacebookProto::OnPublishUtn(FbThriftReader &rdr) +{ + JSONNode root = JSONNode::parse(rdr.rest()); + auto *pUser = FindUser(_wtoi64(root["sender_fbid"].as_mstring())); + if (pUser != nullptr) { + int length = (root["state"].as_int() == 0) ? PROTOTYPE_CONTACTTYPING_OFF : 60; + CallService(MS_PROTO_CONTACTISTYPING, pUser->hContact, length); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +struct +{ + const char *messageType; + void (FacebookProto:: *pFunc)(const JSONNode &); +} +static MsgHandlers[] = +{ + { "deltaNewMessage", &FacebookProto::OnPublishPrivateMessage }, + { "deltaThreadName", &FacebookProto::OnPublishThreadName }, + { "deltaSentMessage", &FacebookProto::OnPublishSentMessage }, + { "deltaReadReceipt", &FacebookProto::OnPublishReadReceipt }, + { "deltaParticipantsAddedToGroupThread", &FacebookProto::OnPublishChatJoin }, + { "deltaParticipantLeftGroupThread", &FacebookProto::OnPublishChatLeave }, +}; + +void FacebookProto::OnPublishMessage(FbThriftReader &rdr) +{ + uint8_t stop; + if (rdr.isStop()) + rdr.readByte(stop); + else { + uint8_t type; + uint16_t id; + rdr.readField(type, id); + _ASSERT(type == FB_THRIFT_TYPE_STRING); + _ASSERT(id == 1 || id == 2); + + char *szShit = nullptr; + rdr.readStr(szShit); + mir_free(szShit); + + rdr.readByte(stop); + } + + CMStringA szJson(rdr.rest()); + debugLogA("MS: <%s>", szJson.c_str()); + JSONNode root = JSONNode::parse(szJson); + + CMStringA errorCode = root["errorCode"].as_mstring(); + if (!errorCode.IsEmpty()) { + if (!m_QueueCreated && (errorCode == "ERROR_QUEUE_OVERFLOW" || errorCode == "ERROR_QUEUE_NOT_FOUND" || errorCode == "ERROR_QUEUE_LOST" || errorCode == "ERROR_QUEUE_EXCEEDS_MAX_DELTAS")) { + m_QueueCreated = true; // prevent queue creation request from being sent twice + delSetting(DBKEY_SYNC_TOKEN); m_szSyncToken.Empty(); + delSetting(DBKEY_SID); m_sid = 0; + if (!RefreshSid()) { + ConnectionFailed(); + return; + } + + MqttQueueConnect(); + } + } + + CMStringW str = root["lastIssuedSeqId"].as_mstring(); + if (!str.IsEmpty()) { + setWString(DBKEY_SID, str); + m_sid = _wtoi64(str); + } + + str = root["syncToken"].as_mstring(); + if (!str.IsEmpty()) { + m_szSyncToken = str; + setString(DBKEY_SYNC_TOKEN, m_szSyncToken); + return; + } + + for (auto &it : root["deltas"]) { + for (auto &handler : MsgHandlers) { + auto &json = it[handler.messageType]; + if (json) { + (this->*(handler.pFunc))(json); + break; + } + } + } +} + +// new message arrived +struct +{ + const char *szTag, *szClientVersion; +} +static facebookClients[] = +{ + { "source:titan:web", "Facebook (website)" }, + { "app_id:256002347743983", "Facebook (Facebook Messenger)" } +}; + +void FacebookProto::FetchAttach(const CMStringA &mid, __int64 fbid, CMStringA &szBody) +{ + for (int iAttempt = 0; iAttempt < 5; iAttempt++) { + auto *pReq = CreateRequest(FB_API_URL_ATTACH, "getAttachment", "messaging.getAttachment"); + pReq << CHAR_PARAM("mid", mid) << INT64_PARAM("aid", fbid); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + switch (reply.error()) { + case 0: + { + std::string uri = reply.data()["redirect_uri"].as_string(); + std::string type = reply.data()["content_type"].as_string(); + if (!uri.empty()) + szBody.AppendFormat("\r\n%s: %s", TranslateU(type.find("image/") != -1 ? "Picture attachment" : "File attachment"), uri.c_str()); + } + return; + + case 509: // attachment isn't ready, wait a bit and retry + ::Sleep(100); + continue; + + default: // shit happened, exiting + return; + } + } +} + +void FacebookProto::OnPublishPrivateMessage(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + if (!offlineId) { + debugLogA("We care about messages only, event skipped"); + return; + } + + bool bIsChat; + CMStringW wszUserId; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + + if (!bIsChat && pUser == nullptr) + pUser = AddContact(wszUserId, true); + else if (bIsChat && (pUser == nullptr || !pUser->bIsChatInitialized)) // chat room does not exists or is not initialized + pUser = RefreshThread(wszUserId); + + if (pUser == nullptr) { + debugLogA("User not found and adding failed, event skipped"); + return; + } + + for (auto &it : metadata["tags"]) { + auto *szTagName = it.name(); + for (auto &cli : facebookClients) { + if (!mir_strcmp(szTagName, cli.szTag)) { + setString(pUser->hContact, "MirVer", cli.szClientVersion); + break; + } + } + } + + CMStringA szId(metadata["messageId"].as_mstring()); + if (CheckOwnMessage(pUser, offlineId, szId)) { + debugLogA("own message <%s> skipped", szId.c_str()); + return; + } + + if (db_event_getById(m_szModuleName, szId)) { + debugLogA("this message <%s> was already stored, exiting", szId.c_str()); + return; + } + + // parse message body + CMStringA szBody(root["body"].as_string().c_str()); + if (szBody.IsEmpty()) + szBody = metadata["snippet"].as_string().c_str(); + + // parse stickers + CMStringA stickerId = root["stickerId"].as_mstring(); + if (!stickerId.IsEmpty()) { + if (ServiceExists(MS_SMILEYADD_LOADCONTACTSMILEYS)) { + CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); + CreateDirectoryTreeW(wszPath); + + bool bSuccess = false; + CMStringW wszFileName(FORMAT, L"%s\\STK{%S}.png", wszPath.c_str(), stickerId.c_str()); + uint32_t dwAttrib = GetFileAttributesW(wszFileName); + if (dwAttrib == INVALID_FILE_ATTRIBUTES) { + wszFileName.Format(L"%s\\STK{%S}.webp", wszPath.c_str(), stickerId.c_str()); + dwAttrib = GetFileAttributesW(wszFileName); + } + + // new sticker + if (dwAttrib == INVALID_FILE_ATTRIBUTES) { + auto *pReq = CreateRequestGQL(FB_API_QUERY_STICKER); + pReq << CHAR_PARAM("query_params", CMStringA(FORMAT, "{\"0\":[\"%s\"]}", stickerId.c_str())); + pReq->CalcSig(); + + JsonReply reply(ExecuteRequest(pReq)); + if (!reply.error()) { + for (auto &sticker : reply.data()) { + // std::string szUrl = sticker["animated_image"]["uri"].as_string(); + // if (szUrl.empty()) + // szUrl = sticker["thread_image"]["uri"].as_string(); + // else + // wszFileName.Format(L"%s\\STK{%S}.webp", wszPath.c_str(), stickerId.c_str()); + std::string szUrl = sticker["thread_image"]["uri"].as_string(); + + NETLIBHTTPREQUEST req = {}; + req.cbSize = sizeof(req); + req.flags = NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; + req.requestType = REQUEST_GET; + req.szUrl = (char*)szUrl.c_str(); + + NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(m_hNetlibUser, &req); + if (pReply != nullptr && pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { + bSuccess = true; + FILE *out = _wfopen(wszFileName, L"wb"); + fwrite(pReply->pData, 1, pReply->dataLength, out); + fclose(out); + } + } + } + } + else bSuccess = true; + + if (bSuccess) { + if (!szBody.IsEmpty()) + szBody += "\r\n"; + szBody += "STK{" + stickerId + "}"; + + SMADD_CONT cont = { 1, m_szModuleName, wszFileName }; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); + } + else szBody += TranslateU("Sticker received"); + } + else szBody += TranslateU("SmileyAdd plugin required to support stickers"); + } + + // parse attachments (links, files, ...) + for (auto &it : root["attachments"]) { + // madness... json inside json + CMStringA szJson(it["xmaGraphQL"].as_mstring()); + if (szJson.IsEmpty()) { + __int64 fbid = _wtoi64(it["fbid"].as_mstring()); + if (fbid == 0) { + debugLogA("Neither a GQL nor an inline attachment, nothing to do"); + continue; + } + + // inline attachment, request its description + FetchAttach(szId, fbid, szBody); + continue; + } + + JSONROOT nBody(szJson); + if (!nBody) + continue; + + const JSONNode &attach = (*nBody).at((json_index_t)0)["story_attachment"]; + szBody += "\r\n-----------------------------------"; + + CMStringA str = attach["url"].as_mstring(); + if (!str.IsEmpty()) { + if (str.Left(8) == "fbrpc://") { + int iStart = str.Find("target_url="); + if (iStart != 0) { + CMStringA tmp; + + iStart += 11; + int iEnd = str.Find("&", iStart); + if (iEnd != -1) + tmp = str.Mid(iStart, iEnd - iStart); + else + tmp = str.Right(iStart); + + mir_urlDecode(tmp.GetBuffer()); + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("URL"), tmp.c_str()); + } + } + else szBody.AppendFormat("\r\n\t%s: %s", TranslateU("URL"), str.c_str()); + } + + str = attach["title"].as_string().c_str(); + if (!str.IsEmpty()) + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Title"), str.c_str()); + + str = attach["source"]["text"].as_string().c_str(); + if (!str.IsEmpty()) + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Source"), str.c_str()); + + str = attach["description"]["text"].as_string().c_str(); + if (!str.IsEmpty()) + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Description"), str.c_str()); + + str = attach["media"]["playable_url"].as_string().c_str(); + if (!str.IsEmpty()) + szBody.AppendFormat("\r\n\t%s: %s", TranslateU("Playable media"), str.c_str()); + } + + // if that's a group chat, send it to the room + CMStringW wszActorFbId(metadata["actorFbId"].as_mstring()); + __int64 actorFbId = _wtoi64(wszActorFbId); + + if (pUser->bIsChat) { + szBody.Replace("%", "%%"); + ptrW wszText(mir_utf8decodeW(szBody)); + + // TODO: GC_EVENT_JOIN for chat participants which are missing (for example added later during group chat) + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; + gce.pszID.w = wszUserId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszActorFbId; + gce.pszText.w = wszText; + gce.time = time(0); + gce.bIsMe = actorFbId == m_uid; + Chat_Event(&gce); + + debugLogA("New channel %lld message from %S: %s", pUser->id, gce.pszUID.w, gce.pszText.w); + } + else { // otherwise store a private message + PROTORECVEVENT pre = {}; + pre.timestamp = uint32_t(_wtoi64(metadata["timestamp"].as_mstring()) / 1000); + pre.szMessage = (char *)szBody.c_str(); + pre.szMsgId = (char *)szId.c_str(); + + if (m_uid == actorFbId) + pre.flags |= PREF_SENT; + + ProtoChainRecvMsg(pUser->hContact, &pre); + } +} + +// changing thread name +void FacebookProto::OnPublishThreadName(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + if (!offlineId) { + debugLogA("We care about messages only, event skipped"); + return; + } + + bool bIsChat; + CMStringW wszUserId; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + if (!bIsChat || pUser == nullptr) + return; + + CMStringW wszTitle = root["name"].as_mstring(); + if (!wszTitle.IsEmpty()) + setWString(pUser->hContact, DBKEY_NICK, wszTitle); + else + delSetting(pUser->hContact, DBKEY_NICK); +} + +// user joined chat +void FacebookProto::OnPublishChatJoin(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + if (!offlineId) { + debugLogA("We care about messages only, event skipped"); + return; + } + + bool bIsChat; + CMStringW wszUserId; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + if (!bIsChat || pUser == nullptr) + return; + + CMStringW wszText(metadata["adminText"].as_mstring()); + for (auto &it : root["addedParticipants"]) { + CMStringW wszNick(it["fullName"].as_mstring()), wszId(it["userFbId"].as_mstring()); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; + gce.pszID.w = wszUserId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszNick.w = wszNick; + gce.pszUID.w = wszId; + gce.pszText.w = wszText; + gce.time = time(0); + gce.bIsMe = _wtoi64(wszId) == m_uid; + Chat_Event(&gce); + } +} + +// user left chat +void FacebookProto::OnPublishChatLeave(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + if (!offlineId) { + debugLogA("We care about messages only, event skipped"); + return; + } + + bool bIsChat; + CMStringW wszUserId; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + if (!bIsChat || pUser == nullptr) + return; + + CMStringW wszText(metadata["adminText"].as_mstring()), wszId(root["leftParticipantFbId"].as_mstring()); + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_PART }; + gce.pszID.w = wszUserId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszId; + gce.pszText.w = wszText; + gce.time = time(0); + gce.bIsMe = _wtoi64(wszId) == m_uid; + Chat_Event(&gce); +} + +// read notification +void FacebookProto::OnPublishReadReceipt(const JSONNode &root) +{ + CMStringW wszUserId; + bool bIsChat; + auto *pUser = UserFromJson(root, wszUserId, bIsChat); + if (pUser == nullptr) { + debugLogA("Message from unknown contact %S, ignored", wszUserId.c_str()); + return; + } + + uint32_t timestamp = _wtoi64(root["watermarkTimestampMs"].as_mstring()); + for (MEVENT ev = db_event_firstUnread(pUser->hContact); ev != 0; ev = db_event_next(pUser->hContact, ev)) { + DBEVENTINFO dbei = {}; + if (db_event_get(ev, &dbei)) + continue; + + if (dbei.timestamp > timestamp) + break; + + if (!dbei.markedRead()) + db_event_markRead(pUser->hContact, ev); + } +} + +// my own message was sent +bool FacebookProto::CheckOwnMessage(FacebookUser *pUser, __int64 offlineId, const char *pszMsgId) +{ + COwnMessage tmp; + if (!ExtractOwnMessage(offlineId, tmp)) + return false; + + if (pUser->bIsChat) { + CMStringW wszId(FORMAT, L"%lld", m_uid); + tmp.wszText.Replace(L"%", L"%%"); + + wchar_t userId[100]; + _i64tow_s(pUser->id, userId, _countof(userId), 10); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; + gce.pszID.w = userId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszId; + gce.pszText.w = tmp.wszText; + gce.time = time(0); + gce.bIsMe = true; + Chat_Event(&gce); + } + else ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)tmp.reqId, (LPARAM)pszMsgId); + + return true; +} + +void FacebookProto::OnPublishSentMessage(const JSONNode &root) +{ + auto &metadata = root["messageMetadata"]; + + __int64 offlineId = _wtoi64(metadata["offlineThreadingId"].as_mstring()); + + CMStringW wszUserId; + bool bIsChat; + auto *pUser = UserFromJson(metadata, wszUserId, bIsChat); + if (pUser == nullptr) { + debugLogA("Message from unknown contact %s, ignored", wszUserId.c_str()); + return; + } + + std::string szMsgId(metadata["messageId"].as_string()); + CheckOwnMessage(pUser, offlineId, szMsgId.c_str()); +} diff --git a/protocols/Facebook/src/stdafx.cxx b/protocols/Facebook/src/stdafx.cxx index d265a4c02e..8c570f6949 100644 --- a/protocols/Facebook/src/stdafx.cxx +++ b/protocols/Facebook/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - -#include "stdafx.h" +/* +Copyright (C) 2012-23 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 . +*/ + +#include "stdafx.h" diff --git a/protocols/Facebook/src/stdafx.h b/protocols/Facebook/src/stdafx.h index 3d5f28057c..aadac15f0e 100644 --- a/protocols/Facebook/src/stdafx.h +++ b/protocols/Facebook/src/stdafx.h @@ -1,67 +1,67 @@ -/* - -Facebook plugin for Miranda NG -Copyright © 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../../libs/zlib/src/zlib.h" - -#include "db.h" -#include "dialogs.h" -#include "mqtt.h" -#include "proto.h" -#include "resource.h" -#include "version.h" - -extern bool g_bMessageState; +/* + +Facebook plugin for Miranda NG +Copyright © 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../libs/zlib/src/zlib.h" + +#include "db.h" +#include "dialogs.h" +#include "mqtt.h" +#include "proto.h" +#include "resource.h" +#include "version.h" + +extern bool g_bMessageState; diff --git a/protocols/Facebook/src/thrift.cpp b/protocols/Facebook/src/thrift.cpp index af0ceb4dd7..c7718c1a60 100644 --- a/protocols/Facebook/src/thrift.cpp +++ b/protocols/Facebook/src/thrift.cpp @@ -1,308 +1,308 @@ -/* - -Facebook plugin for Miranda NG -Copyright 2019-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -///////////////////////////////////////////////////////////////////////////////////////// -// FbThrift class members - -#include "stdafx.h" - -static uint8_t encodeType(int type) -{ - switch (type) { - case FB_THRIFT_TYPE_BOOL: - return 2; - case FB_THRIFT_TYPE_BYTE: - return 3; - case FB_THRIFT_TYPE_I16: - return 4; - case FB_THRIFT_TYPE_I32: - return 5; - case FB_THRIFT_TYPE_I64: - return 6; - case FB_THRIFT_TYPE_DOUBLE: - return 7; - case FB_THRIFT_TYPE_STRING: - return 8; - case FB_THRIFT_TYPE_LIST: - return 9; - case FB_THRIFT_TYPE_SET: - return 10; - case FB_THRIFT_TYPE_MAP: - return 11; - case FB_THRIFT_TYPE_STRUCT: - return 12; - default: - return 0; - } -} - -FbThrift& FbThrift::operator<<(uint8_t value) -{ - m_buf.append(&value, 1); - return *this; -} - -FbThrift& FbThrift::operator<<(const char *str) -{ - size_t len = mir_strlen(str); - writeIntV(len); - m_buf.append(str, len); - return *this; -} - -void FbThrift::writeBool(bool bValue) -{ - uint8_t b = (bValue) ? 0x11 : 0x12; - m_buf.append(&b, 1); -} - -void FbThrift::writeBuf(const void *pData, size_t cbLen) -{ - m_buf.append(pData, cbLen); -} - -void FbThrift::writeField(int iType) -{ - uint8_t type = encodeType(iType) + 0x10; - m_buf.append(&type, 1); -} - -void FbThrift::writeField(int iType, int id, int lastid) -{ - uint8_t type = encodeType(iType); - uint8_t diff = uint8_t(id - lastid); - if (diff > 0x0F) { - m_buf.append(&type, 1); - writeInt64(id); - } - else { - type += (diff << 4); - m_buf.append(&type, 1); - } -} - -void FbThrift::writeList(int iType, int size) -{ - uint8_t type = encodeType(iType); - if (size > 14) { - writeIntV(size); - *this << (type | 0xF0); - } - else *this << (type | (size << 4)); -} - -void FbThrift::writeInt16(uint16_t value) -{ - value = htons(value); - m_buf.append(&value, sizeof(value)); -} - -void FbThrift::writeInt32(int32_t value) -{ - writeIntV((value << 1) ^ (value >> 31)); -} - -void FbThrift::writeInt64(int64_t value) -{ - writeIntV((value << 1) ^ (value >> 63)); -} - -void FbThrift::writeIntV(uint64_t value) -{ - bool bLast; - do { - bLast = (value & ~0x7F) == 0; - uint8_t b = value & 0x7F; - if (!bLast) { - b |= 0x80; - value >>= 7; - } - m_buf.append(&b, 1); - } while (!bLast); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// FbThriftReader class members - -uint8_t FbThriftReader::decodeType(int type) -{ - switch (type) { - case 0: - return FB_THRIFT_TYPE_STOP; - case 1: - m_lastBval = m_lastBool = true; - return FB_THRIFT_TYPE_BOOL; - case 2: - m_lastBool = true; - m_lastBval = false; - return FB_THRIFT_TYPE_BOOL; - case 3: - return FB_THRIFT_TYPE_BYTE; - case 4: - return FB_THRIFT_TYPE_I16; - case 5: - return FB_THRIFT_TYPE_I32; - case 6: - return FB_THRIFT_TYPE_I64; - case 7: - return FB_THRIFT_TYPE_DOUBLE; - case 8: - return FB_THRIFT_TYPE_STRING; - case 9: - return FB_THRIFT_TYPE_LIST; - case 10: - return FB_THRIFT_TYPE_SET; - case 11: - return FB_THRIFT_TYPE_MAP; - case 12: - return FB_THRIFT_TYPE_STRUCT; - default: - return 0; - } -} - -bool FbThriftReader::isStop() -{ - byte b; - if (!readByte(b)) - return true; - - offset--; - return b == FB_THRIFT_TYPE_STOP; -} - -bool FbThriftReader::readBool(bool &bVal) -{ - if (m_lastBool) { - bVal = m_lastBval; - m_lastBool = false; - return true; - } - - byte b; - if (!readByte(b)) - return false; - - bVal = b == 0x11; - return true; -} - -bool FbThriftReader::readByte(uint8_t &b) -{ - if (offset >= size()) - return false; - - b = *((uint8_t *)data() + offset); - offset++; - return true; -} - -bool FbThriftReader::readField(uint8_t &type, uint16_t &id) -{ - byte b; - if (!readByte(b)) - return false; - - type = decodeType(b & 0x0F); - id = (b >> 4); - return (id == 0) ? readInt16(id) : true; -} - -bool FbThriftReader::readIntV(uint64_t &val) -{ - uint8_t b; - unsigned i = 0; - val = 0; - - do { - if (!readByte(b)) - return false; - - val |= (uint64_t(b & 0x7F) << i); - i += 7; - } while ((b & 0x80) != 0); - - return true; -} - -bool FbThriftReader::readList(uint8_t &type, uint32_t &size) -{ - byte b; - if (!readByte(b)) - return false; - - type = decodeType(b & 0x0F); - size = b >> 4; - if (size == 0x0F) { - uint64_t tmp; - if (!readIntV(tmp)) - return false; - size = (uint32_t)tmp; - } - return true; -} - -bool FbThriftReader::readStr(char *&val) -{ - uint64_t tmp; - if (!readIntV(tmp)) - return false; - - uint32_t cbLen = (uint32_t)tmp; - if (offset + cbLen >= size()) - return false; - - if (cbLen > 0) { - val = mir_strndup((char *)data() + offset, cbLen); - offset += cbLen; - } - else val = nullptr; - return true; -} - -bool FbThriftReader::readInt16(uint16_t &val) -{ - if (offset + 2 >= size()) - return false; - - val = ntohs(*(u_short *)((char *)data() + offset)); - offset += 2; - return true; -} - -bool FbThriftReader::readInt32(uint32_t &val) -{ - uint64_t tmp; - if (!readIntV(tmp)) - return false; - - val = (uint32_t )tmp; - return true; -} - -bool FbThriftReader::readInt64(uint64_t &val) -{ - uint64_t tmp; - if (!readIntV(tmp)) - return false; - - val = (tmp >> 0x01) ^ -(tmp & 0x01); - return true; -} +/* + +Facebook plugin for Miranda NG +Copyright 2019-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +///////////////////////////////////////////////////////////////////////////////////////// +// FbThrift class members + +#include "stdafx.h" + +static uint8_t encodeType(int type) +{ + switch (type) { + case FB_THRIFT_TYPE_BOOL: + return 2; + case FB_THRIFT_TYPE_BYTE: + return 3; + case FB_THRIFT_TYPE_I16: + return 4; + case FB_THRIFT_TYPE_I32: + return 5; + case FB_THRIFT_TYPE_I64: + return 6; + case FB_THRIFT_TYPE_DOUBLE: + return 7; + case FB_THRIFT_TYPE_STRING: + return 8; + case FB_THRIFT_TYPE_LIST: + return 9; + case FB_THRIFT_TYPE_SET: + return 10; + case FB_THRIFT_TYPE_MAP: + return 11; + case FB_THRIFT_TYPE_STRUCT: + return 12; + default: + return 0; + } +} + +FbThrift& FbThrift::operator<<(uint8_t value) +{ + m_buf.append(&value, 1); + return *this; +} + +FbThrift& FbThrift::operator<<(const char *str) +{ + size_t len = mir_strlen(str); + writeIntV(len); + m_buf.append(str, len); + return *this; +} + +void FbThrift::writeBool(bool bValue) +{ + uint8_t b = (bValue) ? 0x11 : 0x12; + m_buf.append(&b, 1); +} + +void FbThrift::writeBuf(const void *pData, size_t cbLen) +{ + m_buf.append(pData, cbLen); +} + +void FbThrift::writeField(int iType) +{ + uint8_t type = encodeType(iType) + 0x10; + m_buf.append(&type, 1); +} + +void FbThrift::writeField(int iType, int id, int lastid) +{ + uint8_t type = encodeType(iType); + uint8_t diff = uint8_t(id - lastid); + if (diff > 0x0F) { + m_buf.append(&type, 1); + writeInt64(id); + } + else { + type += (diff << 4); + m_buf.append(&type, 1); + } +} + +void FbThrift::writeList(int iType, int size) +{ + uint8_t type = encodeType(iType); + if (size > 14) { + writeIntV(size); + *this << (type | 0xF0); + } + else *this << (type | (size << 4)); +} + +void FbThrift::writeInt16(uint16_t value) +{ + value = htons(value); + m_buf.append(&value, sizeof(value)); +} + +void FbThrift::writeInt32(int32_t value) +{ + writeIntV((value << 1) ^ (value >> 31)); +} + +void FbThrift::writeInt64(int64_t value) +{ + writeIntV((value << 1) ^ (value >> 63)); +} + +void FbThrift::writeIntV(uint64_t value) +{ + bool bLast; + do { + bLast = (value & ~0x7F) == 0; + uint8_t b = value & 0x7F; + if (!bLast) { + b |= 0x80; + value >>= 7; + } + m_buf.append(&b, 1); + } while (!bLast); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// FbThriftReader class members + +uint8_t FbThriftReader::decodeType(int type) +{ + switch (type) { + case 0: + return FB_THRIFT_TYPE_STOP; + case 1: + m_lastBval = m_lastBool = true; + return FB_THRIFT_TYPE_BOOL; + case 2: + m_lastBool = true; + m_lastBval = false; + return FB_THRIFT_TYPE_BOOL; + case 3: + return FB_THRIFT_TYPE_BYTE; + case 4: + return FB_THRIFT_TYPE_I16; + case 5: + return FB_THRIFT_TYPE_I32; + case 6: + return FB_THRIFT_TYPE_I64; + case 7: + return FB_THRIFT_TYPE_DOUBLE; + case 8: + return FB_THRIFT_TYPE_STRING; + case 9: + return FB_THRIFT_TYPE_LIST; + case 10: + return FB_THRIFT_TYPE_SET; + case 11: + return FB_THRIFT_TYPE_MAP; + case 12: + return FB_THRIFT_TYPE_STRUCT; + default: + return 0; + } +} + +bool FbThriftReader::isStop() +{ + byte b; + if (!readByte(b)) + return true; + + offset--; + return b == FB_THRIFT_TYPE_STOP; +} + +bool FbThriftReader::readBool(bool &bVal) +{ + if (m_lastBool) { + bVal = m_lastBval; + m_lastBool = false; + return true; + } + + byte b; + if (!readByte(b)) + return false; + + bVal = b == 0x11; + return true; +} + +bool FbThriftReader::readByte(uint8_t &b) +{ + if (offset >= size()) + return false; + + b = *((uint8_t *)data() + offset); + offset++; + return true; +} + +bool FbThriftReader::readField(uint8_t &type, uint16_t &id) +{ + byte b; + if (!readByte(b)) + return false; + + type = decodeType(b & 0x0F); + id = (b >> 4); + return (id == 0) ? readInt16(id) : true; +} + +bool FbThriftReader::readIntV(uint64_t &val) +{ + uint8_t b; + unsigned i = 0; + val = 0; + + do { + if (!readByte(b)) + return false; + + val |= (uint64_t(b & 0x7F) << i); + i += 7; + } while ((b & 0x80) != 0); + + return true; +} + +bool FbThriftReader::readList(uint8_t &type, uint32_t &size) +{ + byte b; + if (!readByte(b)) + return false; + + type = decodeType(b & 0x0F); + size = b >> 4; + if (size == 0x0F) { + uint64_t tmp; + if (!readIntV(tmp)) + return false; + size = (uint32_t)tmp; + } + return true; +} + +bool FbThriftReader::readStr(char *&val) +{ + uint64_t tmp; + if (!readIntV(tmp)) + return false; + + uint32_t cbLen = (uint32_t)tmp; + if (offset + cbLen >= size()) + return false; + + if (cbLen > 0) { + val = mir_strndup((char *)data() + offset, cbLen); + offset += cbLen; + } + else val = nullptr; + return true; +} + +bool FbThriftReader::readInt16(uint16_t &val) +{ + if (offset + 2 >= size()) + return false; + + val = ntohs(*(u_short *)((char *)data() + offset)); + offset += 2; + return true; +} + +bool FbThriftReader::readInt32(uint32_t &val) +{ + uint64_t tmp; + if (!readIntV(tmp)) + return false; + + val = (uint32_t )tmp; + return true; +} + +bool FbThriftReader::readInt64(uint64_t &val) +{ + uint64_t tmp; + if (!readIntV(tmp)) + return false; + + val = (tmp >> 0x01) ^ -(tmp & 0x01); + return true; +} diff --git a/protocols/Facebook/src/version.h b/protocols/Facebook/src/version.h index 4a483c02f1..660e8cfe94 100644 --- a/protocols/Facebook/src/version.h +++ b/protocols/Facebook/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Facebook protocol support for Miranda NG." #define __AUTHOR "Miranda NG Team" #define __AUTHORWEB "https://miranda-ng.org/p/Facebook" -#define __COPYRIGHT "© 2019-22 Miranda NG team" +#define __COPYRIGHT "© 2019-23 Miranda NG team" diff --git a/protocols/Gadu-Gadu/src/stdafx.cxx b/protocols/Gadu-Gadu/src/stdafx.cxx index 2e0bdaf2a1..e3dcdf89b5 100644 --- a/protocols/Gadu-Gadu/src/stdafx.cxx +++ b/protocols/Gadu-Gadu/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Gadu-Gadu/src/userinfo.cpp b/protocols/Gadu-Gadu/src/userinfo.cpp index 75caf4f09f..66c63d13d9 100644 --- a/protocols/Gadu-Gadu/src/userinfo.cpp +++ b/protocols/Gadu-Gadu/src/userinfo.cpp @@ -1,328 +1,328 @@ -/* -Copyright (C) 2012-22 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 . -*/ - -#include "gg.h" - -//////////////////////////////////////////////////////////////////////////////// -// Info Page UI dialog - -class GaduUserInfoDlg : public CUserInfoPageDlg -{ - GaduProto *gg; - bool updating = false; - - CCtrlCombo cmbGender; - CCtrlButton btnSave; - - void SetValue(int idCtrl, char *szModule, char *szSetting, int special) - { - DBVARIANT dbv = { 0 }; - wchar_t str[256]; - wchar_t *ptstr = nullptr; - wchar_t* valT = nullptr; - bool unspecified; - - dbv.type = DBVT_DELETED; - if (szModule == nullptr) - unspecified = true; - else - unspecified = db_get(m_hContact, szModule, szSetting, &dbv) != 0; - - if (!unspecified) { - switch (dbv.type) { - case DBVT_BYTE: - if (special == SVS_GENDER) { - if (dbv.cVal == 'M') - ptstr = TranslateT("Male"); - else if (dbv.cVal == 'F') - ptstr = TranslateT("Female"); - else - unspecified = 1; - } - else if (special == SVS_MONTH) { - if (dbv.bVal > 0 && dbv.bVal <= 12) { - ptstr = str; - GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SABBREVMONTHNAME1 - 1 + dbv.bVal, str, _countof(str)); - } - else - unspecified = 1; - } - else if (special == SVS_TIMEZONE) { - if (dbv.cVal == -100) - unspecified = 1; - else { - ptstr = str; - mir_snwprintf(str, dbv.cVal ? L"GMT%+d:%02d" : L"GMT", -dbv.cVal / 2, (dbv.cVal & 1) * 30); - } - } - else { - unspecified = (special == SVS_ZEROISUNSPEC && dbv.bVal == 0); - ptstr = _itow(special == SVS_SIGNED ? dbv.cVal : dbv.bVal, str, 10); - } - break; - case DBVT_WORD: - if (special == SVS_COUNTRY) { - char* pstr = (char*)CallService(MS_UTILS_GETCOUNTRYBYNUMBER, dbv.wVal, 0); - if (pstr == nullptr) { - unspecified = 1; - } - else { - ptstr = str; - mir_snwprintf(str, L"%S", pstr); - } - } - else { - unspecified = (special == SVS_ZEROISUNSPEC && dbv.wVal == 0); - ptstr = _itow(special == SVS_SIGNED ? dbv.sVal : dbv.wVal, str, 10); - } - break; - case DBVT_DWORD: - unspecified = (special == SVS_ZEROISUNSPEC && dbv.dVal == 0); - if (special == SVS_IP) { - struct in_addr ia; - ia.S_un.S_addr = htonl(dbv.dVal); - char* pstr = inet_ntoa(ia); - if (pstr == nullptr) { - unspecified = 1; - } - else { - ptstr = str; - mir_snwprintf(str, L"%S", pstr); - } - if (dbv.dVal == 0) - unspecified = 1; - } - else if (special == SVS_GGVERSION) { - ptstr = str; - mir_snwprintf(str, L"%S", (char *)gg_version2string(dbv.dVal)); - } - else { - ptstr = _itow(special == SVS_SIGNED ? dbv.lVal : dbv.dVal, str, 10); - } - break; - case DBVT_ASCIIZ: - unspecified = (special == SVS_ZEROISUNSPEC && dbv.pszVal[0] == '\0'); - ptstr = str; - mir_snwprintf(str, L"%S", dbv.pszVal); - break; - case DBVT_WCHAR: - unspecified = (special == SVS_ZEROISUNSPEC && dbv.pwszVal[0] == '\0'); - ptstr = dbv.pwszVal; - break; - case DBVT_UTF8: - unspecified = (special == SVS_ZEROISUNSPEC && dbv.pszVal[0] == '\0'); - valT = mir_utf8decodeW(dbv.pszVal); - ptstr = str; - wcscpy_s(str, _countof(str), valT); - mir_free(valT); - break; - default: - ptstr = str; - mir_wstrcpy(str, L"???"); - break; - } - } - - if (m_hContact != 0) { - EnableWindow(GetDlgItem(m_hwnd, idCtrl), !unspecified); - if (unspecified) - SetDlgItemText(m_hwnd, idCtrl, TranslateT("")); - else - SetDlgItemText(m_hwnd, idCtrl, ptstr); - } - else { - EnableWindow(GetDlgItem(m_hwnd, idCtrl), TRUE); - if (!unspecified) - SetDlgItemText(m_hwnd, idCtrl, ptstr); - } - db_free(&dbv); - } - -public: - GaduUserInfoDlg(GaduProto *_gg, int idDialog) : - CUserInfoPageDlg(g_plugin, idDialog), - gg(_gg), - btnSave(this, IDC_SAVE), - cmbGender(this, IDC_GENDER) - { - btnSave.OnClick = Callback(this, &GaduUserInfoDlg::onClick_Save); - } - - bool OnInitDialog() override - { - // Add genders - cmbGender.AddString(L"", 0); - cmbGender.AddString(TranslateT("Female"), 1); - cmbGender.AddString(TranslateT("Male"), 2); - return true; - } - - bool OnRefresh() override - { - // Show updated message - if (updating) { - MessageBox(nullptr, TranslateT("Your details has been uploaded to the public directory."), - gg->m_tszUserName, MB_OK | MB_ICONINFORMATION); - updating = false; - return false; - } - - char *szProto = (m_hContact == NULL) ? gg->m_szModuleName : Proto_GetBaseAccountName(m_hContact); - if (szProto == nullptr) - return false; - - // Disable when updating - m_bInitialized = false; - - SetValue(IDC_UIN, szProto, GG_KEY_UIN, 0); - SetValue(IDC_REALIP, szProto, GG_KEY_CLIENTIP, SVS_IP); - SetValue(IDC_PORT, szProto, GG_KEY_CLIENTPORT, SVS_ZEROISUNSPEC); - SetValue(IDC_VERSION, szProto, GG_KEY_CLIENTVERSION, SVS_GGVERSION); - - SetValue(IDC_FIRSTNAME, szProto, GG_KEY_PD_FIRSTNAME, SVS_NORMAL); - SetValue(IDC_LASTNAME, szProto, GG_KEY_PD_LASTNAME, SVS_NORMAL); - SetValue(IDC_NICKNAME, szProto, GG_KEY_PD_NICKNAME, SVS_NORMAL); - SetValue(IDC_BIRTHYEAR, szProto, GG_KEY_PD_BIRTHYEAR, SVS_ZEROISUNSPEC); - SetValue(IDC_CITY, szProto, GG_KEY_PD_CITY, SVS_NORMAL); - SetValue(IDC_FAMILYNAME, szProto, GG_KEY_PD_FAMILYNAME, SVS_NORMAL); - SetValue(IDC_CITYORIGIN, szProto, GG_KEY_PD_FAMILYCITY, SVS_NORMAL); - - if (m_hContact) { - SetValue(IDC_GENDER, szProto, GG_KEY_PD_GANDER, SVS_GENDER); - SetValue(IDC_STATUSDESCR, "CList", GG_KEY_STATUSDESCR, SVS_NORMAL); - } - else switch ((char)db_get_b(m_hContact, gg->m_szModuleName, GG_KEY_PD_GANDER, (uint8_t)'?')) { - case 'F': - SendDlgItemMessage(m_hwnd, IDC_GENDER, CB_SETCURSEL, 1, 0); - break; - case 'M': - SendDlgItemMessage(m_hwnd, IDC_GENDER, CB_SETCURSEL, 2, 0); - break; - default: - SendDlgItemMessage(m_hwnd, IDC_GENDER, CB_SETCURSEL, 0, 0); - } - - // Disable when updating - m_bInitialized = true; - return false; - } - - void OnChange() override - { - EnableWindow(GetDlgItem(m_hwnd, IDC_SAVE), TRUE); - } - - void onClick_Save(CCtrlButton*) - { - wchar_t text[256]; - - if (!gg->isonline()) { - MessageBox(nullptr, - TranslateT("You have to be logged in before you can change your details."), - gg->m_tszUserName, MB_OK | MB_ICONSTOP); - return; - } - - EnableWindow(GetDlgItem(m_hwnd, IDC_SAVE), FALSE); - - gg_pubdir50_t req = gg_pubdir50_new(GG_PUBDIR50_WRITE); - if (req == nullptr) - return; - - GetDlgItemText(m_hwnd, IDC_FIRSTNAME, text, _countof(text)); - if (mir_wstrlen(text)) - gg_pubdir50_add(req, GG_PUBDIR50_FIRSTNAME, T2Utf(text)); - - GetDlgItemText(m_hwnd, IDC_LASTNAME, text, _countof(text)); - if (mir_wstrlen(text)) - gg_pubdir50_add(req, GG_PUBDIR50_LASTNAME, T2Utf(text)); - - GetDlgItemText(m_hwnd, IDC_NICKNAME, text, _countof(text)); - if (mir_wstrlen(text)) - gg_pubdir50_add(req, GG_PUBDIR50_NICKNAME, T2Utf(text)); - - GetDlgItemText(m_hwnd, IDC_CITY, text, _countof(text)); - if (mir_wstrlen(text)) - gg_pubdir50_add(req, GG_PUBDIR50_CITY, T2Utf(text)); - - // Gadu-Gadu Female <-> Male - switch (SendDlgItemMessage(m_hwnd, IDC_GENDER, CB_GETCURSEL, 0, 0)) { - case 1: - gg_pubdir50_add(req, GG_PUBDIR50_GENDER, GG_PUBDIR50_GENDER_SET_FEMALE); - break; - case 2: - gg_pubdir50_add(req, GG_PUBDIR50_GENDER, GG_PUBDIR50_GENDER_SET_MALE); - break; - default: - gg_pubdir50_add(req, GG_PUBDIR50_GENDER, ""); - } - - GetDlgItemText(m_hwnd, IDC_BIRTHYEAR, text, _countof(text)); - if (mir_wstrlen(text)) - gg_pubdir50_add(req, GG_PUBDIR50_BIRTHYEAR, T2Utf(text)); - - GetDlgItemText(m_hwnd, IDC_FAMILYNAME, text, _countof(text)); - if (mir_wstrlen(text)) - gg_pubdir50_add(req, GG_PUBDIR50_FAMILYNAME, T2Utf(text)); - - GetDlgItemText(m_hwnd, IDC_CITYORIGIN, text, _countof(text)); - if (mir_wstrlen(text)) - gg_pubdir50_add(req, GG_PUBDIR50_FAMILYCITY, T2Utf(text)); - - // Run update - gg_pubdir50_seq_set(req, GG_SEQ_CHINFO); - gg->gg_EnterCriticalSection(&gg->sess_mutex, "gg_detailsdlgproc", 35, "sess_mutex", 1); - gg_pubdir50(gg->m_sess, req); - gg->gg_LeaveCriticalSection(&gg->sess_mutex, "gg_genoptsdlgproc", 35, 1, "sess_mutex", 1); - updating = true; - - gg_pubdir50_free(req); - } -}; - -int GaduProto::details_init(WPARAM wParam, LPARAM hContact) -{ - int idDialog; - - // View/Change My Details - if (hContact == NULL) { - idDialog = IDD_CHINFO_GG; - } - // Other user details - else { - char* szProto = Proto_GetBaseAccountName(hContact); - if (szProto == nullptr) - return 0; - if (mir_strcmp(szProto, m_szModuleName) || isChatRoom(hContact)) - return 0; - idDialog = IDD_INFO_GG; - } - - USERINFOPAGE uip = {}; - uip.flags = ODPF_DONTTRANSLATE | ODPF_UNICODE | ODPF_ICON; - uip.position = -1900000000; - uip.pDialog = new GaduUserInfoDlg(this, idDialog); - uip.szTitle.w = m_tszUserName; - uip.dwInitParam = LPARAM(g_plugin.getIconHandle(IDI_GG)); - g_plugin.addUserInfo(wParam, &uip); - - // Start search for user data - if (hContact == NULL) - GetInfo(NULL, 0); - - return 0; -} +/* +Copyright (C) 2012-23 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 . +*/ + +#include "gg.h" + +//////////////////////////////////////////////////////////////////////////////// +// Info Page UI dialog + +class GaduUserInfoDlg : public CUserInfoPageDlg +{ + GaduProto *gg; + bool updating = false; + + CCtrlCombo cmbGender; + CCtrlButton btnSave; + + void SetValue(int idCtrl, char *szModule, char *szSetting, int special) + { + DBVARIANT dbv = { 0 }; + wchar_t str[256]; + wchar_t *ptstr = nullptr; + wchar_t* valT = nullptr; + bool unspecified; + + dbv.type = DBVT_DELETED; + if (szModule == nullptr) + unspecified = true; + else + unspecified = db_get(m_hContact, szModule, szSetting, &dbv) != 0; + + if (!unspecified) { + switch (dbv.type) { + case DBVT_BYTE: + if (special == SVS_GENDER) { + if (dbv.cVal == 'M') + ptstr = TranslateT("Male"); + else if (dbv.cVal == 'F') + ptstr = TranslateT("Female"); + else + unspecified = 1; + } + else if (special == SVS_MONTH) { + if (dbv.bVal > 0 && dbv.bVal <= 12) { + ptstr = str; + GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SABBREVMONTHNAME1 - 1 + dbv.bVal, str, _countof(str)); + } + else + unspecified = 1; + } + else if (special == SVS_TIMEZONE) { + if (dbv.cVal == -100) + unspecified = 1; + else { + ptstr = str; + mir_snwprintf(str, dbv.cVal ? L"GMT%+d:%02d" : L"GMT", -dbv.cVal / 2, (dbv.cVal & 1) * 30); + } + } + else { + unspecified = (special == SVS_ZEROISUNSPEC && dbv.bVal == 0); + ptstr = _itow(special == SVS_SIGNED ? dbv.cVal : dbv.bVal, str, 10); + } + break; + case DBVT_WORD: + if (special == SVS_COUNTRY) { + char* pstr = (char*)CallService(MS_UTILS_GETCOUNTRYBYNUMBER, dbv.wVal, 0); + if (pstr == nullptr) { + unspecified = 1; + } + else { + ptstr = str; + mir_snwprintf(str, L"%S", pstr); + } + } + else { + unspecified = (special == SVS_ZEROISUNSPEC && dbv.wVal == 0); + ptstr = _itow(special == SVS_SIGNED ? dbv.sVal : dbv.wVal, str, 10); + } + break; + case DBVT_DWORD: + unspecified = (special == SVS_ZEROISUNSPEC && dbv.dVal == 0); + if (special == SVS_IP) { + struct in_addr ia; + ia.S_un.S_addr = htonl(dbv.dVal); + char* pstr = inet_ntoa(ia); + if (pstr == nullptr) { + unspecified = 1; + } + else { + ptstr = str; + mir_snwprintf(str, L"%S", pstr); + } + if (dbv.dVal == 0) + unspecified = 1; + } + else if (special == SVS_GGVERSION) { + ptstr = str; + mir_snwprintf(str, L"%S", (char *)gg_version2string(dbv.dVal)); + } + else { + ptstr = _itow(special == SVS_SIGNED ? dbv.lVal : dbv.dVal, str, 10); + } + break; + case DBVT_ASCIIZ: + unspecified = (special == SVS_ZEROISUNSPEC && dbv.pszVal[0] == '\0'); + ptstr = str; + mir_snwprintf(str, L"%S", dbv.pszVal); + break; + case DBVT_WCHAR: + unspecified = (special == SVS_ZEROISUNSPEC && dbv.pwszVal[0] == '\0'); + ptstr = dbv.pwszVal; + break; + case DBVT_UTF8: + unspecified = (special == SVS_ZEROISUNSPEC && dbv.pszVal[0] == '\0'); + valT = mir_utf8decodeW(dbv.pszVal); + ptstr = str; + wcscpy_s(str, _countof(str), valT); + mir_free(valT); + break; + default: + ptstr = str; + mir_wstrcpy(str, L"???"); + break; + } + } + + if (m_hContact != 0) { + EnableWindow(GetDlgItem(m_hwnd, idCtrl), !unspecified); + if (unspecified) + SetDlgItemText(m_hwnd, idCtrl, TranslateT("")); + else + SetDlgItemText(m_hwnd, idCtrl, ptstr); + } + else { + EnableWindow(GetDlgItem(m_hwnd, idCtrl), TRUE); + if (!unspecified) + SetDlgItemText(m_hwnd, idCtrl, ptstr); + } + db_free(&dbv); + } + +public: + GaduUserInfoDlg(GaduProto *_gg, int idDialog) : + CUserInfoPageDlg(g_plugin, idDialog), + gg(_gg), + btnSave(this, IDC_SAVE), + cmbGender(this, IDC_GENDER) + { + btnSave.OnClick = Callback(this, &GaduUserInfoDlg::onClick_Save); + } + + bool OnInitDialog() override + { + // Add genders + cmbGender.AddString(L"", 0); + cmbGender.AddString(TranslateT("Female"), 1); + cmbGender.AddString(TranslateT("Male"), 2); + return true; + } + + bool OnRefresh() override + { + // Show updated message + if (updating) { + MessageBox(nullptr, TranslateT("Your details has been uploaded to the public directory."), + gg->m_tszUserName, MB_OK | MB_ICONINFORMATION); + updating = false; + return false; + } + + char *szProto = (m_hContact == NULL) ? gg->m_szModuleName : Proto_GetBaseAccountName(m_hContact); + if (szProto == nullptr) + return false; + + // Disable when updating + m_bInitialized = false; + + SetValue(IDC_UIN, szProto, GG_KEY_UIN, 0); + SetValue(IDC_REALIP, szProto, GG_KEY_CLIENTIP, SVS_IP); + SetValue(IDC_PORT, szProto, GG_KEY_CLIENTPORT, SVS_ZEROISUNSPEC); + SetValue(IDC_VERSION, szProto, GG_KEY_CLIENTVERSION, SVS_GGVERSION); + + SetValue(IDC_FIRSTNAME, szProto, GG_KEY_PD_FIRSTNAME, SVS_NORMAL); + SetValue(IDC_LASTNAME, szProto, GG_KEY_PD_LASTNAME, SVS_NORMAL); + SetValue(IDC_NICKNAME, szProto, GG_KEY_PD_NICKNAME, SVS_NORMAL); + SetValue(IDC_BIRTHYEAR, szProto, GG_KEY_PD_BIRTHYEAR, SVS_ZEROISUNSPEC); + SetValue(IDC_CITY, szProto, GG_KEY_PD_CITY, SVS_NORMAL); + SetValue(IDC_FAMILYNAME, szProto, GG_KEY_PD_FAMILYNAME, SVS_NORMAL); + SetValue(IDC_CITYORIGIN, szProto, GG_KEY_PD_FAMILYCITY, SVS_NORMAL); + + if (m_hContact) { + SetValue(IDC_GENDER, szProto, GG_KEY_PD_GANDER, SVS_GENDER); + SetValue(IDC_STATUSDESCR, "CList", GG_KEY_STATUSDESCR, SVS_NORMAL); + } + else switch ((char)db_get_b(m_hContact, gg->m_szModuleName, GG_KEY_PD_GANDER, (uint8_t)'?')) { + case 'F': + SendDlgItemMessage(m_hwnd, IDC_GENDER, CB_SETCURSEL, 1, 0); + break; + case 'M': + SendDlgItemMessage(m_hwnd, IDC_GENDER, CB_SETCURSEL, 2, 0); + break; + default: + SendDlgItemMessage(m_hwnd, IDC_GENDER, CB_SETCURSEL, 0, 0); + } + + // Disable when updating + m_bInitialized = true; + return false; + } + + void OnChange() override + { + EnableWindow(GetDlgItem(m_hwnd, IDC_SAVE), TRUE); + } + + void onClick_Save(CCtrlButton*) + { + wchar_t text[256]; + + if (!gg->isonline()) { + MessageBox(nullptr, + TranslateT("You have to be logged in before you can change your details."), + gg->m_tszUserName, MB_OK | MB_ICONSTOP); + return; + } + + EnableWindow(GetDlgItem(m_hwnd, IDC_SAVE), FALSE); + + gg_pubdir50_t req = gg_pubdir50_new(GG_PUBDIR50_WRITE); + if (req == nullptr) + return; + + GetDlgItemText(m_hwnd, IDC_FIRSTNAME, text, _countof(text)); + if (mir_wstrlen(text)) + gg_pubdir50_add(req, GG_PUBDIR50_FIRSTNAME, T2Utf(text)); + + GetDlgItemText(m_hwnd, IDC_LASTNAME, text, _countof(text)); + if (mir_wstrlen(text)) + gg_pubdir50_add(req, GG_PUBDIR50_LASTNAME, T2Utf(text)); + + GetDlgItemText(m_hwnd, IDC_NICKNAME, text, _countof(text)); + if (mir_wstrlen(text)) + gg_pubdir50_add(req, GG_PUBDIR50_NICKNAME, T2Utf(text)); + + GetDlgItemText(m_hwnd, IDC_CITY, text, _countof(text)); + if (mir_wstrlen(text)) + gg_pubdir50_add(req, GG_PUBDIR50_CITY, T2Utf(text)); + + // Gadu-Gadu Female <-> Male + switch (SendDlgItemMessage(m_hwnd, IDC_GENDER, CB_GETCURSEL, 0, 0)) { + case 1: + gg_pubdir50_add(req, GG_PUBDIR50_GENDER, GG_PUBDIR50_GENDER_SET_FEMALE); + break; + case 2: + gg_pubdir50_add(req, GG_PUBDIR50_GENDER, GG_PUBDIR50_GENDER_SET_MALE); + break; + default: + gg_pubdir50_add(req, GG_PUBDIR50_GENDER, ""); + } + + GetDlgItemText(m_hwnd, IDC_BIRTHYEAR, text, _countof(text)); + if (mir_wstrlen(text)) + gg_pubdir50_add(req, GG_PUBDIR50_BIRTHYEAR, T2Utf(text)); + + GetDlgItemText(m_hwnd, IDC_FAMILYNAME, text, _countof(text)); + if (mir_wstrlen(text)) + gg_pubdir50_add(req, GG_PUBDIR50_FAMILYNAME, T2Utf(text)); + + GetDlgItemText(m_hwnd, IDC_CITYORIGIN, text, _countof(text)); + if (mir_wstrlen(text)) + gg_pubdir50_add(req, GG_PUBDIR50_FAMILYCITY, T2Utf(text)); + + // Run update + gg_pubdir50_seq_set(req, GG_SEQ_CHINFO); + gg->gg_EnterCriticalSection(&gg->sess_mutex, "gg_detailsdlgproc", 35, "sess_mutex", 1); + gg_pubdir50(gg->m_sess, req); + gg->gg_LeaveCriticalSection(&gg->sess_mutex, "gg_genoptsdlgproc", 35, 1, "sess_mutex", 1); + updating = true; + + gg_pubdir50_free(req); + } +}; + +int GaduProto::details_init(WPARAM wParam, LPARAM hContact) +{ + int idDialog; + + // View/Change My Details + if (hContact == NULL) { + idDialog = IDD_CHINFO_GG; + } + // Other user details + else { + char* szProto = Proto_GetBaseAccountName(hContact); + if (szProto == nullptr) + return 0; + if (mir_strcmp(szProto, m_szModuleName) || isChatRoom(hContact)) + return 0; + idDialog = IDD_INFO_GG; + } + + USERINFOPAGE uip = {}; + uip.flags = ODPF_DONTTRANSLATE | ODPF_UNICODE | ODPF_ICON; + uip.position = -1900000000; + uip.pDialog = new GaduUserInfoDlg(this, idDialog); + uip.szTitle.w = m_tszUserName; + uip.dwInitParam = LPARAM(g_plugin.getIconHandle(IDI_GG)); + g_plugin.addUserInfo(wParam, &uip); + + // Start search for user data + if (hContact == NULL) + GetInfo(NULL, 0); + + return 0; +} diff --git a/protocols/GmailNotifier/src/stdafx.cxx b/protocols/GmailNotifier/src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/GmailNotifier/src/stdafx.cxx +++ b/protocols/GmailNotifier/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/ICQ-WIM/src/groupchats.cpp b/protocols/ICQ-WIM/src/groupchats.cpp index e92811baa1..807bec394a 100644 --- a/protocols/ICQ-WIM/src/groupchats.cpp +++ b/protocols/ICQ-WIM/src/groupchats.cpp @@ -1,292 +1,292 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -void CIcqProto::LoadChatInfo(SESSION_INFO *si) -{ - int memberCount = getDword(si->hContact, "MemberCount"); - for (int i = 0; i < memberCount; i++) { - char buf[100]; - mir_snprintf(buf, "m%d", i); - ptrW szSetting(getWStringA(si->hContact, buf)); - JSONNode *node = json_parse(T2Utf(szSetting)); - if (node == nullptr) - continue; - - CMStringW nick((*node)["nick"].as_mstring()); - CMStringW role((*node)["role"].as_mstring()); - CMStringW sn((*node)["sn"].as_mstring()); - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; - gce.dwFlags = GCEF_SILENT; - gce.pszID.w = si->ptszID; - gce.pszNick.w = nick; - gce.pszUID.w = sn; - gce.time = ::time(0); - gce.bIsMe = sn == m_szOwnId; - gce.pszStatus.w = TranslateW(role); - Chat_Event(&gce); - - json_delete(node); - } -} - -void CIcqProto::OnGetChatInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - SESSION_INFO *si = (SESSION_INFO*)pReq->pUserInfo; - - RobustReply root(pReply); - if (root.error() != 20000) - return; - - int n = 0; - char buf[100]; - const JSONNode &results = root.results(); - for (auto &it : results["members"]) { - mir_snprintf(buf, "m%d", n++); - - CMStringW friendly = it["friendly"].as_mstring(); - CMStringW role = it["role"].as_mstring(); - CMStringW sn = it["sn"].as_mstring(); - - JSONNode member; - member << WCHAR_PARAM("nick", friendly) << WCHAR_PARAM("role", role) << WCHAR_PARAM("sn", sn); - ptrW text(json_write(&member)); - setWString(si->hContact, buf, text); - } - - setDword(si->hContact, "MemberCount", n); - setId(si->hContact, "InfoVersion", _wtoi64(results["infoVersion"].as_mstring())); - setId(si->hContact, "MembersVersion", _wtoi64(results["membersVersion"].as_mstring())); - - LoadChatInfo(si); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Invitation dialog - -class CGroupchatInviteDlg : public CIcqDlgBase -{ - CCtrlClc m_clc; - SESSION_INFO *m_si; - - 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(1); - m_clc.SetHideOfflineRoot(1); - } - -public: - CGroupchatInviteDlg(CIcqProto *ppro, SESSION_INFO *si) : - CIcqDlgBase(ppro, IDD_GROUPCHAT_INVITE), - m_si(si), - m_clc(this, IDC_CLIST) - { - m_clc.OnNewContact = - m_clc.OnListRebuilt = Callback(this, &CGroupchatInviteDlg::FilterList); - m_clc.OnOptionsChanged = Callback(this, &CGroupchatInviteDlg::ResetListOptions); - } - - bool OnInitDialog() override - { - SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, - GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_SHOWHIDDEN | CLS_HIDEOFFLINE | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE | CLS_GROUPCHECKBOXES); - m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); - - ResetListOptions(&m_clc); - FilterList(&m_clc); - return true; - } - - bool OnApply() override - { - CMStringW szMembers; - for (auto &hContact : m_proto->AccContacts()) { - if (m_proto->isChatRoom(hContact)) - continue; - - if (HANDLE hItem = m_clc.FindContact(hContact)) { - if (m_clc.GetCheck(hItem)) { - if (!szMembers.IsEmpty()) - szMembers.AppendChar(','); - szMembers.Append(m_proto->GetUserId(hContact)); - } - } - } - - m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/mchat/AddChat") - << AIMSID(m_proto) << WCHAR_PARAM("chat_id", m_si->ptszID) << WCHAR_PARAM("members", szMembers)); - return true; - } -}; - -void CIcqProto::InviteUserToChat(SESSION_INFO *si) -{ - CGroupchatInviteDlg dlg(this, si); - if (si->pDlg) - dlg.SetParent(((CDlgBase*)si->pDlg)->GetHwnd()); - dlg.DoModal(); -} - -void CIcqProto::LeaveDestroyChat(SESSION_INFO *si) -{ - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/hideChat") - << AIMSID(this) << WCHAR_PARAM("buddy", si->ptszID) << INT64_PARAM("lastMsgId", getId(si->hContact, DB_KEY_LASTMSGID))); - - Chat_Terminate(si->pszModule, si->ptszID, true); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Group chats - -static gc_item sttLogListItems[] = -{ - { LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM }, - { nullptr, 0, MENU_SEPARATOR }, - { LPGENW("&Leave/destroy chat"), IDM_LEAVE, MENU_ITEM } -}; - -int CIcqProto::GroupchatMenuHook(WPARAM, LPARAM lParam) -{ - GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam; - if (gcmi == nullptr) - return 0; - - if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) - return 0; - - SESSION_INFO *si = g_chatApi.SM_FindSession(gcmi->pszID, gcmi->pszModule); - if (si == nullptr) - return 0; - - if (gcmi->Type == MENU_ON_LOG) - Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); - - return 0; -} - -int CIcqProto::GroupchatEventHook(WPARAM, LPARAM lParam) -{ - GCHOOK *gch = (GCHOOK*)lParam; - if (gch == nullptr) - return 0; - - if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) - return 0; - - SESSION_INFO *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule); - if (si == nullptr) - return 1; - - switch (gch->iType) { - case GC_USER_MESSAGE: - rtrimw(gch->ptszText); - if (!mir_wstrlen(gch->ptszText)) - break; - - if (m_bOnline) { - wchar_t *wszText = NEWWSTR_ALLOCA(gch->ptszText); - Chat_UnescapeTags(wszText); - SendMsg(si->hContact, 0, T2Utf(wszText)); - } - break; - - case GC_USER_PRIVMESS: - Chat_SendPrivateMessage(gch); - break; - - case GC_USER_LOGMENU: - Chat_ProcessLogMenu(si, gch->dwData); - break; - } - - return 1; -} - -void CIcqProto::Chat_ProcessLogMenu(SESSION_INFO *si, int iChoice) -{ - switch (iChoice) { - case IDM_INVITE: - InviteUserToChat(si); - break; - - case IDM_LEAVE: - LeaveDestroyChat(si); - break; - } -} - -void CIcqProto::Chat_SendPrivateMessage(GCHOOK *gch) -{ - MCONTACT hContact; - auto *pCache = FindContactByUIN(gch->ptszUID); - if (pCache == nullptr) { - hContact = CreateContact(gch->ptszUID, true); - setWString(hContact, "Nick", gch->ptszNick); - Contact::Hide(hContact); - db_set_dw(hContact, "Ignore", "Mask1", 0); - } - else hContact = pCache->m_hContact; - - CallService(MS_MSG_SENDMESSAGE, hContact, 0); -} - -void CIcqProto::ProcessGroupChat(const JSONNode &ev) -{ - for (auto &it : ev["mchats"]) { - CMStringW wszId(it["sender"].as_mstring()); - SESSION_INFO *si = g_chatApi.SM_FindSession(wszId, m_szModuleName); - if (si == nullptr) - continue; - - CMStringW method(it["method"].as_mstring()); - GCEVENT gce = { m_szModuleName, 0, (method == "add_members") ? GC_EVENT_JOIN : GC_EVENT_PART }; - gce.pszID.w = si->ptszID; - - int iStart = 0; - CMStringW members(it["members"].as_mstring()); - while (true) { - CMStringW member = members.Tokenize(L",", iStart); - if (member.IsEmpty()) - break; - - auto *pCache = FindContactByUIN(member); - if (pCache == nullptr) - continue; - - gce.pszNick.w = Clist_GetContactDisplayName(pCache->m_hContact); - gce.pszUID.w = member; - gce.time = ::time(0); - gce.bIsMe = member == m_szOwnId; - Chat_Event(&gce); - } - } -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +void CIcqProto::LoadChatInfo(SESSION_INFO *si) +{ + int memberCount = getDword(si->hContact, "MemberCount"); + for (int i = 0; i < memberCount; i++) { + char buf[100]; + mir_snprintf(buf, "m%d", i); + ptrW szSetting(getWStringA(si->hContact, buf)); + JSONNode *node = json_parse(T2Utf(szSetting)); + if (node == nullptr) + continue; + + CMStringW nick((*node)["nick"].as_mstring()); + CMStringW role((*node)["role"].as_mstring()); + CMStringW sn((*node)["sn"].as_mstring()); + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_JOIN }; + gce.dwFlags = GCEF_SILENT; + gce.pszID.w = si->ptszID; + gce.pszNick.w = nick; + gce.pszUID.w = sn; + gce.time = ::time(0); + gce.bIsMe = sn == m_szOwnId; + gce.pszStatus.w = TranslateW(role); + Chat_Event(&gce); + + json_delete(node); + } +} + +void CIcqProto::OnGetChatInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + SESSION_INFO *si = (SESSION_INFO*)pReq->pUserInfo; + + RobustReply root(pReply); + if (root.error() != 20000) + return; + + int n = 0; + char buf[100]; + const JSONNode &results = root.results(); + for (auto &it : results["members"]) { + mir_snprintf(buf, "m%d", n++); + + CMStringW friendly = it["friendly"].as_mstring(); + CMStringW role = it["role"].as_mstring(); + CMStringW sn = it["sn"].as_mstring(); + + JSONNode member; + member << WCHAR_PARAM("nick", friendly) << WCHAR_PARAM("role", role) << WCHAR_PARAM("sn", sn); + ptrW text(json_write(&member)); + setWString(si->hContact, buf, text); + } + + setDword(si->hContact, "MemberCount", n); + setId(si->hContact, "InfoVersion", _wtoi64(results["infoVersion"].as_mstring())); + setId(si->hContact, "MembersVersion", _wtoi64(results["membersVersion"].as_mstring())); + + LoadChatInfo(si); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Invitation dialog + +class CGroupchatInviteDlg : public CIcqDlgBase +{ + CCtrlClc m_clc; + SESSION_INFO *m_si; + + 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(1); + m_clc.SetHideOfflineRoot(1); + } + +public: + CGroupchatInviteDlg(CIcqProto *ppro, SESSION_INFO *si) : + CIcqDlgBase(ppro, IDD_GROUPCHAT_INVITE), + m_si(si), + m_clc(this, IDC_CLIST) + { + m_clc.OnNewContact = + m_clc.OnListRebuilt = Callback(this, &CGroupchatInviteDlg::FilterList); + m_clc.OnOptionsChanged = Callback(this, &CGroupchatInviteDlg::ResetListOptions); + } + + bool OnInitDialog() override + { + SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, + GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_SHOWHIDDEN | CLS_HIDEOFFLINE | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE | CLS_GROUPCHECKBOXES); + m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); + + ResetListOptions(&m_clc); + FilterList(&m_clc); + return true; + } + + bool OnApply() override + { + CMStringW szMembers; + for (auto &hContact : m_proto->AccContacts()) { + if (m_proto->isChatRoom(hContact)) + continue; + + if (HANDLE hItem = m_clc.FindContact(hContact)) { + if (m_clc.GetCheck(hItem)) { + if (!szMembers.IsEmpty()) + szMembers.AppendChar(','); + szMembers.Append(m_proto->GetUserId(hContact)); + } + } + } + + m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/mchat/AddChat") + << AIMSID(m_proto) << WCHAR_PARAM("chat_id", m_si->ptszID) << WCHAR_PARAM("members", szMembers)); + return true; + } +}; + +void CIcqProto::InviteUserToChat(SESSION_INFO *si) +{ + CGroupchatInviteDlg dlg(this, si); + if (si->pDlg) + dlg.SetParent(((CDlgBase*)si->pDlg)->GetHwnd()); + dlg.DoModal(); +} + +void CIcqProto::LeaveDestroyChat(SESSION_INFO *si) +{ + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/hideChat") + << AIMSID(this) << WCHAR_PARAM("buddy", si->ptszID) << INT64_PARAM("lastMsgId", getId(si->hContact, DB_KEY_LASTMSGID))); + + Chat_Terminate(si->pszModule, si->ptszID, true); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Group chats + +static gc_item sttLogListItems[] = +{ + { LPGENW("&Invite a user"), IDM_INVITE, MENU_ITEM }, + { nullptr, 0, MENU_SEPARATOR }, + { LPGENW("&Leave/destroy chat"), IDM_LEAVE, MENU_ITEM } +}; + +int CIcqProto::GroupchatMenuHook(WPARAM, LPARAM lParam) +{ + GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam; + if (gcmi == nullptr) + return 0; + + if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) + return 0; + + SESSION_INFO *si = g_chatApi.SM_FindSession(gcmi->pszID, gcmi->pszModule); + if (si == nullptr) + return 0; + + if (gcmi->Type == MENU_ON_LOG) + Chat_AddMenuItems(gcmi->hMenu, _countof(sttLogListItems), sttLogListItems, &g_plugin); + + return 0; +} + +int CIcqProto::GroupchatEventHook(WPARAM, LPARAM lParam) +{ + GCHOOK *gch = (GCHOOK*)lParam; + if (gch == nullptr) + return 0; + + if (mir_strcmpi(gch->si->pszModule, m_szModuleName)) + return 0; + + SESSION_INFO *si = g_chatApi.SM_FindSession(gch->si->ptszID, gch->si->pszModule); + if (si == nullptr) + return 1; + + switch (gch->iType) { + case GC_USER_MESSAGE: + rtrimw(gch->ptszText); + if (!mir_wstrlen(gch->ptszText)) + break; + + if (m_bOnline) { + wchar_t *wszText = NEWWSTR_ALLOCA(gch->ptszText); + Chat_UnescapeTags(wszText); + SendMsg(si->hContact, 0, T2Utf(wszText)); + } + break; + + case GC_USER_PRIVMESS: + Chat_SendPrivateMessage(gch); + break; + + case GC_USER_LOGMENU: + Chat_ProcessLogMenu(si, gch->dwData); + break; + } + + return 1; +} + +void CIcqProto::Chat_ProcessLogMenu(SESSION_INFO *si, int iChoice) +{ + switch (iChoice) { + case IDM_INVITE: + InviteUserToChat(si); + break; + + case IDM_LEAVE: + LeaveDestroyChat(si); + break; + } +} + +void CIcqProto::Chat_SendPrivateMessage(GCHOOK *gch) +{ + MCONTACT hContact; + auto *pCache = FindContactByUIN(gch->ptszUID); + if (pCache == nullptr) { + hContact = CreateContact(gch->ptszUID, true); + setWString(hContact, "Nick", gch->ptszNick); + Contact::Hide(hContact); + db_set_dw(hContact, "Ignore", "Mask1", 0); + } + else hContact = pCache->m_hContact; + + CallService(MS_MSG_SENDMESSAGE, hContact, 0); +} + +void CIcqProto::ProcessGroupChat(const JSONNode &ev) +{ + for (auto &it : ev["mchats"]) { + CMStringW wszId(it["sender"].as_mstring()); + SESSION_INFO *si = g_chatApi.SM_FindSession(wszId, m_szModuleName); + if (si == nullptr) + continue; + + CMStringW method(it["method"].as_mstring()); + GCEVENT gce = { m_szModuleName, 0, (method == "add_members") ? GC_EVENT_JOIN : GC_EVENT_PART }; + gce.pszID.w = si->ptszID; + + int iStart = 0; + CMStringW members(it["members"].as_mstring()); + while (true) { + CMStringW member = members.Tokenize(L",", iStart); + if (member.IsEmpty()) + break; + + auto *pCache = FindContactByUIN(member); + if (pCache == nullptr) + continue; + + gce.pszNick.w = Clist_GetContactDisplayName(pCache->m_hContact); + gce.pszUID.w = member; + gce.time = ::time(0); + gce.bIsMe = member == m_szOwnId; + Chat_Event(&gce); + } + } +} diff --git a/protocols/ICQ-WIM/src/http.cpp b/protocols/ICQ-WIM/src/http.cpp index 0d15a71b9f..79910dad11 100644 --- a/protocols/ICQ-WIM/src/http.cpp +++ b/protocols/ICQ-WIM/src/http.cpp @@ -1,385 +1,385 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -#pragma comment(lib, "Rpcrt4.lib") - -void CIcqProto::DropQueue() -{ - mir_cslock lck(m_csHttpQueue); - - while (m_arHttpQueue.getCount()) { - auto *pReq = m_arHttpQueue[0]; - m_arHttpQueue.remove(0); - delete pReq; - } -} - -bool CIcqProto::IsQueueEmpty() -{ - mir_cslock lck(m_csHttpQueue); - return m_arHttpQueue.getCount() == 0; -} - -void __cdecl CIcqProto::ServerThread(void*) -{ - memset(&m_ConnPool, 0, sizeof(m_ConnPool)); - m_bTerminated = false; - - debugLogA("CIcqProto::WorkerThread: %s", "entering"); - - while (true) { - WaitForSingleObject(m_evRequestsQueue, 1000); - if (m_bTerminated) - break; - - while (true) { - bool bNeedSleep = false; - AsyncHttpRequest *pReq; - { - mir_cslock lck(m_csHttpQueue); - if (m_arHttpQueue.getCount() == 0) - break; - - pReq = m_arHttpQueue[0]; - m_arHttpQueue.remove(0); - bNeedSleep = (m_arHttpQueue.getCount() > 1); - } - if (m_bTerminated) - break; - - ExecuteRequest(pReq); - if (bNeedSleep) - Sleep(200); - } - - int ts = time(0); - for (auto &it : m_ConnPool) { - int idx = int(&it - m_ConnPool); - if (idx == CONN_FETCH) - continue; - - if (it.s && it.lastTs + it.timeout < ts) { - debugLogA("Socket #1 (%p) expired", idx, it.s); - Netlib_CloseHandle(it.s); - it.s = nullptr; - it.lastTs = 0; - } - } - } - - m_hWorkerThread = nullptr; - for (auto &it : m_ConnPool) { - if (it.s) - Netlib_CloseHandle(it.s); - it.s = nullptr; - it.lastTs = it.timeout = 0; - } - - debugLogA("CIcqProto::WorkerThread: %s", "leaving"); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -AsyncRapiRequest::AsyncRapiRequest(CIcqProto *ppro, const char *pszMethod, MTHttpRequestHandler pFunc) : - AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, pFunc) -{ - params.set_name("params"); - - if (ppro->getByte(DB_KEY_PHONEREG)) { - m_szUrl.AppendChar('/'); - m_szUrl.Append(pszMethod); - - AddHeader("Content-Type", "application/json"); - request << CHAR_PARAM("aimsid", ppro->m_aimsid); - } - else request << CHAR_PARAM("method", pszMethod); -} - -void AsyncRapiRequest::OnPush() -{ - request << CHAR_PARAM("reqId", m_reqId) << params; - - m_szParam = ptrW(json_write(&request)); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -AsyncHttpRequest::AsyncHttpRequest(IcqConnection conn, int iType, const char *szUrl, MTHttpRequestHandler pFunc) : - m_conn(conn) -{ - flags = NLHRF_HTTP11 | NLHRF_SSL | NLHRF_DUMPASTEXT; - requestType = iType; - m_szUrl = szUrl; - m_pFunc = pFunc; - timeout = 10000; - - GUID packetId; - UuidCreate(&packetId); - - RPC_CSTR szId; - UuidToStringA(&packetId, &szId); - strncpy_s(m_reqId, (char*)szId, _TRUNCATE); - RpcStringFreeA(&szId); - - if (iType == REQUEST_POST) { - AddHeader("Content-Type", "application/x-www-form-urlencoded"); - - dataLength = m_szParam.GetLength(); - pData = m_szParam.Detach(); - } -} - -void AsyncHttpRequest::ReplaceJsonParam(const JSONNode &n) -{ - auto *szNodeName = n.name(); - - JSONNode root = JSONNode::parse(m_szParam); - JSONNode& old = root.at(szNodeName); - if (old) - old = n; - else - root.push_back(n); - m_szParam = root.write().c_str(); - - replaceStr(pData, nullptr); - dataLength = 0; -} - -bool CIcqProto::ExecuteRequest(AsyncHttpRequest *pReq) -{ - CMStringA str; - - pReq->szUrl = pReq->m_szUrl.GetBuffer(); - if (!pReq->m_szParam.IsEmpty()) { - if (pReq->requestType == REQUEST_GET) { - str.Format("%s?%s", pReq->m_szUrl.c_str(), pReq->m_szParam.c_str()); - pReq->szUrl = str.GetBuffer(); - } - else { - pReq->dataLength = pReq->m_szParam.GetLength(); - pReq->pData = mir_strdup(pReq->m_szParam); - } - } - - // replace credentials inside JSON body for pure RAPI requests - if (pReq->m_conn == CONN_RAPI && !mir_strcmp(pReq->szUrl, ICQ_ROBUST_SERVER) && !getByte(DB_KEY_PHONEREG)) { - CMStringA szAgent(FORMAT, "%S Mail.ru Windows ICQ (version 10.0.1999)", (wchar_t*)m_szOwnId); - pReq->AddHeader("User-Agent", szAgent); - pReq->AddHeader("Content-Type", "application/json"); - - if (m_szRToken.IsEmpty()) { - if (!RefreshRobustToken(pReq)) { - delete pReq; - return false; - } - } - - if (m_iRClientId) - pReq->ReplaceJsonParam(JSONNode("clientId", m_iRClientId)); - pReq->ReplaceJsonParam(JSONNode("authToken", m_szRToken)); - pReq->dataLength = pReq->m_szParam.GetLength(); - pReq->pData = mir_strdup(pReq->m_szParam); - } - - debugLogA("Executing request %s:\n%s", pReq->m_reqId, pReq->szUrl); - - if (pReq->m_conn != CONN_NONE) { - pReq->flags |= NLHRF_PERSISTENT; - pReq->nlc = m_ConnPool[pReq->m_conn].s; - m_ConnPool[pReq->m_conn].lastTs = time(0); - } - - bool bRet; - NLHR_PTR reply(Netlib_HttpTransaction(m_hNetlibUser, pReq)); - if (reply != nullptr) { - if (pReq->m_conn != CONN_NONE) { - auto &conn = m_ConnPool[pReq->m_conn]; - conn.s = reply->nlc; - conn.timeout = 0; - if (auto *pszHdr = Netlib_GetHeader(reply, "Keep-Alive")) { - int timeout; - if (1 == sscanf(pszHdr, "timeout=%d", &timeout)) - conn.timeout = timeout; - } - } - - if (pReq->m_conn == CONN_RAPI && reply->pData && strstr(reply->pData, "\"code\": 40201")) { - RobustReply r(reply); - if (r.error() == 40201) { // robust token expired - m_szRToken.Empty(); - - // if token refresh succeeded, replace it in the query and push request back - if (!RefreshRobustToken(pReq)) { - delete pReq; - return false; - } - - Push(pReq); - return true; - } - } - - if (pReq->m_pFunc != nullptr) - (this->*(pReq->m_pFunc))(reply, pReq); - - bRet = true; - } - else { - debugLogA("Request %s failed", pReq->m_reqId); - - if (IsStatusConnecting(m_iStatus)) - ConnectionFailed(LOGINERR_NONETWORK); - - if (pReq->m_conn != CONN_NONE) - m_ConnPool[pReq->m_conn].s = nullptr; - - bRet = false; - } - - delete pReq; - return bRet; -} - -void CIcqProto::Push(MHttpRequest *p) -{ - AsyncHttpRequest *pReq = (AsyncHttpRequest*)p; - - pReq->OnPush(); - { - mir_cslock lck(m_csHttpQueue); - m_arHttpQueue.insert(pReq); - } - - SetEvent(m_evRequestsQueue); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const AIMSID ¶m) -{ - pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", param.m_ppro->m_aimsid) << CHAR_PARAM("r", pReq->m_reqId); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - return pReq; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MHttpRequest* operator<<(MHttpRequest *pReq, const GROUP_PARAM ¶m) -{ - if (param.wszValue) { - CMStringW tmp(param.wszValue); - tmp.Replace(L"\\", L">"); - tmp.Replace(L"/", L">"); - pReq << WCHAR_PARAM(param.szName, tmp); - } - return pReq; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - JSONNode &response = (*m_root)["response"]; - m_errorCode = response["statusCode"].as_int(); - m_requestId = response["requestId"].as_mstring(); - m_detailCode = response["statusDetailCode"].as_int(); - m_data = &response["data"]; -} - -JsonReply::~JsonReply() -{ - json_delete(m_root); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -FileReply::FileReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = (*m_root)["status"].as_int(); - m_data = &(*m_root)["data"]; -} - -FileReply::~FileReply() -{ - json_delete(m_root); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -RobustReply::RobustReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = (*m_root)["status"]["code"].as_int(); - m_result = &(*m_root)["result"]; - m_results = &(*m_root)["results"]; -} - -RobustReply::~RobustReply() -{ - json_delete(m_root); -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +#pragma comment(lib, "Rpcrt4.lib") + +void CIcqProto::DropQueue() +{ + mir_cslock lck(m_csHttpQueue); + + while (m_arHttpQueue.getCount()) { + auto *pReq = m_arHttpQueue[0]; + m_arHttpQueue.remove(0); + delete pReq; + } +} + +bool CIcqProto::IsQueueEmpty() +{ + mir_cslock lck(m_csHttpQueue); + return m_arHttpQueue.getCount() == 0; +} + +void __cdecl CIcqProto::ServerThread(void*) +{ + memset(&m_ConnPool, 0, sizeof(m_ConnPool)); + m_bTerminated = false; + + debugLogA("CIcqProto::WorkerThread: %s", "entering"); + + while (true) { + WaitForSingleObject(m_evRequestsQueue, 1000); + if (m_bTerminated) + break; + + while (true) { + bool bNeedSleep = false; + AsyncHttpRequest *pReq; + { + mir_cslock lck(m_csHttpQueue); + if (m_arHttpQueue.getCount() == 0) + break; + + pReq = m_arHttpQueue[0]; + m_arHttpQueue.remove(0); + bNeedSleep = (m_arHttpQueue.getCount() > 1); + } + if (m_bTerminated) + break; + + ExecuteRequest(pReq); + if (bNeedSleep) + Sleep(200); + } + + int ts = time(0); + for (auto &it : m_ConnPool) { + int idx = int(&it - m_ConnPool); + if (idx == CONN_FETCH) + continue; + + if (it.s && it.lastTs + it.timeout < ts) { + debugLogA("Socket #1 (%p) expired", idx, it.s); + Netlib_CloseHandle(it.s); + it.s = nullptr; + it.lastTs = 0; + } + } + } + + m_hWorkerThread = nullptr; + for (auto &it : m_ConnPool) { + if (it.s) + Netlib_CloseHandle(it.s); + it.s = nullptr; + it.lastTs = it.timeout = 0; + } + + debugLogA("CIcqProto::WorkerThread: %s", "leaving"); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +AsyncRapiRequest::AsyncRapiRequest(CIcqProto *ppro, const char *pszMethod, MTHttpRequestHandler pFunc) : + AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER, pFunc) +{ + params.set_name("params"); + + if (ppro->getByte(DB_KEY_PHONEREG)) { + m_szUrl.AppendChar('/'); + m_szUrl.Append(pszMethod); + + AddHeader("Content-Type", "application/json"); + request << CHAR_PARAM("aimsid", ppro->m_aimsid); + } + else request << CHAR_PARAM("method", pszMethod); +} + +void AsyncRapiRequest::OnPush() +{ + request << CHAR_PARAM("reqId", m_reqId) << params; + + m_szParam = ptrW(json_write(&request)); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +AsyncHttpRequest::AsyncHttpRequest(IcqConnection conn, int iType, const char *szUrl, MTHttpRequestHandler pFunc) : + m_conn(conn) +{ + flags = NLHRF_HTTP11 | NLHRF_SSL | NLHRF_DUMPASTEXT; + requestType = iType; + m_szUrl = szUrl; + m_pFunc = pFunc; + timeout = 10000; + + GUID packetId; + UuidCreate(&packetId); + + RPC_CSTR szId; + UuidToStringA(&packetId, &szId); + strncpy_s(m_reqId, (char*)szId, _TRUNCATE); + RpcStringFreeA(&szId); + + if (iType == REQUEST_POST) { + AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + dataLength = m_szParam.GetLength(); + pData = m_szParam.Detach(); + } +} + +void AsyncHttpRequest::ReplaceJsonParam(const JSONNode &n) +{ + auto *szNodeName = n.name(); + + JSONNode root = JSONNode::parse(m_szParam); + JSONNode& old = root.at(szNodeName); + if (old) + old = n; + else + root.push_back(n); + m_szParam = root.write().c_str(); + + replaceStr(pData, nullptr); + dataLength = 0; +} + +bool CIcqProto::ExecuteRequest(AsyncHttpRequest *pReq) +{ + CMStringA str; + + pReq->szUrl = pReq->m_szUrl.GetBuffer(); + if (!pReq->m_szParam.IsEmpty()) { + if (pReq->requestType == REQUEST_GET) { + str.Format("%s?%s", pReq->m_szUrl.c_str(), pReq->m_szParam.c_str()); + pReq->szUrl = str.GetBuffer(); + } + else { + pReq->dataLength = pReq->m_szParam.GetLength(); + pReq->pData = mir_strdup(pReq->m_szParam); + } + } + + // replace credentials inside JSON body for pure RAPI requests + if (pReq->m_conn == CONN_RAPI && !mir_strcmp(pReq->szUrl, ICQ_ROBUST_SERVER) && !getByte(DB_KEY_PHONEREG)) { + CMStringA szAgent(FORMAT, "%S Mail.ru Windows ICQ (version 10.0.1999)", (wchar_t*)m_szOwnId); + pReq->AddHeader("User-Agent", szAgent); + pReq->AddHeader("Content-Type", "application/json"); + + if (m_szRToken.IsEmpty()) { + if (!RefreshRobustToken(pReq)) { + delete pReq; + return false; + } + } + + if (m_iRClientId) + pReq->ReplaceJsonParam(JSONNode("clientId", m_iRClientId)); + pReq->ReplaceJsonParam(JSONNode("authToken", m_szRToken)); + pReq->dataLength = pReq->m_szParam.GetLength(); + pReq->pData = mir_strdup(pReq->m_szParam); + } + + debugLogA("Executing request %s:\n%s", pReq->m_reqId, pReq->szUrl); + + if (pReq->m_conn != CONN_NONE) { + pReq->flags |= NLHRF_PERSISTENT; + pReq->nlc = m_ConnPool[pReq->m_conn].s; + m_ConnPool[pReq->m_conn].lastTs = time(0); + } + + bool bRet; + NLHR_PTR reply(Netlib_HttpTransaction(m_hNetlibUser, pReq)); + if (reply != nullptr) { + if (pReq->m_conn != CONN_NONE) { + auto &conn = m_ConnPool[pReq->m_conn]; + conn.s = reply->nlc; + conn.timeout = 0; + if (auto *pszHdr = Netlib_GetHeader(reply, "Keep-Alive")) { + int timeout; + if (1 == sscanf(pszHdr, "timeout=%d", &timeout)) + conn.timeout = timeout; + } + } + + if (pReq->m_conn == CONN_RAPI && reply->pData && strstr(reply->pData, "\"code\": 40201")) { + RobustReply r(reply); + if (r.error() == 40201) { // robust token expired + m_szRToken.Empty(); + + // if token refresh succeeded, replace it in the query and push request back + if (!RefreshRobustToken(pReq)) { + delete pReq; + return false; + } + + Push(pReq); + return true; + } + } + + if (pReq->m_pFunc != nullptr) + (this->*(pReq->m_pFunc))(reply, pReq); + + bRet = true; + } + else { + debugLogA("Request %s failed", pReq->m_reqId); + + if (IsStatusConnecting(m_iStatus)) + ConnectionFailed(LOGINERR_NONETWORK); + + if (pReq->m_conn != CONN_NONE) + m_ConnPool[pReq->m_conn].s = nullptr; + + bRet = false; + } + + delete pReq; + return bRet; +} + +void CIcqProto::Push(MHttpRequest *p) +{ + AsyncHttpRequest *pReq = (AsyncHttpRequest*)p; + + pReq->OnPush(); + { + mir_cslock lck(m_csHttpQueue); + m_arHttpQueue.insert(pReq); + } + + SetEvent(m_evRequestsQueue); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const AIMSID ¶m) +{ + pReq << CHAR_PARAM("f", "json") << CHAR_PARAM("aimsid", param.m_ppro->m_aimsid) << CHAR_PARAM("r", pReq->m_reqId); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + return pReq; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MHttpRequest* operator<<(MHttpRequest *pReq, const GROUP_PARAM ¶m) +{ + if (param.wszValue) { + CMStringW tmp(param.wszValue); + tmp.Replace(L"\\", L">"); + tmp.Replace(L"/", L">"); + pReq << WCHAR_PARAM(param.szName, tmp); + } + return pReq; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + JSONNode &response = (*m_root)["response"]; + m_errorCode = response["statusCode"].as_int(); + m_requestId = response["requestId"].as_mstring(); + m_detailCode = response["statusDetailCode"].as_int(); + m_data = &response["data"]; +} + +JsonReply::~JsonReply() +{ + json_delete(m_root); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +FileReply::FileReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = (*m_root)["status"].as_int(); + m_data = &(*m_root)["data"]; +} + +FileReply::~FileReply() +{ + json_delete(m_root); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +RobustReply::RobustReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = (*m_root)["status"]["code"].as_int(); + m_result = &(*m_root)["result"]; + m_results = &(*m_root)["results"]; +} + +RobustReply::~RobustReply() +{ + json_delete(m_root); +} diff --git a/protocols/ICQ-WIM/src/ignore.cpp b/protocols/ICQ-WIM/src/ignore.cpp index d2d6dfb36a..a2ce8c098f 100644 --- a/protocols/ICQ-WIM/src/ignore.cpp +++ b/protocols/ICQ-WIM/src/ignore.cpp @@ -1,82 +1,82 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- -// Server permissions - -#include "stdafx.h" - -void CIcqProto::GetPermitDeny() -{ - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/preference/getPermitDeny", &CIcqProto::OnGetPermitDeny) << AIMSID(this)); -} - -void CIcqProto::OnGetPermitDeny(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - if (root.error() == 200) - ProcessPermissions(root.data()); -} - -void CIcqProto::ProcessPermissions(const JSONNode &ev) -{ - { mir_cslock lck(m_csCache); - for (auto &it : m_arCache) - it->m_iApparentMode = 0; - } - - for (auto &it : ev["allows"]) { - auto *p = FindContactByUIN(it.as_mstring()); - if (p) - p->m_iApparentMode = ID_STATUS_ONLINE; - } - - m_bIgnoreListEmpty = true; - for (auto &it : ev["ignores"]) { - CMStringW wszId(it.as_mstring()); - auto *p = FindContactByUIN(wszId); - if (p == nullptr) { - CreateContact(wszId, false); - p = FindContactByUIN(wszId); - } - p->m_iApparentMode = ID_STATUS_OFFLINE; - Contact::Hide(p->m_hContact); - m_bIgnoreListEmpty = false; - } - - { mir_cslock lck(m_csCache); - for (auto &it : m_arCache) { - int oldMode = getDword(it->m_hContact, "ApparentMode"); - if (oldMode != it->m_iApparentMode) { - if (it->m_iApparentMode == 0) - delSetting(it->m_hContact, "ApparentMode"); - else - setDword(it->m_hContact, "ApparentMode", it->m_iApparentMode); - } - } - } -} - -void CIcqProto::SetPermitDeny(const CMStringW &userId, bool bAllow) -{ - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/preference/setPermitDeny") - << AIMSID(this) << WCHAR_PARAM((bAllow) ? "pdIgnoreRemove" : "pdIgnore", userId); - if (!m_bIgnoreListEmpty) - pReq << CHAR_PARAM("pdMode", "denySome"); - Push(pReq); -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- +// Server permissions + +#include "stdafx.h" + +void CIcqProto::GetPermitDeny() +{ + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/preference/getPermitDeny", &CIcqProto::OnGetPermitDeny) << AIMSID(this)); +} + +void CIcqProto::OnGetPermitDeny(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + if (root.error() == 200) + ProcessPermissions(root.data()); +} + +void CIcqProto::ProcessPermissions(const JSONNode &ev) +{ + { mir_cslock lck(m_csCache); + for (auto &it : m_arCache) + it->m_iApparentMode = 0; + } + + for (auto &it : ev["allows"]) { + auto *p = FindContactByUIN(it.as_mstring()); + if (p) + p->m_iApparentMode = ID_STATUS_ONLINE; + } + + m_bIgnoreListEmpty = true; + for (auto &it : ev["ignores"]) { + CMStringW wszId(it.as_mstring()); + auto *p = FindContactByUIN(wszId); + if (p == nullptr) { + CreateContact(wszId, false); + p = FindContactByUIN(wszId); + } + p->m_iApparentMode = ID_STATUS_OFFLINE; + Contact::Hide(p->m_hContact); + m_bIgnoreListEmpty = false; + } + + { mir_cslock lck(m_csCache); + for (auto &it : m_arCache) { + int oldMode = getDword(it->m_hContact, "ApparentMode"); + if (oldMode != it->m_iApparentMode) { + if (it->m_iApparentMode == 0) + delSetting(it->m_hContact, "ApparentMode"); + else + setDword(it->m_hContact, "ApparentMode", it->m_iApparentMode); + } + } + } +} + +void CIcqProto::SetPermitDeny(const CMStringW &userId, bool bAllow) +{ + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/preference/setPermitDeny") + << AIMSID(this) << WCHAR_PARAM((bAllow) ? "pdIgnoreRemove" : "pdIgnore", userId); + if (!m_bIgnoreListEmpty) + pReq << CHAR_PARAM("pdMode", "denySome"); + Push(pReq); +} diff --git a/protocols/ICQ-WIM/src/main.cpp b/protocols/ICQ-WIM/src/main.cpp index 68245fff9c..b445bc7b29 100644 --- a/protocols/ICQ-WIM/src/main.cpp +++ b/protocols/ICQ-WIM/src/main.cpp @@ -1,102 +1,102 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -bool g_bSecureIM, g_bMessageState; - -///////////////////////////////////////////////////////////////////////////////////////// - -static PLUGININFOEX pluginInfoEx = { - sizeof(PLUGININFOEX), - __PLUGIN_NAME, - PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), - __DESCRIPTION, - __AUTHOR, - __COPYRIGHT, - __AUTHORWEB, - UNICODE_AWARE, - { 0xEFB2355B, 0x82B3, 0x4759, { 0xb7, 0xd8, 0x95, 0xf8, 0xe9, 0x50, 0x62, 0x91 } } // {EFB2355B-82B3-4759-B7D8-95F8E9506291} -}; - -CMPlugin g_plugin; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct CMPluginMra : public ACCPROTOPLUGIN -{ - CMPluginMra() : ACCPROTOPLUGIN("MRA", pluginInfoEx) - { - SetUniqueId(DB_KEY_ID); - } - - void Register() - { - m_hInst = g_plugin.getInst(); - RegisterProtocol(PROTOTYPE_PROTOCOL, g_plugin.fnInit, g_plugin.fnUninit); - } -} -static g_pluginMra; - -///////////////////////////////////////////////////////////////////////////////////////// - -CMPlugin::CMPlugin() : - ACCPROTOPLUGIN(MODULENAME, pluginInfoEx) -{ - SetUniqueId(DB_KEY_ID); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; - -///////////////////////////////////////////////////////////////////////////////////////// - -static int ModuleLoad(WPARAM, LPARAM) -{ - g_bSecureIM = ServiceExists("SecureIM/IsContactSecured"); - g_bMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE); - return 0; -} - -static int OnModulesLoaded(WPARAM, LPARAM) -{ - ModuleLoad(0, 0); - return 0; -} - -static IconItem iconList[] = -{ - { LPGEN("E-mail"), "icq_email", IDI_INBOX }, - { LPGEN("E-mail notification"), "icq_email_notif", IDI_MAIL_NOTIFY } -}; - -int CMPlugin::Load() -{ - // register the second instance of this plugin as MRA - g_pluginMra.Register(); - - registerIcon("Protocols/ICQ", iconList, "ICQ"); - - HookEvent(ME_SYSTEM_MODULELOAD, ModuleLoad); - HookEvent(ME_SYSTEM_MODULEUNLOAD, ModuleLoad); - HookEvent(ME_SYSTEM_MODULESLOADED, OnModulesLoaded); - return 0; -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +bool g_bSecureIM, g_bMessageState; + +///////////////////////////////////////////////////////////////////////////////////////// + +static PLUGININFOEX pluginInfoEx = { + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + { 0xEFB2355B, 0x82B3, 0x4759, { 0xb7, 0xd8, 0x95, 0xf8, 0xe9, 0x50, 0x62, 0x91 } } // {EFB2355B-82B3-4759-B7D8-95F8E9506291} +}; + +CMPlugin g_plugin; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CMPluginMra : public ACCPROTOPLUGIN +{ + CMPluginMra() : ACCPROTOPLUGIN("MRA", pluginInfoEx) + { + SetUniqueId(DB_KEY_ID); + } + + void Register() + { + m_hInst = g_plugin.getInst(); + RegisterProtocol(PROTOTYPE_PROTOCOL, g_plugin.fnInit, g_plugin.fnUninit); + } +} +static g_pluginMra; + +///////////////////////////////////////////////////////////////////////////////////////// + +CMPlugin::CMPlugin() : + ACCPROTOPLUGIN(MODULENAME, pluginInfoEx) +{ + SetUniqueId(DB_KEY_ID); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// + +static int ModuleLoad(WPARAM, LPARAM) +{ + g_bSecureIM = ServiceExists("SecureIM/IsContactSecured"); + g_bMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE); + return 0; +} + +static int OnModulesLoaded(WPARAM, LPARAM) +{ + ModuleLoad(0, 0); + return 0; +} + +static IconItem iconList[] = +{ + { LPGEN("E-mail"), "icq_email", IDI_INBOX }, + { LPGEN("E-mail notification"), "icq_email_notif", IDI_MAIL_NOTIFY } +}; + +int CMPlugin::Load() +{ + // register the second instance of this plugin as MRA + g_pluginMra.Register(); + + registerIcon("Protocols/ICQ", iconList, "ICQ"); + + HookEvent(ME_SYSTEM_MODULELOAD, ModuleLoad); + HookEvent(ME_SYSTEM_MODULEUNLOAD, ModuleLoad); + HookEvent(ME_SYSTEM_MODULESLOADED, OnModulesLoaded); + return 0; +} diff --git a/protocols/ICQ-WIM/src/mra.cpp b/protocols/ICQ-WIM/src/mra.cpp index f5dcaae595..0c1422ddab 100644 --- a/protocols/ICQ-WIM/src/mra.cpp +++ b/protocols/ICQ-WIM/src/mra.cpp @@ -1,141 +1,141 @@ -/* -Copyright (C) 2012-22 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 . -*/ - -#include "stdafx.h" - -void CIcqProto::SendMrimLogin(NETLIBHTTPREQUEST *pReply) -{ - if (pReply) { - for (int i=0; i < pReply->headersCount; i++) { - if (!mir_strcmpi(pReply->headers[i].szName, "Set-Cookie")) { - char *p = strchr(pReply->headers[i].szValue, ';'); - if (p) *p = 0; - if (!m_szMraCookie.IsEmpty()) - m_szMraCookie.Append("; "); - - m_szMraCookie.Append(pReply->headers[i].szValue); - } - } - } - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://icqapilogin.mail.ru/auth/mrimLogin", &CIcqProto::OnCheckMrimLogin); - pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); - if (!m_szMraCookie.IsEmpty()) - pReq->AddHeader("Cookie", m_szMraCookie); - pReq << CHAR_PARAM("clientName", "webagent") << INT_PARAM("clientVersion", 711) << CHAR_PARAM("devId", MRA_APP_ID) - << CHAR_PARAM("r", "91640-1626423568") << CHAR_PARAM("f", "json"); -#ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; -#endif - Push(pReq); -} - -void CIcqProto::OnCheckMrimLogin(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - switch (root.error()) { - case 200: - case 302: - break; - - case 460: // no cookies at all, obtain them via MRA auth site - { - CMStringW uin(m_szOwnId); - - int iStart = 0; - CMStringW wszLogin = uin.Tokenize(L"@", iStart); - CMStringW wszDomain = uin.Tokenize(L"@", iStart); - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://auth.mail.ru/cgi-bin/auth?from=splash", &CIcqProto::OnCheckMraAuth); - pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); - if (!m_szMraCookie.IsEmpty()) - pReq->AddHeader("Cookie", m_szMraCookie); - pReq << WCHAR_PARAM("Domain", wszDomain) << WCHAR_PARAM("Login", wszLogin) << CHAR_PARAM("Password", m_szPassword) - << INT_PARAM("new_auth_form", 1) << INT_PARAM("saveauth", 1); -#ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; -#endif - Push(pReq); - } - return; - - case 462: // bad cookies, refresh them - if (!m_bError462) { - m_bError462 = true; - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://auth.mail.ru/sdc?JSONP_call=jscb_tmp_c85825&from=https%3A%2F%2Ficqapilogin.mail.ru%2Fauth%2FmrimLogin", &CIcqProto::OnCheckMraAuthFinal); - pReq->flags |= NLHRF_REDIRECT; - pReq->AddHeader("Cookie", m_szMraCookie); - pReq->AddHeader("Referer", "https://webagent.mail.ru/"); - pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); - Push(pReq); - return; - } - - m_bError462 = false; - __fallthrough; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); - return; - } - - JSONNode &data = root.data(); - m_szAToken = data["token"]["a"].as_mstring(); - mir_urlDecode(m_szAToken.GetBuffer()); - setString(DB_KEY_ATOKEN, m_szAToken); - - m_szSessionKey = data["sessionKey"].as_mstring(); - - CMStringW szUin = data["loginId"].as_mstring(); - if (szUin) - m_szOwnId = szUin; - - int srvTS = data["hostTime"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - - StartSession(); -} - -void CIcqProto::OnCheckMraAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - switch (root.error()) { - case 200: - case 302: - m_szMraCookie.Empty(); - SendMrimLogin(pReply); - break; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); - } -} - -void CIcqProto::OnCheckMraAuthFinal(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - switch (pReply->resultCode) { - case 200: - case 302: - // accumulate sdcs cookie and resend request - SendMrimLogin(pReply); - break; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, 500); - } -} +/* +Copyright (C) 2012-23 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 . +*/ + +#include "stdafx.h" + +void CIcqProto::SendMrimLogin(NETLIBHTTPREQUEST *pReply) +{ + if (pReply) { + for (int i=0; i < pReply->headersCount; i++) { + if (!mir_strcmpi(pReply->headers[i].szName, "Set-Cookie")) { + char *p = strchr(pReply->headers[i].szValue, ';'); + if (p) *p = 0; + if (!m_szMraCookie.IsEmpty()) + m_szMraCookie.Append("; "); + + m_szMraCookie.Append(pReply->headers[i].szValue); + } + } + } + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://icqapilogin.mail.ru/auth/mrimLogin", &CIcqProto::OnCheckMrimLogin); + pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); + if (!m_szMraCookie.IsEmpty()) + pReq->AddHeader("Cookie", m_szMraCookie); + pReq << CHAR_PARAM("clientName", "webagent") << INT_PARAM("clientVersion", 711) << CHAR_PARAM("devId", MRA_APP_ID) + << CHAR_PARAM("r", "91640-1626423568") << CHAR_PARAM("f", "json"); +#ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; +#endif + Push(pReq); +} + +void CIcqProto::OnCheckMrimLogin(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + switch (root.error()) { + case 200: + case 302: + break; + + case 460: // no cookies at all, obtain them via MRA auth site + { + CMStringW uin(m_szOwnId); + + int iStart = 0; + CMStringW wszLogin = uin.Tokenize(L"@", iStart); + CMStringW wszDomain = uin.Tokenize(L"@", iStart); + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://auth.mail.ru/cgi-bin/auth?from=splash", &CIcqProto::OnCheckMraAuth); + pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); + if (!m_szMraCookie.IsEmpty()) + pReq->AddHeader("Cookie", m_szMraCookie); + pReq << WCHAR_PARAM("Domain", wszDomain) << WCHAR_PARAM("Login", wszLogin) << CHAR_PARAM("Password", m_szPassword) + << INT_PARAM("new_auth_form", 1) << INT_PARAM("saveauth", 1); +#ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; +#endif + Push(pReq); + } + return; + + case 462: // bad cookies, refresh them + if (!m_bError462) { + m_bError462 = true; + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, "https://auth.mail.ru/sdc?JSONP_call=jscb_tmp_c85825&from=https%3A%2F%2Ficqapilogin.mail.ru%2Fauth%2FmrimLogin", &CIcqProto::OnCheckMraAuthFinal); + pReq->flags |= NLHRF_REDIRECT; + pReq->AddHeader("Cookie", m_szMraCookie); + pReq->AddHeader("Referer", "https://webagent.mail.ru/"); + pReq->AddHeader("User-Agent", NETLIB_USER_AGENT); + Push(pReq); + return; + } + + m_bError462 = false; + __fallthrough; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); + return; + } + + JSONNode &data = root.data(); + m_szAToken = data["token"]["a"].as_mstring(); + mir_urlDecode(m_szAToken.GetBuffer()); + setString(DB_KEY_ATOKEN, m_szAToken); + + m_szSessionKey = data["sessionKey"].as_mstring(); + + CMStringW szUin = data["loginId"].as_mstring(); + if (szUin) + m_szOwnId = szUin; + + int srvTS = data["hostTime"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + + StartSession(); +} + +void CIcqProto::OnCheckMraAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + switch (root.error()) { + case 200: + case 302: + m_szMraCookie.Empty(); + SendMrimLogin(pReply); + break; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); + } +} + +void CIcqProto::OnCheckMraAuthFinal(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + switch (pReply->resultCode) { + case 200: + case 302: + // accumulate sdcs cookie and resend request + SendMrimLogin(pReply); + break; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, 500); + } +} diff --git a/protocols/ICQ-WIM/src/options.cpp b/protocols/ICQ-WIM/src/options.cpp index 7a53219a3d..2de6641efc 100644 --- a/protocols/ICQ-WIM/src/options.cpp +++ b/protocols/ICQ-WIM/src/options.cpp @@ -1,394 +1,394 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -class CIcqEnterLoginDlg : public CIcqDlgBase -{ - CCtrlEdit edtPass; - CCtrlCheck chkSave; - -public: - CIcqEnterLoginDlg(CIcqProto *ppro) : - CIcqDlgBase(ppro, IDD_LOGINPW), - edtPass(this, IDC_PASSWORD), - chkSave(this, IDC_SAVEPASS) - { - } - - bool OnInitDialog() override - { - m_proto->m_bDlgActive = true; - chkSave.SetState(m_proto->getBool("RememberPass")); - Window_SetIcon_IcoLib(m_hwnd, m_proto->m_hProtoIcon); - return true; - } - - bool OnApply() override - { - m_proto->setByte("RememberPass", m_proto->m_bRememberPwd = chkSave.GetState()); - m_proto->m_szPassword = ptrA(edtPass.GetTextU()); - EndModal(true); - return true; - } - - void OnDestroy() override - { - m_proto->m_bDlgActive = false; - } -}; - -bool CIcqProto::RetrievePassword() -{ - // if we registered via phone (i.e., server holds the password), we don't need to enter it - if (getByte(DB_KEY_PHONEREG)) - return true; - - if (!m_szPassword.IsEmpty() && m_bRememberPwd) - return true; - - m_szPassword = getMStringA("Password"); - if (!m_szPassword.IsEmpty()) { - m_bRememberPwd = true; - return true; - } - - if (m_bDlgActive) - return false; - - return CIcqEnterLoginDlg(this).DoModal(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -struct CIcqRegistrationDlg : public CIcqDlgBase -{ - CMStringA szTrans, szMsisdn; - int iErrorCode = 0; - - CCtrlEdit edtPhone, edtCode; - CCtrlButton btnSendSms; - - CIcqRegistrationDlg(CIcqProto *ppro) : - CIcqDlgBase(ppro, IDD_REGISTER), - edtPhone(this, IDC_PHONE), - edtCode(this, IDC_CODE), - btnSendSms(this, IDC_SENDSMS) - { - btnSendSms.OnClick = Callback(this, &CIcqRegistrationDlg::onClick_SendSms); - edtPhone.OnChange = Callback(this, &CIcqRegistrationDlg::onChange_Phone); - } - - bool OnApply() override - { - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsreg/loginWithPhoneNumber.php", &CIcqProto::OnLoginViaPhone); - pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("trans_id", szTrans) << CHAR_PARAM("k", m_proto->appId()) - << CHAR_PARAM("r", pReq->m_reqId) << CHAR_PARAM("f", "json") << WCHAR_PARAM("sms_code", ptrW(edtCode.GetText())) << INT_PARAM("create_account", 1); - pReq->pUserInfo = this; - - SetCursor(LoadCursor(0, IDC_WAIT)); - m_proto->ExecuteRequest(pReq); - SetCursor(LoadCursor(0, IDC_ARROW)); - - if (iErrorCode != 200) - return false; - - EndDialog(m_hwnd, 1); - return true; - } - - void onChange_Phone(CCtrlEdit*) - { - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsapi/fcgi-bin/smsphoneinfo", &CIcqProto::OnCheckPhone); - pReq << CHAR_PARAM("service", "icq_registration") << CHAR_PARAM("info", "typing_check,score,iso_country_code") << CHAR_PARAM("lang", "ru") - << WCHAR_PARAM("phone", ptrW(edtPhone.GetText())) << CHAR_PARAM("id", pReq->m_reqId); - pReq->pUserInfo = this; - m_proto->Push(pReq); - } - - void onClick_SendSms(CCtrlButton*) - { - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsreg/requestPhoneValidation.php", &CIcqProto::OnValidateSms); - pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("r", pReq->m_reqId) - << CHAR_PARAM("smsFormatType", "human") << CHAR_PARAM("k", m_proto->appId()); - pReq->pUserInfo = this; - m_proto->Push(pReq); - } -}; - -void CIcqProto::OnCheckPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - if (pReply == nullptr || pReply->resultCode != 200) - return; - - CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; - pDlg->btnSendSms.Disable(); - pDlg->edtCode.Disable(); - - JSONROOT root(pReply->pData); - CMStringW wszStatus((*root)["status"].as_mstring()); - if (wszStatus != L"OK") { - pDlg->edtCode.SetText((*root)["printable"].as_mstring()); - return; - } - - CMStringA szPhoneNumber((*root)["typing_check"]["modified_phone_number"].as_mstring()); - CMStringA szPrefix((*root)["modified_prefix"].as_mstring()); - - auto *pNew = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://www.icq.com/smsreg/normalizePhoneNumber.php", &CIcqProto::OnNormalizePhone); - pNew << CHAR_PARAM("countryCode", szPrefix) << CHAR_PARAM("phoneNumber", szPhoneNumber.c_str() + szPrefix.GetLength()) - << CHAR_PARAM("k", appId()) << CHAR_PARAM("r", pReq->m_reqId); - pNew->pUserInfo = pDlg; - Push(pNew); -} - -void CIcqProto::OnNormalizePhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; - - JsonReply root(pReply); - pDlg->iErrorCode = root.error(); - if (root.error() != 200) - return; - - const JSONNode &data = root.data(); - pDlg->szMsisdn = data["msisdn"].as_mstring(); - pDlg->btnSendSms.Enable(); -} - -void CIcqProto::OnValidateSms(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - JsonReply root(pReply); - if (root.error() != 200) - return; - - CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; - const JSONNode &data = root.data(); - pDlg->szTrans = data["trans_id"].as_mstring(); - - pDlg->edtCode.Enable(); - pDlg->edtCode.SetText(L""); -} - -void CIcqProto::OnLoginViaPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; - - JsonReply root(pReply); - pDlg->iErrorCode = root.error(); - if (root.error() != 200) - return; - - const JSONNode &data = root.data(); - m_szAToken = data["token"]["a"].as_mstring(); - mir_urlDecode(m_szAToken.GetBuffer()); - setString(DB_KEY_ATOKEN, m_szAToken); - - int srvTS = data["hostTime"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - - m_szSessionKey = data["sessionKey"].as_mstring(); - setString(DB_KEY_SESSIONKEY, m_szSessionKey); - - m_szOwnId = data["loginId"].as_mstring(); - setByte(DB_KEY_PHONEREG, 1); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -class CIcqOptionsDlg : public CIcqDlgBase -{ - CCtrlEdit edtUin, edtPassword; - CCtrlCheck chkHideChats, chkTrayIcon, chkLaunchMailbox, chkShowErrorPopups; - CCtrlButton btnCreate; - CMStringW wszOldPass; - -public: - CIcqOptionsDlg(CIcqProto *ppro, int iDlgID, bool bFullDlg) : - CIcqDlgBase(ppro, iDlgID), - edtUin(this, IDC_UIN), - btnCreate(this, IDC_REGISTER), - edtPassword(this, IDC_PASSWORD), - chkTrayIcon(this, IDC_USETRAYICON), - chkHideChats(this, IDC_HIDECHATS), - chkLaunchMailbox(this, IDC_LAUNCH_MAILBOX), - chkShowErrorPopups(this, IDC_SHOWERRORPOPUPS) - { - btnCreate.OnClick = Callback(this, &CIcqOptionsDlg::onClick_Register); - - CreateLink(edtUin, ppro->m_szOwnId); - if (bFullDlg) { - CreateLink(chkHideChats, ppro->m_bHideGroupchats); - CreateLink(chkTrayIcon, ppro->m_bUseTrayIcon); - CreateLink(chkLaunchMailbox, ppro->m_bLaunchMailbox); - CreateLink(chkShowErrorPopups, ppro->m_bErrorPopups); - - chkTrayIcon.OnChange = Callback(this, &CIcqOptionsDlg::onChange_Tray); - } - } - - bool OnInitDialog() override - { - if (m_proto->m_isMra) - btnCreate.Hide(); - else - SetDlgItemText(m_hwnd, IDC_UIN_LABEL, TranslateT("UIN:")); - - wszOldPass = m_proto->getMStringW("Password"); - edtPassword.SetText(wszOldPass); - return true; - } - - bool OnApply() override - { - ptrW wszPass(edtPassword.GetText()); - if (wszPass) - m_proto->setWString("Password", wszPass); - else - m_proto->delSetting("Password"); - - if (wszOldPass != wszPass) { - m_proto->delSetting(DB_KEY_ATOKEN); - m_proto->delSetting(DB_KEY_SESSIONKEY); - m_proto->delSetting(DB_KEY_PHONEREG); - } - - if (mir_wstrlen(wszPass)) { - m_proto->m_szPassword = T2Utf(wszPass).get(); - m_proto->m_bRememberPwd = true; - } - else m_proto->m_bRememberPwd = m_proto->getByte("RememberPass"); - - return true; - } - - void onChange_Tray(CCtrlCheck*) - { - chkLaunchMailbox.Enable(chkTrayIcon.GetState()); - } - - void onClick_Register(CCtrlButton*) - { - CIcqRegistrationDlg dlg(m_proto); - dlg.SetParent(m_hwnd); - if (dlg.DoModal()) // force exit to avoid data corruption - PostMessage(m_hwndParent, WM_COMMAND, MAKELONG(IDCANCEL, BN_CLICKED), 0); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// Advanced options - -class CIcqOptionsAdv : public CIcqDlgBase -{ - CCtrlEdit edtDiff1, edtDiff2; - CCtrlSpin spin1, spin2; - CCtrlCombo cmbStatus1, cmbStatus2; - -public: - CIcqOptionsAdv(CIcqProto *ppro) : - CIcqDlgBase(ppro, IDD_OPTIONS_ADV), - spin1(this, IDC_SPIN1, 32000), - spin2(this, IDC_SPIN2, 32000), - edtDiff1(this, IDC_DIFF1), - edtDiff2(this, IDC_DIFF2), - cmbStatus1(this, IDC_STATUS1), - cmbStatus2(this, IDC_STATUS2) - { - edtDiff1.OnChange = Callback(this, &CIcqOptionsAdv::onChange_Timeout1); - edtDiff2.OnChange = Callback(this, &CIcqOptionsAdv::onChange_Timeout2); - - CreateLink(spin1, ppro->m_iTimeDiff1); - CreateLink(spin2, ppro->m_iTimeDiff2); - } - - bool OnInitDialog() override - { - if (cmbStatus1.GetHwnd()) { - for (uint32_t iStatus = ID_STATUS_OFFLINE; iStatus <= ID_STATUS_MAX; iStatus++) { - int idx = cmbStatus1.AddString(Clist_GetStatusModeDescription(iStatus, 0)); - cmbStatus1.SetItemData(idx, iStatus); - if (iStatus == m_proto->m_iStatus1) - cmbStatus1.SetCurSel(idx); - - idx = cmbStatus2.AddString(Clist_GetStatusModeDescription(iStatus, 0)); - cmbStatus2.SetItemData(idx, iStatus); - if (iStatus == m_proto->m_iStatus2) - cmbStatus2.SetCurSel(idx); - } - } - - return true; - } - - bool OnApply() override - { - if (cmbStatus1.GetHwnd()) { - m_proto->m_iStatus1 = cmbStatus1.GetCurData(); - m_proto->m_iStatus2 = cmbStatus2.GetCurData(); - } - - return true; - } - - void onChange_Timeout1(CCtrlEdit*) - { - bool bEnabled = edtDiff1.GetInt() != 0; - spin2.Enable(bEnabled); - edtDiff2.Enable(bEnabled); - cmbStatus1.Enable(bEnabled); - cmbStatus2.Enable(bEnabled && edtDiff2.GetInt() != 0); - } - - void onChange_Timeout2(CCtrlEdit*) - { - bool bEnabled = edtDiff2.GetInt() != 0; - cmbStatus2.Enable(bEnabled); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// Services - -INT_PTR CIcqProto::CreateAccMgrUI(WPARAM, LPARAM hwndParent) -{ - CIcqOptionsDlg *pDlg = new CIcqOptionsDlg(this, IDD_OPTIONS_ACCMGR, false); - pDlg->SetParent((HWND)hwndParent); - pDlg->Create(); - return (INT_PTR)pDlg->GetHwnd(); -} - -int CIcqProto::OnOptionsInit(WPARAM wParam, LPARAM) -{ - OPTIONSDIALOGPAGE odp = {}; - odp.szTitle.w = m_tszUserName; - odp.flags = ODPF_UNICODE | ODPF_BOLDGROUPS; - odp.szGroup.w = LPGENW("Network"); - odp.position = 1; - - odp.szTab.w = LPGENW("General"); - odp.pDialog = new CIcqOptionsDlg(this, IDD_OPTIONS_FULL, true); - g_plugin.addOptions(wParam, &odp); - - odp.szTab.w = LPGENW("Advanced"); - odp.pDialog = new CIcqOptionsAdv(this); - g_plugin.addOptions(wParam, &odp); - return 0; -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +class CIcqEnterLoginDlg : public CIcqDlgBase +{ + CCtrlEdit edtPass; + CCtrlCheck chkSave; + +public: + CIcqEnterLoginDlg(CIcqProto *ppro) : + CIcqDlgBase(ppro, IDD_LOGINPW), + edtPass(this, IDC_PASSWORD), + chkSave(this, IDC_SAVEPASS) + { + } + + bool OnInitDialog() override + { + m_proto->m_bDlgActive = true; + chkSave.SetState(m_proto->getBool("RememberPass")); + Window_SetIcon_IcoLib(m_hwnd, m_proto->m_hProtoIcon); + return true; + } + + bool OnApply() override + { + m_proto->setByte("RememberPass", m_proto->m_bRememberPwd = chkSave.GetState()); + m_proto->m_szPassword = ptrA(edtPass.GetTextU()); + EndModal(true); + return true; + } + + void OnDestroy() override + { + m_proto->m_bDlgActive = false; + } +}; + +bool CIcqProto::RetrievePassword() +{ + // if we registered via phone (i.e., server holds the password), we don't need to enter it + if (getByte(DB_KEY_PHONEREG)) + return true; + + if (!m_szPassword.IsEmpty() && m_bRememberPwd) + return true; + + m_szPassword = getMStringA("Password"); + if (!m_szPassword.IsEmpty()) { + m_bRememberPwd = true; + return true; + } + + if (m_bDlgActive) + return false; + + return CIcqEnterLoginDlg(this).DoModal(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +struct CIcqRegistrationDlg : public CIcqDlgBase +{ + CMStringA szTrans, szMsisdn; + int iErrorCode = 0; + + CCtrlEdit edtPhone, edtCode; + CCtrlButton btnSendSms; + + CIcqRegistrationDlg(CIcqProto *ppro) : + CIcqDlgBase(ppro, IDD_REGISTER), + edtPhone(this, IDC_PHONE), + edtCode(this, IDC_CODE), + btnSendSms(this, IDC_SENDSMS) + { + btnSendSms.OnClick = Callback(this, &CIcqRegistrationDlg::onClick_SendSms); + edtPhone.OnChange = Callback(this, &CIcqRegistrationDlg::onChange_Phone); + } + + bool OnApply() override + { + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsreg/loginWithPhoneNumber.php", &CIcqProto::OnLoginViaPhone); + pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("trans_id", szTrans) << CHAR_PARAM("k", m_proto->appId()) + << CHAR_PARAM("r", pReq->m_reqId) << CHAR_PARAM("f", "json") << WCHAR_PARAM("sms_code", ptrW(edtCode.GetText())) << INT_PARAM("create_account", 1); + pReq->pUserInfo = this; + + SetCursor(LoadCursor(0, IDC_WAIT)); + m_proto->ExecuteRequest(pReq); + SetCursor(LoadCursor(0, IDC_ARROW)); + + if (iErrorCode != 200) + return false; + + EndDialog(m_hwnd, 1); + return true; + } + + void onChange_Phone(CCtrlEdit*) + { + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsapi/fcgi-bin/smsphoneinfo", &CIcqProto::OnCheckPhone); + pReq << CHAR_PARAM("service", "icq_registration") << CHAR_PARAM("info", "typing_check,score,iso_country_code") << CHAR_PARAM("lang", "ru") + << WCHAR_PARAM("phone", ptrW(edtPhone.GetText())) << CHAR_PARAM("id", pReq->m_reqId); + pReq->pUserInfo = this; + m_proto->Push(pReq); + } + + void onClick_SendSms(CCtrlButton*) + { + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://u.icq.net/api/v60/smsreg/requestPhoneValidation.php", &CIcqProto::OnValidateSms); + pReq << CHAR_PARAM("locale", "en") << CHAR_PARAM("msisdn", szMsisdn) << CHAR_PARAM("r", pReq->m_reqId) + << CHAR_PARAM("smsFormatType", "human") << CHAR_PARAM("k", m_proto->appId()); + pReq->pUserInfo = this; + m_proto->Push(pReq); + } +}; + +void CIcqProto::OnCheckPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + if (pReply == nullptr || pReply->resultCode != 200) + return; + + CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; + pDlg->btnSendSms.Disable(); + pDlg->edtCode.Disable(); + + JSONROOT root(pReply->pData); + CMStringW wszStatus((*root)["status"].as_mstring()); + if (wszStatus != L"OK") { + pDlg->edtCode.SetText((*root)["printable"].as_mstring()); + return; + } + + CMStringA szPhoneNumber((*root)["typing_check"]["modified_phone_number"].as_mstring()); + CMStringA szPrefix((*root)["modified_prefix"].as_mstring()); + + auto *pNew = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, "https://www.icq.com/smsreg/normalizePhoneNumber.php", &CIcqProto::OnNormalizePhone); + pNew << CHAR_PARAM("countryCode", szPrefix) << CHAR_PARAM("phoneNumber", szPhoneNumber.c_str() + szPrefix.GetLength()) + << CHAR_PARAM("k", appId()) << CHAR_PARAM("r", pReq->m_reqId); + pNew->pUserInfo = pDlg; + Push(pNew); +} + +void CIcqProto::OnNormalizePhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; + + JsonReply root(pReply); + pDlg->iErrorCode = root.error(); + if (root.error() != 200) + return; + + const JSONNode &data = root.data(); + pDlg->szMsisdn = data["msisdn"].as_mstring(); + pDlg->btnSendSms.Enable(); +} + +void CIcqProto::OnValidateSms(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + JsonReply root(pReply); + if (root.error() != 200) + return; + + CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; + const JSONNode &data = root.data(); + pDlg->szTrans = data["trans_id"].as_mstring(); + + pDlg->edtCode.Enable(); + pDlg->edtCode.SetText(L""); +} + +void CIcqProto::OnLoginViaPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + CIcqRegistrationDlg *pDlg = (CIcqRegistrationDlg*)pReq->pUserInfo; + + JsonReply root(pReply); + pDlg->iErrorCode = root.error(); + if (root.error() != 200) + return; + + const JSONNode &data = root.data(); + m_szAToken = data["token"]["a"].as_mstring(); + mir_urlDecode(m_szAToken.GetBuffer()); + setString(DB_KEY_ATOKEN, m_szAToken); + + int srvTS = data["hostTime"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + + m_szSessionKey = data["sessionKey"].as_mstring(); + setString(DB_KEY_SESSIONKEY, m_szSessionKey); + + m_szOwnId = data["loginId"].as_mstring(); + setByte(DB_KEY_PHONEREG, 1); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +class CIcqOptionsDlg : public CIcqDlgBase +{ + CCtrlEdit edtUin, edtPassword; + CCtrlCheck chkHideChats, chkTrayIcon, chkLaunchMailbox, chkShowErrorPopups; + CCtrlButton btnCreate; + CMStringW wszOldPass; + +public: + CIcqOptionsDlg(CIcqProto *ppro, int iDlgID, bool bFullDlg) : + CIcqDlgBase(ppro, iDlgID), + edtUin(this, IDC_UIN), + btnCreate(this, IDC_REGISTER), + edtPassword(this, IDC_PASSWORD), + chkTrayIcon(this, IDC_USETRAYICON), + chkHideChats(this, IDC_HIDECHATS), + chkLaunchMailbox(this, IDC_LAUNCH_MAILBOX), + chkShowErrorPopups(this, IDC_SHOWERRORPOPUPS) + { + btnCreate.OnClick = Callback(this, &CIcqOptionsDlg::onClick_Register); + + CreateLink(edtUin, ppro->m_szOwnId); + if (bFullDlg) { + CreateLink(chkHideChats, ppro->m_bHideGroupchats); + CreateLink(chkTrayIcon, ppro->m_bUseTrayIcon); + CreateLink(chkLaunchMailbox, ppro->m_bLaunchMailbox); + CreateLink(chkShowErrorPopups, ppro->m_bErrorPopups); + + chkTrayIcon.OnChange = Callback(this, &CIcqOptionsDlg::onChange_Tray); + } + } + + bool OnInitDialog() override + { + if (m_proto->m_isMra) + btnCreate.Hide(); + else + SetDlgItemText(m_hwnd, IDC_UIN_LABEL, TranslateT("UIN:")); + + wszOldPass = m_proto->getMStringW("Password"); + edtPassword.SetText(wszOldPass); + return true; + } + + bool OnApply() override + { + ptrW wszPass(edtPassword.GetText()); + if (wszPass) + m_proto->setWString("Password", wszPass); + else + m_proto->delSetting("Password"); + + if (wszOldPass != wszPass) { + m_proto->delSetting(DB_KEY_ATOKEN); + m_proto->delSetting(DB_KEY_SESSIONKEY); + m_proto->delSetting(DB_KEY_PHONEREG); + } + + if (mir_wstrlen(wszPass)) { + m_proto->m_szPassword = T2Utf(wszPass).get(); + m_proto->m_bRememberPwd = true; + } + else m_proto->m_bRememberPwd = m_proto->getByte("RememberPass"); + + return true; + } + + void onChange_Tray(CCtrlCheck*) + { + chkLaunchMailbox.Enable(chkTrayIcon.GetState()); + } + + void onClick_Register(CCtrlButton*) + { + CIcqRegistrationDlg dlg(m_proto); + dlg.SetParent(m_hwnd); + if (dlg.DoModal()) // force exit to avoid data corruption + PostMessage(m_hwndParent, WM_COMMAND, MAKELONG(IDCANCEL, BN_CLICKED), 0); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// Advanced options + +class CIcqOptionsAdv : public CIcqDlgBase +{ + CCtrlEdit edtDiff1, edtDiff2; + CCtrlSpin spin1, spin2; + CCtrlCombo cmbStatus1, cmbStatus2; + +public: + CIcqOptionsAdv(CIcqProto *ppro) : + CIcqDlgBase(ppro, IDD_OPTIONS_ADV), + spin1(this, IDC_SPIN1, 32000), + spin2(this, IDC_SPIN2, 32000), + edtDiff1(this, IDC_DIFF1), + edtDiff2(this, IDC_DIFF2), + cmbStatus1(this, IDC_STATUS1), + cmbStatus2(this, IDC_STATUS2) + { + edtDiff1.OnChange = Callback(this, &CIcqOptionsAdv::onChange_Timeout1); + edtDiff2.OnChange = Callback(this, &CIcqOptionsAdv::onChange_Timeout2); + + CreateLink(spin1, ppro->m_iTimeDiff1); + CreateLink(spin2, ppro->m_iTimeDiff2); + } + + bool OnInitDialog() override + { + if (cmbStatus1.GetHwnd()) { + for (uint32_t iStatus = ID_STATUS_OFFLINE; iStatus <= ID_STATUS_MAX; iStatus++) { + int idx = cmbStatus1.AddString(Clist_GetStatusModeDescription(iStatus, 0)); + cmbStatus1.SetItemData(idx, iStatus); + if (iStatus == m_proto->m_iStatus1) + cmbStatus1.SetCurSel(idx); + + idx = cmbStatus2.AddString(Clist_GetStatusModeDescription(iStatus, 0)); + cmbStatus2.SetItemData(idx, iStatus); + if (iStatus == m_proto->m_iStatus2) + cmbStatus2.SetCurSel(idx); + } + } + + return true; + } + + bool OnApply() override + { + if (cmbStatus1.GetHwnd()) { + m_proto->m_iStatus1 = cmbStatus1.GetCurData(); + m_proto->m_iStatus2 = cmbStatus2.GetCurData(); + } + + return true; + } + + void onChange_Timeout1(CCtrlEdit*) + { + bool bEnabled = edtDiff1.GetInt() != 0; + spin2.Enable(bEnabled); + edtDiff2.Enable(bEnabled); + cmbStatus1.Enable(bEnabled); + cmbStatus2.Enable(bEnabled && edtDiff2.GetInt() != 0); + } + + void onChange_Timeout2(CCtrlEdit*) + { + bool bEnabled = edtDiff2.GetInt() != 0; + cmbStatus2.Enable(bEnabled); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// Services + +INT_PTR CIcqProto::CreateAccMgrUI(WPARAM, LPARAM hwndParent) +{ + CIcqOptionsDlg *pDlg = new CIcqOptionsDlg(this, IDD_OPTIONS_ACCMGR, false); + pDlg->SetParent((HWND)hwndParent); + pDlg->Create(); + return (INT_PTR)pDlg->GetHwnd(); +} + +int CIcqProto::OnOptionsInit(WPARAM wParam, LPARAM) +{ + OPTIONSDIALOGPAGE odp = {}; + odp.szTitle.w = m_tszUserName; + odp.flags = ODPF_UNICODE | ODPF_BOLDGROUPS; + odp.szGroup.w = LPGENW("Network"); + odp.position = 1; + + odp.szTab.w = LPGENW("General"); + odp.pDialog = new CIcqOptionsDlg(this, IDD_OPTIONS_FULL, true); + g_plugin.addOptions(wParam, &odp); + + odp.szTab.w = LPGENW("Advanced"); + odp.pDialog = new CIcqOptionsAdv(this); + g_plugin.addOptions(wParam, &odp); + return 0; +} diff --git a/protocols/ICQ-WIM/src/poll.cpp b/protocols/ICQ-WIM/src/poll.cpp index d7ca7298ad..6fca78728f 100644 --- a/protocols/ICQ-WIM/src/poll.cpp +++ b/protocols/ICQ-WIM/src/poll.cpp @@ -1,409 +1,409 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- -// Long-poll thread and its item handlers - -#include "stdafx.h" - -void CIcqProto::ProcessBuddyList(const JSONNode &ev) -{ - m_arGroups.destroy(); - - LIST tmpGroups(10); - bool bEnableMenu = false; - - for (auto &it : ev["groups"]) { - auto *pGroup = new IcqGroup(it["id"].as_int(), it["name"].as_mstring()); - debugLogA("new group: id=%d, level=%d, name=%S", pGroup->id, pGroup->level, pGroup->wszName.c_str()); - if (pGroup->level != 0) { - for (auto &p : tmpGroups.rev_iter()) { - if (p->level == pGroup->level-1) { - pGroup->wszName = p->wszName + L"\\" + pGroup->wszName; - debugLogA("Group name fixed as %S", pGroup->wszName.c_str()); - break; - } - } - } - tmpGroups.insert(pGroup); - m_arGroups.insert(pGroup); - - bool bCreated = false; - - for (auto &buddy : it["buddies"]) { - MCONTACT hContact = ParseBuddyInfo(buddy); - if (hContact == INVALID_CONTACT_ID) - continue; - - setWString(hContact, "IcqGroup", pGroup->wszName); - - if (!bCreated) { - Clist_GroupCreate(0, pGroup->wszName); - bCreated = true; - } - - ptrW mirGroup(Clist_GetGroup(hContact)); - if (mir_wstrcmp(mirGroup, pGroup->wszName)) - bEnableMenu = true; - - if (!mirGroup) - Clist_SetGroup(hContact, pGroup->wszName); - } - } - - if (bEnableMenu) - Menu_ShowItem(m_hUploadGroups, true); - - for (auto &it : m_arCache) - if (!it->m_bInList && !getBool(it->m_hContact, "IcqDeleted")) - Contact::RemoveFromList(it->m_hContact); -} - -void CIcqProto::ProcessDiff(const JSONNode &ev) -{ - for (auto &block : ev) { - CMStringW szType = block["type"].as_mstring(); - if (szType != "updated" && szType != "created" && szType != "deleted") - continue; - - for (auto &it : block["data"]) { - int grpId = it["id"].as_int(); - CMStringW wszName = it["name"].as_mstring(); - - auto *pGroup = m_arGroups.find((IcqGroup *)&grpId); - if (pGroup == nullptr) { - if (szType != "created") { - debugLogA("Group %d isn't found", grpId); - continue; - } - - pGroup = new IcqGroup(grpId, wszName); - m_arGroups.insert(pGroup); - } - else { - pGroup->wszSrvName = wszName; - pGroup->SetName(wszName); - } - - bool bCreated = false, bDeleted = (szType == "deleted"); - - for (auto &buddy : it["buddies"]) { - if (bDeleted) - continue; - - MCONTACT hContact = ParseBuddyInfo(buddy, true); - if (hContact == INVALID_CONTACT_ID) - continue; - - setWString(hContact, "IcqGroup", pGroup->wszName); - - if (!bCreated) { - Clist_GroupCreate(0, pGroup->wszName); - bCreated = true; - } - - ptrW wszGroup(Clist_GetGroup(hContact)); - if (!wszGroup) - Clist_SetGroup(hContact, pGroup->wszName); - } - - if (bDeleted) - m_arGroups.remove(pGroup); - } - - RefreshGroups(); - } -} - -void CIcqProto::ProcessEvent(const JSONNode &ev) -{ - const JSONNode &pData = ev["eventData"]; - CMStringW szType = ev["type"].as_mstring(); - if (szType == L"buddylist") - ProcessBuddyList(pData); - else if (szType == L"diff") - ProcessDiff(pData); - else if (szType == L"histDlgState") - ProcessHistData(pData); - else if (szType == L"imState") - ProcessImState(pData); - else if (szType == L"mchat") - ProcessGroupChat(pData); - else if (szType == L"myInfo") - ProcessMyInfo(pData); - else if (szType == L"notification") - ProcessNotification(pData); - else if (szType == L"permitDeny") - ProcessPermissions(pData); - else if (szType == L"presence") - ProcessPresence(pData); - else if (szType == L"sessionEnded") - ProcessSessionEnd(pData); - else if (szType == L"typing") - ProcessTyping(pData); -} - -void CIcqProto::ProcessHistData(const JSONNode &ev) -{ - MCONTACT hContact; - bool bVeryBeginning = m_bFirstBos; - - CMStringW wszId(ev["sn"].as_mstring()); - auto *pCache = FindContactByUIN(wszId); // might be NULL for groupchats - - if (IsChat(wszId)) { - SESSION_INFO *si = g_chatApi.SM_FindSession(wszId, m_szModuleName); - if (si == nullptr) - return; - - hContact = si->hContact; - - if (si->arUsers.getCount() == 0) { - __int64 srvInfoVer = _wtoi64(ev["mchatState"]["infoVersion"].as_mstring()); - __int64 srvMembersVer = _wtoi64(ev["mchatState"]["membersVersion"].as_mstring()); - if (srvInfoVer != getId(hContact, "InfoVersion") || srvMembersVer != getId(hContact, "MembersVersion")) { - auto *pReq = new AsyncRapiRequest(this, "getChatInfo", &CIcqProto::OnGetChatInfo); - pReq->params << WCHAR_PARAM("sn", wszId) << INT_PARAM("memberLimit", 100) << CHAR_PARAM("aimSid", m_aimsid); - pReq->pUserInfo = si; - Push(pReq); - } - else LoadChatInfo(si); - } - } - else { - hContact = CreateContact(wszId, true); - - // for temporary contacts that just gonna be created - if (pCache == nullptr) { - bVeryBeginning = true; - pCache = FindContactByUIN(wszId); - } - } - - // restore reading from the previous point, if we just installed Miranda - __int64 lastMsgId = getId(hContact, DB_KEY_LASTMSGID); - if (lastMsgId == 0) { - lastMsgId = _wtoi64(ev["yours"]["lastRead"].as_mstring()); - setId(hContact, DB_KEY_LASTMSGID, lastMsgId); - } - - __int64 patchVersion = _wtoi64(ev["patchVersion"].as_mstring()); - setId(hContact, DB_KEY_PATCHVER, patchVersion); - - __int64 srvLastId = _wtoi64(ev["lastMsgId"].as_mstring()); - - // we load history in the very beginning or if the previous message - if (bVeryBeginning) { - if (pCache) { - debugLogA("Setting cache = %lld for %d", srvLastId, hContact); - pCache->m_iProcessedMsgId = srvLastId; - } - - if (srvLastId > lastMsgId) { - debugLogA("We need to retrieve history for %S: %lld > %lld", wszId.c_str(), srvLastId, lastMsgId); - RetrieveUserHistory(hContact, lastMsgId, false); - } - } - else { - if (!(pCache && pCache->m_iProcessedMsgId >= srvLastId)) { - if (pCache) - debugLogA("Proceeding with cache for %d: %lld < %lld", hContact, pCache->m_iProcessedMsgId, srvLastId); - else - debugLogA("Proceeding with empty cache for %d", hContact); - - for (auto &it : ev["tail"]["messages"]) - ParseMessage(hContact, lastMsgId, it, false, true); - - setId(hContact, DB_KEY_LASTMSGID, lastMsgId); - if (pCache) { - pCache->m_iProcessedMsgId = lastMsgId; - debugLogA("Setting second cache = %lld for %d", srvLastId, hContact); - } - } - } - - // check remote read - if (g_bMessageState) { - __int64 srvRemoteRead = _wtoi64(ev["theirs"]["lastRead"].as_mstring()); - __int64 lastRemoteRead = getId(hContact, DB_KEY_REMOTEREAD); - if (srvRemoteRead > lastRemoteRead) { - setId(hContact, DB_KEY_REMOTEREAD, srvRemoteRead); - - if (g_bMessageState) - CallService(MS_MESSAGESTATE_UPDATE, hContact, MRD_TYPE_READ); - } - } -} - -void CIcqProto::ProcessImState(const JSONNode &ev) -{ - for (auto &it : ev["imStates"]) { - if (it["state"].as_mstring() != L"sent") - continue; - - CMStringA reqId(it["sendReqId"].as_mstring()); - CMStringA msgId(it["histMsgId"].as_mstring()); - MCONTACT hContact = CheckOwnMessage(reqId, msgId, false); - if (hContact) - CheckLastId(hContact, ev); - } -} - -void CIcqProto::ProcessMyInfo(const JSONNode &ev) -{ - if (auto &var = ev["friendly"]) - setWString("Nick", var.as_mstring()); - - if (auto &var = ev["attachedPhoneNumber"]) - setWString(DB_KEY_PHONE, var.as_mstring()); - - CheckAvatarChange(0, ev); -} - -void CIcqProto::ProcessNotification(const JSONNode &ev) -{ - for (auto &fld : ev["fields"]) { - const JSONNode &email = fld["mailbox.newMessage"]; - if (email) { - JSONROOT root(email.as_string().c_str()); - CMStringW wszFrom((*root)["from"].as_mstring()); - CMStringW wszSubj((*root)["subject"].as_mstring()); - m_unreadEmails = (*root)["unreadCount"].as_int(); - debugLogW(L"You received e-mail (%d) from <%s>: <%s>", m_unreadEmails, wszFrom.c_str(), wszSubj.c_str()); - - CMStringW wszMessage(FORMAT, TranslateT("You received e-mail from %s: %s"), wszFrom.c_str(), wszSubj.c_str()); - EmailNotification(wszMessage); - } - - const JSONNode &status = fld["mailbox.status"]; - if (status) { - int iOldCount = m_unreadEmails; - - JSONROOT root(status.as_string().c_str()); - m_szMailBox = (*root)["email"].as_mstring(); - m_unreadEmails = (*root)["unreadCount"].as_int(); - - // we've read/removed some messages from server - if (iOldCount > m_unreadEmails) { - g_clistApi.pfnRemoveEvent(0, ICQ_FAKE_EVENT_ID); - return; - } - - // we notify about initial mail count only during login - if (m_bFirstBos && m_unreadEmails > 0) { - CMStringW wszMessage(FORMAT, TranslateT("You have %d unread emails"), m_unreadEmails); - EmailNotification(wszMessage); - } - } - } -} - -void CIcqProto::ProcessPresence(const JSONNode &ev) -{ - CMStringW aimId = ev["aimId"].as_mstring(); - - IcqCacheItem *pCache = FindContactByUIN(aimId); - if (pCache == nullptr) - return; - - int iNewStatus = StatusFromPresence(ev, pCache->m_hContact); - if (iNewStatus == -1) - iNewStatus = ID_STATUS_OFFLINE; - - // major crutch dedicated to the official client behaviour to go offline - // when its window gets closed. we change the status of a contact to the - // first chosen one from options and initialize a timer - if (iNewStatus == ID_STATUS_OFFLINE) { - if (m_iTimeDiff1) { - iNewStatus = m_iStatus1; - pCache->m_timer1 = time(0); - } - } - // if a client returns back online, we clear timers not to play with statuses anymore - else pCache->m_timer1 = pCache->m_timer2 = 0; - - setWord(pCache->m_hContact, "Status", iNewStatus); - - Json2string(pCache->m_hContact, ev, "friendly", "Nick", true); - CheckAvatarChange(pCache->m_hContact, ev); -} - -void CIcqProto::ProcessSessionEnd(const JSONNode &/*ev*/) -{ - m_szRToken.Empty(); - m_iRClientId = 0; - delSetting(DB_KEY_RCLIENTID); - - ShutdownSession(); -} - -void CIcqProto::ProcessTyping(const JSONNode &ev) -{ - CMStringW aimId = ev["aimId"].as_mstring(); - CMStringW wszStatus = ev["typingStatus"].as_mstring(); - - IcqCacheItem *pCache = FindContactByUIN(aimId); - if (pCache) { - if (wszStatus == "typing") - CallService(MS_PROTO_CONTACTISTYPING, pCache->m_hContact, 60); - else - CallService(MS_PROTO_CONTACTISTYPING, pCache->m_hContact, PROTOTYPE_CONTACTTYPING_OFF); - } -} - -void CIcqProto::OnFetchEvents(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - if (root.error() != 200) { - ShutdownSession(); - return; - } - - JSONNode &data = root.data(); - m_fetchBaseURL = data["fetchBaseURL"].as_mstring(); - - for (auto &it : data["events"]) - ProcessEvent(it); -} - -void __cdecl CIcqProto::PollThread(void*) -{ - debugLogA("Polling thread started"); - m_bFirstBos = true; - - while (m_bOnline && !m_fetchBaseURL.IsEmpty()) { - CMStringA szUrl = m_fetchBaseURL; - if (m_bFirstBos) - szUrl.Append("&first=1"); - else - szUrl.Append("&timeout=25000"); - - auto *pReq = new AsyncHttpRequest(CONN_FETCH, REQUEST_GET, szUrl, &CIcqProto::OnFetchEvents); - if (!m_bFirstBos) - pReq->timeout = 62000; - - if (!ExecuteRequest(pReq)) { - ShutdownSession(); - break; - } - - m_bFirstBos = false; - } - - debugLogA("Polling thread ended"); -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- +// Long-poll thread and its item handlers + +#include "stdafx.h" + +void CIcqProto::ProcessBuddyList(const JSONNode &ev) +{ + m_arGroups.destroy(); + + LIST tmpGroups(10); + bool bEnableMenu = false; + + for (auto &it : ev["groups"]) { + auto *pGroup = new IcqGroup(it["id"].as_int(), it["name"].as_mstring()); + debugLogA("new group: id=%d, level=%d, name=%S", pGroup->id, pGroup->level, pGroup->wszName.c_str()); + if (pGroup->level != 0) { + for (auto &p : tmpGroups.rev_iter()) { + if (p->level == pGroup->level-1) { + pGroup->wszName = p->wszName + L"\\" + pGroup->wszName; + debugLogA("Group name fixed as %S", pGroup->wszName.c_str()); + break; + } + } + } + tmpGroups.insert(pGroup); + m_arGroups.insert(pGroup); + + bool bCreated = false; + + for (auto &buddy : it["buddies"]) { + MCONTACT hContact = ParseBuddyInfo(buddy); + if (hContact == INVALID_CONTACT_ID) + continue; + + setWString(hContact, "IcqGroup", pGroup->wszName); + + if (!bCreated) { + Clist_GroupCreate(0, pGroup->wszName); + bCreated = true; + } + + ptrW mirGroup(Clist_GetGroup(hContact)); + if (mir_wstrcmp(mirGroup, pGroup->wszName)) + bEnableMenu = true; + + if (!mirGroup) + Clist_SetGroup(hContact, pGroup->wszName); + } + } + + if (bEnableMenu) + Menu_ShowItem(m_hUploadGroups, true); + + for (auto &it : m_arCache) + if (!it->m_bInList && !getBool(it->m_hContact, "IcqDeleted")) + Contact::RemoveFromList(it->m_hContact); +} + +void CIcqProto::ProcessDiff(const JSONNode &ev) +{ + for (auto &block : ev) { + CMStringW szType = block["type"].as_mstring(); + if (szType != "updated" && szType != "created" && szType != "deleted") + continue; + + for (auto &it : block["data"]) { + int grpId = it["id"].as_int(); + CMStringW wszName = it["name"].as_mstring(); + + auto *pGroup = m_arGroups.find((IcqGroup *)&grpId); + if (pGroup == nullptr) { + if (szType != "created") { + debugLogA("Group %d isn't found", grpId); + continue; + } + + pGroup = new IcqGroup(grpId, wszName); + m_arGroups.insert(pGroup); + } + else { + pGroup->wszSrvName = wszName; + pGroup->SetName(wszName); + } + + bool bCreated = false, bDeleted = (szType == "deleted"); + + for (auto &buddy : it["buddies"]) { + if (bDeleted) + continue; + + MCONTACT hContact = ParseBuddyInfo(buddy, true); + if (hContact == INVALID_CONTACT_ID) + continue; + + setWString(hContact, "IcqGroup", pGroup->wszName); + + if (!bCreated) { + Clist_GroupCreate(0, pGroup->wszName); + bCreated = true; + } + + ptrW wszGroup(Clist_GetGroup(hContact)); + if (!wszGroup) + Clist_SetGroup(hContact, pGroup->wszName); + } + + if (bDeleted) + m_arGroups.remove(pGroup); + } + + RefreshGroups(); + } +} + +void CIcqProto::ProcessEvent(const JSONNode &ev) +{ + const JSONNode &pData = ev["eventData"]; + CMStringW szType = ev["type"].as_mstring(); + if (szType == L"buddylist") + ProcessBuddyList(pData); + else if (szType == L"diff") + ProcessDiff(pData); + else if (szType == L"histDlgState") + ProcessHistData(pData); + else if (szType == L"imState") + ProcessImState(pData); + else if (szType == L"mchat") + ProcessGroupChat(pData); + else if (szType == L"myInfo") + ProcessMyInfo(pData); + else if (szType == L"notification") + ProcessNotification(pData); + else if (szType == L"permitDeny") + ProcessPermissions(pData); + else if (szType == L"presence") + ProcessPresence(pData); + else if (szType == L"sessionEnded") + ProcessSessionEnd(pData); + else if (szType == L"typing") + ProcessTyping(pData); +} + +void CIcqProto::ProcessHistData(const JSONNode &ev) +{ + MCONTACT hContact; + bool bVeryBeginning = m_bFirstBos; + + CMStringW wszId(ev["sn"].as_mstring()); + auto *pCache = FindContactByUIN(wszId); // might be NULL for groupchats + + if (IsChat(wszId)) { + SESSION_INFO *si = g_chatApi.SM_FindSession(wszId, m_szModuleName); + if (si == nullptr) + return; + + hContact = si->hContact; + + if (si->arUsers.getCount() == 0) { + __int64 srvInfoVer = _wtoi64(ev["mchatState"]["infoVersion"].as_mstring()); + __int64 srvMembersVer = _wtoi64(ev["mchatState"]["membersVersion"].as_mstring()); + if (srvInfoVer != getId(hContact, "InfoVersion") || srvMembersVer != getId(hContact, "MembersVersion")) { + auto *pReq = new AsyncRapiRequest(this, "getChatInfo", &CIcqProto::OnGetChatInfo); + pReq->params << WCHAR_PARAM("sn", wszId) << INT_PARAM("memberLimit", 100) << CHAR_PARAM("aimSid", m_aimsid); + pReq->pUserInfo = si; + Push(pReq); + } + else LoadChatInfo(si); + } + } + else { + hContact = CreateContact(wszId, true); + + // for temporary contacts that just gonna be created + if (pCache == nullptr) { + bVeryBeginning = true; + pCache = FindContactByUIN(wszId); + } + } + + // restore reading from the previous point, if we just installed Miranda + __int64 lastMsgId = getId(hContact, DB_KEY_LASTMSGID); + if (lastMsgId == 0) { + lastMsgId = _wtoi64(ev["yours"]["lastRead"].as_mstring()); + setId(hContact, DB_KEY_LASTMSGID, lastMsgId); + } + + __int64 patchVersion = _wtoi64(ev["patchVersion"].as_mstring()); + setId(hContact, DB_KEY_PATCHVER, patchVersion); + + __int64 srvLastId = _wtoi64(ev["lastMsgId"].as_mstring()); + + // we load history in the very beginning or if the previous message + if (bVeryBeginning) { + if (pCache) { + debugLogA("Setting cache = %lld for %d", srvLastId, hContact); + pCache->m_iProcessedMsgId = srvLastId; + } + + if (srvLastId > lastMsgId) { + debugLogA("We need to retrieve history for %S: %lld > %lld", wszId.c_str(), srvLastId, lastMsgId); + RetrieveUserHistory(hContact, lastMsgId, false); + } + } + else { + if (!(pCache && pCache->m_iProcessedMsgId >= srvLastId)) { + if (pCache) + debugLogA("Proceeding with cache for %d: %lld < %lld", hContact, pCache->m_iProcessedMsgId, srvLastId); + else + debugLogA("Proceeding with empty cache for %d", hContact); + + for (auto &it : ev["tail"]["messages"]) + ParseMessage(hContact, lastMsgId, it, false, true); + + setId(hContact, DB_KEY_LASTMSGID, lastMsgId); + if (pCache) { + pCache->m_iProcessedMsgId = lastMsgId; + debugLogA("Setting second cache = %lld for %d", srvLastId, hContact); + } + } + } + + // check remote read + if (g_bMessageState) { + __int64 srvRemoteRead = _wtoi64(ev["theirs"]["lastRead"].as_mstring()); + __int64 lastRemoteRead = getId(hContact, DB_KEY_REMOTEREAD); + if (srvRemoteRead > lastRemoteRead) { + setId(hContact, DB_KEY_REMOTEREAD, srvRemoteRead); + + if (g_bMessageState) + CallService(MS_MESSAGESTATE_UPDATE, hContact, MRD_TYPE_READ); + } + } +} + +void CIcqProto::ProcessImState(const JSONNode &ev) +{ + for (auto &it : ev["imStates"]) { + if (it["state"].as_mstring() != L"sent") + continue; + + CMStringA reqId(it["sendReqId"].as_mstring()); + CMStringA msgId(it["histMsgId"].as_mstring()); + MCONTACT hContact = CheckOwnMessage(reqId, msgId, false); + if (hContact) + CheckLastId(hContact, ev); + } +} + +void CIcqProto::ProcessMyInfo(const JSONNode &ev) +{ + if (auto &var = ev["friendly"]) + setWString("Nick", var.as_mstring()); + + if (auto &var = ev["attachedPhoneNumber"]) + setWString(DB_KEY_PHONE, var.as_mstring()); + + CheckAvatarChange(0, ev); +} + +void CIcqProto::ProcessNotification(const JSONNode &ev) +{ + for (auto &fld : ev["fields"]) { + const JSONNode &email = fld["mailbox.newMessage"]; + if (email) { + JSONROOT root(email.as_string().c_str()); + CMStringW wszFrom((*root)["from"].as_mstring()); + CMStringW wszSubj((*root)["subject"].as_mstring()); + m_unreadEmails = (*root)["unreadCount"].as_int(); + debugLogW(L"You received e-mail (%d) from <%s>: <%s>", m_unreadEmails, wszFrom.c_str(), wszSubj.c_str()); + + CMStringW wszMessage(FORMAT, TranslateT("You received e-mail from %s: %s"), wszFrom.c_str(), wszSubj.c_str()); + EmailNotification(wszMessage); + } + + const JSONNode &status = fld["mailbox.status"]; + if (status) { + int iOldCount = m_unreadEmails; + + JSONROOT root(status.as_string().c_str()); + m_szMailBox = (*root)["email"].as_mstring(); + m_unreadEmails = (*root)["unreadCount"].as_int(); + + // we've read/removed some messages from server + if (iOldCount > m_unreadEmails) { + g_clistApi.pfnRemoveEvent(0, ICQ_FAKE_EVENT_ID); + return; + } + + // we notify about initial mail count only during login + if (m_bFirstBos && m_unreadEmails > 0) { + CMStringW wszMessage(FORMAT, TranslateT("You have %d unread emails"), m_unreadEmails); + EmailNotification(wszMessage); + } + } + } +} + +void CIcqProto::ProcessPresence(const JSONNode &ev) +{ + CMStringW aimId = ev["aimId"].as_mstring(); + + IcqCacheItem *pCache = FindContactByUIN(aimId); + if (pCache == nullptr) + return; + + int iNewStatus = StatusFromPresence(ev, pCache->m_hContact); + if (iNewStatus == -1) + iNewStatus = ID_STATUS_OFFLINE; + + // major crutch dedicated to the official client behaviour to go offline + // when its window gets closed. we change the status of a contact to the + // first chosen one from options and initialize a timer + if (iNewStatus == ID_STATUS_OFFLINE) { + if (m_iTimeDiff1) { + iNewStatus = m_iStatus1; + pCache->m_timer1 = time(0); + } + } + // if a client returns back online, we clear timers not to play with statuses anymore + else pCache->m_timer1 = pCache->m_timer2 = 0; + + setWord(pCache->m_hContact, "Status", iNewStatus); + + Json2string(pCache->m_hContact, ev, "friendly", "Nick", true); + CheckAvatarChange(pCache->m_hContact, ev); +} + +void CIcqProto::ProcessSessionEnd(const JSONNode &/*ev*/) +{ + m_szRToken.Empty(); + m_iRClientId = 0; + delSetting(DB_KEY_RCLIENTID); + + ShutdownSession(); +} + +void CIcqProto::ProcessTyping(const JSONNode &ev) +{ + CMStringW aimId = ev["aimId"].as_mstring(); + CMStringW wszStatus = ev["typingStatus"].as_mstring(); + + IcqCacheItem *pCache = FindContactByUIN(aimId); + if (pCache) { + if (wszStatus == "typing") + CallService(MS_PROTO_CONTACTISTYPING, pCache->m_hContact, 60); + else + CallService(MS_PROTO_CONTACTISTYPING, pCache->m_hContact, PROTOTYPE_CONTACTTYPING_OFF); + } +} + +void CIcqProto::OnFetchEvents(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + if (root.error() != 200) { + ShutdownSession(); + return; + } + + JSONNode &data = root.data(); + m_fetchBaseURL = data["fetchBaseURL"].as_mstring(); + + for (auto &it : data["events"]) + ProcessEvent(it); +} + +void __cdecl CIcqProto::PollThread(void*) +{ + debugLogA("Polling thread started"); + m_bFirstBos = true; + + while (m_bOnline && !m_fetchBaseURL.IsEmpty()) { + CMStringA szUrl = m_fetchBaseURL; + if (m_bFirstBos) + szUrl.Append("&first=1"); + else + szUrl.Append("&timeout=25000"); + + auto *pReq = new AsyncHttpRequest(CONN_FETCH, REQUEST_GET, szUrl, &CIcqProto::OnFetchEvents); + if (!m_bFirstBos) + pReq->timeout = 62000; + + if (!ExecuteRequest(pReq)) { + ShutdownSession(); + break; + } + + m_bFirstBos = false; + } + + debugLogA("Polling thread ended"); +} diff --git a/protocols/ICQ-WIM/src/proto.cpp b/protocols/ICQ-WIM/src/proto.cpp index 84839ad923..5a86c896d8 100644 --- a/protocols/ICQ-WIM/src/proto.cpp +++ b/protocols/ICQ-WIM/src/proto.cpp @@ -1,678 +1,678 @@ -// ---------------------------------------------------------------------------80 -// ICQ plugin for Miranda Instant Messenger -// ________________________________________ -// -// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede -// Copyright © 2001-2002 Jon Keating, Richard Hughes -// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater -// Copyright © 2004-2010 Joe Kucera, George Hazan -// Copyright © 2012-2022 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- -// DESCRIPTION: -// -// Protocol Interface Implementation -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -#include "m_icolib.h" - -#pragma warning(disable:4355) - -static int CompareCache(const IcqCacheItem *p1, const IcqCacheItem *p2) -{ - return mir_wstrcmp(p1->m_aimid, p2->m_aimid); -} - -CIcqProto::CIcqProto(const char *aProtoName, const wchar_t *aUserName) : - PROTO(aProtoName, aUserName), - m_impl(*this), - m_arHttpQueue(10), - m_arOwnIds(1, PtrKeySortT), - m_arCache(20, &CompareCache), - m_arGroups(10, NumericKeySortT), - m_arMarkReadQueue(10, NumericKeySortT), - m_evRequestsQueue(CreateEvent(nullptr, FALSE, FALSE, nullptr)), - m_szOwnId(this, DB_KEY_ID), - m_iStatus1(this, "Status1", ID_STATUS_AWAY), - m_iStatus2(this, "Status2", ID_STATUS_NA), - m_iTimeDiff1(this, "TimeDiff1", 0), - m_iTimeDiff2(this, "TimeDiff2", 0), - m_bHideGroupchats(this, "HideChats", true), - m_bUseTrayIcon(this, "UseTrayIcon", false), - m_bErrorPopups(this, "ShowErrorPopups", true), - m_bLaunchMailbox(this, "LaunchMailbox", true) -{ - db_set_resident(m_szModuleName, DB_KEY_IDLE); - db_set_resident(m_szModuleName, DB_KEY_ONLINETS); - - m_isMra = !stricmp(Proto_GetAccount(m_szModuleName)->szProtoName, "MRA"); - - // services - CreateProtoService(PS_CREATEACCMGRUI, &CIcqProto::CreateAccMgrUI); - - CreateProtoService(PS_GETAVATARCAPS, &CIcqProto::GetAvatarCaps); - CreateProtoService(PS_GETAVATARINFO, &CIcqProto::GetAvatarInfo); - CreateProtoService(PS_GETMYAVATAR, &CIcqProto::GetAvatar); - CreateProtoService(PS_SETMYAVATAR, &CIcqProto::SetAvatar); - - CreateProtoService(PS_MENU_LOADHISTORY, &CIcqProto::OnMenuLoadHistory); - CreateProtoService(PS_GETUNREADEMAILCOUNT, &CIcqProto::GetEmailCount); - CreateProtoService(PS_GOTO_INBOX, &CIcqProto::GotoInbox); - - // events - HookProtoEvent(ME_CLIST_GROUPCHANGE, &CIcqProto::OnGroupChange); - HookProtoEvent(ME_DB_EVENT_MARKED_READ, &CIcqProto::OnDbEventRead); - HookProtoEvent(ME_GC_EVENT, &CIcqProto::GroupchatEventHook); - HookProtoEvent(ME_GC_BUILDMENU, &CIcqProto::GroupchatMenuHook); - HookProtoEvent(ME_OPT_INITIALISE, &CIcqProto::OnOptionsInit); - - // group chats - GCREGISTER gcr = {}; - gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR; - gcr.ptszDispName = m_tszUserName; - gcr.pszModule = m_szModuleName; - Chat_Register(&gcr); - - // netlib handle - NETLIBUSER nlu = {}; - nlu.szSettingsModule = m_szModuleName; - nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; - nlu.szDescriptiveName.w = m_tszUserName; - m_hNetlibUser = Netlib_RegisterUser(&nlu); - - // this was previously an old ICQ account - ptrW wszUin(GetUIN(0)); - if (wszUin != nullptr) { - delSetting("UIN"); - - m_szOwnId = wszUin; - - for (auto &it : AccContacts()) - delSetting(it, "e-mail"); - } - // this was previously an old MRA account - else { - CMStringW wszEmail(getMStringW("e-mail")); - if (!wszEmail.IsEmpty()) { - m_szOwnId = wszEmail; - delSetting("e-mail"); - } - } - - m_hWorkerThread = ForkThreadEx(&CIcqProto::ServerThread, nullptr, nullptr); -} - -CIcqProto::~CIcqProto() -{ - ::CloseHandle(m_evRequestsQueue); -} - -//////////////////////////////////////////////////////////////////////////////////////// -// OnModulesLoadedEx - performs hook registration - -void CIcqProto::OnModulesLoaded() -{ - InitContactCache(); - - HookProtoEvent(ME_USERINFO_INITIALISE, &CIcqProto::OnUserInfoInit); - - // load custom smilies - CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); - SMADD_CONT cont = { 2, m_szModuleName, wszPath }; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); -} - -void CIcqProto::OnShutdown() -{ - m_bTerminated = true; -} - -void CIcqProto::OnContactAdded(MCONTACT hContact) -{ - CMStringW wszId(getMStringW(hContact, DB_KEY_ID)); - if (!wszId.IsEmpty() && !FindContactByUIN(wszId)) { - mir_cslock l(m_csCache); - m_arCache.insert(new IcqCacheItem(wszId, hContact)); - } -} - -void CIcqProto::OnContactDeleted(MCONTACT hContact) -{ - CMStringW szId(GetUserId(hContact)); - if (!isChatRoom(hContact)) - m_arCache.remove(FindContactByUIN(szId)); - - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeBuddy") - << AIMSID(this) << WCHAR_PARAM("buddy", szId) << INT_PARAM("allGroups", 1)); -} - -void CIcqProto::OnEventEdited(MCONTACT, MEVENT) -{ - -} - -INT_PTR CIcqProto::OnMenuLoadHistory(WPARAM hContact, LPARAM) -{ - delSetting(hContact, DB_KEY_LASTMSGID); - - RetrieveUserHistory(hContact, 1, true); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnBuildProtoMenu() -{ - CMenuItem mi(&g_plugin); - mi.root = Menu_GetProtocolRoot(this); - mi.flags = CMIF_UNMOVABLE; - - // "Bookmarks..." - mi.pszService = "/UploadGroups"; - CreateProtoService(mi.pszService, &CIcqProto::UploadGroups); - mi.name.a = LPGEN("Synchronize server groups"); - mi.position = 200001; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_GROUP); - m_hUploadGroups = Menu_AddProtoMenuItem(&mi, m_szModuleName); - - mi.pszService = "/EditGroups"; - CreateProtoService(mi.pszService, &CIcqProto::EditGroups); - mi.name.a = LPGEN("Edit server groups"); - mi.position = 200002; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_GROUP); - Menu_AddProtoMenuItem(&mi, m_szModuleName); - - mi.pszService = "/EditProfile"; - CreateProtoService(mi.pszService, &CIcqProto::EditProfile); - mi.name.a = LPGEN("Edit my web profile"); - mi.position = 210001; - mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_MIRANDAWEB); - Menu_AddProtoMenuItem(&mi, m_szModuleName); - - Menu_ShowItem(m_hUploadGroups, false); -} - -INT_PTR CIcqProto::UploadGroups(WPARAM, LPARAM) -{ - for (auto &it : AccContacts()) { - if (isChatRoom(it)) - continue; - - ptrW wszIcqGroup(getWStringA(it, "IcqGroup")); - if (wszIcqGroup == nullptr) - continue; - - ptrW wszMirGroup(Clist_GetGroup(it)); - if (!wszMirGroup) - wszMirGroup = mir_wstrdup(L"General"); - if (mir_wstrcmp(wszIcqGroup, wszMirGroup)) - MoveContactToGroup(it, wszIcqGroup, wszMirGroup); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -class CGroupEditDlg : public CIcqDlgBase -{ - CCtrlListView groups; - -public: - - static CGroupEditDlg *pDlg; - - CGroupEditDlg(CIcqProto *ppro) : - CIcqDlgBase(ppro, IDD_EDITGROUPS), - groups(this, IDC_GROUPS) - { - groups.OnBuildMenu = Callback(this, &CGroupEditDlg::onMenu); - } - - void RefreshGroups() - { - for (auto &it : m_proto->m_arGroups.rev_iter()) - groups.AddItem(it->wszName, 0, (LPARAM)it); - } - - bool OnInitDialog() override - { - pDlg = this; - groups.AddColumn(0, TranslateT("Name"), 300); - RefreshGroups(); - return true; - } - - void OnDestroy() override - { - pDlg = nullptr; - } - - void onMenu(void *) - { - int cur = groups.GetSelectionMark(); - if (cur == -1) - return; - - IcqGroup *pGroup = (IcqGroup *)groups.GetItemData(cur); - - HMENU hMenu = CreatePopupMenu(); - AppendMenu(hMenu, MF_STRING, 1, TranslateT("Rename")); - AppendMenu(hMenu, MF_STRING, 2, TranslateT("Delete")); - - POINT pt; - GetCursorPos(&pt); - int cmd = TrackPopupMenu(hMenu, TPM_RIGHTBUTTON | TPM_RETURNCMD, pt.x, pt.y, 0, m_hwnd, nullptr); - DestroyMenu(hMenu); - - if (cmd == 1) { // rename - ENTER_STRING es = {}; - es.szModuleName = m_proto->m_szModuleName; - es.caption = TranslateT("Enter new group name"); - if (!EnterString(&es)) - return; - - m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/renameGroup") - << AIMSID(m_proto) << WCHAR_PARAM("oldGroup", pGroup->wszSrvName) << GROUP_PARAM("newGroup", es.ptszResult)); - - mir_free(es.ptszResult); - } - else if (cmd == 2) { // delete - m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeGroup") - << AIMSID(m_proto) << WCHAR_PARAM("group", pGroup->wszSrvName)); - } - } -}; - -CGroupEditDlg *CGroupEditDlg::pDlg = nullptr; - -INT_PTR CIcqProto::EditGroups(WPARAM, LPARAM) -{ - (new CGroupEditDlg(this))->Show(); - return 0; -} - -INT_PTR CIcqProto::EditProfile(WPARAM, LPARAM) -{ - if (mir_wstrlen(m_szOwnId)) - Utils_OpenUrlW(CMStringW(FORMAT, L"https://icq.com/people/%s/edit/", (wchar_t*)m_szOwnId)); - return 0; -} - -void RefreshGroups(void) -{ - if (CGroupEditDlg::pDlg != nullptr) - CGroupEditDlg::pDlg->RefreshGroups(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR CIcqProto::GetEmailCount(WPARAM, LPARAM) -{ - if (!m_bOnline) - return 0; - return m_unreadEmails; -} - -INT_PTR CIcqProto::GotoInbox(WPARAM, LPARAM) -{ - Utils_OpenUrl("https://e.mail.ru/messages/inbox"); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::SendMarkRead() -{ - mir_cslock lck(m_csMarkReadQueue); - while (m_arMarkReadQueue.getCount()) { - IcqCacheItem *pUser = m_arMarkReadQueue[0]; - - auto *pReq = new AsyncRapiRequest(this, "setDlgStateWim"); - pReq->params << WCHAR_PARAM("sn", GetUserId(pUser->m_hContact)) << INT64_PARAM("lastRead", getId(pUser->m_hContact, DB_KEY_LASTMSGID)); - Push(pReq); - - m_arMarkReadQueue.remove(0); - } -} - -int CIcqProto::OnDbEventRead(WPARAM, LPARAM hDbEvent) -{ - MCONTACT hContact = db_event_getContact(hDbEvent); - if (!hContact) - return 0; - - // filter out only events of my protocol - const char *szProto = Proto_GetBaseAccountName(hContact); - if (mir_strcmp(szProto, m_szModuleName)) - return 0; - - MarkAsRead(hContact); - return 0; -} - -int CIcqProto::OnGroupChange(WPARAM hContact, LPARAM lParam) -{ - if (!m_bOnline) - return 0; - - CLISTGROUPCHANGE *pParam = (CLISTGROUPCHANGE*)lParam; - if (hContact == 0) { // whole group is changed - if (pParam->pszOldName == nullptr) { - for (auto &it : m_arGroups) - if (it->wszName == pParam->pszNewName) - return 0; - - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/addGroup") - << AIMSID(this) << GROUP_PARAM("group", pParam->pszNewName)); - } - else if (pParam->pszNewName == nullptr) { - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeGroup") - << AIMSID(this) << GROUP_PARAM("group", pParam->pszOldName)); - } - else { - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/renameGroup") - << AIMSID(this) << GROUP_PARAM("oldGroup", pParam->pszOldName) << GROUP_PARAM("newGroup", pParam->pszNewName)); - } - } - else MoveContactToGroup(hContact, ptrW(getWStringA(hContact, "IcqGroup")), pParam->pszNewName); - - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PS_AddToList - adds a contact to the contact list - -MCONTACT CIcqProto::AddToList(int, PROTOSEARCHRESULT *psr) -{ - if (mir_wstrlen(psr->id.w) == 0) - return 0; - - MCONTACT hContact = CreateContact(psr->id.w, true); - if (psr->nick.w) - setWString(hContact, "Nick", psr->nick.w); - if (psr->firstName.w) - setWString(hContact, "FirstName", psr->firstName.w); - if (psr->lastName.w) - setWString(hContact, "LastName", psr->lastName.w); - - return hContact; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PSR_AUTH - -int CIcqProto::AuthRecv(MCONTACT, PROTORECVEVENT *pre) -{ - return Proto_AuthRecv(m_szModuleName, pre); -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PSS_AUTHREQUEST - -int CIcqProto::AuthRequest(MCONTACT hContact, const wchar_t* szMessage) -{ - ptrW wszGroup(Clist_GetGroup(hContact)); - if (!wszGroup) - wszGroup = mir_wstrdup(L"General"); - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/buddylist/addBuddy", &CIcqProto::OnAddBuddy); - pReq << AIMSID(this) << WCHAR_PARAM("authorizationMsg", szMessage) << WCHAR_PARAM("buddy", GetUserId(hContact)) << WCHAR_PARAM("group", wszGroup) << INT_PARAM("preAuthorized", 1); - pReq->hContact = hContact; - Push(pReq); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// File operations - -HANDLE CIcqProto::FileAllow(MCONTACT, HANDLE hTransfer, const wchar_t *pwszSavePath) -{ - if (!m_bOnline) - return nullptr; - - auto *ft = (IcqFileTransfer *)hTransfer; - ft->m_wszFileName.Insert(0, pwszSavePath); - ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, ft->m_szHost, &CIcqProto::OnFileRecv); - pReq->pUserInfo = ft; - pReq->AddHeader("Sec-Fetch-User", "?1"); - pReq->AddHeader("Sec-Fetch-Site", "cross-site"); - pReq->AddHeader("Sec-Fetch-Mode", "navigate"); - Push(pReq); - - return hTransfer; -} - -int CIcqProto::FileCancel(MCONTACT hContact, HANDLE hTransfer) -{ - ProtoBroadcastAck(hContact, ACKTYPE_FILE, ACKRESULT_FAILED, hTransfer); - - auto *ft = (IcqFileTransfer *)hTransfer; - if (ft->pfts.currentFileTime != 0) - ft->m_bCanceled = true; - else - delete ft; - return 0; -} - -int CIcqProto::FileResume(HANDLE hTransfer, int, const wchar_t *szFilename) -{ - auto *ft = (IcqFileTransfer *)hTransfer; - if (!m_bOnline || ft == nullptr) - return 1; - - if (szFilename != nullptr) { - ft->m_wszFileName = szFilename; - ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); - } - - ::SetEvent(ft->hWaitEvent); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// GetCaps - return protocol capabilities bits - -INT_PTR CIcqProto::GetCaps(int type, MCONTACT) -{ - INT_PTR nReturn = 0; - - switch (type) { - case PFLAGNUM_1: - nReturn = PF1_IM | PF1_AUTHREQ | PF1_BASICSEARCH | PF1_ADDSEARCHRES | /*PF1_SEARCHBYNAME | TODO */ - PF1_VISLIST | PF1_FILE | PF1_CONTACT | PF1_SERVERCLIST; - break; - - case PFLAGNUM_2: - return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; - - case PFLAGNUM_3: - return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; - - case PFLAGNUM_5: - return PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; - - case PFLAGNUM_4: - nReturn = PF4_FORCEAUTH | PF4_SUPPORTIDLE | PF4_OFFLINEFILES | PF4_IMSENDOFFLINE | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_SERVERMSGID | PF4_READNOTIFY; - break; - - case PFLAG_UNIQUEIDTEXT: - return (INT_PTR)TranslateT("UIN/e-mail/phone"); - } - - return nReturn; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// GetInfo - retrieves a contact info - -int CIcqProto::GetInfo(MCONTACT hContact, int) -{ - RetrieveUserInfo(hContact); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// SearchBasic - searches the contact by UID - -HANDLE CIcqProto::SearchBasic(const wchar_t *pszSearch) -{ - if (!m_bOnline) - return nullptr; - - auto *pReq = new AsyncRapiRequest(this, "search", &CIcqProto::OnSearchResults); - pReq->params << WCHAR_PARAM(*pszSearch == '+' ? "phonenum" : "keyword", pszSearch); - Push(pReq); - - return pReq; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// SendFile - sends a file - -HANDLE CIcqProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) -{ - // we can't send more than one file at a time - if (ppszFiles[1] != 0) - return nullptr; - - struct _stat statbuf; - if (_wstat(ppszFiles[0], &statbuf)) { - debugLogW(L"'%s' is an invalid filename", ppszFiles[0]); - return nullptr; - } - - int iFileId = _wopen(ppszFiles[0], _O_RDONLY | _O_BINARY, _S_IREAD); - if (iFileId < 0) - return nullptr; - - auto *pTransfer = new IcqFileTransfer(hContact, ppszFiles[0]); - pTransfer->pfts.totalFiles = 1; - pTransfer->pfts.currentFileSize = pTransfer->pfts.totalBytes = statbuf.st_size; - pTransfer->m_fileId = iFileId; - if (mir_wstrlen(szDescription)) - pTransfer->m_wszDescr = szDescription; - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, "https://files.icq.com/files/init", &CIcqProto::OnFileInit); - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("f", "json") << WCHAR_PARAM("filename", pTransfer->m_wszShortName) - << CHAR_PARAM("k", appId()) << INT_PARAM("size", statbuf.st_size) << INT_PARAM("ts", TS()); - CalcHash(pReq); - pReq->pUserInfo = pTransfer; - Push(pReq); - - return pTransfer; // Failure -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PS_SendMessage - sends a message - -int CIcqProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) -{ - CMStringA szUserid(GetUserId(hContact)); - if (szUserid.IsEmpty()) - return 0; - - int id = InterlockedIncrement(&m_msgId); - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/im/sendIM", &CIcqProto::OnSendMessage); - - auto *pOwn = new IcqOwnMessage(hContact, id, pReq->m_reqId); - pReq->pUserInfo = pOwn; - { - mir_cslock lck(m_csOwnIds); - m_arOwnIds.insert(pOwn); - } - - pReq << AIMSID(this) << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("mentions", "") - << CHAR_PARAM("message", pszSrc) << CHAR_PARAM("offlineIM", "true") << CHAR_PARAM("t", szUserid) << INT_PARAM("ts", TS()); - Push(pReq); - return id; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PS_SetStatus - sets the protocol status - -int CIcqProto::SetStatus(int iNewStatus) -{ - debugLogA("CIcqProto::SetStatus iNewStatus = %d, m_iStatus = %d, m_iDesiredStatus = %d m_hWorkerThread = %p", iNewStatus, m_iStatus, m_iDesiredStatus, m_hWorkerThread); - - if (iNewStatus == m_iStatus) - return 0; - - m_iDesiredStatus = iNewStatus; - int iOldStatus = m_iStatus; - - // go offline - if (iNewStatus == ID_STATUS_OFFLINE) { - if (m_bOnline) - SetServerStatus(ID_STATUS_OFFLINE); - - m_iStatus = m_iDesiredStatus; - setAllContactStatuses(ID_STATUS_OFFLINE, false); - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - } - // not logged in? come on - else if (!m_bOnline && !IsStatusConnecting(m_iStatus)) { - m_iStatus = ID_STATUS_CONNECTING; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); - - if (mir_wstrlen(m_szOwnId) == 0) { - debugLogA("Thread ended, UIN/password are not configured"); - ConnectionFailed(LOGINERR_BADUSERID); - return 0; - } - - if (!RetrievePassword()) { - debugLogA("Thread ended, password is not configured"); - ConnectionFailed(LOGINERR_BADUSERID); - return 0; - } - - CheckPassword(); - } - else if (m_bOnline) { - debugLogA("setting server online status to %d", iNewStatus); - SetServerStatus(iNewStatus); - } - - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// PS_UserIsTyping - sends a UTN notification - -int CIcqProto::UserIsTyping(MCONTACT hContact, int type) -{ - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/im/setTyping") - << AIMSID(this) << WCHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("typingStatus", (type == PROTOTYPE_SELFTYPING_ON) ? "typing" : "typed")); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////////////// -// PS_SetApparentMode - sets the visibility status - -int CIcqProto::SetApparentMode(MCONTACT hContact, int iMode) -{ - int oldMode = getWord(hContact, "ApparentMode"); - if (oldMode != iMode) { - setWord(hContact, "ApparentMode", iMode); - SetPermitDeny(GetUserId(hContact), iMode != ID_STATUS_OFFLINE); - } - return 0; -} +// ---------------------------------------------------------------------------80 +// ICQ plugin for Miranda Instant Messenger +// ________________________________________ +// +// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede +// Copyright © 2001-2002 Jon Keating, Richard Hughes +// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater +// Copyright © 2004-2010 Joe Kucera, George Hazan +// Copyright © 2012-2023 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- +// DESCRIPTION: +// +// Protocol Interface Implementation +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +#include "m_icolib.h" + +#pragma warning(disable:4355) + +static int CompareCache(const IcqCacheItem *p1, const IcqCacheItem *p2) +{ + return mir_wstrcmp(p1->m_aimid, p2->m_aimid); +} + +CIcqProto::CIcqProto(const char *aProtoName, const wchar_t *aUserName) : + PROTO(aProtoName, aUserName), + m_impl(*this), + m_arHttpQueue(10), + m_arOwnIds(1, PtrKeySortT), + m_arCache(20, &CompareCache), + m_arGroups(10, NumericKeySortT), + m_arMarkReadQueue(10, NumericKeySortT), + m_evRequestsQueue(CreateEvent(nullptr, FALSE, FALSE, nullptr)), + m_szOwnId(this, DB_KEY_ID), + m_iStatus1(this, "Status1", ID_STATUS_AWAY), + m_iStatus2(this, "Status2", ID_STATUS_NA), + m_iTimeDiff1(this, "TimeDiff1", 0), + m_iTimeDiff2(this, "TimeDiff2", 0), + m_bHideGroupchats(this, "HideChats", true), + m_bUseTrayIcon(this, "UseTrayIcon", false), + m_bErrorPopups(this, "ShowErrorPopups", true), + m_bLaunchMailbox(this, "LaunchMailbox", true) +{ + db_set_resident(m_szModuleName, DB_KEY_IDLE); + db_set_resident(m_szModuleName, DB_KEY_ONLINETS); + + m_isMra = !stricmp(Proto_GetAccount(m_szModuleName)->szProtoName, "MRA"); + + // services + CreateProtoService(PS_CREATEACCMGRUI, &CIcqProto::CreateAccMgrUI); + + CreateProtoService(PS_GETAVATARCAPS, &CIcqProto::GetAvatarCaps); + CreateProtoService(PS_GETAVATARINFO, &CIcqProto::GetAvatarInfo); + CreateProtoService(PS_GETMYAVATAR, &CIcqProto::GetAvatar); + CreateProtoService(PS_SETMYAVATAR, &CIcqProto::SetAvatar); + + CreateProtoService(PS_MENU_LOADHISTORY, &CIcqProto::OnMenuLoadHistory); + CreateProtoService(PS_GETUNREADEMAILCOUNT, &CIcqProto::GetEmailCount); + CreateProtoService(PS_GOTO_INBOX, &CIcqProto::GotoInbox); + + // events + HookProtoEvent(ME_CLIST_GROUPCHANGE, &CIcqProto::OnGroupChange); + HookProtoEvent(ME_DB_EVENT_MARKED_READ, &CIcqProto::OnDbEventRead); + HookProtoEvent(ME_GC_EVENT, &CIcqProto::GroupchatEventHook); + HookProtoEvent(ME_GC_BUILDMENU, &CIcqProto::GroupchatMenuHook); + HookProtoEvent(ME_OPT_INITIALISE, &CIcqProto::OnOptionsInit); + + // group chats + GCREGISTER gcr = {}; + gcr.dwFlags = GC_TYPNOTIF | GC_CHANMGR; + gcr.ptszDispName = m_tszUserName; + gcr.pszModule = m_szModuleName; + Chat_Register(&gcr); + + // netlib handle + NETLIBUSER nlu = {}; + nlu.szSettingsModule = m_szModuleName; + nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szDescriptiveName.w = m_tszUserName; + m_hNetlibUser = Netlib_RegisterUser(&nlu); + + // this was previously an old ICQ account + ptrW wszUin(GetUIN(0)); + if (wszUin != nullptr) { + delSetting("UIN"); + + m_szOwnId = wszUin; + + for (auto &it : AccContacts()) + delSetting(it, "e-mail"); + } + // this was previously an old MRA account + else { + CMStringW wszEmail(getMStringW("e-mail")); + if (!wszEmail.IsEmpty()) { + m_szOwnId = wszEmail; + delSetting("e-mail"); + } + } + + m_hWorkerThread = ForkThreadEx(&CIcqProto::ServerThread, nullptr, nullptr); +} + +CIcqProto::~CIcqProto() +{ + ::CloseHandle(m_evRequestsQueue); +} + +//////////////////////////////////////////////////////////////////////////////////////// +// OnModulesLoadedEx - performs hook registration + +void CIcqProto::OnModulesLoaded() +{ + InitContactCache(); + + HookProtoEvent(ME_USERINFO_INITIALISE, &CIcqProto::OnUserInfoInit); + + // load custom smilies + CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers\\*.png", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); + SMADD_CONT cont = { 2, m_szModuleName, wszPath }; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); +} + +void CIcqProto::OnShutdown() +{ + m_bTerminated = true; +} + +void CIcqProto::OnContactAdded(MCONTACT hContact) +{ + CMStringW wszId(getMStringW(hContact, DB_KEY_ID)); + if (!wszId.IsEmpty() && !FindContactByUIN(wszId)) { + mir_cslock l(m_csCache); + m_arCache.insert(new IcqCacheItem(wszId, hContact)); + } +} + +void CIcqProto::OnContactDeleted(MCONTACT hContact) +{ + CMStringW szId(GetUserId(hContact)); + if (!isChatRoom(hContact)) + m_arCache.remove(FindContactByUIN(szId)); + + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeBuddy") + << AIMSID(this) << WCHAR_PARAM("buddy", szId) << INT_PARAM("allGroups", 1)); +} + +void CIcqProto::OnEventEdited(MCONTACT, MEVENT) +{ + +} + +INT_PTR CIcqProto::OnMenuLoadHistory(WPARAM hContact, LPARAM) +{ + delSetting(hContact, DB_KEY_LASTMSGID); + + RetrieveUserHistory(hContact, 1, true); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnBuildProtoMenu() +{ + CMenuItem mi(&g_plugin); + mi.root = Menu_GetProtocolRoot(this); + mi.flags = CMIF_UNMOVABLE; + + // "Bookmarks..." + mi.pszService = "/UploadGroups"; + CreateProtoService(mi.pszService, &CIcqProto::UploadGroups); + mi.name.a = LPGEN("Synchronize server groups"); + mi.position = 200001; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_GROUP); + m_hUploadGroups = Menu_AddProtoMenuItem(&mi, m_szModuleName); + + mi.pszService = "/EditGroups"; + CreateProtoService(mi.pszService, &CIcqProto::EditGroups); + mi.name.a = LPGEN("Edit server groups"); + mi.position = 200002; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_GROUP); + Menu_AddProtoMenuItem(&mi, m_szModuleName); + + mi.pszService = "/EditProfile"; + CreateProtoService(mi.pszService, &CIcqProto::EditProfile); + mi.name.a = LPGEN("Edit my web profile"); + mi.position = 210001; + mi.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_MIRANDAWEB); + Menu_AddProtoMenuItem(&mi, m_szModuleName); + + Menu_ShowItem(m_hUploadGroups, false); +} + +INT_PTR CIcqProto::UploadGroups(WPARAM, LPARAM) +{ + for (auto &it : AccContacts()) { + if (isChatRoom(it)) + continue; + + ptrW wszIcqGroup(getWStringA(it, "IcqGroup")); + if (wszIcqGroup == nullptr) + continue; + + ptrW wszMirGroup(Clist_GetGroup(it)); + if (!wszMirGroup) + wszMirGroup = mir_wstrdup(L"General"); + if (mir_wstrcmp(wszIcqGroup, wszMirGroup)) + MoveContactToGroup(it, wszIcqGroup, wszMirGroup); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +class CGroupEditDlg : public CIcqDlgBase +{ + CCtrlListView groups; + +public: + + static CGroupEditDlg *pDlg; + + CGroupEditDlg(CIcqProto *ppro) : + CIcqDlgBase(ppro, IDD_EDITGROUPS), + groups(this, IDC_GROUPS) + { + groups.OnBuildMenu = Callback(this, &CGroupEditDlg::onMenu); + } + + void RefreshGroups() + { + for (auto &it : m_proto->m_arGroups.rev_iter()) + groups.AddItem(it->wszName, 0, (LPARAM)it); + } + + bool OnInitDialog() override + { + pDlg = this; + groups.AddColumn(0, TranslateT("Name"), 300); + RefreshGroups(); + return true; + } + + void OnDestroy() override + { + pDlg = nullptr; + } + + void onMenu(void *) + { + int cur = groups.GetSelectionMark(); + if (cur == -1) + return; + + IcqGroup *pGroup = (IcqGroup *)groups.GetItemData(cur); + + HMENU hMenu = CreatePopupMenu(); + AppendMenu(hMenu, MF_STRING, 1, TranslateT("Rename")); + AppendMenu(hMenu, MF_STRING, 2, TranslateT("Delete")); + + POINT pt; + GetCursorPos(&pt); + int cmd = TrackPopupMenu(hMenu, TPM_RIGHTBUTTON | TPM_RETURNCMD, pt.x, pt.y, 0, m_hwnd, nullptr); + DestroyMenu(hMenu); + + if (cmd == 1) { // rename + ENTER_STRING es = {}; + es.szModuleName = m_proto->m_szModuleName; + es.caption = TranslateT("Enter new group name"); + if (!EnterString(&es)) + return; + + m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/renameGroup") + << AIMSID(m_proto) << WCHAR_PARAM("oldGroup", pGroup->wszSrvName) << GROUP_PARAM("newGroup", es.ptszResult)); + + mir_free(es.ptszResult); + } + else if (cmd == 2) { // delete + m_proto->Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeGroup") + << AIMSID(m_proto) << WCHAR_PARAM("group", pGroup->wszSrvName)); + } + } +}; + +CGroupEditDlg *CGroupEditDlg::pDlg = nullptr; + +INT_PTR CIcqProto::EditGroups(WPARAM, LPARAM) +{ + (new CGroupEditDlg(this))->Show(); + return 0; +} + +INT_PTR CIcqProto::EditProfile(WPARAM, LPARAM) +{ + if (mir_wstrlen(m_szOwnId)) + Utils_OpenUrlW(CMStringW(FORMAT, L"https://icq.com/people/%s/edit/", (wchar_t*)m_szOwnId)); + return 0; +} + +void RefreshGroups(void) +{ + if (CGroupEditDlg::pDlg != nullptr) + CGroupEditDlg::pDlg->RefreshGroups(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR CIcqProto::GetEmailCount(WPARAM, LPARAM) +{ + if (!m_bOnline) + return 0; + return m_unreadEmails; +} + +INT_PTR CIcqProto::GotoInbox(WPARAM, LPARAM) +{ + Utils_OpenUrl("https://e.mail.ru/messages/inbox"); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::SendMarkRead() +{ + mir_cslock lck(m_csMarkReadQueue); + while (m_arMarkReadQueue.getCount()) { + IcqCacheItem *pUser = m_arMarkReadQueue[0]; + + auto *pReq = new AsyncRapiRequest(this, "setDlgStateWim"); + pReq->params << WCHAR_PARAM("sn", GetUserId(pUser->m_hContact)) << INT64_PARAM("lastRead", getId(pUser->m_hContact, DB_KEY_LASTMSGID)); + Push(pReq); + + m_arMarkReadQueue.remove(0); + } +} + +int CIcqProto::OnDbEventRead(WPARAM, LPARAM hDbEvent) +{ + MCONTACT hContact = db_event_getContact(hDbEvent); + if (!hContact) + return 0; + + // filter out only events of my protocol + const char *szProto = Proto_GetBaseAccountName(hContact); + if (mir_strcmp(szProto, m_szModuleName)) + return 0; + + MarkAsRead(hContact); + return 0; +} + +int CIcqProto::OnGroupChange(WPARAM hContact, LPARAM lParam) +{ + if (!m_bOnline) + return 0; + + CLISTGROUPCHANGE *pParam = (CLISTGROUPCHANGE*)lParam; + if (hContact == 0) { // whole group is changed + if (pParam->pszOldName == nullptr) { + for (auto &it : m_arGroups) + if (it->wszName == pParam->pszNewName) + return 0; + + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/addGroup") + << AIMSID(this) << GROUP_PARAM("group", pParam->pszNewName)); + } + else if (pParam->pszNewName == nullptr) { + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/removeGroup") + << AIMSID(this) << GROUP_PARAM("group", pParam->pszOldName)); + } + else { + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/renameGroup") + << AIMSID(this) << GROUP_PARAM("oldGroup", pParam->pszOldName) << GROUP_PARAM("newGroup", pParam->pszNewName)); + } + } + else MoveContactToGroup(hContact, ptrW(getWStringA(hContact, "IcqGroup")), pParam->pszNewName); + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PS_AddToList - adds a contact to the contact list + +MCONTACT CIcqProto::AddToList(int, PROTOSEARCHRESULT *psr) +{ + if (mir_wstrlen(psr->id.w) == 0) + return 0; + + MCONTACT hContact = CreateContact(psr->id.w, true); + if (psr->nick.w) + setWString(hContact, "Nick", psr->nick.w); + if (psr->firstName.w) + setWString(hContact, "FirstName", psr->firstName.w); + if (psr->lastName.w) + setWString(hContact, "LastName", psr->lastName.w); + + return hContact; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PSR_AUTH + +int CIcqProto::AuthRecv(MCONTACT, PROTORECVEVENT *pre) +{ + return Proto_AuthRecv(m_szModuleName, pre); +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PSS_AUTHREQUEST + +int CIcqProto::AuthRequest(MCONTACT hContact, const wchar_t* szMessage) +{ + ptrW wszGroup(Clist_GetGroup(hContact)); + if (!wszGroup) + wszGroup = mir_wstrdup(L"General"); + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/buddylist/addBuddy", &CIcqProto::OnAddBuddy); + pReq << AIMSID(this) << WCHAR_PARAM("authorizationMsg", szMessage) << WCHAR_PARAM("buddy", GetUserId(hContact)) << WCHAR_PARAM("group", wszGroup) << INT_PARAM("preAuthorized", 1); + pReq->hContact = hContact; + Push(pReq); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// File operations + +HANDLE CIcqProto::FileAllow(MCONTACT, HANDLE hTransfer, const wchar_t *pwszSavePath) +{ + if (!m_bOnline) + return nullptr; + + auto *ft = (IcqFileTransfer *)hTransfer; + ft->m_wszFileName.Insert(0, pwszSavePath); + ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, ft->m_szHost, &CIcqProto::OnFileRecv); + pReq->pUserInfo = ft; + pReq->AddHeader("Sec-Fetch-User", "?1"); + pReq->AddHeader("Sec-Fetch-Site", "cross-site"); + pReq->AddHeader("Sec-Fetch-Mode", "navigate"); + Push(pReq); + + return hTransfer; +} + +int CIcqProto::FileCancel(MCONTACT hContact, HANDLE hTransfer) +{ + ProtoBroadcastAck(hContact, ACKTYPE_FILE, ACKRESULT_FAILED, hTransfer); + + auto *ft = (IcqFileTransfer *)hTransfer; + if (ft->pfts.currentFileTime != 0) + ft->m_bCanceled = true; + else + delete ft; + return 0; +} + +int CIcqProto::FileResume(HANDLE hTransfer, int, const wchar_t *szFilename) +{ + auto *ft = (IcqFileTransfer *)hTransfer; + if (!m_bOnline || ft == nullptr) + return 1; + + if (szFilename != nullptr) { + ft->m_wszFileName = szFilename; + ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); + } + + ::SetEvent(ft->hWaitEvent); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// GetCaps - return protocol capabilities bits + +INT_PTR CIcqProto::GetCaps(int type, MCONTACT) +{ + INT_PTR nReturn = 0; + + switch (type) { + case PFLAGNUM_1: + nReturn = PF1_IM | PF1_AUTHREQ | PF1_BASICSEARCH | PF1_ADDSEARCHRES | /*PF1_SEARCHBYNAME | TODO */ + PF1_VISLIST | PF1_FILE | PF1_CONTACT | PF1_SERVERCLIST; + break; + + case PFLAGNUM_2: + return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; + + case PFLAGNUM_3: + return PF2_ONLINE | PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; + + case PFLAGNUM_5: + return PF2_SHORTAWAY | PF2_LONGAWAY | PF2_LIGHTDND | PF2_HEAVYDND | PF2_INVISIBLE; + + case PFLAGNUM_4: + nReturn = PF4_FORCEAUTH | PF4_SUPPORTIDLE | PF4_OFFLINEFILES | PF4_IMSENDOFFLINE | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_SERVERMSGID | PF4_READNOTIFY; + break; + + case PFLAG_UNIQUEIDTEXT: + return (INT_PTR)TranslateT("UIN/e-mail/phone"); + } + + return nReturn; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// GetInfo - retrieves a contact info + +int CIcqProto::GetInfo(MCONTACT hContact, int) +{ + RetrieveUserInfo(hContact); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// SearchBasic - searches the contact by UID + +HANDLE CIcqProto::SearchBasic(const wchar_t *pszSearch) +{ + if (!m_bOnline) + return nullptr; + + auto *pReq = new AsyncRapiRequest(this, "search", &CIcqProto::OnSearchResults); + pReq->params << WCHAR_PARAM(*pszSearch == '+' ? "phonenum" : "keyword", pszSearch); + Push(pReq); + + return pReq; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// SendFile - sends a file + +HANDLE CIcqProto::SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) +{ + // we can't send more than one file at a time + if (ppszFiles[1] != 0) + return nullptr; + + struct _stat statbuf; + if (_wstat(ppszFiles[0], &statbuf)) { + debugLogW(L"'%s' is an invalid filename", ppszFiles[0]); + return nullptr; + } + + int iFileId = _wopen(ppszFiles[0], _O_RDONLY | _O_BINARY, _S_IREAD); + if (iFileId < 0) + return nullptr; + + auto *pTransfer = new IcqFileTransfer(hContact, ppszFiles[0]); + pTransfer->pfts.totalFiles = 1; + pTransfer->pfts.currentFileSize = pTransfer->pfts.totalBytes = statbuf.st_size; + pTransfer->m_fileId = iFileId; + if (mir_wstrlen(szDescription)) + pTransfer->m_wszDescr = szDescription; + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, "https://files.icq.com/files/init", &CIcqProto::OnFileInit); + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("f", "json") << WCHAR_PARAM("filename", pTransfer->m_wszShortName) + << CHAR_PARAM("k", appId()) << INT_PARAM("size", statbuf.st_size) << INT_PARAM("ts", TS()); + CalcHash(pReq); + pReq->pUserInfo = pTransfer; + Push(pReq); + + return pTransfer; // Failure +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PS_SendMessage - sends a message + +int CIcqProto::SendMsg(MCONTACT hContact, int, const char *pszSrc) +{ + CMStringA szUserid(GetUserId(hContact)); + if (szUserid.IsEmpty()) + return 0; + + int id = InterlockedIncrement(&m_msgId); + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/im/sendIM", &CIcqProto::OnSendMessage); + + auto *pOwn = new IcqOwnMessage(hContact, id, pReq->m_reqId); + pReq->pUserInfo = pOwn; + { + mir_cslock lck(m_csOwnIds); + m_arOwnIds.insert(pOwn); + } + + pReq << AIMSID(this) << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("mentions", "") + << CHAR_PARAM("message", pszSrc) << CHAR_PARAM("offlineIM", "true") << CHAR_PARAM("t", szUserid) << INT_PARAM("ts", TS()); + Push(pReq); + return id; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PS_SetStatus - sets the protocol status + +int CIcqProto::SetStatus(int iNewStatus) +{ + debugLogA("CIcqProto::SetStatus iNewStatus = %d, m_iStatus = %d, m_iDesiredStatus = %d m_hWorkerThread = %p", iNewStatus, m_iStatus, m_iDesiredStatus, m_hWorkerThread); + + if (iNewStatus == m_iStatus) + return 0; + + m_iDesiredStatus = iNewStatus; + int iOldStatus = m_iStatus; + + // go offline + if (iNewStatus == ID_STATUS_OFFLINE) { + if (m_bOnline) + SetServerStatus(ID_STATUS_OFFLINE); + + m_iStatus = m_iDesiredStatus; + setAllContactStatuses(ID_STATUS_OFFLINE, false); + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + } + // not logged in? come on + else if (!m_bOnline && !IsStatusConnecting(m_iStatus)) { + m_iStatus = ID_STATUS_CONNECTING; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); + + if (mir_wstrlen(m_szOwnId) == 0) { + debugLogA("Thread ended, UIN/password are not configured"); + ConnectionFailed(LOGINERR_BADUSERID); + return 0; + } + + if (!RetrievePassword()) { + debugLogA("Thread ended, password is not configured"); + ConnectionFailed(LOGINERR_BADUSERID); + return 0; + } + + CheckPassword(); + } + else if (m_bOnline) { + debugLogA("setting server online status to %d", iNewStatus); + SetServerStatus(iNewStatus); + } + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// PS_UserIsTyping - sends a UTN notification + +int CIcqProto::UserIsTyping(MCONTACT hContact, int type) +{ + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/im/setTyping") + << AIMSID(this) << WCHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("typingStatus", (type == PROTOTYPE_SELFTYPING_ON) ? "typing" : "typed")); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// PS_SetApparentMode - sets the visibility status + +int CIcqProto::SetApparentMode(MCONTACT hContact, int iMode) +{ + int oldMode = getWord(hContact, "ApparentMode"); + if (oldMode != iMode) { + setWord(hContact, "ApparentMode", iMode); + SetPermitDeny(GetUserId(hContact), iMode != ID_STATUS_OFFLINE); + } + return 0; +} diff --git a/protocols/ICQ-WIM/src/proto.h b/protocols/ICQ-WIM/src/proto.h index b909a0d74a..20636cc4c6 100644 --- a/protocols/ICQ-WIM/src/proto.h +++ b/protocols/ICQ-WIM/src/proto.h @@ -1,494 +1,494 @@ -// ---------------------------------------------------------------------------80 -// ICQ plugin for Miranda Instant Messenger -// ________________________________________ -// -// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede -// Copyright © 2001-2002 Jon Keating, Richard Hughes -// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater -// Copyright © 2004-2010 Joe Kucera, George Hazan -// Copyright © 2012-2022 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- -// DESCRIPTION: -// -// Protocol Interface declarations -// ----------------------------------------------------------------------------- - -#ifndef _ICQ_PROTO_H_ -#define _ICQ_PROTO_H_ - -#include "m_system.h" -#include "m_protoint.h" - -#define MRA_APP_ID "ic1pzYNtEU6dDnEQ" -#define ICQ_APP_ID "ic1nmMjqg7Yu-0hL" -#define ICQ_API_SERVER "https://u.icq.net/wim" -#define ICQ_FILE_SERVER "https://u.icq.net/files/api/v1.1" -#define ICQ_FAKE_EVENT_ID 0xBABAEB -#define ICQ_ROBUST_SERVER "https://u.icq.net/rapi" - -#define PS_DUMMY "/DoNothing" -#define PS_GOTO_INBOX "/GotoInbox" - -#define WIM_CAP_VOIP_VOICE "094613504c7f11d18222444553540000" -#define WIM_CAP_VOIP_VIDEO "094613514c7f11d18222444553540000" -#define WIM_CAP_FILETRANSFER "094613434c7f11d18222444553540000" -#define WIM_CAP_UNIQ_REQ_ID "094613534c7f11d18222444553540000" -#define WIM_CAP_EMOJI "094613544c7f11d18222444553540000" -#define WIM_CAP_MENTIONS "0946135b4c7f11d18222444553540000" -#define WIM_CAP_MAIL_NOTIFICATIONS "094613594c7f11d18222444553540000" -#define WIM_CAP_INTRO_DLG_STATE "0946135a4c7f11d18222444553540000" - -#define NG_CAP_SECUREIM "4d69724e47536563757265494d000000" - -typedef CProtoDlgBase CIcqDlgBase; - -struct AIMSID -{ - AIMSID(CIcqProto *_ppro) : - m_ppro(_ppro) - {} - - CIcqProto *m_ppro; -}; - -enum ChatMenuItems -{ - IDM_INVITE = 10, IDM_LEAVE -}; - -struct IcqFileInfo -{ - IcqFileInfo(const std::string &pszUrl, const CMStringW &pwszDescr, uint32_t dwSize) : - szUrl(pszUrl.c_str()), - wszDescr(pwszDescr), - dwFileSize(dwSize) - {} - - CMStringA szUrl; - CMStringW wszDescr; - uint32_t dwFileSize; - bool bIsSticker = false; -}; - -struct IcqGroup -{ - IcqGroup(int _p1, const CMStringW &_p2) : - id(_p1), - wszSrvName(_p2) - { - SetName(_p2); - } - - int id; - int level; - CMStringW wszName, wszSrvName; - - void SetName(const CMStringW &str) - { - wszName = str; - level = wszName.SpanIncluding(L">").GetLength(); - if (level != 0) - wszName.Delete(0, level); - wszName.Replace(L">", L"\\"); - } -}; - -struct IcqCacheItem : public MZeroedObject -{ - IcqCacheItem(const CMStringW &wszId, MCONTACT _contact) : - m_aimid(wszId), - m_hContact(_contact) - {} - - CMStringW m_aimid; - MCONTACT m_hContact; - bool m_bInList; - __int64 m_iProcessedMsgId; - int m_iApparentMode; - time_t m_timer1, m_timer2; -}; - -struct IcqOwnMessage -{ - IcqOwnMessage(MCONTACT _hContact, int _msgid, const char *guid) - : m_hContact(_hContact), m_msgid(_msgid) - { - strncpy_s(m_guid, guid, _TRUNCATE); - } - - MCONTACT m_hContact; - int m_msgid; - char m_guid[50]; -}; - -struct IcqConn -{ - HNETLIBCONN s; - int lastTs, timeout; -}; - -struct IcqFileTransfer : public MZeroedObject -{ - // create an object for receiving - IcqFileTransfer(MCONTACT hContact, const char *pszUrl) : - m_szHost(pszUrl) - { - pfts.hContact = hContact; - pfts.totalFiles = 1; - pfts.flags = PFTS_UNICODE | PFTS_RECEIVING; - - ptrW pwszFileName(mir_utf8decodeW(pszUrl)); - if (pwszFileName == nullptr) - pwszFileName = mir_a2u(pszUrl); - - const wchar_t *p = wcsrchr(pwszFileName, '/'); - m_wszFileName = (p == nullptr) ? pwszFileName : p + 1; - m_wszShortName = m_wszFileName; - } - - // create an object for sending - IcqFileTransfer(MCONTACT hContact, const wchar_t *pwszFileName) : - m_wszFileName(pwszFileName) - { - pfts.flags = PFTS_UNICODE | PFTS_SENDING; - pfts.hContact = hContact; - pfts.szCurrentFile.w = m_wszFileName.GetBuffer(); - - const wchar_t *p = wcsrchr(pfts.szCurrentFile.w, '\\'); - if (pwszFileName != nullptr) - p++; - else - p = pfts.szCurrentFile.w; - m_wszShortName = p; - } - - ~IcqFileTransfer() - { - if (m_fileId >= 0) - _close(m_fileId); - } - - bool m_bCanceled = false, m_bStarted = false; - int m_fileId = -1; - CMStringA m_szHost; - CMStringW m_wszFileName, m_wszDescr; - const wchar_t *m_wszShortName; - PROTOFILETRANSFERSTATUS pfts; - HANDLE hWaitEvent; - - void FillHeaders(AsyncHttpRequest *pReq) - { - pReq->AddHeader("Content-Type", "application/octet-stream"); - pReq->AddHeader("Content-Disposition", CMStringA(FORMAT, "attachment; filename=\"%s\"", T2Utf(m_wszShortName).get())); - - uint32_t dwPortion = pfts.currentFileSize - pfts.currentFileProgress; - if (dwPortion > 1000000) - dwPortion = 1000000; - - pReq->AddHeader("Content-Range", CMStringA(FORMAT, "bytes %lld-%lld/%lld", pfts.currentFileProgress, pfts.currentFileProgress + dwPortion - 1, pfts.currentFileSize)); - pReq->AddHeader("Content-Length", CMStringA(FORMAT, "%d", dwPortion)); - - pReq->dataLength = dwPortion; - pReq->pData = (char*)mir_alloc(dwPortion); - _lseek(m_fileId, pfts.currentFileProgress, SEEK_SET); - _read(m_fileId, pReq->pData, dwPortion); - - pfts.currentFileProgress += dwPortion; - pfts.totalProgress += dwPortion; - } -}; - -class CIcqProto : public PROTO -{ - friend struct AsyncRapiRequest; - - class CIcqProtoImpl - { - friend class CIcqProto; - - CIcqProto &m_proto; - CTimer m_heartBeat, m_markRead; - - void OnHeartBeat(CTimer *) { - m_proto.CheckStatus(); - } - - void OnMarkRead(CTimer *pTimer) { - m_proto.SendMarkRead(); - pTimer->Stop(); - } - - CIcqProtoImpl(CIcqProto &pro) : - m_proto(pro), - m_markRead(Miranda_GetSystemWindow(), UINT_PTR(this)), - m_heartBeat(Miranda_GetSystemWindow(), UINT_PTR(this) + 1) - { - m_markRead.OnEvent = Callback(this, &CIcqProtoImpl::OnMarkRead); - m_heartBeat.OnEvent = Callback(this, &CIcqProtoImpl::OnHeartBeat); - } - } m_impl; - - friend struct CIcqRegistrationDlg; - friend class CGroupchatInviteDlg; - friend class CEditIgnoreListDlg; - friend class CIcqEnterLoginDlg; - friend class CIcqOptionsDlg; - friend class CGroupEditDlg; - - friend AsyncHttpRequest* operator <<(AsyncHttpRequest*, const AIMSID&); - - bool m_bOnline, m_bTerminated, m_bFirstBos, m_isMra, m_bError462; - int m_iTimeShift; - - MCONTACT CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove); - void CheckPassword(void); - void ConnectionFailed(int iReason, int iErrorCode = 0); - void EmailNotification(const wchar_t *pwszText); - void GetPermitDeny(); - wchar_t* GetUIN(MCONTACT hContact); - void MarkAsRead(MCONTACT hContact); - void MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup); - bool RetrievePassword(); - void RetrieveUserHistory(MCONTACT, __int64 startMsgId, bool bCreateRead); - void RetrieveUserInfo(MCONTACT hContact); - void SendMrimLogin(NETLIBHTTPREQUEST *pReply); - void SetServerStatus(int iNewStatus); - void ShutdownSession(void); - void StartSession(void); - - void CheckAvatarChange(MCONTACT hContact, const JSONNode&); - IcqFileInfo* CheckFile(MCONTACT hContact, CMStringW &wszFileName, bool &bIsFile); - void CheckLastId(MCONTACT hContact, const JSONNode&); - void Json2int(MCONTACT, const JSONNode&, const char *szJson, const char *szSetting, bool bIsPartial); - void Json2string(MCONTACT, const JSONNode&, const char *szJson, const char *szSetting, bool bIsPartial); - MCONTACT ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact = INVALID_CONTACT_ID, bool bIsPartial = false); - void ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &msg, bool bCreateRead, bool bLocalTime); - int StatusFromPresence(const JSONNode &presence, MCONTACT hContact); - - void OnLoggedIn(void); - void OnLoggedOut(void); - - mir_cs m_csMarkReadQueue; - LIST m_arMarkReadQueue; - void SendMarkRead(); - - __int64 getId(MCONTACT hContact, const char *szSetting); - void setId(MCONTACT hContact, const char *szSetting, __int64 iValue); - - void OnAddBuddy(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnAddClient(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckMraAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckMraAuthFinal(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckMrimLogin(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckPassword(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnCheckPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFetchEvents(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFileContinue(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFileInit(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFileInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnFileRecv(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGenToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetChatInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetPermitDeny(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetSticker(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnLoginViaPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnNormalizePhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnReceiveAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnSearchResults(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnSendMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnSessionEnd(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - void OnValidateSms(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); - - void ProcessBuddyList(const JSONNode &pRoot); - void ProcessDiff(const JSONNode &pRoot); - void ProcessEvent(const JSONNode &pRoot); - void ProcessGroupChat(const JSONNode &pRoot); - void ProcessHistData(const JSONNode &pRoot); - void ProcessImState(const JSONNode &pRoot); - void ProcessMyInfo(const JSONNode &pRoot); - void ProcessNotification(const JSONNode &pRoot); - void ProcessPermissions(const JSONNode &pRoot); - void ProcessPresence(const JSONNode &pRoot); - void ProcessSessionEnd(const JSONNode &pRoot); - void ProcessTyping(const JSONNode &pRoot); - - IcqConn m_ConnPool[CONN_LAST]; - CMStringA m_szPassword; - CMStringA m_szSessionKey; - CMStringA m_szAToken; - CMStringA m_szRToken; - CMStringA m_fetchBaseURL; - CMStringA m_aimsid; - CMStringA m_szMraCookie; - LONG m_msgId = 1; - int m_iRClientId; - HGENMENU m_hUploadGroups; - - mir_cs m_csOwnIds; - OBJLIST m_arOwnIds; - - OBJLIST m_arGroups; - - int m_unreadEmails = -1; - CMStringA m_szMailBox; - - bool m_bIgnoreListEmpty = true; - bool m_bRememberPwd; // store password in a database - bool m_bDlgActive; - - //////////////////////////////////////////////////////////////////////////////////////// - // group chats - - int __cdecl GroupchatEventHook(WPARAM, LPARAM); - int __cdecl GroupchatMenuHook(WPARAM, LPARAM); - - void Chat_ProcessLogMenu(SESSION_INFO *si, int); - void Chat_SendPrivateMessage(GCHOOK *gch); - - void InviteUserToChat(SESSION_INFO *si); - void LeaveDestroyChat(SESSION_INFO *si); - void LoadChatInfo(SESSION_INFO *si); - - //////////////////////////////////////////////////////////////////////////////////////// - // http queue - - mir_cs m_csHttpQueue; - HANDLE m_evRequestsQueue; - LIST m_arHttpQueue; - - void CalcHash(AsyncHttpRequest*); - void DropQueue(); - bool ExecuteRequest(AsyncHttpRequest*); - bool IsQueueEmpty(); - void Push(MHttpRequest*); - bool RefreshRobustToken(AsyncHttpRequest *pReq); - - //////////////////////////////////////////////////////////////////////////////////////// - // cache - - mir_cs m_csCache; - OBJLIST m_arCache; - - void InitContactCache(void); - IcqCacheItem* FindContactByUIN(const CMStringW &pwszId); - MCONTACT CreateContact(const CMStringW &pwszId, bool bTemporary); - - void GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen); - - //////////////////////////////////////////////////////////////////////////////////////// - // threads - - HANDLE m_hWorkerThread; - void __cdecl ServerThread(void*); - void __cdecl PollThread(void*); - - //////////////////////////////////////////////////////////////////////////////////////// - // services - - INT_PTR __cdecl GetAvatar(WPARAM, LPARAM); - INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); - INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); - INT_PTR __cdecl SetAvatar(WPARAM, LPARAM); - - INT_PTR __cdecl CreateAccMgrUI(WPARAM, LPARAM); - INT_PTR __cdecl EditGroups(WPARAM, LPARAM); - INT_PTR __cdecl EditProfile(WPARAM, LPARAM); - INT_PTR __cdecl GetEmailCount(WPARAM, LPARAM); - INT_PTR __cdecl GotoInbox(WPARAM, LPARAM); - INT_PTR __cdecl UploadGroups(WPARAM, LPARAM); - - INT_PTR __cdecl OnMenuLoadHistory(WPARAM, LPARAM); - - //////////////////////////////////////////////////////////////////////////////////////// - // events - - int __cdecl OnGroupChange(WPARAM, LPARAM); - int __cdecl OnDbEventRead(WPARAM, LPARAM); - int __cdecl OnOptionsInit(WPARAM, LPARAM); - int __cdecl OnUserInfoInit(WPARAM, LPARAM); - - //////////////////////////////////////////////////////////////////////////////////////// - // PROTO_INTERFACE - - MCONTACT AddToList( int flags, PROTOSEARCHRESULT *psr) override; - - int AuthRecv(MCONTACT, PROTORECVEVENT *pre) override; - int AuthRequest(MCONTACT hContact, const wchar_t *szMessage) override; - - INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; - int GetInfo(MCONTACT hContact, int infoType) override; - - HANDLE SearchBasic(const wchar_t *id) override; - - HANDLE FileAllow(MCONTACT hContact, HANDLE hTransfer, const wchar_t *szPath) override; - int FileCancel(MCONTACT hContact, HANDLE hTransfer) override; - int FileResume(HANDLE hTransfer, int action, const wchar_t *szFilename) override; - - HANDLE SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override; - int SendMsg(MCONTACT hContact, int flags, const char *msg) override; - - int SetApparentMode(MCONTACT hContact, int mode) override; - int SetStatus(int iNewStatus) override; - - int UserIsTyping(MCONTACT hContact, int type) override; - - void OnBuildProtoMenu(void) override; - void OnContactAdded(MCONTACT) override; - void OnContactDeleted(MCONTACT) override; - void OnEventEdited(MCONTACT, MEVENT) override; - void OnModulesLoaded() override; - void OnShutdown() override; - -public: - CIcqProto(const char*, const wchar_t*); - ~CIcqProto(); - - CMOption m_szOwnId; // our own aim id - CMOption m_bHideGroupchats; // don't pop up group chat windows on startup - CMOption m_bUseTrayIcon; // use tray icon notifications - CMOption m_bErrorPopups; // display popups with errors - CMOption m_bLaunchMailbox; // launch browser to view email - CMOption m_iTimeDiff1; // set this status to m_iStatus1 after this interval of secs - CMOption m_iStatus1; - CMOption m_iTimeDiff2; // set this status to m_iStatus2 after this interval of secs - CMOption m_iStatus2; - - void CheckStatus(void); - CMStringW GetUserId(MCONTACT); - - __forceinline int TS() const - { return time(0) - m_iTimeShift; - } - - __forceinline const char *appId() const - { return (m_isMra) ? MRA_APP_ID : ICQ_APP_ID; - } - - void SetPermitDeny(const CMStringW &userId, bool bAllow); -}; - -struct CMPlugin : public ACCPROTOPLUGIN -{ - CMPlugin(); - - int Load() override; -}; - -#endif +// ---------------------------------------------------------------------------80 +// ICQ plugin for Miranda Instant Messenger +// ________________________________________ +// +// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede +// Copyright © 2001-2002 Jon Keating, Richard Hughes +// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater +// Copyright © 2004-2010 Joe Kucera, George Hazan +// Copyright © 2012-2023 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- +// DESCRIPTION: +// +// Protocol Interface declarations +// ----------------------------------------------------------------------------- + +#ifndef _ICQ_PROTO_H_ +#define _ICQ_PROTO_H_ + +#include "m_system.h" +#include "m_protoint.h" + +#define MRA_APP_ID "ic1pzYNtEU6dDnEQ" +#define ICQ_APP_ID "ic1nmMjqg7Yu-0hL" +#define ICQ_API_SERVER "https://u.icq.net/wim" +#define ICQ_FILE_SERVER "https://u.icq.net/files/api/v1.1" +#define ICQ_FAKE_EVENT_ID 0xBABAEB +#define ICQ_ROBUST_SERVER "https://u.icq.net/rapi" + +#define PS_DUMMY "/DoNothing" +#define PS_GOTO_INBOX "/GotoInbox" + +#define WIM_CAP_VOIP_VOICE "094613504c7f11d18222444553540000" +#define WIM_CAP_VOIP_VIDEO "094613514c7f11d18222444553540000" +#define WIM_CAP_FILETRANSFER "094613434c7f11d18222444553540000" +#define WIM_CAP_UNIQ_REQ_ID "094613534c7f11d18222444553540000" +#define WIM_CAP_EMOJI "094613544c7f11d18222444553540000" +#define WIM_CAP_MENTIONS "0946135b4c7f11d18222444553540000" +#define WIM_CAP_MAIL_NOTIFICATIONS "094613594c7f11d18222444553540000" +#define WIM_CAP_INTRO_DLG_STATE "0946135a4c7f11d18222444553540000" + +#define NG_CAP_SECUREIM "4d69724e47536563757265494d000000" + +typedef CProtoDlgBase CIcqDlgBase; + +struct AIMSID +{ + AIMSID(CIcqProto *_ppro) : + m_ppro(_ppro) + {} + + CIcqProto *m_ppro; +}; + +enum ChatMenuItems +{ + IDM_INVITE = 10, IDM_LEAVE +}; + +struct IcqFileInfo +{ + IcqFileInfo(const std::string &pszUrl, const CMStringW &pwszDescr, uint32_t dwSize) : + szUrl(pszUrl.c_str()), + wszDescr(pwszDescr), + dwFileSize(dwSize) + {} + + CMStringA szUrl; + CMStringW wszDescr; + uint32_t dwFileSize; + bool bIsSticker = false; +}; + +struct IcqGroup +{ + IcqGroup(int _p1, const CMStringW &_p2) : + id(_p1), + wszSrvName(_p2) + { + SetName(_p2); + } + + int id; + int level; + CMStringW wszName, wszSrvName; + + void SetName(const CMStringW &str) + { + wszName = str; + level = wszName.SpanIncluding(L">").GetLength(); + if (level != 0) + wszName.Delete(0, level); + wszName.Replace(L">", L"\\"); + } +}; + +struct IcqCacheItem : public MZeroedObject +{ + IcqCacheItem(const CMStringW &wszId, MCONTACT _contact) : + m_aimid(wszId), + m_hContact(_contact) + {} + + CMStringW m_aimid; + MCONTACT m_hContact; + bool m_bInList; + __int64 m_iProcessedMsgId; + int m_iApparentMode; + time_t m_timer1, m_timer2; +}; + +struct IcqOwnMessage +{ + IcqOwnMessage(MCONTACT _hContact, int _msgid, const char *guid) + : m_hContact(_hContact), m_msgid(_msgid) + { + strncpy_s(m_guid, guid, _TRUNCATE); + } + + MCONTACT m_hContact; + int m_msgid; + char m_guid[50]; +}; + +struct IcqConn +{ + HNETLIBCONN s; + int lastTs, timeout; +}; + +struct IcqFileTransfer : public MZeroedObject +{ + // create an object for receiving + IcqFileTransfer(MCONTACT hContact, const char *pszUrl) : + m_szHost(pszUrl) + { + pfts.hContact = hContact; + pfts.totalFiles = 1; + pfts.flags = PFTS_UNICODE | PFTS_RECEIVING; + + ptrW pwszFileName(mir_utf8decodeW(pszUrl)); + if (pwszFileName == nullptr) + pwszFileName = mir_a2u(pszUrl); + + const wchar_t *p = wcsrchr(pwszFileName, '/'); + m_wszFileName = (p == nullptr) ? pwszFileName : p + 1; + m_wszShortName = m_wszFileName; + } + + // create an object for sending + IcqFileTransfer(MCONTACT hContact, const wchar_t *pwszFileName) : + m_wszFileName(pwszFileName) + { + pfts.flags = PFTS_UNICODE | PFTS_SENDING; + pfts.hContact = hContact; + pfts.szCurrentFile.w = m_wszFileName.GetBuffer(); + + const wchar_t *p = wcsrchr(pfts.szCurrentFile.w, '\\'); + if (pwszFileName != nullptr) + p++; + else + p = pfts.szCurrentFile.w; + m_wszShortName = p; + } + + ~IcqFileTransfer() + { + if (m_fileId >= 0) + _close(m_fileId); + } + + bool m_bCanceled = false, m_bStarted = false; + int m_fileId = -1; + CMStringA m_szHost; + CMStringW m_wszFileName, m_wszDescr; + const wchar_t *m_wszShortName; + PROTOFILETRANSFERSTATUS pfts; + HANDLE hWaitEvent; + + void FillHeaders(AsyncHttpRequest *pReq) + { + pReq->AddHeader("Content-Type", "application/octet-stream"); + pReq->AddHeader("Content-Disposition", CMStringA(FORMAT, "attachment; filename=\"%s\"", T2Utf(m_wszShortName).get())); + + uint32_t dwPortion = pfts.currentFileSize - pfts.currentFileProgress; + if (dwPortion > 1000000) + dwPortion = 1000000; + + pReq->AddHeader("Content-Range", CMStringA(FORMAT, "bytes %lld-%lld/%lld", pfts.currentFileProgress, pfts.currentFileProgress + dwPortion - 1, pfts.currentFileSize)); + pReq->AddHeader("Content-Length", CMStringA(FORMAT, "%d", dwPortion)); + + pReq->dataLength = dwPortion; + pReq->pData = (char*)mir_alloc(dwPortion); + _lseek(m_fileId, pfts.currentFileProgress, SEEK_SET); + _read(m_fileId, pReq->pData, dwPortion); + + pfts.currentFileProgress += dwPortion; + pfts.totalProgress += dwPortion; + } +}; + +class CIcqProto : public PROTO +{ + friend struct AsyncRapiRequest; + + class CIcqProtoImpl + { + friend class CIcqProto; + + CIcqProto &m_proto; + CTimer m_heartBeat, m_markRead; + + void OnHeartBeat(CTimer *) { + m_proto.CheckStatus(); + } + + void OnMarkRead(CTimer *pTimer) { + m_proto.SendMarkRead(); + pTimer->Stop(); + } + + CIcqProtoImpl(CIcqProto &pro) : + m_proto(pro), + m_markRead(Miranda_GetSystemWindow(), UINT_PTR(this)), + m_heartBeat(Miranda_GetSystemWindow(), UINT_PTR(this) + 1) + { + m_markRead.OnEvent = Callback(this, &CIcqProtoImpl::OnMarkRead); + m_heartBeat.OnEvent = Callback(this, &CIcqProtoImpl::OnHeartBeat); + } + } m_impl; + + friend struct CIcqRegistrationDlg; + friend class CGroupchatInviteDlg; + friend class CEditIgnoreListDlg; + friend class CIcqEnterLoginDlg; + friend class CIcqOptionsDlg; + friend class CGroupEditDlg; + + friend AsyncHttpRequest* operator <<(AsyncHttpRequest*, const AIMSID&); + + bool m_bOnline, m_bTerminated, m_bFirstBos, m_isMra, m_bError462; + int m_iTimeShift; + + MCONTACT CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove); + void CheckPassword(void); + void ConnectionFailed(int iReason, int iErrorCode = 0); + void EmailNotification(const wchar_t *pwszText); + void GetPermitDeny(); + wchar_t* GetUIN(MCONTACT hContact); + void MarkAsRead(MCONTACT hContact); + void MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup); + bool RetrievePassword(); + void RetrieveUserHistory(MCONTACT, __int64 startMsgId, bool bCreateRead); + void RetrieveUserInfo(MCONTACT hContact); + void SendMrimLogin(NETLIBHTTPREQUEST *pReply); + void SetServerStatus(int iNewStatus); + void ShutdownSession(void); + void StartSession(void); + + void CheckAvatarChange(MCONTACT hContact, const JSONNode&); + IcqFileInfo* CheckFile(MCONTACT hContact, CMStringW &wszFileName, bool &bIsFile); + void CheckLastId(MCONTACT hContact, const JSONNode&); + void Json2int(MCONTACT, const JSONNode&, const char *szJson, const char *szSetting, bool bIsPartial); + void Json2string(MCONTACT, const JSONNode&, const char *szJson, const char *szSetting, bool bIsPartial); + MCONTACT ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact = INVALID_CONTACT_ID, bool bIsPartial = false); + void ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &msg, bool bCreateRead, bool bLocalTime); + int StatusFromPresence(const JSONNode &presence, MCONTACT hContact); + + void OnLoggedIn(void); + void OnLoggedOut(void); + + mir_cs m_csMarkReadQueue; + LIST m_arMarkReadQueue; + void SendMarkRead(); + + __int64 getId(MCONTACT hContact, const char *szSetting); + void setId(MCONTACT hContact, const char *szSetting, __int64 iValue); + + void OnAddBuddy(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnAddClient(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckMraAuth(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckMraAuthFinal(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckMrimLogin(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckPassword(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnCheckPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFetchEvents(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFileContinue(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFileInit(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFileInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnFileRecv(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGenToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetChatInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetPermitDeny(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetSticker(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnLoginViaPhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnNormalizePhone(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnReceiveAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnSearchResults(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnSendMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnSessionEnd(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + void OnValidateSms(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq); + + void ProcessBuddyList(const JSONNode &pRoot); + void ProcessDiff(const JSONNode &pRoot); + void ProcessEvent(const JSONNode &pRoot); + void ProcessGroupChat(const JSONNode &pRoot); + void ProcessHistData(const JSONNode &pRoot); + void ProcessImState(const JSONNode &pRoot); + void ProcessMyInfo(const JSONNode &pRoot); + void ProcessNotification(const JSONNode &pRoot); + void ProcessPermissions(const JSONNode &pRoot); + void ProcessPresence(const JSONNode &pRoot); + void ProcessSessionEnd(const JSONNode &pRoot); + void ProcessTyping(const JSONNode &pRoot); + + IcqConn m_ConnPool[CONN_LAST]; + CMStringA m_szPassword; + CMStringA m_szSessionKey; + CMStringA m_szAToken; + CMStringA m_szRToken; + CMStringA m_fetchBaseURL; + CMStringA m_aimsid; + CMStringA m_szMraCookie; + LONG m_msgId = 1; + int m_iRClientId; + HGENMENU m_hUploadGroups; + + mir_cs m_csOwnIds; + OBJLIST m_arOwnIds; + + OBJLIST m_arGroups; + + int m_unreadEmails = -1; + CMStringA m_szMailBox; + + bool m_bIgnoreListEmpty = true; + bool m_bRememberPwd; // store password in a database + bool m_bDlgActive; + + //////////////////////////////////////////////////////////////////////////////////////// + // group chats + + int __cdecl GroupchatEventHook(WPARAM, LPARAM); + int __cdecl GroupchatMenuHook(WPARAM, LPARAM); + + void Chat_ProcessLogMenu(SESSION_INFO *si, int); + void Chat_SendPrivateMessage(GCHOOK *gch); + + void InviteUserToChat(SESSION_INFO *si); + void LeaveDestroyChat(SESSION_INFO *si); + void LoadChatInfo(SESSION_INFO *si); + + //////////////////////////////////////////////////////////////////////////////////////// + // http queue + + mir_cs m_csHttpQueue; + HANDLE m_evRequestsQueue; + LIST m_arHttpQueue; + + void CalcHash(AsyncHttpRequest*); + void DropQueue(); + bool ExecuteRequest(AsyncHttpRequest*); + bool IsQueueEmpty(); + void Push(MHttpRequest*); + bool RefreshRobustToken(AsyncHttpRequest *pReq); + + //////////////////////////////////////////////////////////////////////////////////////// + // cache + + mir_cs m_csCache; + OBJLIST m_arCache; + + void InitContactCache(void); + IcqCacheItem* FindContactByUIN(const CMStringW &pwszId); + MCONTACT CreateContact(const CMStringW &pwszId, bool bTemporary); + + void GetAvatarFileName(MCONTACT hContact, wchar_t *pszDest, size_t cbLen); + + //////////////////////////////////////////////////////////////////////////////////////// + // threads + + HANDLE m_hWorkerThread; + void __cdecl ServerThread(void*); + void __cdecl PollThread(void*); + + //////////////////////////////////////////////////////////////////////////////////////// + // services + + INT_PTR __cdecl GetAvatar(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarCaps(WPARAM, LPARAM); + INT_PTR __cdecl GetAvatarInfo(WPARAM, LPARAM); + INT_PTR __cdecl SetAvatar(WPARAM, LPARAM); + + INT_PTR __cdecl CreateAccMgrUI(WPARAM, LPARAM); + INT_PTR __cdecl EditGroups(WPARAM, LPARAM); + INT_PTR __cdecl EditProfile(WPARAM, LPARAM); + INT_PTR __cdecl GetEmailCount(WPARAM, LPARAM); + INT_PTR __cdecl GotoInbox(WPARAM, LPARAM); + INT_PTR __cdecl UploadGroups(WPARAM, LPARAM); + + INT_PTR __cdecl OnMenuLoadHistory(WPARAM, LPARAM); + + //////////////////////////////////////////////////////////////////////////////////////// + // events + + int __cdecl OnGroupChange(WPARAM, LPARAM); + int __cdecl OnDbEventRead(WPARAM, LPARAM); + int __cdecl OnOptionsInit(WPARAM, LPARAM); + int __cdecl OnUserInfoInit(WPARAM, LPARAM); + + //////////////////////////////////////////////////////////////////////////////////////// + // PROTO_INTERFACE + + MCONTACT AddToList( int flags, PROTOSEARCHRESULT *psr) override; + + int AuthRecv(MCONTACT, PROTORECVEVENT *pre) override; + int AuthRequest(MCONTACT hContact, const wchar_t *szMessage) override; + + INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; + int GetInfo(MCONTACT hContact, int infoType) override; + + HANDLE SearchBasic(const wchar_t *id) override; + + HANDLE FileAllow(MCONTACT hContact, HANDLE hTransfer, const wchar_t *szPath) override; + int FileCancel(MCONTACT hContact, HANDLE hTransfer) override; + int FileResume(HANDLE hTransfer, int action, const wchar_t *szFilename) override; + + HANDLE SendFile(MCONTACT hContact, const wchar_t *szDescription, wchar_t **ppszFiles) override; + int SendMsg(MCONTACT hContact, int flags, const char *msg) override; + + int SetApparentMode(MCONTACT hContact, int mode) override; + int SetStatus(int iNewStatus) override; + + int UserIsTyping(MCONTACT hContact, int type) override; + + void OnBuildProtoMenu(void) override; + void OnContactAdded(MCONTACT) override; + void OnContactDeleted(MCONTACT) override; + void OnEventEdited(MCONTACT, MEVENT) override; + void OnModulesLoaded() override; + void OnShutdown() override; + +public: + CIcqProto(const char*, const wchar_t*); + ~CIcqProto(); + + CMOption m_szOwnId; // our own aim id + CMOption m_bHideGroupchats; // don't pop up group chat windows on startup + CMOption m_bUseTrayIcon; // use tray icon notifications + CMOption m_bErrorPopups; // display popups with errors + CMOption m_bLaunchMailbox; // launch browser to view email + CMOption m_iTimeDiff1; // set this status to m_iStatus1 after this interval of secs + CMOption m_iStatus1; + CMOption m_iTimeDiff2; // set this status to m_iStatus2 after this interval of secs + CMOption m_iStatus2; + + void CheckStatus(void); + CMStringW GetUserId(MCONTACT); + + __forceinline int TS() const + { return time(0) - m_iTimeShift; + } + + __forceinline const char *appId() const + { return (m_isMra) ? MRA_APP_ID : ICQ_APP_ID; + } + + void SetPermitDeny(const CMStringW &userId, bool bAllow); +}; + +struct CMPlugin : public ACCPROTOPLUGIN +{ + CMPlugin(); + + int Load() override; +}; + +#endif diff --git a/protocols/ICQ-WIM/src/server.cpp b/protocols/ICQ-WIM/src/server.cpp index 167aabfee9..114ad5ed99 100644 --- a/protocols/ICQ-WIM/src/server.cpp +++ b/protocols/ICQ-WIM/src/server.cpp @@ -1,1220 +1,1220 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -#pragma comment(lib, "libcrypto.lib") - -void CIcqProto::CheckAvatarChange(MCONTACT hContact, const JSONNode &ev) -{ - CMStringW wszIconId(ev["bigIconId"].as_mstring()); - if (wszIconId.IsEmpty()) - wszIconId = ev["iconId"].as_mstring(); - - if (!wszIconId.IsEmpty()) { - CMStringW oldIconID(getMStringW(hContact, "IconId")); - if (wszIconId == oldIconID) { - wchar_t wszFullName[MAX_PATH]; - GetAvatarFileName(hContact, wszFullName, _countof(wszFullName)); - if (_waccess(wszFullName, 0) == 0) - return; - } - - setWString(hContact, "IconId", wszIconId); - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/expressions/get", &CIcqProto::OnReceiveAvatar); - pReq << CHAR_PARAM("f", "native") << WCHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("type", "bigBuddyIcon"); - pReq->hContact = hContact; - Push(pReq); - } - else delSetting(hContact, "IconId"); -} - -void CIcqProto::CheckLastId(MCONTACT hContact, const JSONNode &ev) -{ - __int64 msgId = _wtoi64(ev["histMsgId"].as_mstring()); - __int64 lastId = getId(hContact, DB_KEY_LASTMSGID); - if (msgId > lastId) - setId(hContact, DB_KEY_LASTMSGID, msgId); -} - -MCONTACT CIcqProto::CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove) -{ - IcqOwnMessage *pOwn = nullptr; - { - mir_cslock lck(m_csOwnIds); - for (auto &it : m_arOwnIds) { - if (reqId == it->m_guid) { - pOwn = it; - break; - } - } - } - - if (pOwn == nullptr) - return 0; - - ProtoBroadcastAck(pOwn->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)pOwn->m_msgid, (LPARAM)msgId.c_str()); - - MCONTACT ret = pOwn->m_hContact; - if (bRemove) { - mir_cslock lck(m_csOwnIds); - m_arOwnIds.remove(pOwn); - } - return ret; -} - -void CIcqProto::CheckPassword() -{ - char mirVer[100]; - Miranda_GetVersionText(mirVer, _countof(mirVer)); - - m_szAToken = getMStringA(DB_KEY_ATOKEN); - m_iRClientId = getDword(DB_KEY_RCLIENTID); - m_szSessionKey = getMStringA(DB_KEY_SESSIONKEY); - if (!m_szAToken.IsEmpty() && !m_szSessionKey.IsEmpty()) { - StartSession(); - return; - } - - if (m_isMra) { - m_bError462 = false; - SendMrimLogin(nullptr); - } - else { - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, "https://api.login.icq.net/auth/clientLogin", &CIcqProto::OnCheckPassword); - pReq << CHAR_PARAM("clientName", "Miranda NG") << CHAR_PARAM("clientVersion", mirVer) << CHAR_PARAM("devId", appId()) - << CHAR_PARAM("f", "json") << CHAR_PARAM("tokenType", "longTerm") << WCHAR_PARAM("s", m_szOwnId) << CHAR_PARAM("pwd", m_szPassword); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - Push(pReq); - } -} - -IcqFileInfo* CIcqProto::CheckFile(MCONTACT hContact, CMStringW &wszText, bool &bIsFile) -{ - CMStringW wszUrl(wszText.Mid(26)); - int idx = wszUrl.Find(' '); - if (idx != -1) - wszUrl.Truncate(idx); - - bIsFile = false; - IcqFileInfo *pFileInfo = nullptr; - - // is it already downloaded sticker? - CMStringW wszLoadedPath(FORMAT, L"%s\\%S\\Stickers\\STK{%s}.png", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName, wszUrl.c_str()); - if (!_waccess(wszLoadedPath, 0)) { - pFileInfo = (IcqFileInfo *)this; - wszText.Format(L"STK{%s}", wszUrl.c_str()); - } - else { - // download file info - CMStringA szUrl(FORMAT, ICQ_FILE_SERVER "/info/%S/", wszUrl.c_str()); - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, szUrl, &CIcqProto::OnFileInfo); - pReq->hContact = hContact; - pReq->pUserInfo = &pFileInfo; - pReq << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("previews", "192,600,xlarge"); - if (!ExecuteRequest(pReq)) - return nullptr; - - // is it a sticker? - if (pFileInfo->bIsSticker) { - if (ServiceExists(MS_SMILEYADD_LOADCONTACTSMILEYS)) { - auto *pNew = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, pFileInfo->szUrl, &CIcqProto::OnGetSticker); - pNew->flags |= NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; - pNew->pUserInfo = wszUrl.GetBuffer(); - pNew->AddHeader("Sec-Fetch-User", "?1"); - pNew->AddHeader("Sec-Fetch-Site", "cross-site"); - pNew->AddHeader("Sec-Fetch-Mode", "navigate"); - if (!ExecuteRequest(pNew)) - return nullptr; - - wszText.Format(L"STK{%s}", wszUrl.c_str()); - delete pFileInfo; - } - else wszText = TranslateT("SmileyAdd plugin required to support stickers"); - } - else bIsFile = true; - } - - return pFileInfo; -} - -void CIcqProto::CheckStatus() -{ - time_t now = time(0); - int diff1 = m_iTimeDiff1, diff2 = m_iTimeDiff2; - - for (auto &it : m_arCache) { - // this contact is really offline and is on the first timer - // if the first timer is expired, we clear it and look for the second status - if (diff1 && it->m_timer1 && now - it->m_timer1 > diff1) { - it->m_timer1 = 0; - - // if the second timer is set up, activate it - if (m_iTimeDiff2) { - setWord(it->m_hContact, "Status", m_iStatus2); - it->m_timer2 = now; - } - // if the second timer is not set, simply mark a contact as offline - else setWord(it->m_hContact, "Status", ID_STATUS_OFFLINE); - continue; - } - - // if the second timer is expired, set status to offline - if (diff2 && it->m_timer2 && now - it->m_timer2 > m_iTimeDiff2) { - it->m_timer2 = 0; - setWord(it->m_hContact, "Status", ID_STATUS_OFFLINE); - } - } -} - -void CIcqProto::ConnectionFailed(int iReason, int iErrorCode) -{ - debugLogA("ConnectionFailed -> reason %d", iReason); - - if (m_bErrorPopups) { - POPUPDATAW Popup = {}; - Popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); - wcscpy_s(Popup.lpwzContactName, m_tszUserName); - switch (iReason) { - case LOGINERR_BADUSERID: - mir_snwprintf(Popup.lpwzText, TranslateT("You have not entered a login or password.\nConfigure this in Options -> Network -> ICQ and try again.")); - break; - case LOGINERR_WRONGPASSWORD: - mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nYour login or password was rejected (%d)."), iErrorCode); - break; - case LOGINERR_NONETWORK: - case LOGINERR_NOSERVER: - mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nThe server is temporarily unavailable (%d)."), iErrorCode); - break; - default: - mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nUnknown error during sign on: %d"), iErrorCode); - break; - } - PUAddPopupW(&Popup); - } - - ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, nullptr, iReason); - ShutdownSession(); -} - -void CIcqProto::MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup) -{ - // otherwise we'll get a server error - if (!mir_wstrlen(pwszGroup)) - return; - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/moveBuddy") << AIMSID(this) << WCHAR_PARAM("buddy", GetUserId(hContact)) - << GROUP_PARAM("group", pwszGroup); - if (mir_wstrlen(pwszNewGroup)) - pReq << GROUP_PARAM("newGroup", pwszNewGroup); - Push(pReq); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnLoggedIn() -{ - debugLogA("CIcqProto::OnLoggedIn"); - m_bOnline = true; - m_impl.m_heartBeat.Start(1000); - - for (auto &it : m_arCache) - it->m_timer1 = it->m_timer2 = 0; - - SetServerStatus(m_iDesiredStatus); - RetrieveUserInfo(0); - GetPermitDeny(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnLoggedOut() -{ - debugLogA("CIcqProto::OnLoggedOut"); - m_bOnline = false; - m_impl.m_heartBeat.Stop(); - - for (auto &it : m_arCache) - it->m_timer1 = it->m_timer2 = 0; - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - - setAllContactStatuses(ID_STATUS_OFFLINE, false); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::MarkAsRead(MCONTACT hContact) -{ - if (!m_bOnline) - return; - - m_impl.m_markRead.Start(200); - - auto *pCache = FindContactByUIN(GetUserId(hContact)); - if (pCache) { - mir_cslock lck(m_csMarkReadQueue); - if (m_arMarkReadQueue.indexOf(pCache) == -1) - m_arMarkReadQueue.insert(pCache); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MCONTACT CIcqProto::ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact, bool bIsPartial) -{ - // user chat? - CMStringW wszId(buddy["aimId"].as_mstring()); - if (IsChat(wszId)) { - CMStringW wszChatId(buddy["aimId"].as_mstring()); - CMStringW wszChatName(buddy["friendly"].as_mstring()); - - auto *pContact = FindContactByUIN(wszId); - if (pContact && pContact->m_iApparentMode == ID_STATUS_OFFLINE) - return INVALID_CONTACT_ID; - - auto *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszChatId, wszChatName); - if (si == nullptr) - return INVALID_CONTACT_ID; - - Chat_AddGroup(si, TranslateT("admin")); - Chat_AddGroup(si, TranslateT("member")); - Chat_Control(m_szModuleName, wszChatId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); - Chat_Control(m_szModuleName, wszChatId, SESSION_ONLINE); - return si->hContact; - } - - bool bIgnored = !IsValidType(buddy); - if (hContact == INVALID_CONTACT_ID) { - if (bIgnored) - return INVALID_CONTACT_ID; - - hContact = CreateContact(wszId, false); - FindContactByUIN(wszId)->m_bInList = true; - } - else if (bIgnored) { - db_delete_contact(hContact); - return INVALID_CONTACT_ID; - } - - CMStringA szVer; - bool bVersionDetected = false, bSecureIM = false; - - for (auto &it : buddy["capabilities"]) { - CMStringW wszCap(it.as_mstring()); - if (wszCap.GetLength() != 32) - continue; - - uint8_t cap[16]; - hex2binW(wszCap, cap, sizeof(cap)); - if (!memcmp(cap, "MiNG", 4)) { // Miranda - int v[4]; - if (4 == swscanf(wszCap.c_str() + 16, L"%04x%04x%04x%04x", &v[0], &v[1], &v[2], &v[3])) { - szVer.Format("Miranda NG %d.%d.%d.%d (ICQ %d.%d.%d.%d)", v[0], v[1], v[2], v[3], cap[4], cap[5], cap[6], cap[7]); - setString(hContact, "MirVer", szVer); - bVersionDetected = true; - } - } - else if (wszCap == _A2W(NG_CAP_SECUREIM)) { - bSecureIM = bVersionDetected = true; - } - else if (!memcmp(cap, "Mod by Mikanoshi", 16)) { - szVer = "R&Q build by Mikanoshi"; - bVersionDetected = true; - } - else if (!memcmp(cap, "Mandarin IM", 11)) { - szVer = "Mandarin IM"; - bVersionDetected = true; - } - } - - if (bVersionDetected) { - if (bSecureIM) - szVer.Append(" + SecureIM"); - setString(hContact, "MirVer", szVer); - } - else delSetting(hContact, "MirVer"); - - const JSONNode &var = buddy["friendly"]; - if (var) - setWString(hContact, "Nick", var.as_mstring()); - - if (buddy["deleted"].as_bool()) { - setByte(hContact, "IcqDeleted", 1); - Contact::PutOnList(hContact); - } - - Json2string(hContact, buddy, "emailId", "Email", bIsPartial); - Json2string(hContact, buddy, "cellNumber", "Cellular", bIsPartial); - Json2string(hContact, buddy, "workNumber", "CompanyPhone", bIsPartial); - - // we shall not remove existing phone number anyhow - Json2string(hContact, buddy, "phoneNumber", DB_KEY_PHONE, true); - - Json2int(hContact, buddy, "official", "Official", bIsPartial); - Json2int(hContact, buddy, "onlineTime", DB_KEY_ONLINETS, bIsPartial); - Json2int(hContact, buddy, "idleTime", "IdleTS", bIsPartial); - Json2int(hContact, buddy, "memberSince", DB_KEY_MEMBERSINCE, bIsPartial); - - int iStatus = StatusFromPresence(buddy, hContact); - if (iStatus > 0) - setWord(hContact, "Status", iStatus); - - const JSONNode &profile = buddy["profile"]; - if (profile) { - Json2string(hContact, profile, "friendlyName", DB_KEY_ICQNICK, bIsPartial); - Json2string(hContact, profile, "firstName", "FirstName", bIsPartial); - Json2string(hContact, profile, "lastName", "LastName", bIsPartial); - Json2string(hContact, profile, "aboutMe", DB_KEY_ABOUT, bIsPartial); - - ptrW wszNick(getWStringA(hContact, "Nick")); - if (!wszNick) { - CMStringW srvNick = profile["friendlyName"].as_mstring(); - if (!srvNick.IsEmpty()) - setWString(hContact, "Nick", srvNick); - } - - time_t birthDate = profile["birthDate"].as_int(); - if (birthDate != 0) { - struct tm *timeinfo = localtime(&birthDate); - if (timeinfo != nullptr) { - setWord(hContact, "BirthDay", timeinfo->tm_mday); - setWord(hContact, "BirthMonth", timeinfo->tm_mon+1); - setWord(hContact, "BirthYear", timeinfo->tm_year+1900); - } - } - - CMStringW str = profile["gender"].as_mstring(); - if (!str.IsEmpty()) { - if (str == "male") - setByte(hContact, "Gender", 'M'); - else if (str == "female") - setByte(hContact, "Gender", 'F'); - } - - for (auto &it : profile["homeAddress"]) { - Json2string(hContact, it, "city", "City", bIsPartial); - Json2string(hContact, it, "state", "State", bIsPartial); - Json2string(hContact, it, "country", "Country", bIsPartial); - } - } - - CMStringW str = buddy["statusMsg"].as_mstring(); - if (str.IsEmpty()) - db_unset(hContact, "CList", "StatusMsg"); - else - db_set_ws(hContact, "CList", "StatusMsg", str); - - CheckAvatarChange(hContact, buddy); - return hContact; -} - -void CIcqProto::ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &it, bool bCreateRead, bool bLocalTime) -{ - CMStringA szMsgId(it["msgId"].as_mstring()); - __int64 msgId = _atoi64(szMsgId); - if (msgId > lastMsgId) - lastMsgId = msgId; - - CMStringW wszText; - const JSONNode &sticker = it["sticker"]; - if (sticker) { - CMStringW wszUrl, wszSticker(sticker["id"].as_mstring()); - int iCollectionId, iStickerId; - if (2 == swscanf(wszSticker, L"ext:%d:sticker:%d", &iCollectionId, &iStickerId)) - wszUrl.Format(L"https://c.icq.com/store/stickers/%d/%d/medium", iCollectionId, iStickerId); - else - wszUrl = TranslateT("Unknown sticker"); - wszText.Format(L"%s\n%s", TranslateT("User sent a sticker:"), wszUrl.c_str()); - } - else { - wszText = it["text"].as_mstring(); - wszText.TrimRight(); - - // user added you - if (it["class"].as_mstring() == L"event" && it["eventTypeId"].as_mstring() == L"27:33000") { - if (bLocalTime) { - CMStringA id = getMStringA(hContact, DB_KEY_ID); - int pos = id.Find('@'); - CMStringA nick = (pos == -1) ? id : id.Left(pos); - DB::AUTH_BLOB blob(hContact, nick, nullptr, nullptr, id, nullptr); - - PROTORECVEVENT pre = {}; - pre.timestamp = (uint32_t)time(0); - pre.lParam = blob.size(); - pre.szMessage = blob; - ProtoChainRecv(hContact, PSR_AUTH, 0, (LPARAM)&pre); - } - return; - } - } - - int iMsgTime = (bLocalTime) ? time(0) : it["time"].as_int(); - bool bIsOutgoing = it["outgoing"].as_bool(), bIsFileTransfer = false; - IcqFileInfo *pFileInfo = nullptr; - - if (!bCreateRead && !bIsOutgoing && wszText.Left(26) == L"https://files.icq.net/get/") { - pFileInfo = CheckFile(hContact, wszText, bIsFileTransfer); - if (!pFileInfo) - return; - - for (auto &jt : it["parts"]) { - CMStringW wszDescr(jt["captionedContent"]["caption"].as_mstring()); - if (!wszDescr.IsEmpty()) - pFileInfo->wszDescr = wszDescr; - } - } - - if (isChatRoom(hContact)) { - CMStringA reqId(it["reqId"].as_mstring()); - CheckOwnMessage(reqId, szMsgId, true); - - CMStringW wszSender(it["chat"]["sender"].as_mstring()); - CMStringW wszChatId(getMStringW(hContact, "ChatRoomID")); - - if (bIsFileTransfer) { - wszText = pFileInfo->szUrl; - if (!pFileInfo->wszDescr) - wszText.AppendFormat(L"\r\n%s", pFileInfo->wszDescr.c_str()); - delete pFileInfo; - } - - GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; - gce.pszID.w = wszChatId; - gce.dwFlags = GCEF_ADDTOLOG; - gce.pszUID.w = wszSender; - gce.pszText.w = wszText; - gce.time = iMsgTime; - gce.bIsMe = wszSender == m_szOwnId; - Chat_Event(&gce); - return; - } - - // skip own messages, just set the server msgid - CMStringA reqId(it["reqId"].as_mstring()); - if (CheckOwnMessage(reqId, szMsgId, true)) { - debugLogA("Skipping our own message %s", szMsgId.c_str()); - return; - } - - // ignore duplicates - MEVENT hDbEvent = db_event_getById(m_szModuleName, szMsgId); - if (hDbEvent != 0) { - debugLogA("Message %s already exists", szMsgId.c_str()); - return; - } - - // convert a file info into Miranda's file transfer - if (bIsFileTransfer) { - auto *ft = new IcqFileTransfer(hContact, pFileInfo->szUrl); - ft->pfts.totalBytes = ft->pfts.currentFileSize = pFileInfo->dwFileSize; - ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); - - PROTORECVFILE pre = {}; - pre.dwFlags = PRFF_UNICODE; - pre.fileCount = 1; - pre.timestamp = iMsgTime; - pre.files.w = &ft->m_wszShortName; - pre.descr.w = pFileInfo->wszDescr; - pre.lParam = (LPARAM)ft; - ProtoChainRecvFile(hContact, &pre); - - delete pFileInfo; - return; - } - - // suppress notifications for already loaded/processed messages - __int64 storedLastId = getId(hContact, DB_KEY_LASTMSGID); - if (msgId <= storedLastId) { - debugLogA("Parsing old/processed message with id %lld < %lld, setting CR to true", msgId, storedLastId); - bCreateRead = true; - } - - debugLogA("Adding message %d:%lld (CR=%d)", hContact, msgId, bCreateRead); - - ptrA szUtf(mir_utf8encodeW(wszText)); - - PROTORECVEVENT pre = {}; - if (bIsOutgoing) pre.flags |= PREF_SENT; - if (bCreateRead) pre.flags |= PREF_CREATEREAD; - pre.szMsgId = szMsgId; - pre.timestamp = iMsgTime; - pre.szMessage = szUtf; - ProtoChainRecvMsg(hContact, &pre); -} - -bool CIcqProto::RefreshRobustToken(AsyncHttpRequest *pOrigReq) -{ - if (!m_szRToken.IsEmpty()) - return true; - - auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/genToken", &CIcqProto::OnGenToken); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - - int ts = TS(); - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("nonce", CMStringA(FORMAT, "%d-%d", ts, rand() % 10)) << INT_PARAM("ts", ts); - CalcHash(pReq); - - CMStringA szAgent(FORMAT, "%S Mail.ru Windows ICQ (version 10.0.1999)", (wchar_t*)m_szOwnId); - pReq->AddHeader("User-Agent", szAgent); - if (!ExecuteRequest(pReq)) { -LBL_Error: - (this->*(pOrigReq->m_pFunc))(nullptr, pOrigReq); - return false; - } - if (m_szRToken.IsEmpty()) - goto LBL_Error; - - // now add this token - bool bRet = false; - pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/addClient", &CIcqProto::OnAddClient); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("f", "json") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", ts) - << CHAR_PARAM("client", "icq") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken); - pReq->pUserInfo = &bRet; - if (!ExecuteRequest(pReq)) - goto LBL_Error; - - return bRet; -} - -void CIcqProto::RetrieveUserInfo(MCONTACT hContact) -{ - auto *pReq = new AsyncRapiRequest(this, "getUserInfo", &CIcqProto::OnGetUserInfo); - pReq->params << WCHAR_PARAM("sn", GetUserId(hContact)); - pReq->hContact = hContact; - Push(pReq); -} - -void CIcqProto::RetrieveUserHistory(MCONTACT hContact, __int64 startMsgId, bool bCreateRead) -{ - if (startMsgId == 0) - startMsgId = -1; - - __int64 patchVer = getId(hContact, DB_KEY_PATCHVER); - if (patchVer == 0) - patchVer = 1; - - auto *pReq = new AsyncRapiRequest(this, "getHistory", &CIcqProto::OnGetUserHistory); - #ifndef _DEBUG - pReq->flags |= NLHRF_NODUMPSEND; - #endif - pReq->hContact = hContact; - pReq->pUserInfo = (bCreateRead) ? pReq : 0; - pReq->params << WCHAR_PARAM("sn", GetUserId(hContact)) << INT64_PARAM("fromMsgId", startMsgId) << INT_PARAM("count", 1000) - << SINT64_PARAM("patchVersion", patchVer) << CHAR_PARAM("language", "ru-ru"); - Push(pReq); -} - -void CIcqProto::SetServerStatus(int iStatus) -{ - const char *szStatus = "online"; - int invisible = 0; - - switch (iStatus) { - case ID_STATUS_OFFLINE: szStatus = "offline"; break; - case ID_STATUS_NA: szStatus = "occupied"; break; - case ID_STATUS_AWAY: - case ID_STATUS_DND: szStatus = "away"; break; - case ID_STATUS_INVISIBLE: - invisible = 1; - } - - Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/presence/setState") - << AIMSID(this) << CHAR_PARAM("view", szStatus) << INT_PARAM("invisible", invisible)); - - if (iStatus == ID_STATUS_OFFLINE && !getByte(DB_KEY_PHONEREG)) { - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, ICQ_API_SERVER "/aim/endSession", &CIcqProto::OnSessionEnd); - pReq << AIMSID(this) << INT_PARAM("invalidateToken", 1); - ExecuteRequest(pReq); - } - - int iOldStatus = m_iStatus; m_iStatus = iStatus; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); -} - -void CIcqProto::ShutdownSession() -{ - if (m_bTerminated) - return; - - debugLogA("CIcqProto::ShutdownSession"); - - // shutdown all resources - DropQueue(); - - if (m_hWorkerThread) - SetEvent(m_evRequestsQueue); - - OnLoggedOut(); - - for (auto &it : m_ConnPool) { - if (it.s) { - Netlib_Shutdown(it.s); - it.s = nullptr; - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -#define EVENTS "myInfo,presence,buddylist,typing,dataIM,userAddedToBuddyList,mchat,hist,hiddenChat,diff,permitDeny,imState,notification,apps" -#define FIELDS "aimId,buddyIcon,bigBuddyIcon,iconId,bigIconId,largeIconId,displayId,friendly,offlineMsg,state,statusMsg,userType,phoneNumber,cellNumber,smsNumber,workNumber,otherNumber,capabilities,ssl,abPhoneNumber,moodIcon,lastName,abPhones,abContactName,lastseen,mute,livechat,official" - -void CIcqProto::StartSession() -{ - ptrA szDeviceId(getStringA("DeviceId")); - if (szDeviceId == nullptr) { - UUID deviceId; - UuidCreate(&deviceId); - RPC_CSTR szId; - UuidToStringA(&deviceId, &szId); - szDeviceId = mir_strdup((char*)szId); - setString("DeviceId", szDeviceId); - RpcStringFreeA(&szId); - } - - int ts = TS(); - CMStringA nonce(FORMAT, "%d-2", ts); - CMStringA caps(WIM_CAP_UNIQ_REQ_ID "," WIM_CAP_EMOJI "," WIM_CAP_MAIL_NOTIFICATIONS "," WIM_CAP_INTRO_DLG_STATE); - if (g_bSecureIM) { - caps.AppendChar(','); - caps.Append(NG_CAP_SECUREIM); - } - - MFileVersion v; - Miranda_GetFileVersion(&v); - caps.AppendFormat(",%02x%02x%02x%02x%02x%02x%02x%02x%04x%04x%04x%04x", 'M', 'i', 'N', 'G', - __MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM, v[0], v[1], v[2], v[3]); - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/aim/startSession", &CIcqProto::OnStartSession); - pReq << CHAR_PARAM("a", m_szAToken) << INT_PARAM("activeTimeout", 180) << CHAR_PARAM("assertCaps", caps) - << INT_PARAM("buildNumber", __BUILD_NUM) << CHAR_PARAM("deviceId", szDeviceId) << CHAR_PARAM("events", EVENTS) - << CHAR_PARAM("f", "json") << CHAR_PARAM("imf", "plain") << CHAR_PARAM("inactiveView", "offline") - << CHAR_PARAM("includePresenceFields", FIELDS) << CHAR_PARAM("invisible", "false") - << CHAR_PARAM("k", appId()) << INT_PARAM("mobile", 0) << CHAR_PARAM("nonce", nonce) << CHAR_PARAM("r", pReq->m_reqId) - << INT_PARAM("rawMsg", 0) << INT_PARAM("sessionTimeout", 7776000) << INT_PARAM("ts", ts) << CHAR_PARAM("view", "online"); - - CalcHash(pReq); - Push(pReq); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnAddBuddy(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - JsonReply root(pReply); - if (root.error() != 200) - return; - - CMStringW wszId = getMStringW(pReq->hContact, DB_KEY_ID); - for (auto &it : root.data()["results"]) { - if (it["buddy"].as_mstring() != wszId) - continue; - - switch (int iResultCode = it["resultCode"].as_int()) { - case 0: // success - case 3: // already in contact list - break; - - default: - debugLogA("Contact %d failed to add: error %d", pReq->hContact, iResultCode); - - POPUPDATAW Popup = {}; - Popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR)); - wcsncpy_s(Popup.lpwzText, TranslateT("Buddy addition failed"), _TRUNCATE); - wcsncpy_s(Popup.lpwzContactName, Clist_GetContactDisplayName(pReq->hContact), _TRUNCATE); - Popup.iSeconds = 20; - PUAddPopupW(&Popup); - - // Contact::RemoveFromList(pReq->hContact); - } - - RetrieveUserInfo(pReq->hContact); - Contact::PutOnList(pReq->hContact); - } -} - -void CIcqProto::OnAddClient(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - bool *pRet = (bool*)pReq->pUserInfo; - - RobustReply reply(pReply); - if (reply.error() != 20000) { - *pRet = false; - return; - } - - const JSONNode &results = reply.results(); - m_iRClientId = results["clientId"].as_int(); - setDword(DB_KEY_RCLIENTID, m_iRClientId); - *pRet = true; -} - -void CIcqProto::OnCheckPassword(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - JsonReply root(pReply); - switch (root.error()) { - case 200: - break; - - case 330: - case 440: - ConnectionFailed(LOGINERR_WRONGPASSWORD, root.error()); - return; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); - return; - } - - JSONNode &data = root.data(); - m_szAToken = data["token"]["a"].as_mstring(); - mir_urlDecode(m_szAToken.GetBuffer()); - setString(DB_KEY_ATOKEN, m_szAToken); - - CMStringA szSessionSecret = data["sessionSecret"].as_mstring(); - CMStringA szPassTemp = m_szPassword; - - unsigned int len; - uint8_t hashOut[MIR_SHA256_HASH_SIZE]; - HMAC(EVP_sha256(), szPassTemp, szPassTemp.GetLength(), (uint8_t*)szSessionSecret.c_str(), szSessionSecret.GetLength(), hashOut, &len); - m_szSessionKey = ptrA(mir_base64_encode(hashOut, sizeof(hashOut))); - setString(DB_KEY_SESSIONKEY, m_szSessionKey); - - CMStringW szUin = data["loginId"].as_mstring(); - if (szUin) - m_szOwnId = szUin; - - int srvTS = data["hostTime"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - - StartSession(); -} - -void CIcqProto::OnFileContinue(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pOld) -{ - IcqFileTransfer *pTransfer = (IcqFileTransfer*)pOld->pUserInfo; - if (pTransfer->m_bCanceled) { -LBL_Error: - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, pTransfer); - delete pTransfer; - return; - } - - switch (pReply->resultCode) { - case 200: // final ok - case 206: // partial ok - break; - - default: - goto LBL_Error; - } - - // file transfer succeeded? - if (pTransfer->pfts.currentFileProgress == pTransfer->pfts.currentFileSize) { - FileReply root(pReply); - if (root.error() != 200) - goto LBL_Error; - - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, pTransfer); - - const JSONNode &data = root.data(); - CMStringW wszUrl(data["static_url"].as_mstring()); - - JSONNode bundle, contents; contents.set_name("captionedContent"); - contents << WCHAR_PARAM("caption", pTransfer->m_wszDescr) << WCHAR_PARAM("url", wszUrl); - bundle << CHAR_PARAM("mediaType", "text") << CHAR_PARAM("text", "") << contents; - CMStringW wszParts(FORMAT, L"[%s]", ptrW(json_write(&bundle)).get()); - - if (!pTransfer->m_wszDescr.IsEmpty()) - wszUrl += L" " + pTransfer->m_wszDescr; - - int id = InterlockedIncrement(&m_msgId); - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/im/sendIM", &CIcqProto::OnSendMessage); - - auto *pOwn = new IcqOwnMessage(pTransfer->pfts.hContact, id, pReq->m_reqId); - pReq->pUserInfo = pOwn; - { - mir_cslock lck(m_csOwnIds); - m_arOwnIds.insert(pOwn); - } - - pReq << AIMSID(this) << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("mentions", "") << WCHAR_PARAM("message", wszUrl) - << CHAR_PARAM("offlineIM", "true") << WCHAR_PARAM("parts", wszParts) << WCHAR_PARAM("t", GetUserId(pTransfer->pfts.hContact)) << INT_PARAM("ts", TS()); - Push(pReq); - - delete pTransfer; - return; - } - - // else send the next portion - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, pTransfer->m_szHost, &CIcqProto::OnFileContinue); - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", TS()); - CalcHash(pReq); - pReq->m_szUrl.AppendChar('?'); - pReq->m_szUrl += pReq->m_szParam; pReq->m_szParam.Empty(); - pReq->pUserInfo = pTransfer; - pTransfer->FillHeaders(pReq); - Push(pReq); - - pTransfer->pfts.currentFileTime = time(0); - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, pTransfer, (LPARAM)&pTransfer->pfts); -} - -void CIcqProto::OnFileInit(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pOld) -{ - IcqFileTransfer *pTransfer = (IcqFileTransfer*)pOld->pUserInfo; - if (pTransfer->m_bCanceled) { -LBL_Error: - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, pTransfer); - delete pTransfer; - return; - } - - FileReply root(pReply); - if (root.error() != 200) - goto LBL_Error; - - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, pTransfer); - pTransfer->pfts.currentFileTime = time(0); - - const JSONNode &data = root.data(); - CMStringW wszHost(data["host"].as_mstring()); - CMStringW wszUrl(data["url"].as_mstring()); - pTransfer->m_szHost = L"https://" + wszHost + wszUrl; - - auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, pTransfer->m_szHost, &CIcqProto::OnFileContinue); - pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", TS()); - CalcHash(pReq); - pReq->m_szUrl.AppendChar('?'); - pReq->m_szUrl += pReq->m_szParam; pReq->m_szParam.Empty(); - pReq->pUserInfo = pTransfer; - pTransfer->FillHeaders(pReq); - Push(pReq); - - ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, pTransfer, (LPARAM)&pTransfer->pfts); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Support for stickers - -void CIcqProto::OnGetSticker(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - if (pReply->resultCode != 200) { - debugLogA("Error getting sticker: %d", pReply->resultCode); - return; - } - - CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); - CreateDirectoryTreeW(wszPath); - - CMStringW wszFileName(FORMAT, L"%s\\STK{%s}.png", wszPath.c_str(), pReq->pUserInfo); - FILE *out = _wfopen(wszFileName, L"wb"); - fwrite(pReply->pData, 1, pReply->dataLength, out); - fclose(out); - - SMADD_CONT cont = { 1, m_szModuleName, wszFileName }; - CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// File info request - -void CIcqProto::OnFileInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - IcqFileInfo **res = (IcqFileInfo **)pReq->pUserInfo; - *res = nullptr; - - RobustReply root(pReply); - if (root.error() != 200) - return; - - auto &pData = root.result(); - auto &pInfo = pData["info"] ; - std::string szUrl(pInfo["dlink"].as_string()); - if (szUrl.empty()) - return; - - MarkAsRead(pReq->hContact); - - bool bIsSticker; - CMStringW wszDescr(pInfo["file_name"].as_mstring()); - if (!mir_wstrncmp(wszDescr, L"dnld", 4)) { - bIsSticker = true; - - std::string szPreview = pData["previews"]["192"].as_string(); - if (!szPreview.empty()) - szUrl = szPreview; - } - else bIsSticker = false; - - mir_urlDecode(&*szUrl.begin()); - - *res = new IcqFileInfo(szUrl, wszDescr, pInfo["file_size"].as_int()); - res[0]->bIsSticker = bIsSticker; -} - -void CIcqProto::OnFileRecv(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - auto *ft = (IcqFileTransfer*)pReq->pUserInfo; - - if (pReply->resultCode != 200) { -LBL_Error: - FileCancel(pReq->hContact, ft); - return; - } - - ft->hWaitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); - if (ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, ft, (LPARAM)&ft->pfts)) - WaitForSingleObject(ft->hWaitEvent, INFINITE); - CloseHandle(ft->hWaitEvent); - - debugLogW(L"Saving to [%s]", ft->pfts.szCurrentFile.w); - int fileId = _wopen(ft->pfts.szCurrentFile.w, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, _S_IREAD | _S_IWRITE); - if (fileId == -1) { - debugLogW(L"Cannot open [%s] for writing", ft->pfts.szCurrentFile.w); - goto LBL_Error; - } - - int result = _write(fileId, pReply->pData, pReply->dataLength); - _close(fileId); - if (result != pReply->dataLength) { - debugLogW(L"Error writing data into [%s]", ft->pfts.szCurrentFile.w); - goto LBL_Error; - } - - ft->pfts.totalProgress += pReply->dataLength; - ft->pfts.currentFileProgress += pReply->dataLength; - ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts); - - ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, ft); - delete ft; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::OnGenToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) -{ - RobustReply root(pReply); - if (root.error() != 20000) - return; - - auto &results = root.results(); - m_szRToken = results["authToken"].as_mstring(); -} - -void CIcqProto::OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - RobustReply root(pReply); - if (root.error() != 20000) - return; - - __int64 lastMsgId = getId(pReq->hContact, DB_KEY_LASTMSGID); - - int count = 0; - auto &results = root.results(); - for (auto &it : results["messages"]) { - ParseMessage(pReq->hContact, lastMsgId, it, pReq->pUserInfo != nullptr, false); - count++; - } - - setId(pReq->hContact, DB_KEY_LASTMSGID, lastMsgId); - - if (count >= 999) - RetrieveUserHistory(pReq->hContact, lastMsgId, pReq->pUserInfo != nullptr); -} - -void CIcqProto::OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - RobustReply root(pReply); - if (root.error() != 20000) { - ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, nullptr); - return; - } - - ParseBuddyInfo(root.results(), pReq->hContact, true); - - ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, nullptr); -} - -void CIcqProto::OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - switch (root.error()) { - case 200: - break; - - case 451: - // session forcibly closed from site - delSetting(DB_KEY_ATOKEN); - delSetting(DB_KEY_SESSIONKEY); - CheckPassword(); - return; - - case 401: - delSetting(DB_KEY_ATOKEN); - delSetting(DB_KEY_SESSIONKEY); - if (root.detail() == 1002) // session expired - CheckPassword(); - else - ConnectionFailed(LOGINERR_WRONGPASSWORD, root.error()); - return; - - case 400: - if (root.detail() == 1015 && m_iTimeShift == 0) { // wrong timestamp - JSONNode &data = root.data(); - int srvTS = data["ts"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - StartSession(); - return; - } - __fallthrough; - - default: - ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); - return; - } - - JSONNode &data = root.data(); - m_fetchBaseURL = data["fetchBaseURL"].as_mstring(); - m_aimsid = data["aimsid"].as_mstring(); - - ProcessMyInfo(data["myInfo"]); - - int srvTS = data["ts"].as_int(); - m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; - - OnLoggedIn(); - - for (auto &it : data["events"]) - ProcessEvent(it); - - ForkThread(&CIcqProto::PollThread); -} - -void CIcqProto::OnReceiveAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - PROTO_AVATAR_INFORMATION ai = {}; - ai.hContact = pReq->hContact; - - if (pReply->resultCode != 200 || pReply->pData == nullptr) { -LBL_Error: - ProtoBroadcastAck(pReq->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, HANDLE(&ai), 0); - return; - } - - const char *szContentType = Netlib_GetHeader(pReply, "Content-Type"); - if (szContentType == nullptr) - szContentType = "image/jpeg"; - - ai.format = ProtoGetAvatarFormatByMimeType(szContentType); - setByte(pReq->hContact, "AvatarType", ai.format); - GetAvatarFileName(pReq->hContact, ai.filename, _countof(ai.filename)); - - FILE *out = _wfopen(ai.filename, L"wb"); - if (out == nullptr) - goto LBL_Error; - - fwrite(pReply->pData, pReply->dataLength, 1, out); - fclose(out); - - if (pReq->hContact != 0) { - ProtoBroadcastAck(pReq->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, HANDLE(&ai), 0); - debugLogW(L"Broadcast new avatar: %s", ai.filename); - } - else ReportSelfAvatarChanged(); -} - -void CIcqProto::OnSearchResults(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - RobustReply root(pReply); - if (root.error() != 20000) { - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_FAILED, (HANDLE)pReq, 0); - return; - } - - const JSONNode &results = root.results(); - - PROTOSEARCHRESULT psr = {}; - psr.cbSize = sizeof(psr); - psr.flags = PSR_UNICODE; - for (auto &it : results["persons"]) { - CMStringW wszId = it["sn"].as_mstring(); - if (wszId == m_szOwnId) - continue; - - CMStringW wszNick = it["friendly"].as_mstring(); - CMStringW wszFirst = it["firstName"].as_mstring(); - CMStringW wszLast = it["lastName"].as_mstring(); - - psr.id.w = wszId.GetBuffer(); - psr.nick.w = wszNick.GetBuffer(); - psr.firstName.w = wszFirst.GetBuffer(); - psr.lastName.w = wszLast.GetBuffer(); - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)pReq, LPARAM(&psr)); - } - - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)pReq); -} - -void CIcqProto::OnSendMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) -{ - IcqOwnMessage *ownMsg = (IcqOwnMessage*)pReq->pUserInfo; - - JsonReply root(pReply); - if (root.error() != 200) { - ProtoBroadcastAck(ownMsg->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)ownMsg->m_msgid, 0); - - mir_cslock lck(m_csOwnIds); - m_arOwnIds.remove(ownMsg); - } - - if (g_bMessageState) - CallService(MS_MESSAGESTATE_UPDATE, ownMsg->m_hContact, MRD_TYPE_DELIVERED); - - const JSONNode &data = root.data(); - CMStringA reqId(root.requestId()); - CMStringA msgId(data["histMsgId"].as_mstring()); - CheckOwnMessage(reqId, msgId, false); - CheckLastId(ownMsg->m_hContact, data); -} - -void CIcqProto::OnSessionEnd(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) -{ - JsonReply root(pReply); - if (root.error() == 200) { - m_szAToken.Empty(); - delSetting(DB_KEY_ATOKEN); - - m_szSessionKey.Empty(); - delSetting(DB_KEY_SESSIONKEY); - - ShutdownSession(); - } -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +#pragma comment(lib, "libcrypto.lib") + +void CIcqProto::CheckAvatarChange(MCONTACT hContact, const JSONNode &ev) +{ + CMStringW wszIconId(ev["bigIconId"].as_mstring()); + if (wszIconId.IsEmpty()) + wszIconId = ev["iconId"].as_mstring(); + + if (!wszIconId.IsEmpty()) { + CMStringW oldIconID(getMStringW(hContact, "IconId")); + if (wszIconId == oldIconID) { + wchar_t wszFullName[MAX_PATH]; + GetAvatarFileName(hContact, wszFullName, _countof(wszFullName)); + if (_waccess(wszFullName, 0) == 0) + return; + } + + setWString(hContact, "IconId", wszIconId); + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/expressions/get", &CIcqProto::OnReceiveAvatar); + pReq << CHAR_PARAM("f", "native") << WCHAR_PARAM("t", GetUserId(hContact)) << CHAR_PARAM("type", "bigBuddyIcon"); + pReq->hContact = hContact; + Push(pReq); + } + else delSetting(hContact, "IconId"); +} + +void CIcqProto::CheckLastId(MCONTACT hContact, const JSONNode &ev) +{ + __int64 msgId = _wtoi64(ev["histMsgId"].as_mstring()); + __int64 lastId = getId(hContact, DB_KEY_LASTMSGID); + if (msgId > lastId) + setId(hContact, DB_KEY_LASTMSGID, msgId); +} + +MCONTACT CIcqProto::CheckOwnMessage(const CMStringA &reqId, const CMStringA &msgId, bool bRemove) +{ + IcqOwnMessage *pOwn = nullptr; + { + mir_cslock lck(m_csOwnIds); + for (auto &it : m_arOwnIds) { + if (reqId == it->m_guid) { + pOwn = it; + break; + } + } + } + + if (pOwn == nullptr) + return 0; + + ProtoBroadcastAck(pOwn->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)pOwn->m_msgid, (LPARAM)msgId.c_str()); + + MCONTACT ret = pOwn->m_hContact; + if (bRemove) { + mir_cslock lck(m_csOwnIds); + m_arOwnIds.remove(pOwn); + } + return ret; +} + +void CIcqProto::CheckPassword() +{ + char mirVer[100]; + Miranda_GetVersionText(mirVer, _countof(mirVer)); + + m_szAToken = getMStringA(DB_KEY_ATOKEN); + m_iRClientId = getDword(DB_KEY_RCLIENTID); + m_szSessionKey = getMStringA(DB_KEY_SESSIONKEY); + if (!m_szAToken.IsEmpty() && !m_szSessionKey.IsEmpty()) { + StartSession(); + return; + } + + if (m_isMra) { + m_bError462 = false; + SendMrimLogin(nullptr); + } + else { + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, "https://api.login.icq.net/auth/clientLogin", &CIcqProto::OnCheckPassword); + pReq << CHAR_PARAM("clientName", "Miranda NG") << CHAR_PARAM("clientVersion", mirVer) << CHAR_PARAM("devId", appId()) + << CHAR_PARAM("f", "json") << CHAR_PARAM("tokenType", "longTerm") << WCHAR_PARAM("s", m_szOwnId) << CHAR_PARAM("pwd", m_szPassword); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + Push(pReq); + } +} + +IcqFileInfo* CIcqProto::CheckFile(MCONTACT hContact, CMStringW &wszText, bool &bIsFile) +{ + CMStringW wszUrl(wszText.Mid(26)); + int idx = wszUrl.Find(' '); + if (idx != -1) + wszUrl.Truncate(idx); + + bIsFile = false; + IcqFileInfo *pFileInfo = nullptr; + + // is it already downloaded sticker? + CMStringW wszLoadedPath(FORMAT, L"%s\\%S\\Stickers\\STK{%s}.png", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName, wszUrl.c_str()); + if (!_waccess(wszLoadedPath, 0)) { + pFileInfo = (IcqFileInfo *)this; + wszText.Format(L"STK{%s}", wszUrl.c_str()); + } + else { + // download file info + CMStringA szUrl(FORMAT, ICQ_FILE_SERVER "/info/%S/", wszUrl.c_str()); + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, szUrl, &CIcqProto::OnFileInfo); + pReq->hContact = hContact; + pReq->pUserInfo = &pFileInfo; + pReq << CHAR_PARAM("aimsid", m_aimsid) << CHAR_PARAM("previews", "192,600,xlarge"); + if (!ExecuteRequest(pReq)) + return nullptr; + + // is it a sticker? + if (pFileInfo->bIsSticker) { + if (ServiceExists(MS_SMILEYADD_LOADCONTACTSMILEYS)) { + auto *pNew = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, pFileInfo->szUrl, &CIcqProto::OnGetSticker); + pNew->flags |= NLHRF_NODUMP | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; + pNew->pUserInfo = wszUrl.GetBuffer(); + pNew->AddHeader("Sec-Fetch-User", "?1"); + pNew->AddHeader("Sec-Fetch-Site", "cross-site"); + pNew->AddHeader("Sec-Fetch-Mode", "navigate"); + if (!ExecuteRequest(pNew)) + return nullptr; + + wszText.Format(L"STK{%s}", wszUrl.c_str()); + delete pFileInfo; + } + else wszText = TranslateT("SmileyAdd plugin required to support stickers"); + } + else bIsFile = true; + } + + return pFileInfo; +} + +void CIcqProto::CheckStatus() +{ + time_t now = time(0); + int diff1 = m_iTimeDiff1, diff2 = m_iTimeDiff2; + + for (auto &it : m_arCache) { + // this contact is really offline and is on the first timer + // if the first timer is expired, we clear it and look for the second status + if (diff1 && it->m_timer1 && now - it->m_timer1 > diff1) { + it->m_timer1 = 0; + + // if the second timer is set up, activate it + if (m_iTimeDiff2) { + setWord(it->m_hContact, "Status", m_iStatus2); + it->m_timer2 = now; + } + // if the second timer is not set, simply mark a contact as offline + else setWord(it->m_hContact, "Status", ID_STATUS_OFFLINE); + continue; + } + + // if the second timer is expired, set status to offline + if (diff2 && it->m_timer2 && now - it->m_timer2 > m_iTimeDiff2) { + it->m_timer2 = 0; + setWord(it->m_hContact, "Status", ID_STATUS_OFFLINE); + } + } +} + +void CIcqProto::ConnectionFailed(int iReason, int iErrorCode) +{ + debugLogA("ConnectionFailed -> reason %d", iReason); + + if (m_bErrorPopups) { + POPUPDATAW Popup = {}; + Popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR), true); + wcscpy_s(Popup.lpwzContactName, m_tszUserName); + switch (iReason) { + case LOGINERR_BADUSERID: + mir_snwprintf(Popup.lpwzText, TranslateT("You have not entered a login or password.\nConfigure this in Options -> Network -> ICQ and try again.")); + break; + case LOGINERR_WRONGPASSWORD: + mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nYour login or password was rejected (%d)."), iErrorCode); + break; + case LOGINERR_NONETWORK: + case LOGINERR_NOSERVER: + mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nThe server is temporarily unavailable (%d)."), iErrorCode); + break; + default: + mir_snwprintf(Popup.lpwzText, TranslateT("Connection failed.\nUnknown error during sign on: %d"), iErrorCode); + break; + } + PUAddPopupW(&Popup); + } + + ProtoBroadcastAck(0, ACKTYPE_LOGIN, ACKRESULT_FAILED, nullptr, iReason); + ShutdownSession(); +} + +void CIcqProto::MoveContactToGroup(MCONTACT hContact, const wchar_t *pwszGroup, const wchar_t *pwszNewGroup) +{ + // otherwise we'll get a server error + if (!mir_wstrlen(pwszGroup)) + return; + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/buddylist/moveBuddy") << AIMSID(this) << WCHAR_PARAM("buddy", GetUserId(hContact)) + << GROUP_PARAM("group", pwszGroup); + if (mir_wstrlen(pwszNewGroup)) + pReq << GROUP_PARAM("newGroup", pwszNewGroup); + Push(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnLoggedIn() +{ + debugLogA("CIcqProto::OnLoggedIn"); + m_bOnline = true; + m_impl.m_heartBeat.Start(1000); + + for (auto &it : m_arCache) + it->m_timer1 = it->m_timer2 = 0; + + SetServerStatus(m_iDesiredStatus); + RetrieveUserInfo(0); + GetPermitDeny(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnLoggedOut() +{ + debugLogA("CIcqProto::OnLoggedOut"); + m_bOnline = false; + m_impl.m_heartBeat.Stop(); + + for (auto &it : m_arCache) + it->m_timer1 = it->m_timer2 = 0; + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)m_iStatus, ID_STATUS_OFFLINE); + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + + setAllContactStatuses(ID_STATUS_OFFLINE, false); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::MarkAsRead(MCONTACT hContact) +{ + if (!m_bOnline) + return; + + m_impl.m_markRead.Start(200); + + auto *pCache = FindContactByUIN(GetUserId(hContact)); + if (pCache) { + mir_cslock lck(m_csMarkReadQueue); + if (m_arMarkReadQueue.indexOf(pCache) == -1) + m_arMarkReadQueue.insert(pCache); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MCONTACT CIcqProto::ParseBuddyInfo(const JSONNode &buddy, MCONTACT hContact, bool bIsPartial) +{ + // user chat? + CMStringW wszId(buddy["aimId"].as_mstring()); + if (IsChat(wszId)) { + CMStringW wszChatId(buddy["aimId"].as_mstring()); + CMStringW wszChatName(buddy["friendly"].as_mstring()); + + auto *pContact = FindContactByUIN(wszId); + if (pContact && pContact->m_iApparentMode == ID_STATUS_OFFLINE) + return INVALID_CONTACT_ID; + + auto *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszChatId, wszChatName); + if (si == nullptr) + return INVALID_CONTACT_ID; + + Chat_AddGroup(si, TranslateT("admin")); + Chat_AddGroup(si, TranslateT("member")); + Chat_Control(m_szModuleName, wszChatId, m_bHideGroupchats ? WINDOW_HIDDEN : SESSION_INITDONE); + Chat_Control(m_szModuleName, wszChatId, SESSION_ONLINE); + return si->hContact; + } + + bool bIgnored = !IsValidType(buddy); + if (hContact == INVALID_CONTACT_ID) { + if (bIgnored) + return INVALID_CONTACT_ID; + + hContact = CreateContact(wszId, false); + FindContactByUIN(wszId)->m_bInList = true; + } + else if (bIgnored) { + db_delete_contact(hContact); + return INVALID_CONTACT_ID; + } + + CMStringA szVer; + bool bVersionDetected = false, bSecureIM = false; + + for (auto &it : buddy["capabilities"]) { + CMStringW wszCap(it.as_mstring()); + if (wszCap.GetLength() != 32) + continue; + + uint8_t cap[16]; + hex2binW(wszCap, cap, sizeof(cap)); + if (!memcmp(cap, "MiNG", 4)) { // Miranda + int v[4]; + if (4 == swscanf(wszCap.c_str() + 16, L"%04x%04x%04x%04x", &v[0], &v[1], &v[2], &v[3])) { + szVer.Format("Miranda NG %d.%d.%d.%d (ICQ %d.%d.%d.%d)", v[0], v[1], v[2], v[3], cap[4], cap[5], cap[6], cap[7]); + setString(hContact, "MirVer", szVer); + bVersionDetected = true; + } + } + else if (wszCap == _A2W(NG_CAP_SECUREIM)) { + bSecureIM = bVersionDetected = true; + } + else if (!memcmp(cap, "Mod by Mikanoshi", 16)) { + szVer = "R&Q build by Mikanoshi"; + bVersionDetected = true; + } + else if (!memcmp(cap, "Mandarin IM", 11)) { + szVer = "Mandarin IM"; + bVersionDetected = true; + } + } + + if (bVersionDetected) { + if (bSecureIM) + szVer.Append(" + SecureIM"); + setString(hContact, "MirVer", szVer); + } + else delSetting(hContact, "MirVer"); + + const JSONNode &var = buddy["friendly"]; + if (var) + setWString(hContact, "Nick", var.as_mstring()); + + if (buddy["deleted"].as_bool()) { + setByte(hContact, "IcqDeleted", 1); + Contact::PutOnList(hContact); + } + + Json2string(hContact, buddy, "emailId", "Email", bIsPartial); + Json2string(hContact, buddy, "cellNumber", "Cellular", bIsPartial); + Json2string(hContact, buddy, "workNumber", "CompanyPhone", bIsPartial); + + // we shall not remove existing phone number anyhow + Json2string(hContact, buddy, "phoneNumber", DB_KEY_PHONE, true); + + Json2int(hContact, buddy, "official", "Official", bIsPartial); + Json2int(hContact, buddy, "onlineTime", DB_KEY_ONLINETS, bIsPartial); + Json2int(hContact, buddy, "idleTime", "IdleTS", bIsPartial); + Json2int(hContact, buddy, "memberSince", DB_KEY_MEMBERSINCE, bIsPartial); + + int iStatus = StatusFromPresence(buddy, hContact); + if (iStatus > 0) + setWord(hContact, "Status", iStatus); + + const JSONNode &profile = buddy["profile"]; + if (profile) { + Json2string(hContact, profile, "friendlyName", DB_KEY_ICQNICK, bIsPartial); + Json2string(hContact, profile, "firstName", "FirstName", bIsPartial); + Json2string(hContact, profile, "lastName", "LastName", bIsPartial); + Json2string(hContact, profile, "aboutMe", DB_KEY_ABOUT, bIsPartial); + + ptrW wszNick(getWStringA(hContact, "Nick")); + if (!wszNick) { + CMStringW srvNick = profile["friendlyName"].as_mstring(); + if (!srvNick.IsEmpty()) + setWString(hContact, "Nick", srvNick); + } + + time_t birthDate = profile["birthDate"].as_int(); + if (birthDate != 0) { + struct tm *timeinfo = localtime(&birthDate); + if (timeinfo != nullptr) { + setWord(hContact, "BirthDay", timeinfo->tm_mday); + setWord(hContact, "BirthMonth", timeinfo->tm_mon+1); + setWord(hContact, "BirthYear", timeinfo->tm_year+1900); + } + } + + CMStringW str = profile["gender"].as_mstring(); + if (!str.IsEmpty()) { + if (str == "male") + setByte(hContact, "Gender", 'M'); + else if (str == "female") + setByte(hContact, "Gender", 'F'); + } + + for (auto &it : profile["homeAddress"]) { + Json2string(hContact, it, "city", "City", bIsPartial); + Json2string(hContact, it, "state", "State", bIsPartial); + Json2string(hContact, it, "country", "Country", bIsPartial); + } + } + + CMStringW str = buddy["statusMsg"].as_mstring(); + if (str.IsEmpty()) + db_unset(hContact, "CList", "StatusMsg"); + else + db_set_ws(hContact, "CList", "StatusMsg", str); + + CheckAvatarChange(hContact, buddy); + return hContact; +} + +void CIcqProto::ParseMessage(MCONTACT hContact, __int64 &lastMsgId, const JSONNode &it, bool bCreateRead, bool bLocalTime) +{ + CMStringA szMsgId(it["msgId"].as_mstring()); + __int64 msgId = _atoi64(szMsgId); + if (msgId > lastMsgId) + lastMsgId = msgId; + + CMStringW wszText; + const JSONNode &sticker = it["sticker"]; + if (sticker) { + CMStringW wszUrl, wszSticker(sticker["id"].as_mstring()); + int iCollectionId, iStickerId; + if (2 == swscanf(wszSticker, L"ext:%d:sticker:%d", &iCollectionId, &iStickerId)) + wszUrl.Format(L"https://c.icq.com/store/stickers/%d/%d/medium", iCollectionId, iStickerId); + else + wszUrl = TranslateT("Unknown sticker"); + wszText.Format(L"%s\n%s", TranslateT("User sent a sticker:"), wszUrl.c_str()); + } + else { + wszText = it["text"].as_mstring(); + wszText.TrimRight(); + + // user added you + if (it["class"].as_mstring() == L"event" && it["eventTypeId"].as_mstring() == L"27:33000") { + if (bLocalTime) { + CMStringA id = getMStringA(hContact, DB_KEY_ID); + int pos = id.Find('@'); + CMStringA nick = (pos == -1) ? id : id.Left(pos); + DB::AUTH_BLOB blob(hContact, nick, nullptr, nullptr, id, nullptr); + + PROTORECVEVENT pre = {}; + pre.timestamp = (uint32_t)time(0); + pre.lParam = blob.size(); + pre.szMessage = blob; + ProtoChainRecv(hContact, PSR_AUTH, 0, (LPARAM)&pre); + } + return; + } + } + + int iMsgTime = (bLocalTime) ? time(0) : it["time"].as_int(); + bool bIsOutgoing = it["outgoing"].as_bool(), bIsFileTransfer = false; + IcqFileInfo *pFileInfo = nullptr; + + if (!bCreateRead && !bIsOutgoing && wszText.Left(26) == L"https://files.icq.net/get/") { + pFileInfo = CheckFile(hContact, wszText, bIsFileTransfer); + if (!pFileInfo) + return; + + for (auto &jt : it["parts"]) { + CMStringW wszDescr(jt["captionedContent"]["caption"].as_mstring()); + if (!wszDescr.IsEmpty()) + pFileInfo->wszDescr = wszDescr; + } + } + + if (isChatRoom(hContact)) { + CMStringA reqId(it["reqId"].as_mstring()); + CheckOwnMessage(reqId, szMsgId, true); + + CMStringW wszSender(it["chat"]["sender"].as_mstring()); + CMStringW wszChatId(getMStringW(hContact, "ChatRoomID")); + + if (bIsFileTransfer) { + wszText = pFileInfo->szUrl; + if (!pFileInfo->wszDescr) + wszText.AppendFormat(L"\r\n%s", pFileInfo->wszDescr.c_str()); + delete pFileInfo; + } + + GCEVENT gce = { m_szModuleName, 0, GC_EVENT_MESSAGE }; + gce.pszID.w = wszChatId; + gce.dwFlags = GCEF_ADDTOLOG; + gce.pszUID.w = wszSender; + gce.pszText.w = wszText; + gce.time = iMsgTime; + gce.bIsMe = wszSender == m_szOwnId; + Chat_Event(&gce); + return; + } + + // skip own messages, just set the server msgid + CMStringA reqId(it["reqId"].as_mstring()); + if (CheckOwnMessage(reqId, szMsgId, true)) { + debugLogA("Skipping our own message %s", szMsgId.c_str()); + return; + } + + // ignore duplicates + MEVENT hDbEvent = db_event_getById(m_szModuleName, szMsgId); + if (hDbEvent != 0) { + debugLogA("Message %s already exists", szMsgId.c_str()); + return; + } + + // convert a file info into Miranda's file transfer + if (bIsFileTransfer) { + auto *ft = new IcqFileTransfer(hContact, pFileInfo->szUrl); + ft->pfts.totalBytes = ft->pfts.currentFileSize = pFileInfo->dwFileSize; + ft->pfts.szCurrentFile.w = ft->m_wszFileName.GetBuffer(); + + PROTORECVFILE pre = {}; + pre.dwFlags = PRFF_UNICODE; + pre.fileCount = 1; + pre.timestamp = iMsgTime; + pre.files.w = &ft->m_wszShortName; + pre.descr.w = pFileInfo->wszDescr; + pre.lParam = (LPARAM)ft; + ProtoChainRecvFile(hContact, &pre); + + delete pFileInfo; + return; + } + + // suppress notifications for already loaded/processed messages + __int64 storedLastId = getId(hContact, DB_KEY_LASTMSGID); + if (msgId <= storedLastId) { + debugLogA("Parsing old/processed message with id %lld < %lld, setting CR to true", msgId, storedLastId); + bCreateRead = true; + } + + debugLogA("Adding message %d:%lld (CR=%d)", hContact, msgId, bCreateRead); + + ptrA szUtf(mir_utf8encodeW(wszText)); + + PROTORECVEVENT pre = {}; + if (bIsOutgoing) pre.flags |= PREF_SENT; + if (bCreateRead) pre.flags |= PREF_CREATEREAD; + pre.szMsgId = szMsgId; + pre.timestamp = iMsgTime; + pre.szMessage = szUtf; + ProtoChainRecvMsg(hContact, &pre); +} + +bool CIcqProto::RefreshRobustToken(AsyncHttpRequest *pOrigReq) +{ + if (!m_szRToken.IsEmpty()) + return true; + + auto *pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/genToken", &CIcqProto::OnGenToken); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + + int ts = TS(); + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("nonce", CMStringA(FORMAT, "%d-%d", ts, rand() % 10)) << INT_PARAM("ts", ts); + CalcHash(pReq); + + CMStringA szAgent(FORMAT, "%S Mail.ru Windows ICQ (version 10.0.1999)", (wchar_t*)m_szOwnId); + pReq->AddHeader("User-Agent", szAgent); + if (!ExecuteRequest(pReq)) { +LBL_Error: + (this->*(pOrigReq->m_pFunc))(nullptr, pOrigReq); + return false; + } + if (m_szRToken.IsEmpty()) + goto LBL_Error; + + // now add this token + bool bRet = false; + pReq = new AsyncHttpRequest(CONN_RAPI, REQUEST_POST, ICQ_ROBUST_SERVER "/addClient", &CIcqProto::OnAddClient); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("f", "json") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", ts) + << CHAR_PARAM("client", "icq") << CHAR_PARAM("reqId", pReq->m_reqId) << CHAR_PARAM("authToken", m_szRToken); + pReq->pUserInfo = &bRet; + if (!ExecuteRequest(pReq)) + goto LBL_Error; + + return bRet; +} + +void CIcqProto::RetrieveUserInfo(MCONTACT hContact) +{ + auto *pReq = new AsyncRapiRequest(this, "getUserInfo", &CIcqProto::OnGetUserInfo); + pReq->params << WCHAR_PARAM("sn", GetUserId(hContact)); + pReq->hContact = hContact; + Push(pReq); +} + +void CIcqProto::RetrieveUserHistory(MCONTACT hContact, __int64 startMsgId, bool bCreateRead) +{ + if (startMsgId == 0) + startMsgId = -1; + + __int64 patchVer = getId(hContact, DB_KEY_PATCHVER); + if (patchVer == 0) + patchVer = 1; + + auto *pReq = new AsyncRapiRequest(this, "getHistory", &CIcqProto::OnGetUserHistory); + #ifndef _DEBUG + pReq->flags |= NLHRF_NODUMPSEND; + #endif + pReq->hContact = hContact; + pReq->pUserInfo = (bCreateRead) ? pReq : 0; + pReq->params << WCHAR_PARAM("sn", GetUserId(hContact)) << INT64_PARAM("fromMsgId", startMsgId) << INT_PARAM("count", 1000) + << SINT64_PARAM("patchVersion", patchVer) << CHAR_PARAM("language", "ru-ru"); + Push(pReq); +} + +void CIcqProto::SetServerStatus(int iStatus) +{ + const char *szStatus = "online"; + int invisible = 0; + + switch (iStatus) { + case ID_STATUS_OFFLINE: szStatus = "offline"; break; + case ID_STATUS_NA: szStatus = "occupied"; break; + case ID_STATUS_AWAY: + case ID_STATUS_DND: szStatus = "away"; break; + case ID_STATUS_INVISIBLE: + invisible = 1; + } + + Push(new AsyncHttpRequest(CONN_MAIN, REQUEST_GET, ICQ_API_SERVER "/presence/setState") + << AIMSID(this) << CHAR_PARAM("view", szStatus) << INT_PARAM("invisible", invisible)); + + if (iStatus == ID_STATUS_OFFLINE && !getByte(DB_KEY_PHONEREG)) { + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_GET, ICQ_API_SERVER "/aim/endSession", &CIcqProto::OnSessionEnd); + pReq << AIMSID(this) << INT_PARAM("invalidateToken", 1); + ExecuteRequest(pReq); + } + + int iOldStatus = m_iStatus; m_iStatus = iStatus; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)iOldStatus, m_iStatus); +} + +void CIcqProto::ShutdownSession() +{ + if (m_bTerminated) + return; + + debugLogA("CIcqProto::ShutdownSession"); + + // shutdown all resources + DropQueue(); + + if (m_hWorkerThread) + SetEvent(m_evRequestsQueue); + + OnLoggedOut(); + + for (auto &it : m_ConnPool) { + if (it.s) { + Netlib_Shutdown(it.s); + it.s = nullptr; + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +#define EVENTS "myInfo,presence,buddylist,typing,dataIM,userAddedToBuddyList,mchat,hist,hiddenChat,diff,permitDeny,imState,notification,apps" +#define FIELDS "aimId,buddyIcon,bigBuddyIcon,iconId,bigIconId,largeIconId,displayId,friendly,offlineMsg,state,statusMsg,userType,phoneNumber,cellNumber,smsNumber,workNumber,otherNumber,capabilities,ssl,abPhoneNumber,moodIcon,lastName,abPhones,abContactName,lastseen,mute,livechat,official" + +void CIcqProto::StartSession() +{ + ptrA szDeviceId(getStringA("DeviceId")); + if (szDeviceId == nullptr) { + UUID deviceId; + UuidCreate(&deviceId); + RPC_CSTR szId; + UuidToStringA(&deviceId, &szId); + szDeviceId = mir_strdup((char*)szId); + setString("DeviceId", szDeviceId); + RpcStringFreeA(&szId); + } + + int ts = TS(); + CMStringA nonce(FORMAT, "%d-2", ts); + CMStringA caps(WIM_CAP_UNIQ_REQ_ID "," WIM_CAP_EMOJI "," WIM_CAP_MAIL_NOTIFICATIONS "," WIM_CAP_INTRO_DLG_STATE); + if (g_bSecureIM) { + caps.AppendChar(','); + caps.Append(NG_CAP_SECUREIM); + } + + MFileVersion v; + Miranda_GetFileVersion(&v); + caps.AppendFormat(",%02x%02x%02x%02x%02x%02x%02x%02x%04x%04x%04x%04x", 'M', 'i', 'N', 'G', + __MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM, v[0], v[1], v[2], v[3]); + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/aim/startSession", &CIcqProto::OnStartSession); + pReq << CHAR_PARAM("a", m_szAToken) << INT_PARAM("activeTimeout", 180) << CHAR_PARAM("assertCaps", caps) + << INT_PARAM("buildNumber", __BUILD_NUM) << CHAR_PARAM("deviceId", szDeviceId) << CHAR_PARAM("events", EVENTS) + << CHAR_PARAM("f", "json") << CHAR_PARAM("imf", "plain") << CHAR_PARAM("inactiveView", "offline") + << CHAR_PARAM("includePresenceFields", FIELDS) << CHAR_PARAM("invisible", "false") + << CHAR_PARAM("k", appId()) << INT_PARAM("mobile", 0) << CHAR_PARAM("nonce", nonce) << CHAR_PARAM("r", pReq->m_reqId) + << INT_PARAM("rawMsg", 0) << INT_PARAM("sessionTimeout", 7776000) << INT_PARAM("ts", ts) << CHAR_PARAM("view", "online"); + + CalcHash(pReq); + Push(pReq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnAddBuddy(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + JsonReply root(pReply); + if (root.error() != 200) + return; + + CMStringW wszId = getMStringW(pReq->hContact, DB_KEY_ID); + for (auto &it : root.data()["results"]) { + if (it["buddy"].as_mstring() != wszId) + continue; + + switch (int iResultCode = it["resultCode"].as_int()) { + case 0: // success + case 3: // already in contact list + break; + + default: + debugLogA("Contact %d failed to add: error %d", pReq->hContact, iResultCode); + + POPUPDATAW Popup = {}; + Popup.lchIcon = IcoLib_GetIconByHandle(Skin_GetIconHandle(SKINICON_ERROR)); + wcsncpy_s(Popup.lpwzText, TranslateT("Buddy addition failed"), _TRUNCATE); + wcsncpy_s(Popup.lpwzContactName, Clist_GetContactDisplayName(pReq->hContact), _TRUNCATE); + Popup.iSeconds = 20; + PUAddPopupW(&Popup); + + // Contact::RemoveFromList(pReq->hContact); + } + + RetrieveUserInfo(pReq->hContact); + Contact::PutOnList(pReq->hContact); + } +} + +void CIcqProto::OnAddClient(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + bool *pRet = (bool*)pReq->pUserInfo; + + RobustReply reply(pReply); + if (reply.error() != 20000) { + *pRet = false; + return; + } + + const JSONNode &results = reply.results(); + m_iRClientId = results["clientId"].as_int(); + setDword(DB_KEY_RCLIENTID, m_iRClientId); + *pRet = true; +} + +void CIcqProto::OnCheckPassword(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + JsonReply root(pReply); + switch (root.error()) { + case 200: + break; + + case 330: + case 440: + ConnectionFailed(LOGINERR_WRONGPASSWORD, root.error()); + return; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); + return; + } + + JSONNode &data = root.data(); + m_szAToken = data["token"]["a"].as_mstring(); + mir_urlDecode(m_szAToken.GetBuffer()); + setString(DB_KEY_ATOKEN, m_szAToken); + + CMStringA szSessionSecret = data["sessionSecret"].as_mstring(); + CMStringA szPassTemp = m_szPassword; + + unsigned int len; + uint8_t hashOut[MIR_SHA256_HASH_SIZE]; + HMAC(EVP_sha256(), szPassTemp, szPassTemp.GetLength(), (uint8_t*)szSessionSecret.c_str(), szSessionSecret.GetLength(), hashOut, &len); + m_szSessionKey = ptrA(mir_base64_encode(hashOut, sizeof(hashOut))); + setString(DB_KEY_SESSIONKEY, m_szSessionKey); + + CMStringW szUin = data["loginId"].as_mstring(); + if (szUin) + m_szOwnId = szUin; + + int srvTS = data["hostTime"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + + StartSession(); +} + +void CIcqProto::OnFileContinue(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pOld) +{ + IcqFileTransfer *pTransfer = (IcqFileTransfer*)pOld->pUserInfo; + if (pTransfer->m_bCanceled) { +LBL_Error: + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, pTransfer); + delete pTransfer; + return; + } + + switch (pReply->resultCode) { + case 200: // final ok + case 206: // partial ok + break; + + default: + goto LBL_Error; + } + + // file transfer succeeded? + if (pTransfer->pfts.currentFileProgress == pTransfer->pfts.currentFileSize) { + FileReply root(pReply); + if (root.error() != 200) + goto LBL_Error; + + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, pTransfer); + + const JSONNode &data = root.data(); + CMStringW wszUrl(data["static_url"].as_mstring()); + + JSONNode bundle, contents; contents.set_name("captionedContent"); + contents << WCHAR_PARAM("caption", pTransfer->m_wszDescr) << WCHAR_PARAM("url", wszUrl); + bundle << CHAR_PARAM("mediaType", "text") << CHAR_PARAM("text", "") << contents; + CMStringW wszParts(FORMAT, L"[%s]", ptrW(json_write(&bundle)).get()); + + if (!pTransfer->m_wszDescr.IsEmpty()) + wszUrl += L" " + pTransfer->m_wszDescr; + + int id = InterlockedIncrement(&m_msgId); + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/im/sendIM", &CIcqProto::OnSendMessage); + + auto *pOwn = new IcqOwnMessage(pTransfer->pfts.hContact, id, pReq->m_reqId); + pReq->pUserInfo = pOwn; + { + mir_cslock lck(m_csOwnIds); + m_arOwnIds.insert(pOwn); + } + + pReq << AIMSID(this) << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("k", appId()) << CHAR_PARAM("mentions", "") << WCHAR_PARAM("message", wszUrl) + << CHAR_PARAM("offlineIM", "true") << WCHAR_PARAM("parts", wszParts) << WCHAR_PARAM("t", GetUserId(pTransfer->pfts.hContact)) << INT_PARAM("ts", TS()); + Push(pReq); + + delete pTransfer; + return; + } + + // else send the next portion + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, pTransfer->m_szHost, &CIcqProto::OnFileContinue); + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", TS()); + CalcHash(pReq); + pReq->m_szUrl.AppendChar('?'); + pReq->m_szUrl += pReq->m_szParam; pReq->m_szParam.Empty(); + pReq->pUserInfo = pTransfer; + pTransfer->FillHeaders(pReq); + Push(pReq); + + pTransfer->pfts.currentFileTime = time(0); + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, pTransfer, (LPARAM)&pTransfer->pfts); +} + +void CIcqProto::OnFileInit(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pOld) +{ + IcqFileTransfer *pTransfer = (IcqFileTransfer*)pOld->pUserInfo; + if (pTransfer->m_bCanceled) { +LBL_Error: + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FAILED, pTransfer); + delete pTransfer; + return; + } + + FileReply root(pReply); + if (root.error() != 200) + goto LBL_Error; + + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_INITIALISING, pTransfer); + pTransfer->pfts.currentFileTime = time(0); + + const JSONNode &data = root.data(); + CMStringW wszHost(data["host"].as_mstring()); + CMStringW wszUrl(data["url"].as_mstring()); + pTransfer->m_szHost = L"https://" + wszHost + wszUrl; + + auto *pReq = new AsyncHttpRequest(CONN_NONE, REQUEST_POST, pTransfer->m_szHost, &CIcqProto::OnFileContinue); + pReq << CHAR_PARAM("a", m_szAToken) << CHAR_PARAM("client", "icq") << CHAR_PARAM("k", appId()) << INT_PARAM("ts", TS()); + CalcHash(pReq); + pReq->m_szUrl.AppendChar('?'); + pReq->m_szUrl += pReq->m_szParam; pReq->m_szParam.Empty(); + pReq->pUserInfo = pTransfer; + pTransfer->FillHeaders(pReq); + Push(pReq); + + ProtoBroadcastAck(pTransfer->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, pTransfer, (LPARAM)&pTransfer->pfts); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Support for stickers + +void CIcqProto::OnGetSticker(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + if (pReply->resultCode != 200) { + debugLogA("Error getting sticker: %d", pReply->resultCode); + return; + } + + CMStringW wszPath(FORMAT, L"%s\\%S\\Stickers", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); + CreateDirectoryTreeW(wszPath); + + CMStringW wszFileName(FORMAT, L"%s\\STK{%s}.png", wszPath.c_str(), pReq->pUserInfo); + FILE *out = _wfopen(wszFileName, L"wb"); + fwrite(pReply->pData, 1, pReply->dataLength, out); + fclose(out); + + SMADD_CONT cont = { 1, m_szModuleName, wszFileName }; + CallService(MS_SMILEYADD_LOADCONTACTSMILEYS, 0, LPARAM(&cont)); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// File info request + +void CIcqProto::OnFileInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + IcqFileInfo **res = (IcqFileInfo **)pReq->pUserInfo; + *res = nullptr; + + RobustReply root(pReply); + if (root.error() != 200) + return; + + auto &pData = root.result(); + auto &pInfo = pData["info"] ; + std::string szUrl(pInfo["dlink"].as_string()); + if (szUrl.empty()) + return; + + MarkAsRead(pReq->hContact); + + bool bIsSticker; + CMStringW wszDescr(pInfo["file_name"].as_mstring()); + if (!mir_wstrncmp(wszDescr, L"dnld", 4)) { + bIsSticker = true; + + std::string szPreview = pData["previews"]["192"].as_string(); + if (!szPreview.empty()) + szUrl = szPreview; + } + else bIsSticker = false; + + mir_urlDecode(&*szUrl.begin()); + + *res = new IcqFileInfo(szUrl, wszDescr, pInfo["file_size"].as_int()); + res[0]->bIsSticker = bIsSticker; +} + +void CIcqProto::OnFileRecv(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + auto *ft = (IcqFileTransfer*)pReq->pUserInfo; + + if (pReply->resultCode != 200) { +LBL_Error: + FileCancel(pReq->hContact, ft); + return; + } + + ft->hWaitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_FILERESUME, ft, (LPARAM)&ft->pfts)) + WaitForSingleObject(ft->hWaitEvent, INFINITE); + CloseHandle(ft->hWaitEvent); + + debugLogW(L"Saving to [%s]", ft->pfts.szCurrentFile.w); + int fileId = _wopen(ft->pfts.szCurrentFile.w, _O_BINARY | _O_CREAT | _O_TRUNC | _O_WRONLY, _S_IREAD | _S_IWRITE); + if (fileId == -1) { + debugLogW(L"Cannot open [%s] for writing", ft->pfts.szCurrentFile.w); + goto LBL_Error; + } + + int result = _write(fileId, pReply->pData, pReply->dataLength); + _close(fileId); + if (result != pReply->dataLength) { + debugLogW(L"Error writing data into [%s]", ft->pfts.szCurrentFile.w); + goto LBL_Error; + } + + ft->pfts.totalProgress += pReply->dataLength; + ft->pfts.currentFileProgress += pReply->dataLength; + ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_DATA, ft, (LPARAM)&ft->pfts); + + ProtoBroadcastAck(ft->pfts.hContact, ACKTYPE_FILE, ACKRESULT_SUCCESS, ft); + delete ft; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::OnGenToken(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest*) +{ + RobustReply root(pReply); + if (root.error() != 20000) + return; + + auto &results = root.results(); + m_szRToken = results["authToken"].as_mstring(); +} + +void CIcqProto::OnGetUserHistory(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + RobustReply root(pReply); + if (root.error() != 20000) + return; + + __int64 lastMsgId = getId(pReq->hContact, DB_KEY_LASTMSGID); + + int count = 0; + auto &results = root.results(); + for (auto &it : results["messages"]) { + ParseMessage(pReq->hContact, lastMsgId, it, pReq->pUserInfo != nullptr, false); + count++; + } + + setId(pReq->hContact, DB_KEY_LASTMSGID, lastMsgId); + + if (count >= 999) + RetrieveUserHistory(pReq->hContact, lastMsgId, pReq->pUserInfo != nullptr); +} + +void CIcqProto::OnGetUserInfo(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + RobustReply root(pReply); + if (root.error() != 20000) { + ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_FAILED, nullptr); + return; + } + + ParseBuddyInfo(root.results(), pReq->hContact, true); + + ProtoBroadcastAck(pReq->hContact, ACKTYPE_GETINFO, ACKRESULT_SUCCESS, nullptr); +} + +void CIcqProto::OnStartSession(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + switch (root.error()) { + case 200: + break; + + case 451: + // session forcibly closed from site + delSetting(DB_KEY_ATOKEN); + delSetting(DB_KEY_SESSIONKEY); + CheckPassword(); + return; + + case 401: + delSetting(DB_KEY_ATOKEN); + delSetting(DB_KEY_SESSIONKEY); + if (root.detail() == 1002) // session expired + CheckPassword(); + else + ConnectionFailed(LOGINERR_WRONGPASSWORD, root.error()); + return; + + case 400: + if (root.detail() == 1015 && m_iTimeShift == 0) { // wrong timestamp + JSONNode &data = root.data(); + int srvTS = data["ts"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + StartSession(); + return; + } + __fallthrough; + + default: + ConnectionFailed(LOGINERR_WRONGPROTOCOL, root.error()); + return; + } + + JSONNode &data = root.data(); + m_fetchBaseURL = data["fetchBaseURL"].as_mstring(); + m_aimsid = data["aimsid"].as_mstring(); + + ProcessMyInfo(data["myInfo"]); + + int srvTS = data["ts"].as_int(); + m_iTimeShift = (srvTS) ? time(0) - srvTS : 0; + + OnLoggedIn(); + + for (auto &it : data["events"]) + ProcessEvent(it); + + ForkThread(&CIcqProto::PollThread); +} + +void CIcqProto::OnReceiveAvatar(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + PROTO_AVATAR_INFORMATION ai = {}; + ai.hContact = pReq->hContact; + + if (pReply->resultCode != 200 || pReply->pData == nullptr) { +LBL_Error: + ProtoBroadcastAck(pReq->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, HANDLE(&ai), 0); + return; + } + + const char *szContentType = Netlib_GetHeader(pReply, "Content-Type"); + if (szContentType == nullptr) + szContentType = "image/jpeg"; + + ai.format = ProtoGetAvatarFormatByMimeType(szContentType); + setByte(pReq->hContact, "AvatarType", ai.format); + GetAvatarFileName(pReq->hContact, ai.filename, _countof(ai.filename)); + + FILE *out = _wfopen(ai.filename, L"wb"); + if (out == nullptr) + goto LBL_Error; + + fwrite(pReply->pData, pReply->dataLength, 1, out); + fclose(out); + + if (pReq->hContact != 0) { + ProtoBroadcastAck(pReq->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, HANDLE(&ai), 0); + debugLogW(L"Broadcast new avatar: %s", ai.filename); + } + else ReportSelfAvatarChanged(); +} + +void CIcqProto::OnSearchResults(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + RobustReply root(pReply); + if (root.error() != 20000) { + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_FAILED, (HANDLE)pReq, 0); + return; + } + + const JSONNode &results = root.results(); + + PROTOSEARCHRESULT psr = {}; + psr.cbSize = sizeof(psr); + psr.flags = PSR_UNICODE; + for (auto &it : results["persons"]) { + CMStringW wszId = it["sn"].as_mstring(); + if (wszId == m_szOwnId) + continue; + + CMStringW wszNick = it["friendly"].as_mstring(); + CMStringW wszFirst = it["firstName"].as_mstring(); + CMStringW wszLast = it["lastName"].as_mstring(); + + psr.id.w = wszId.GetBuffer(); + psr.nick.w = wszNick.GetBuffer(); + psr.firstName.w = wszFirst.GetBuffer(); + psr.lastName.w = wszLast.GetBuffer(); + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, (HANDLE)pReq, LPARAM(&psr)); + } + + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)pReq); +} + +void CIcqProto::OnSendMessage(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *pReq) +{ + IcqOwnMessage *ownMsg = (IcqOwnMessage*)pReq->pUserInfo; + + JsonReply root(pReply); + if (root.error() != 200) { + ProtoBroadcastAck(ownMsg->m_hContact, ACKTYPE_MESSAGE, ACKRESULT_FAILED, (HANDLE)ownMsg->m_msgid, 0); + + mir_cslock lck(m_csOwnIds); + m_arOwnIds.remove(ownMsg); + } + + if (g_bMessageState) + CallService(MS_MESSAGESTATE_UPDATE, ownMsg->m_hContact, MRD_TYPE_DELIVERED); + + const JSONNode &data = root.data(); + CMStringA reqId(root.requestId()); + CMStringA msgId(data["histMsgId"].as_mstring()); + CheckOwnMessage(reqId, msgId, false); + CheckLastId(ownMsg->m_hContact, data); +} + +void CIcqProto::OnSessionEnd(NETLIBHTTPREQUEST *pReply, AsyncHttpRequest *) +{ + JsonReply root(pReply); + if (root.error() == 200) { + m_szAToken.Empty(); + delSetting(DB_KEY_ATOKEN); + + m_szSessionKey.Empty(); + delSetting(DB_KEY_SESSIONKEY); + + ShutdownSession(); + } +} diff --git a/protocols/ICQ-WIM/src/stdafx.cxx b/protocols/ICQ-WIM/src/stdafx.cxx index d265a4c02e..8c570f6949 100644 --- a/protocols/ICQ-WIM/src/stdafx.cxx +++ b/protocols/ICQ-WIM/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - -#include "stdafx.h" +/* +Copyright (C) 2012-23 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 . +*/ + +#include "stdafx.h" diff --git a/protocols/ICQ-WIM/src/stdafx.h b/protocols/ICQ-WIM/src/stdafx.h index 465a385f02..b45810a867 100644 --- a/protocols/ICQ-WIM/src/stdafx.h +++ b/protocols/ICQ-WIM/src/stdafx.h @@ -1,109 +1,109 @@ -// ---------------------------------------------------------------------------80 -// ICQ plugin for Miranda Instant Messenger -// ________________________________________ -// -// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede -// Copyright © 2001-2002 Jon Keating, Richard Hughes -// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater -// Copyright © 2004-2010 Joe Kucera -// Copyright © 2012-2022 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- -// DESCRIPTION: -// -// Includes all header files that should be precompiled to speed up compilation. -// ----------------------------------------------------------------------------- - -#pragma once - -// Windows includes -#include - -// Standard includes -#include -#include -#include -#include -#include -#include -#include - -// Miranda IM SDK includes -#include // This must be included first -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -// Project resources -#include "resource.h" - -// ICQ plugin includes -#include "version.h" - -#define MODULENAME "ICQ" - -#define DB_KEY_ID "aimId" -#define DB_KEY_IDLE "IdleTS" -#define DB_KEY_ABOUT "About" -#define DB_KEY_PHONE "Phone" -#define DB_KEY_ATOKEN "AToken" -#define DB_KEY_ICQNICK "IcqNick" -#define DB_KEY_PHONEREG "PhoneReg" -#define DB_KEY_LASTSEEN "LastSeen" -#define DB_KEY_ONLINETS "OnlineTS" -#define DB_KEY_PATCHVER "PatchVersion" -#define DB_KEY_RCLIENTID "RClientID" -#define DB_KEY_LASTMSGID "LastMsgId" -#define DB_KEY_REMOTEREAD "RemoteReadId" -#define DB_KEY_SESSIONKEY "SessionKey" -#define DB_KEY_MEMBERSINCE "MemberSince" - -#include "http.h" -#include "proto.h" - -bool IsChat(const CMStringW &aimid); -bool IsValidType(const JSONNode &aimid); - -void RefreshGroups(void); -wchar_t* time2text(time_t time); - -extern bool g_bSecureIM, g_bMessageState; +// ---------------------------------------------------------------------------80 +// ICQ plugin for Miranda Instant Messenger +// ________________________________________ +// +// Copyright © 2000-2001 Richard Hughes, Roland Rabien, Tristan Van de Vreede +// Copyright © 2001-2002 Jon Keating, Richard Hughes +// Copyright © 2002-2004 Martin Öberg, Sam Kothari, Robert Rainwater +// Copyright © 2004-2010 Joe Kucera +// Copyright © 2012-2023 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- +// DESCRIPTION: +// +// Includes all header files that should be precompiled to speed up compilation. +// ----------------------------------------------------------------------------- + +#pragma once + +// Windows includes +#include + +// Standard includes +#include +#include +#include +#include +#include +#include +#include + +// Miranda IM SDK includes +#include // This must be included first +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// Project resources +#include "resource.h" + +// ICQ plugin includes +#include "version.h" + +#define MODULENAME "ICQ" + +#define DB_KEY_ID "aimId" +#define DB_KEY_IDLE "IdleTS" +#define DB_KEY_ABOUT "About" +#define DB_KEY_PHONE "Phone" +#define DB_KEY_ATOKEN "AToken" +#define DB_KEY_ICQNICK "IcqNick" +#define DB_KEY_PHONEREG "PhoneReg" +#define DB_KEY_LASTSEEN "LastSeen" +#define DB_KEY_ONLINETS "OnlineTS" +#define DB_KEY_PATCHVER "PatchVersion" +#define DB_KEY_RCLIENTID "RClientID" +#define DB_KEY_LASTMSGID "LastMsgId" +#define DB_KEY_REMOTEREAD "RemoteReadId" +#define DB_KEY_SESSIONKEY "SessionKey" +#define DB_KEY_MEMBERSINCE "MemberSince" + +#include "http.h" +#include "proto.h" + +bool IsChat(const CMStringW &aimid); +bool IsValidType(const JSONNode &aimid); + +void RefreshGroups(void); +wchar_t* time2text(time_t time); + +extern bool g_bSecureIM, g_bMessageState; diff --git a/protocols/ICQ-WIM/src/userinfo.cpp b/protocols/ICQ-WIM/src/userinfo.cpp index 887d9c2d92..61f661a31b 100644 --- a/protocols/ICQ-WIM/src/userinfo.cpp +++ b/protocols/ICQ-WIM/src/userinfo.cpp @@ -1,64 +1,64 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -struct IcqUserInfoDlg : public CUserInfoPageDlg -{ - CIcqProto *ppro; - - IcqUserInfoDlg(CIcqProto *_ppro) : - CUserInfoPageDlg(g_plugin, IDD_INFO_ICQ), - ppro(_ppro) - { - } - - bool OnRefresh() override - { - SetDlgItemTextW(m_hwnd, IDC_UIN, ppro->GetUserId(m_hContact)); - SetDlgItemTextW(m_hwnd, IDC_NICK, ppro->getMStringW(m_hContact, DB_KEY_ICQNICK)); - SetDlgItemTextW(m_hwnd, IDC_PHONE, ppro->getMStringW(m_hContact, DB_KEY_PHONE)); - - SetDlgItemTextW(m_hwnd, IDC_IDLETIME, time2text(ppro->getDword(m_hContact, DB_KEY_IDLE))); - SetDlgItemTextW(m_hwnd, IDC_LASTSEEN, time2text(ppro->getDword(m_hContact, DB_KEY_LASTSEEN))); - SetDlgItemTextW(m_hwnd, IDC_MEMBERSINCE, time2text(ppro->getDword(m_hContact, DB_KEY_MEMBERSINCE))); - SetDlgItemTextW(m_hwnd, IDC_ONLINESINCE, time2text(time(0) - ppro->getDword(m_hContact, DB_KEY_ONLINETS))); - return false; - } -}; - -int CIcqProto::OnUserInfoInit(WPARAM wParam, LPARAM hContact) -{ - if (hContact && mir_strcmp(Proto_GetBaseAccountName(hContact), m_szModuleName)) - return 0; - - if (isChatRoom(hContact)) - return 0; - - USERINFOPAGE uip = {}; - uip.flags = ODPF_UNICODE | ODPF_USERINFOTAB | ODPF_DONTTRANSLATE | ODPF_ICON; - uip.dwInitParam = (LPARAM)Skin_GetProtoIcon(m_szModuleName, ID_STATUS_ONLINE); - uip.szTitle.w = L"ICQ"; - uip.szGroup.w = m_tszUserName; - uip.position = -1900000000; - uip.pDialog = new IcqUserInfoDlg(this); - g_plugin.addUserInfo(wParam, &uip); - return 0; -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +struct IcqUserInfoDlg : public CUserInfoPageDlg +{ + CIcqProto *ppro; + + IcqUserInfoDlg(CIcqProto *_ppro) : + CUserInfoPageDlg(g_plugin, IDD_INFO_ICQ), + ppro(_ppro) + { + } + + bool OnRefresh() override + { + SetDlgItemTextW(m_hwnd, IDC_UIN, ppro->GetUserId(m_hContact)); + SetDlgItemTextW(m_hwnd, IDC_NICK, ppro->getMStringW(m_hContact, DB_KEY_ICQNICK)); + SetDlgItemTextW(m_hwnd, IDC_PHONE, ppro->getMStringW(m_hContact, DB_KEY_PHONE)); + + SetDlgItemTextW(m_hwnd, IDC_IDLETIME, time2text(ppro->getDword(m_hContact, DB_KEY_IDLE))); + SetDlgItemTextW(m_hwnd, IDC_LASTSEEN, time2text(ppro->getDword(m_hContact, DB_KEY_LASTSEEN))); + SetDlgItemTextW(m_hwnd, IDC_MEMBERSINCE, time2text(ppro->getDword(m_hContact, DB_KEY_MEMBERSINCE))); + SetDlgItemTextW(m_hwnd, IDC_ONLINESINCE, time2text(time(0) - ppro->getDword(m_hContact, DB_KEY_ONLINETS))); + return false; + } +}; + +int CIcqProto::OnUserInfoInit(WPARAM wParam, LPARAM hContact) +{ + if (hContact && mir_strcmp(Proto_GetBaseAccountName(hContact), m_szModuleName)) + return 0; + + if (isChatRoom(hContact)) + return 0; + + USERINFOPAGE uip = {}; + uip.flags = ODPF_UNICODE | ODPF_USERINFOTAB | ODPF_DONTTRANSLATE | ODPF_ICON; + uip.dwInitParam = (LPARAM)Skin_GetProtoIcon(m_szModuleName, ID_STATUS_ONLINE); + uip.szTitle.w = L"ICQ"; + uip.szGroup.w = m_tszUserName; + uip.position = -1900000000; + uip.pDialog = new IcqUserInfoDlg(this); + g_plugin.addUserInfo(wParam, &uip); + return 0; +} diff --git a/protocols/ICQ-WIM/src/utils.cpp b/protocols/ICQ-WIM/src/utils.cpp index 4fa65d0302..dfa73b8fb4 100644 --- a/protocols/ICQ-WIM/src/utils.cpp +++ b/protocols/ICQ-WIM/src/utils.cpp @@ -1,396 +1,396 @@ -// ----------------------------------------------------------------------------- -// ICQ plugin for Miranda NG -// ----------------------------------------------------------------------------- -// Copyright © 2018-22 Miranda NG team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// ----------------------------------------------------------------------------- - -#include "stdafx.h" - -void CIcqProto::InitContactCache() -{ - mir_cslock l(m_csCache); - for (auto &it : AccContacts()) { - if (isChatRoom(it)) - continue; - - // that was previously an ICQ contact - ptrW wszUin(GetUIN(it)); - if (wszUin != nullptr) { - delSetting(it, "UIN"); - setWString(it, DB_KEY_ID, wszUin); - } - // that was previously a MRA contact - else { - CMStringW wszEmail(getMStringW(it, "e-mail")); - if (!wszEmail.IsEmpty()) { - delSetting(it, "e-mail"); - setWString(it, DB_KEY_ID, wszEmail); - } - } - - CMStringW wszId = GetUserId(it); - auto *pCache = FindContactByUIN(wszId); - if (pCache == nullptr) { - pCache = new IcqCacheItem(wszId, it); - m_arCache.insert(pCache); - } - pCache->m_iProcessedMsgId = getId(it, DB_KEY_LASTMSGID); - } -} - -IcqCacheItem* CIcqProto::FindContactByUIN(const CMStringW &wszId) -{ - IcqCacheItem tmp(wszId, -1); - - mir_cslock l(m_csCache); - return m_arCache.find(&tmp); -} - -wchar_t* CIcqProto::GetUIN(MCONTACT hContact) -{ - DBVARIANT dbv = {}; - if (!db_get(hContact, m_szModuleName, "UIN", &dbv)) { - switch (dbv.type) { - case DBVT_DWORD: - wchar_t buf[40], *ret; - _itow_s(dbv.dVal, buf, 10); - return mir_wstrdup(buf); - - case DBVT_ASCIIZ: - ret = mir_a2u(dbv.pszVal); - db_free(&dbv); - return ret; - - case DBVT_UTF8: - ret = mir_utf8decodeW(dbv.pszVal); - db_free(&dbv); - return ret; - - case DBVT_WCHAR: - return dbv.pwszVal; - } - db_free(&dbv); - } - return nullptr; -} - -MCONTACT CIcqProto::CreateContact(const CMStringW &wszId, bool bTemporary) -{ - auto *pCache = FindContactByUIN(wszId); - if (pCache != nullptr) - return pCache->m_hContact; - - MCONTACT hContact = db_add_contact(); - setWString(hContact, DB_KEY_ID, wszId); - Proto_AddToContact(hContact, m_szModuleName); - RetrieveUserInfo(hContact); - - if (bTemporary) - Contact::RemoveFromList(hContact); - - return hContact; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::CalcHash(AsyncHttpRequest *pReq) -{ - CMStringA hashData(FORMAT, "%s&%s&%s", - pReq->requestType == REQUEST_POST ? "POST" : "GET", - mir_urlEncode(pReq->m_szUrl).c_str(), mir_urlEncode(pReq->m_szParam).c_str()); - - unsigned int len; - uint8_t hashOut[MIR_SHA256_HASH_SIZE]; - HMAC(EVP_sha256(), m_szSessionKey, m_szSessionKey.GetLength(), (uint8_t*)hashData.c_str(), hashData.GetLength(), hashOut, &len); - pReq << CHAR_PARAM("sig_sha256", ptrA(mir_base64_encode(hashOut, sizeof(hashOut)))); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CIcqProto::Json2int(MCONTACT hContact, const JSONNode &node, const char *szJson, const char *szSetting, bool bIsPartial) -{ - const JSONNode &var = node[szJson]; - if (var) - setDword(hContact, szSetting, var.as_int()); - else if (!bIsPartial) - delSetting(hContact, szSetting); -} - -void CIcqProto::Json2string(MCONTACT hContact, const JSONNode &node, const char *szJson, const char *szSetting, bool bIsPartial) -{ - const JSONNode &var = node[szJson]; - if (var) { - CMStringW wszStr(var.as_mstring()); - if (wszStr == L"[deleted]") { - setByte(hContact, "IcqDeleted", 1); - Contact::PutOnList(hContact); - } - else setWString(hContact, szSetting, wszStr); - } - else if (!bIsPartial) - delSetting(hContact, szSetting); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Avatars - -void CIcqProto::GetAvatarFileName(MCONTACT hContact, wchar_t* pszDest, size_t cbLen) -{ - int tPathLen = mir_snwprintf(pszDest, cbLen, L"%s\\%S", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); - CreateDirectoryTreeW(pszDest); - pszDest[tPathLen++] = '\\'; - - CMStringW wszFileName(getMStringW(hContact, "IconId")); - const wchar_t* szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_PNG)); - mir_snwprintf(pszDest + tPathLen, MAX_PATH - tPathLen, L"%s%s", wszFileName.c_str(), szFileType); -} - -INT_PTR __cdecl CIcqProto::GetAvatar(WPARAM wParam, LPARAM lParam) -{ - wchar_t *buf = (wchar_t*)wParam; - int size = (int)lParam; - if (buf == nullptr || size <= 0) - return -1; - - GetAvatarFileName(0, buf, size); - return 0; -} - -INT_PTR __cdecl CIcqProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) -{ - switch (wParam) { - case AF_MAXSIZE: - ((POINT*)lParam)->x = -1; - ((POINT*)lParam)->y = -1; - return 0; - - case AF_FORMATSUPPORTED: // nobody - return 1; - - case AF_DELAYAFTERFAIL: - return 10 * 60 * 1000; - - case AF_ENABLED: - case AF_FETCHIFPROTONOTVISIBLE: - case AF_FETCHIFCONTACTOFFLINE: - return 1; - } - return 0; -} - -INT_PTR __cdecl CIcqProto::GetAvatarInfo(WPARAM, LPARAM lParam) -{ - PROTO_AVATAR_INFORMATION* pai = (PROTO_AVATAR_INFORMATION*)lParam; - - ptrW szIconId(getWStringA(pai->hContact, "IconId")); - if (szIconId == nullptr) { - debugLogA("No avatar"); - return GAIR_NOAVATAR; - } - - GetAvatarFileName(pai->hContact, pai->filename, _countof(pai->filename)); - pai->format = getByte(pai->hContact, "AvatarType", 0); - - if (::_waccess(pai->filename, 0) == 0) - return GAIR_SUCCESS; - - debugLogA("No avatar"); - return GAIR_NOAVATAR; -} - -INT_PTR __cdecl CIcqProto::SetAvatar(WPARAM, LPARAM lParam) -{ - wchar_t* pwszFileName = (wchar_t*)lParam; - - wchar_t wszOldName[MAX_PATH]; - GetAvatarFileName(0, wszOldName, _countof(wszOldName)); - _wremove(wszOldName); - - auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/expressions/upload"); - pReq->m_szUrl.AppendFormat("?f=json&aimsid=%s&r=%s&type=largeBuddyIcon", mir_urlEncode(m_aimsid.c_str()).c_str(), pReq->m_reqId); - - if (pwszFileName == nullptr) - delSetting("AvatarHash"); - else { - int fileId = _wopen(pwszFileName, _O_RDONLY | _O_BINARY, _S_IREAD); - if (fileId < 0) { - delete pReq; - return 1; - } - - unsigned dwSize = (unsigned)_filelengthi64(fileId); - char* pData = (char*)mir_alloc(dwSize); - if (pData == nullptr) { - _close(fileId); - delete pReq; - return 2; - } - - _read(fileId, pData, dwSize); - _close(fileId); - - pReq->pData = pData; - pReq->dataLength = dwSize; - - int iAvatarType = ProtoGetBufferFormat(pData); - if (iAvatarType == PA_FORMAT_UNKNOWN) { - delete pReq; - delete pData; - return 3; - } - - pReq->AddHeader("Content-Type", ProtoGetAvatarMimeType(iAvatarType)); - } - Push(pReq); - - return 0; // TODO -} - -///////////////////////////////////////////////////////////////////////////////////////// - -CMStringW CIcqProto::GetUserId(MCONTACT hContact) -{ - if (isChatRoom(hContact)) - return getMStringW(hContact, "ChatRoomID"); - - return getMStringW(hContact, DB_KEY_ID); -} - -bool IsChat(const CMStringW &aimid) -{ - return aimid.Right(11) == "@chat.agent"; -} - -bool IsValidType(const JSONNode &n) -{ - auto type = n["userType"].as_string(); - return type == "icq" || type == "aim" || type == "interop" || type == ""; -} - -int CIcqProto::StatusFromPresence(const JSONNode &presence, MCONTACT hContact) -{ - CMStringW wszStatus = presence["state"].as_mstring(); - int iStatus; - if (wszStatus == L"online") - iStatus = ID_STATUS_ONLINE; - else if (wszStatus == L"offline") - iStatus = ID_STATUS_OFFLINE; - else if (wszStatus == L"n/a") - iStatus = ID_STATUS_NA; - else if (wszStatus == L"away") - iStatus = ID_STATUS_AWAY; - else if (wszStatus == L"occupied") - iStatus = ID_STATUS_OCCUPIED; - else if (wszStatus == L"dnd") - iStatus = ID_STATUS_DND; - else - iStatus = -1; - - int iLastSeen = presence["lastseen"].as_int(); - if (iLastSeen != 0) - setDword(hContact, DB_KEY_LASTSEEN, iLastSeen); - else if (getDword(hContact, DB_KEY_ONLINETS)) - iStatus = ID_STATUS_ONLINE; - - return iStatus; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -__int64 CIcqProto::getId(MCONTACT hContact, const char *szSetting) -{ - DBVARIANT dbv; - dbv.type = DBVT_BLOB; - if (db_get(hContact, m_szModuleName, szSetting, &dbv)) - return 0; - - __int64 result = (dbv.cpbVal == sizeof(__int64)) ? *(__int64*)dbv.pbVal : 0; - db_free(&dbv); - return result; -} - -void CIcqProto::setId(MCONTACT hContact, const char *szSetting, __int64 iValue) -{ - __int64 oldVal = getId(hContact, szSetting); - if (oldVal != iValue) - db_set_blob(hContact, m_szModuleName, szSetting, &iValue, sizeof(iValue)); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -wchar_t* time2text(time_t ts) -{ - if (ts == 0) - return L""; - - static wchar_t buf[100]; - TimeZone_PrintTimeStamp(NULL, ts, L"D t", buf, _countof(buf), 0); - return buf; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static LRESULT CALLBACK PopupDlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - switch (message) { - case WM_CONTEXTMENU: - PUDeletePopup(hWnd); - break; - - case WM_COMMAND: - CIcqProto *ppro = (CIcqProto*)PUGetPluginData(hWnd); - CallProtoService(ppro->m_szModuleName, PS_GOTO_INBOX); - PUDeletePopup(hWnd); - break; - } - - return DefWindowProc(hWnd, message, wParam, lParam); -} - -void CIcqProto::EmailNotification(const wchar_t *pwszText) -{ - POPUPDATAW Popup = {}; - Popup.lchIcon = g_plugin.getIcon(IDI_INBOX); - wcsncpy_s(Popup.lpwzText, pwszText, _TRUNCATE); - wcsncpy_s(Popup.lpwzContactName, m_tszUserName, _TRUNCATE); - Popup.iSeconds = 20; - Popup.PluginData = this; - Popup.PluginWindowProc = PopupDlgProc; - PUAddPopupW(&Popup); - - if (m_bUseTrayIcon) { - char szServiceFunction[MAX_PATH]; - if (m_bLaunchMailbox) - mir_snprintf(szServiceFunction, "%s%s", m_szModuleName, PS_GOTO_INBOX); - else - mir_snprintf(szServiceFunction, "%s%s", m_szModuleName, PS_DUMMY); - - int i = 0; - while (CLISTEVENT *pcle = g_clistApi.pfnGetEvent(-1, i++)) - if (!mir_strcmp(pcle->pszService, szServiceFunction)) - return; - - CLISTEVENT cle = {}; - cle.hDbEvent = ICQ_FAKE_EVENT_ID; - cle.moduleName = m_szModuleName; - cle.hIcon = g_plugin.getIcon(IDI_INBOX); - cle.flags = CLEF_UNICODE | CLEF_PROTOCOLGLOBAL; - cle.pszService = szServiceFunction; - cle.szTooltip.w = pwszText; - g_clistApi.pfnAddEvent(&cle); - } -} +// ----------------------------------------------------------------------------- +// ICQ plugin for Miranda NG +// ----------------------------------------------------------------------------- +// Copyright © 2018-23 Miranda NG team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// ----------------------------------------------------------------------------- + +#include "stdafx.h" + +void CIcqProto::InitContactCache() +{ + mir_cslock l(m_csCache); + for (auto &it : AccContacts()) { + if (isChatRoom(it)) + continue; + + // that was previously an ICQ contact + ptrW wszUin(GetUIN(it)); + if (wszUin != nullptr) { + delSetting(it, "UIN"); + setWString(it, DB_KEY_ID, wszUin); + } + // that was previously a MRA contact + else { + CMStringW wszEmail(getMStringW(it, "e-mail")); + if (!wszEmail.IsEmpty()) { + delSetting(it, "e-mail"); + setWString(it, DB_KEY_ID, wszEmail); + } + } + + CMStringW wszId = GetUserId(it); + auto *pCache = FindContactByUIN(wszId); + if (pCache == nullptr) { + pCache = new IcqCacheItem(wszId, it); + m_arCache.insert(pCache); + } + pCache->m_iProcessedMsgId = getId(it, DB_KEY_LASTMSGID); + } +} + +IcqCacheItem* CIcqProto::FindContactByUIN(const CMStringW &wszId) +{ + IcqCacheItem tmp(wszId, -1); + + mir_cslock l(m_csCache); + return m_arCache.find(&tmp); +} + +wchar_t* CIcqProto::GetUIN(MCONTACT hContact) +{ + DBVARIANT dbv = {}; + if (!db_get(hContact, m_szModuleName, "UIN", &dbv)) { + switch (dbv.type) { + case DBVT_DWORD: + wchar_t buf[40], *ret; + _itow_s(dbv.dVal, buf, 10); + return mir_wstrdup(buf); + + case DBVT_ASCIIZ: + ret = mir_a2u(dbv.pszVal); + db_free(&dbv); + return ret; + + case DBVT_UTF8: + ret = mir_utf8decodeW(dbv.pszVal); + db_free(&dbv); + return ret; + + case DBVT_WCHAR: + return dbv.pwszVal; + } + db_free(&dbv); + } + return nullptr; +} + +MCONTACT CIcqProto::CreateContact(const CMStringW &wszId, bool bTemporary) +{ + auto *pCache = FindContactByUIN(wszId); + if (pCache != nullptr) + return pCache->m_hContact; + + MCONTACT hContact = db_add_contact(); + setWString(hContact, DB_KEY_ID, wszId); + Proto_AddToContact(hContact, m_szModuleName); + RetrieveUserInfo(hContact); + + if (bTemporary) + Contact::RemoveFromList(hContact); + + return hContact; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::CalcHash(AsyncHttpRequest *pReq) +{ + CMStringA hashData(FORMAT, "%s&%s&%s", + pReq->requestType == REQUEST_POST ? "POST" : "GET", + mir_urlEncode(pReq->m_szUrl).c_str(), mir_urlEncode(pReq->m_szParam).c_str()); + + unsigned int len; + uint8_t hashOut[MIR_SHA256_HASH_SIZE]; + HMAC(EVP_sha256(), m_szSessionKey, m_szSessionKey.GetLength(), (uint8_t*)hashData.c_str(), hashData.GetLength(), hashOut, &len); + pReq << CHAR_PARAM("sig_sha256", ptrA(mir_base64_encode(hashOut, sizeof(hashOut)))); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CIcqProto::Json2int(MCONTACT hContact, const JSONNode &node, const char *szJson, const char *szSetting, bool bIsPartial) +{ + const JSONNode &var = node[szJson]; + if (var) + setDword(hContact, szSetting, var.as_int()); + else if (!bIsPartial) + delSetting(hContact, szSetting); +} + +void CIcqProto::Json2string(MCONTACT hContact, const JSONNode &node, const char *szJson, const char *szSetting, bool bIsPartial) +{ + const JSONNode &var = node[szJson]; + if (var) { + CMStringW wszStr(var.as_mstring()); + if (wszStr == L"[deleted]") { + setByte(hContact, "IcqDeleted", 1); + Contact::PutOnList(hContact); + } + else setWString(hContact, szSetting, wszStr); + } + else if (!bIsPartial) + delSetting(hContact, szSetting); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Avatars + +void CIcqProto::GetAvatarFileName(MCONTACT hContact, wchar_t* pszDest, size_t cbLen) +{ + int tPathLen = mir_snwprintf(pszDest, cbLen, L"%s\\%S", VARSW(L"%miranda_avatarcache%").get(), m_szModuleName); + CreateDirectoryTreeW(pszDest); + pszDest[tPathLen++] = '\\'; + + CMStringW wszFileName(getMStringW(hContact, "IconId")); + const wchar_t* szFileType = ProtoGetAvatarExtension(getByte(hContact, "AvatarType", PA_FORMAT_PNG)); + mir_snwprintf(pszDest + tPathLen, MAX_PATH - tPathLen, L"%s%s", wszFileName.c_str(), szFileType); +} + +INT_PTR __cdecl CIcqProto::GetAvatar(WPARAM wParam, LPARAM lParam) +{ + wchar_t *buf = (wchar_t*)wParam; + int size = (int)lParam; + if (buf == nullptr || size <= 0) + return -1; + + GetAvatarFileName(0, buf, size); + return 0; +} + +INT_PTR __cdecl CIcqProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + switch (wParam) { + case AF_MAXSIZE: + ((POINT*)lParam)->x = -1; + ((POINT*)lParam)->y = -1; + return 0; + + case AF_FORMATSUPPORTED: // nobody + return 1; + + case AF_DELAYAFTERFAIL: + return 10 * 60 * 1000; + + case AF_ENABLED: + case AF_FETCHIFPROTONOTVISIBLE: + case AF_FETCHIFCONTACTOFFLINE: + return 1; + } + return 0; +} + +INT_PTR __cdecl CIcqProto::GetAvatarInfo(WPARAM, LPARAM lParam) +{ + PROTO_AVATAR_INFORMATION* pai = (PROTO_AVATAR_INFORMATION*)lParam; + + ptrW szIconId(getWStringA(pai->hContact, "IconId")); + if (szIconId == nullptr) { + debugLogA("No avatar"); + return GAIR_NOAVATAR; + } + + GetAvatarFileName(pai->hContact, pai->filename, _countof(pai->filename)); + pai->format = getByte(pai->hContact, "AvatarType", 0); + + if (::_waccess(pai->filename, 0) == 0) + return GAIR_SUCCESS; + + debugLogA("No avatar"); + return GAIR_NOAVATAR; +} + +INT_PTR __cdecl CIcqProto::SetAvatar(WPARAM, LPARAM lParam) +{ + wchar_t* pwszFileName = (wchar_t*)lParam; + + wchar_t wszOldName[MAX_PATH]; + GetAvatarFileName(0, wszOldName, _countof(wszOldName)); + _wremove(wszOldName); + + auto *pReq = new AsyncHttpRequest(CONN_MAIN, REQUEST_POST, ICQ_API_SERVER "/expressions/upload"); + pReq->m_szUrl.AppendFormat("?f=json&aimsid=%s&r=%s&type=largeBuddyIcon", mir_urlEncode(m_aimsid.c_str()).c_str(), pReq->m_reqId); + + if (pwszFileName == nullptr) + delSetting("AvatarHash"); + else { + int fileId = _wopen(pwszFileName, _O_RDONLY | _O_BINARY, _S_IREAD); + if (fileId < 0) { + delete pReq; + return 1; + } + + unsigned dwSize = (unsigned)_filelengthi64(fileId); + char* pData = (char*)mir_alloc(dwSize); + if (pData == nullptr) { + _close(fileId); + delete pReq; + return 2; + } + + _read(fileId, pData, dwSize); + _close(fileId); + + pReq->pData = pData; + pReq->dataLength = dwSize; + + int iAvatarType = ProtoGetBufferFormat(pData); + if (iAvatarType == PA_FORMAT_UNKNOWN) { + delete pReq; + delete pData; + return 3; + } + + pReq->AddHeader("Content-Type", ProtoGetAvatarMimeType(iAvatarType)); + } + Push(pReq); + + return 0; // TODO +} + +///////////////////////////////////////////////////////////////////////////////////////// + +CMStringW CIcqProto::GetUserId(MCONTACT hContact) +{ + if (isChatRoom(hContact)) + return getMStringW(hContact, "ChatRoomID"); + + return getMStringW(hContact, DB_KEY_ID); +} + +bool IsChat(const CMStringW &aimid) +{ + return aimid.Right(11) == "@chat.agent"; +} + +bool IsValidType(const JSONNode &n) +{ + auto type = n["userType"].as_string(); + return type == "icq" || type == "aim" || type == "interop" || type == ""; +} + +int CIcqProto::StatusFromPresence(const JSONNode &presence, MCONTACT hContact) +{ + CMStringW wszStatus = presence["state"].as_mstring(); + int iStatus; + if (wszStatus == L"online") + iStatus = ID_STATUS_ONLINE; + else if (wszStatus == L"offline") + iStatus = ID_STATUS_OFFLINE; + else if (wszStatus == L"n/a") + iStatus = ID_STATUS_NA; + else if (wszStatus == L"away") + iStatus = ID_STATUS_AWAY; + else if (wszStatus == L"occupied") + iStatus = ID_STATUS_OCCUPIED; + else if (wszStatus == L"dnd") + iStatus = ID_STATUS_DND; + else + iStatus = -1; + + int iLastSeen = presence["lastseen"].as_int(); + if (iLastSeen != 0) + setDword(hContact, DB_KEY_LASTSEEN, iLastSeen); + else if (getDword(hContact, DB_KEY_ONLINETS)) + iStatus = ID_STATUS_ONLINE; + + return iStatus; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +__int64 CIcqProto::getId(MCONTACT hContact, const char *szSetting) +{ + DBVARIANT dbv; + dbv.type = DBVT_BLOB; + if (db_get(hContact, m_szModuleName, szSetting, &dbv)) + return 0; + + __int64 result = (dbv.cpbVal == sizeof(__int64)) ? *(__int64*)dbv.pbVal : 0; + db_free(&dbv); + return result; +} + +void CIcqProto::setId(MCONTACT hContact, const char *szSetting, __int64 iValue) +{ + __int64 oldVal = getId(hContact, szSetting); + if (oldVal != iValue) + db_set_blob(hContact, m_szModuleName, szSetting, &iValue, sizeof(iValue)); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +wchar_t* time2text(time_t ts) +{ + if (ts == 0) + return L""; + + static wchar_t buf[100]; + TimeZone_PrintTimeStamp(NULL, ts, L"D t", buf, _countof(buf), 0); + return buf; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static LRESULT CALLBACK PopupDlgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch (message) { + case WM_CONTEXTMENU: + PUDeletePopup(hWnd); + break; + + case WM_COMMAND: + CIcqProto *ppro = (CIcqProto*)PUGetPluginData(hWnd); + CallProtoService(ppro->m_szModuleName, PS_GOTO_INBOX); + PUDeletePopup(hWnd); + break; + } + + return DefWindowProc(hWnd, message, wParam, lParam); +} + +void CIcqProto::EmailNotification(const wchar_t *pwszText) +{ + POPUPDATAW Popup = {}; + Popup.lchIcon = g_plugin.getIcon(IDI_INBOX); + wcsncpy_s(Popup.lpwzText, pwszText, _TRUNCATE); + wcsncpy_s(Popup.lpwzContactName, m_tszUserName, _TRUNCATE); + Popup.iSeconds = 20; + Popup.PluginData = this; + Popup.PluginWindowProc = PopupDlgProc; + PUAddPopupW(&Popup); + + if (m_bUseTrayIcon) { + char szServiceFunction[MAX_PATH]; + if (m_bLaunchMailbox) + mir_snprintf(szServiceFunction, "%s%s", m_szModuleName, PS_GOTO_INBOX); + else + mir_snprintf(szServiceFunction, "%s%s", m_szModuleName, PS_DUMMY); + + int i = 0; + while (CLISTEVENT *pcle = g_clistApi.pfnGetEvent(-1, i++)) + if (!mir_strcmp(pcle->pszService, szServiceFunction)) + return; + + CLISTEVENT cle = {}; + cle.hDbEvent = ICQ_FAKE_EVENT_ID; + cle.moduleName = m_szModuleName; + cle.hIcon = g_plugin.getIcon(IDI_INBOX); + cle.flags = CLEF_UNICODE | CLEF_PROTOCOLGLOBAL; + cle.pszService = szServiceFunction; + cle.szTooltip.w = pwszText; + g_clistApi.pfnAddEvent(&cle); + } +} diff --git a/protocols/ICQ-WIM/src/version.h b/protocols/ICQ-WIM/src/version.h index c1cb84630e..d5270a8de9 100644 --- a/protocols/ICQ-WIM/src/version.h +++ b/protocols/ICQ-WIM/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "ICQ protocol support for Miranda NG." #define __AUTHOR "George Hazan" #define __AUTHORWEB "https://miranda-ng.org/p/ICQ" -#define __COPYRIGHT "© 2018-22 George Hazan" +#define __COPYRIGHT "© 2018-23 George Hazan" diff --git a/protocols/ICQCorp/src/stdafx.cxx b/protocols/ICQCorp/src/stdafx.cxx index 23ff0fd56d..2ee4e19cb6 100644 --- a/protocols/ICQCorp/src/stdafx.cxx +++ b/protocols/ICQCorp/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (c) 2014-22 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 . -*/ - -#include "stdafx.h" +/* +Copyright (c) 2014-23 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 . +*/ + +#include "stdafx.h" diff --git a/protocols/ICQCorp/src/version.h b/protocols/ICQCorp/src/version.h index 4bc47a5b88..c6d8e28aa5 100644 --- a/protocols/ICQCorp/src/version.h +++ b/protocols/ICQCorp/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "ICQ corporate protocol support for Miranda NG." #define __AUTHOR "Miranda NG team, Eugene Tarasenko" #define __AUTHORWEB "https://miranda-ng.org/p/ICQCorp" -#define __COPYRIGHT "© 2014-22 Miranda NG team, 2003-2005 Eugene Tarasenko" +#define __COPYRIGHT "© 2014-23 Miranda NG team, 2003-2005 Eugene Tarasenko" diff --git a/protocols/IRCG/src/stdafx.cxx b/protocols/IRCG/src/stdafx.cxx index 564f422ca2..8c570f6949 100644 --- a/protocols/IRCG/src/stdafx.cxx +++ b/protocols/IRCG/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/IRCG/src/version.h b/protocols/IRCG/src/version.h index 273e48ec22..e46b073e3c 100644 --- a/protocols/IRCG/src/version.h +++ b/protocols/IRCG/src/version.h @@ -7,7 +7,7 @@ #define __DESCRIPTION "Internet Relay Chat (IRC) protocol support for Miranda NG." #define __AUTHOR "Miranda team" -#define __COPYRIGHT "© 2003-22 Jurgen Persson, George Hazan" +#define __COPYRIGHT "© 2003-23 Jurgen Persson, George Hazan" #define __AUTHORWEB "https://miranda-ng.org/p/IRC" #define __PLUGIN_NAME "IRC protocol" diff --git a/protocols/JabberG/src/jabber.cpp b/protocols/JabberG/src/jabber.cpp index c0fdff4ccd..50dac791a6 100644 --- a/protocols/JabberG/src/jabber.cpp +++ b/protocols/JabberG/src/jabber.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_adhoc.cpp b/protocols/JabberG/src/jabber_adhoc.cpp index d5b6d429f1..1871262e16 100644 --- a/protocols/JabberG/src/jabber_adhoc.cpp +++ b/protocols/JabberG/src/jabber_adhoc.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Artem Shpynov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team Module implements an XMPP protocol extension for reporting and executing ad-hoc, human-oriented commands according to XEP-0050: Ad-Hoc Commands diff --git a/protocols/JabberG/src/jabber_agent.cpp b/protocols/JabberG/src/jabber_agent.cpp index a198827672..a8720b5a56 100644 --- a/protocols/JabberG/src/jabber_agent.cpp +++ b/protocols/JabberG/src/jabber_agent.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_api.cpp b/protocols/JabberG/src/jabber_api.cpp index db750517f3..738e1bb439 100644 --- a/protocols/JabberG/src/jabber_api.cpp +++ b/protocols/JabberG/src/jabber_api.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_archive.cpp b/protocols/JabberG/src/jabber_archive.cpp index 97c76993ba..b60f98ac97 100644 --- a/protocols/JabberG/src/jabber_archive.cpp +++ b/protocols/JabberG/src/jabber_archive.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_bookmarks.cpp b/protocols/JabberG/src/jabber_bookmarks.cpp index 0c9944677a..48cc195e17 100644 --- a/protocols/JabberG/src/jabber_bookmarks.cpp +++ b/protocols/JabberG/src/jabber_bookmarks.cpp @@ -3,7 +3,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2007 Michael Stepura, George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_byte.cpp b/protocols/JabberG/src/jabber_byte.cpp index 685174347d..060b10ee6c 100644 --- a/protocols/JabberG/src/jabber_byte.cpp +++ b/protocols/JabberG/src/jabber_byte.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_byte.h b/protocols/JabberG/src/jabber_byte.h index 6e3f5b732b..5de0be588c 100644 --- a/protocols/JabberG/src/jabber_byte.h +++ b/protocols/JabberG/src/jabber_byte.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_caps.cpp b/protocols/JabberG/src/jabber_caps.cpp index e49f93d8a8..16ad003a75 100644 --- a/protocols/JabberG/src/jabber_caps.cpp +++ b/protocols/JabberG/src/jabber_caps.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_caps.h b/protocols/JabberG/src/jabber_caps.h index fc37993fb0..e8ceccf4a0 100644 --- a/protocols/JabberG/src/jabber_caps.h +++ b/protocols/JabberG/src/jabber_caps.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_captcha.cpp b/protocols/JabberG/src/jabber_captcha.cpp index 641c310395..c2cc8ec2eb 100644 --- a/protocols/JabberG/src/jabber_captcha.cpp +++ b/protocols/JabberG/src/jabber_captcha.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_chat.cpp b/protocols/JabberG/src/jabber_chat.cpp index a32591e576..90bf730ac4 100644 --- a/protocols/JabberG/src/jabber_chat.cpp +++ b/protocols/JabberG/src/jabber_chat.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_console.cpp b/protocols/JabberG/src/jabber_console.cpp index 7f4ab9c19f..cbcfc0f19d 100644 --- a/protocols/JabberG/src/jabber_console.cpp +++ b/protocols/JabberG/src/jabber_console.cpp @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov Copyright (c) 2007 Victor Pavlychko -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_disco.cpp b/protocols/JabberG/src/jabber_disco.cpp index cca24a8d29..50081898fd 100644 --- a/protocols/JabberG/src/jabber_disco.cpp +++ b/protocols/JabberG/src/jabber_disco.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_disco.h b/protocols/JabberG/src/jabber_disco.h index 48a01bfa93..5f3bd510df 100644 --- a/protocols/JabberG/src/jabber_disco.h +++ b/protocols/JabberG/src/jabber_disco.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2005-07 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_events.cpp b/protocols/JabberG/src/jabber_events.cpp index 043c438c95..da9f81c7bc 100644 --- a/protocols/JabberG/src/jabber_events.cpp +++ b/protocols/JabberG/src/jabber_events.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_file.cpp b/protocols/JabberG/src/jabber_file.cpp index b01b32f32a..f39cbbdf32 100644 --- a/protocols/JabberG/src/jabber_file.cpp +++ b/protocols/JabberG/src/jabber_file.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_form.cpp b/protocols/JabberG/src/jabber_form.cpp index e4e51658ea..e85e91566a 100644 --- a/protocols/JabberG/src/jabber_form.cpp +++ b/protocols/JabberG/src/jabber_form.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_ft.cpp b/protocols/JabberG/src/jabber_ft.cpp index 096351d713..0329d16747 100644 --- a/protocols/JabberG/src/jabber_ft.cpp +++ b/protocols/JabberG/src/jabber_ft.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_groupchat.cpp b/protocols/JabberG/src/jabber_groupchat.cpp index 7fd448657f..f4f3e6c008 100644 --- a/protocols/JabberG/src/jabber_groupchat.cpp +++ b/protocols/JabberG/src/jabber_groupchat.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_ibb.cpp b/protocols/JabberG/src/jabber_ibb.cpp index 22d1f2e41e..67ba5ba2c9 100644 --- a/protocols/JabberG/src/jabber_ibb.cpp +++ b/protocols/JabberG/src/jabber_ibb.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_ibb.h b/protocols/JabberG/src/jabber_ibb.h index b96ae971f9..f620b52ba4 100644 --- a/protocols/JabberG/src/jabber_ibb.h +++ b/protocols/JabberG/src/jabber_ibb.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_icolib.cpp b/protocols/JabberG/src/jabber_icolib.cpp index 4fe3a7d96c..7e08277418 100644 --- a/protocols/JabberG/src/jabber_icolib.cpp +++ b/protocols/JabberG/src/jabber_icolib.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team Idea & portions of code by Artem Shpynov diff --git a/protocols/JabberG/src/jabber_icolib.h b/protocols/JabberG/src/jabber_icolib.h index 7d2c0b1ce2..9066560f1d 100644 --- a/protocols/JabberG/src/jabber_icolib.h +++ b/protocols/JabberG/src/jabber_icolib.h @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007-09 Maxim Mluhov Copyright (c) 2007-09 Victor Pavlychko -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_iq.cpp b/protocols/JabberG/src/jabber_iq.cpp index 9e1dc7dc20..850e755420 100644 --- a/protocols/JabberG/src/jabber_iq.cpp +++ b/protocols/JabberG/src/jabber_iq.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_iq.h b/protocols/JabberG/src/jabber_iq.h index 50694fc73e..01da599737 100644 --- a/protocols/JabberG/src/jabber_iq.h +++ b/protocols/JabberG/src/jabber_iq.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_iq_handlers.cpp b/protocols/JabberG/src/jabber_iq_handlers.cpp index d73785b9f5..bf9f7ad573 100644 --- a/protocols/JabberG/src/jabber_iq_handlers.cpp +++ b/protocols/JabberG/src/jabber_iq_handlers.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_iqid.cpp b/protocols/JabberG/src/jabber_iqid.cpp index df222552fa..68783083c7 100644 --- a/protocols/JabberG/src/jabber_iqid.cpp +++ b/protocols/JabberG/src/jabber_iqid.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_iqid_muc.cpp b/protocols/JabberG/src/jabber_iqid_muc.cpp index 8bf9621ab6..f811d7cb82 100644 --- a/protocols/JabberG/src/jabber_iqid_muc.cpp +++ b/protocols/JabberG/src/jabber_iqid_muc.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_libstr.cpp b/protocols/JabberG/src/jabber_libstr.cpp index 0c20ff4cc2..2b7e91aea5 100644 --- a/protocols/JabberG/src/jabber_libstr.cpp +++ b/protocols/JabberG/src/jabber_libstr.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_list.cpp b/protocols/JabberG/src/jabber_list.cpp index bbfbb1d35e..73abe09e2b 100644 --- a/protocols/JabberG/src/jabber_list.cpp +++ b/protocols/JabberG/src/jabber_list.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_list.h b/protocols/JabberG/src/jabber_list.h index 082d247350..e418131443 100644 --- a/protocols/JabberG/src/jabber_list.h +++ b/protocols/JabberG/src/jabber_list.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_mam.cpp b/protocols/JabberG/src/jabber_mam.cpp index 989630ac07..0665e5df2f 100644 --- a/protocols/JabberG/src/jabber_mam.cpp +++ b/protocols/JabberG/src/jabber_mam.cpp @@ -1,160 +1,160 @@ -/* - -Jabber Protocol Plugin for Miranda NG - -Copyright (c) 2002-04 Santithorn Bunchua -Copyright (c) 2005-12 George Hazan -Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -*/ - -#include "stdafx.h" -#include "jabber_iq.h" -#include "jabber_caps.h" - -void CJabberProto::OnIqResultMamInfo(const TiXmlElement *iqNode, CJabberIqInfo *pInfo) -{ - if (pInfo->GetIqType() == JABBER_IQ_TYPE_RESULT) { - if (auto *n = XmlFirstChild(iqNode, "prefs")) { - m_bMamPrefsAvailable = true; - - if (auto *type = n->Attribute("default")) { - if (!strcmp(type, "never")) - m_iMamMode = 0; - else if (!strcmp(type, "roster")) - m_iMamMode = 1; - else - m_iMamMode = 2; - } - } - } - - // shall we retrieve missing messages? - if (pInfo->GetUserData()) - MamRetrieveMissingMessages(); -} - -void CJabberProto::MamSetMode(int iNewMode) -{ - if (!m_bEnableMam) - return; - - const char *szMode; - switch (iNewMode) { - case 0: szMode = "never"; break; - case 1: szMode = "roster"; break; - default: szMode = "always"; break; - } - - XmlNodeIq iq(AddIQ(&CJabberProto::OnIqResultMamInfo, JABBER_IQ_TYPE_SET)); - auto *node = iq << XCHILDNS("prefs", JABBER_FEAT_MAM) << XATTR("default", szMode); - node << XCHILD("always"); node << XCHILD("never"); - m_ThreadInfo->send(iq); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CJabberProto::MamRetrieveMissingMessages() -{ - CMStringA szLastId = getMStringA("LastMamId"); - - XmlNodeIq iq("set", SerialNext()); - auto *query = iq << XCHILDNS("query", JABBER_FEAT_MAM); - - if (szLastId.IsEmpty()) { - m_bMamDisableMessages = true; // our goal is to save message id, not to store messages - m_bMamCreateRead = false; - - char buf[100]; - time2str(time(0), buf, _countof(buf)); - - auto *form = query << XCHILDNS("x", JABBER_FEAT_DATA_FORMS) << XATTR("type", "submit"); - form << XCHILD("field") << XATTR("var", "FORM_TYPE") << XATTR("type", "hidden") << XCHILD("value", JABBER_FEAT_MAM); - form << XCHILD("field") << XATTR("var", "end") << XCHILD("value", buf); - } - else { - auto *set = query << XCHILDNS("set", "http://jabber.org/protocol/rsm"); - set << XCHILD("max", "1000"); - set << XCHILD("after", szLastId); - } - - m_ThreadInfo->send(iq); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Contact's history loader - -void CJabberProto::MamSendForm(const char *pszWith, const char *pszAfter) -{ - auto *pReq = AddIQ(&CJabberProto::OnIqResultRsm, JABBER_IQ_TYPE_SET); - pReq->SetParamsToParse(JABBER_IQ_PARSE_FROM); - - XmlNodeIq iq(pReq); - auto *query = iq << XCHILDNS("query", JABBER_FEAT_MAM); - - auto *form = query << XCHILDNS("x", JABBER_FEAT_DATA_FORMS) << XATTR("type", "submit"); - form << XCHILD("field") << XATTR("var", "FORM_TYPE") << XATTR("type", "hidden") << XCHILD("value", JABBER_FEAT_MAM); - if (pszWith != nullptr) - form << XCHILD("field") << XATTR("var", "with") << XCHILD("value", pszWith); - - auto *rsm = query << XCHILDNS("set", "http://jabber.org/protocol/rsm"); - rsm << XCHILD("max", "1000"); - if (pszAfter != nullptr) - rsm << XCHILD("after", pszAfter); - m_ThreadInfo->send(iq); -} - - -void CJabberProto::OnIqResultRsm(const TiXmlElement *iqNode, CJabberIqInfo *pInfo) -{ - // even if that flag was enabled, unset it - m_bMamDisableMessages = false; - - if (auto *fin = XmlGetChildByTag(iqNode, "fin", "xmlns", JABBER_FEAT_MAM)) { - // if dataset is complete, there's nothing more to do - if (!mir_strcmp(XmlGetAttr(fin, "complete"), "true")) - return; - - if (auto *set = XmlGetChildByTag(fin, "set", "xmlns", "http://jabber.org/protocol/rsm")) - if (auto *lastId = XmlGetChildText(set, "last")) - MamSendForm(ptrA(getUStringA(pInfo->GetHContact(), "jid")), lastId); - } -} - -INT_PTR __cdecl CJabberProto::OnMenuLoadHistory(WPARAM hContact, LPARAM) -{ - if (hContact == 0 || !m_bEnableMam) - return 0; - - // wipe out old history first - if (IDYES == MessageBoxW(NULL, TranslateT("Do you want to erase local history before loading it from server?"), m_tszUserName, MB_YESNOCANCEL | MB_ICONQUESTION)) { - DB::ECPTR pCursor(DB::Events(hContact)); - while (pCursor.FetchNext()) - pCursor.DeleteEvent(); - } - - // load remaining items from server - if (m_bJabberOnline) { - ptrA jid(getUStringA(hContact, "jid")); - if (jid != nullptr) { - m_bMamCreateRead = true; - MamSendForm(jid); - } - } - return 0; -} +/* + +Jabber Protocol Plugin for Miranda NG + +Copyright (c) 2002-04 Santithorn Bunchua +Copyright (c) 2005-12 George Hazan +Copyright (c) 2007 Maxim Mluhov +Copyright (C) 2012-23 Miranda NG team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "stdafx.h" +#include "jabber_iq.h" +#include "jabber_caps.h" + +void CJabberProto::OnIqResultMamInfo(const TiXmlElement *iqNode, CJabberIqInfo *pInfo) +{ + if (pInfo->GetIqType() == JABBER_IQ_TYPE_RESULT) { + if (auto *n = XmlFirstChild(iqNode, "prefs")) { + m_bMamPrefsAvailable = true; + + if (auto *type = n->Attribute("default")) { + if (!strcmp(type, "never")) + m_iMamMode = 0; + else if (!strcmp(type, "roster")) + m_iMamMode = 1; + else + m_iMamMode = 2; + } + } + } + + // shall we retrieve missing messages? + if (pInfo->GetUserData()) + MamRetrieveMissingMessages(); +} + +void CJabberProto::MamSetMode(int iNewMode) +{ + if (!m_bEnableMam) + return; + + const char *szMode; + switch (iNewMode) { + case 0: szMode = "never"; break; + case 1: szMode = "roster"; break; + default: szMode = "always"; break; + } + + XmlNodeIq iq(AddIQ(&CJabberProto::OnIqResultMamInfo, JABBER_IQ_TYPE_SET)); + auto *node = iq << XCHILDNS("prefs", JABBER_FEAT_MAM) << XATTR("default", szMode); + node << XCHILD("always"); node << XCHILD("never"); + m_ThreadInfo->send(iq); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CJabberProto::MamRetrieveMissingMessages() +{ + CMStringA szLastId = getMStringA("LastMamId"); + + XmlNodeIq iq("set", SerialNext()); + auto *query = iq << XCHILDNS("query", JABBER_FEAT_MAM); + + if (szLastId.IsEmpty()) { + m_bMamDisableMessages = true; // our goal is to save message id, not to store messages + m_bMamCreateRead = false; + + char buf[100]; + time2str(time(0), buf, _countof(buf)); + + auto *form = query << XCHILDNS("x", JABBER_FEAT_DATA_FORMS) << XATTR("type", "submit"); + form << XCHILD("field") << XATTR("var", "FORM_TYPE") << XATTR("type", "hidden") << XCHILD("value", JABBER_FEAT_MAM); + form << XCHILD("field") << XATTR("var", "end") << XCHILD("value", buf); + } + else { + auto *set = query << XCHILDNS("set", "http://jabber.org/protocol/rsm"); + set << XCHILD("max", "1000"); + set << XCHILD("after", szLastId); + } + + m_ThreadInfo->send(iq); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Contact's history loader + +void CJabberProto::MamSendForm(const char *pszWith, const char *pszAfter) +{ + auto *pReq = AddIQ(&CJabberProto::OnIqResultRsm, JABBER_IQ_TYPE_SET); + pReq->SetParamsToParse(JABBER_IQ_PARSE_FROM); + + XmlNodeIq iq(pReq); + auto *query = iq << XCHILDNS("query", JABBER_FEAT_MAM); + + auto *form = query << XCHILDNS("x", JABBER_FEAT_DATA_FORMS) << XATTR("type", "submit"); + form << XCHILD("field") << XATTR("var", "FORM_TYPE") << XATTR("type", "hidden") << XCHILD("value", JABBER_FEAT_MAM); + if (pszWith != nullptr) + form << XCHILD("field") << XATTR("var", "with") << XCHILD("value", pszWith); + + auto *rsm = query << XCHILDNS("set", "http://jabber.org/protocol/rsm"); + rsm << XCHILD("max", "1000"); + if (pszAfter != nullptr) + rsm << XCHILD("after", pszAfter); + m_ThreadInfo->send(iq); +} + + +void CJabberProto::OnIqResultRsm(const TiXmlElement *iqNode, CJabberIqInfo *pInfo) +{ + // even if that flag was enabled, unset it + m_bMamDisableMessages = false; + + if (auto *fin = XmlGetChildByTag(iqNode, "fin", "xmlns", JABBER_FEAT_MAM)) { + // if dataset is complete, there's nothing more to do + if (!mir_strcmp(XmlGetAttr(fin, "complete"), "true")) + return; + + if (auto *set = XmlGetChildByTag(fin, "set", "xmlns", "http://jabber.org/protocol/rsm")) + if (auto *lastId = XmlGetChildText(set, "last")) + MamSendForm(ptrA(getUStringA(pInfo->GetHContact(), "jid")), lastId); + } +} + +INT_PTR __cdecl CJabberProto::OnMenuLoadHistory(WPARAM hContact, LPARAM) +{ + if (hContact == 0 || !m_bEnableMam) + return 0; + + // wipe out old history first + if (IDYES == MessageBoxW(NULL, TranslateT("Do you want to erase local history before loading it from server?"), m_tszUserName, MB_YESNOCANCEL | MB_ICONQUESTION)) { + DB::ECPTR pCursor(DB::Events(hContact)); + while (pCursor.FetchNext()) + pCursor.DeleteEvent(); + } + + // load remaining items from server + if (m_bJabberOnline) { + ptrA jid(getUStringA(hContact, "jid")); + if (jid != nullptr) { + m_bMamCreateRead = true; + MamSendForm(jid); + } + } + return 0; +} diff --git a/protocols/JabberG/src/jabber_menu.cpp b/protocols/JabberG/src/jabber_menu.cpp index 920d390f1a..0d510f6859 100644 --- a/protocols/JabberG/src/jabber_menu.cpp +++ b/protocols/JabberG/src/jabber_menu.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_message_handlers.cpp b/protocols/JabberG/src/jabber_message_handlers.cpp index 4301a4a5c8..7610908dfe 100644 --- a/protocols/JabberG/src/jabber_message_handlers.cpp +++ b/protocols/JabberG/src/jabber_message_handlers.cpp @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-08 George Hazan Copyright (c) 2007 Maxim Mluhov Copyright (c) 2008-09 Dmitriy Chervov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_message_manager.cpp b/protocols/JabberG/src/jabber_message_manager.cpp index a794974a41..b1db1f299a 100644 --- a/protocols/JabberG/src/jabber_message_manager.cpp +++ b/protocols/JabberG/src/jabber_message_manager.cpp @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-08 George Hazan Copyright (c) 2007 Maxim Mluhov Copyright (c) 2008-09 Dmitriy Chervov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_message_manager.h b/protocols/JabberG/src/jabber_message_manager.h index 07d381e90f..f7ae0d82ee 100644 --- a/protocols/JabberG/src/jabber_message_manager.h +++ b/protocols/JabberG/src/jabber_message_manager.h @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-08 George Hazan Copyright (c) 2007 Maxim Mluhov Copyright (c) 2008-09 Dmitriy Chervov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_misc.cpp b/protocols/JabberG/src/jabber_misc.cpp index 88a6909eca..71115ef87a 100644 --- a/protocols/JabberG/src/jabber_misc.cpp +++ b/protocols/JabberG/src/jabber_misc.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_notes.cpp b/protocols/JabberG/src/jabber_notes.cpp index 6734e7c26c..ac6272f5b7 100644 --- a/protocols/JabberG/src/jabber_notes.cpp +++ b/protocols/JabberG/src/jabber_notes.cpp @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007-09 Maxim Mluhov Copyright (c) 2007-09 Victor Pavlychko -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_notes.h b/protocols/JabberG/src/jabber_notes.h index d103a938f4..bb7cc22181 100644 --- a/protocols/JabberG/src/jabber_notes.h +++ b/protocols/JabberG/src/jabber_notes.h @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007-09 Maxim Mluhov Copyright (c) 2007-09 Victor Pavlychko -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_omemo.cpp b/protocols/JabberG/src/jabber_omemo.cpp index e5edb53d57..f51245d6bd 100644 --- a/protocols/JabberG/src/jabber_omemo.cpp +++ b/protocols/JabberG/src/jabber_omemo.cpp @@ -2,7 +2,7 @@ Jabber Protocol Plugin for Miranda NG -Copyright (c) 2017-22 Miranda NG team +Copyright (c) 2017-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_omemo.h b/protocols/JabberG/src/jabber_omemo.h index 8563735c36..94b9fd8dbb 100644 --- a/protocols/JabberG/src/jabber_omemo.h +++ b/protocols/JabberG/src/jabber_omemo.h @@ -2,7 +2,7 @@ Jabber Protocol Plugin for Miranda NG -Copyright (c) 2017-22 Miranda NG team +Copyright (c) 2017-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_opt.cpp b/protocols/JabberG/src/jabber_opt.cpp index c3f735c223..c7e85211cd 100644 --- a/protocols/JabberG/src/jabber_opt.cpp +++ b/protocols/JabberG/src/jabber_opt.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_password.cpp b/protocols/JabberG/src/jabber_password.cpp index d39c6a5aa5..0fe0700998 100644 --- a/protocols/JabberG/src/jabber_password.cpp +++ b/protocols/JabberG/src/jabber_password.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_presence_manager.cpp b/protocols/JabberG/src/jabber_presence_manager.cpp index de0dabbe65..873b2a1076 100644 --- a/protocols/JabberG/src/jabber_presence_manager.cpp +++ b/protocols/JabberG/src/jabber_presence_manager.cpp @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-08 George Hazan Copyright (c) 2007 Maxim Mluhov Copyright (c) 2008-09 Dmitriy Chervov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_presence_manager.h b/protocols/JabberG/src/jabber_presence_manager.h index 4dab8a4991..0c488747c5 100644 --- a/protocols/JabberG/src/jabber_presence_manager.h +++ b/protocols/JabberG/src/jabber_presence_manager.h @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-08 George Hazan Copyright (c) 2007 Maxim Mluhov Copyright (c) 2008-09 Dmitriy Chervov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_privacy.cpp b/protocols/JabberG/src/jabber_privacy.cpp index 8a02af9bba..b1087fde09 100644 --- a/protocols/JabberG/src/jabber_privacy.cpp +++ b/protocols/JabberG/src/jabber_privacy.cpp @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007-09 Maxim Mluhov Copyright (c) 2007-09 Victor Pavlychko -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_privacy.h b/protocols/JabberG/src/jabber_privacy.h index 4ad6f4294a..d6918a5f1e 100644 --- a/protocols/JabberG/src/jabber_privacy.h +++ b/protocols/JabberG/src/jabber_privacy.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_proto.cpp b/protocols/JabberG/src/jabber_proto.cpp index 8e2f349cb6..3811becab3 100644 --- a/protocols/JabberG/src/jabber_proto.cpp +++ b/protocols/JabberG/src/jabber_proto.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_proto.h b/protocols/JabberG/src/jabber_proto.h index 29b05e25c0..6037019f46 100644 --- a/protocols/JabberG/src/jabber_proto.h +++ b/protocols/JabberG/src/jabber_proto.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_rc.cpp b/protocols/JabberG/src/jabber_rc.cpp index 99e642d38d..a5bae9e944 100644 --- a/protocols/JabberG/src/jabber_rc.cpp +++ b/protocols/JabberG/src/jabber_rc.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team XEP-0146 support for Miranda IM diff --git a/protocols/JabberG/src/jabber_rc.h b/protocols/JabberG/src/jabber_rc.h index 430261602d..7549723872 100644 --- a/protocols/JabberG/src/jabber_rc.h +++ b/protocols/JabberG/src/jabber_rc.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team XEP-0146 support for Miranda IM diff --git a/protocols/JabberG/src/jabber_roster.cpp b/protocols/JabberG/src/jabber_roster.cpp index 5072fe9ebb..03d9e9ffdd 100644 --- a/protocols/JabberG/src/jabber_roster.cpp +++ b/protocols/JabberG/src/jabber_roster.cpp @@ -1,551 +1,551 @@ -/* - -Jabber Protocol Plugin for Miranda NG - -Copyright (c) 2002-04 Santithorn Bunchua -Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -*/ - -#include "stdafx.h" - -#include - -////////////////////////////////////////////////////////////////////////// -// roster editor -// - -enum -{ - RRA_FILLLIST = 0, - RRA_SYNCROSTER, - RRA_SYNCDONE -}; - -struct ROSTEREDITDAT -{ - HWND hList; - int index; - int subindex; -}; - -static void _RosterItemEditEnd(HWND hEditor, ROSTEREDITDAT *edat, BOOL bCancel) -{ - if (!bCancel) { - int len = GetWindowTextLength(hEditor) + 1; - wchar_t *buff = (wchar_t*)mir_alloc(len*sizeof(wchar_t)); - if (buff) { - GetWindowText(hEditor, buff, len); - ListView_SetItemText(edat->hList, edat->index, edat->subindex, buff); - } - mir_free(buff); - } - DestroyWindow(hEditor); -} - -static LRESULT CALLBACK _RosterItemNewEditProc(HWND hEditor, UINT msg, WPARAM wParam, LPARAM lParam) -{ - ROSTEREDITDAT * edat = (ROSTEREDITDAT *)GetWindowLongPtr(hEditor, GWLP_USERDATA); - if (!edat) return 0; - switch (msg) { - case WM_KEYDOWN: - switch (wParam) { - case VK_RETURN: - _RosterItemEditEnd(hEditor, edat, FALSE); - return 0; - case VK_ESCAPE: - _RosterItemEditEnd(hEditor, edat, TRUE); - return 0; - } - break; - - case WM_GETDLGCODE: - if (lParam) { - MSG *msg2 = (MSG*)lParam; - if (msg2->message == WM_KEYDOWN && msg2->wParam == VK_TAB) return 0; - if (msg2->message == WM_CHAR && msg2->wParam == '\t') return 0; - } - return DLGC_WANTMESSAGE; - - case WM_KILLFOCUS: - _RosterItemEditEnd(hEditor, edat, FALSE); - return 0; - - case WM_DESTROY: - SetWindowLongPtr(hEditor, GWLP_USERDATA, (LONG_PTR)0); - free(edat); - return 0; - } - - return mir_callNextSubclass(hEditor, _RosterItemNewEditProc, msg, wParam, lParam); -} - -static LRESULT CALLBACK _RosterNewListProc(HWND hList, UINT msg, WPARAM wParam, LPARAM lParam) -{ - if (msg == WM_MOUSEWHEEL || msg == WM_NCLBUTTONDOWN || msg == WM_NCRBUTTONDOWN) - SetFocus(hList); - - if (msg == WM_LBUTTONDOWN) { - POINT pt; - GetCursorPos(&pt); - ScreenToClient(hList, &pt); - - LVHITTESTINFO lvhti = { 0 }; - lvhti.pt = pt; - ListView_SubItemHitTest(hList, &lvhti); - if (lvhti.flags&LVHT_ONITEM && lvhti.iSubItem != 0) { - RECT rc; - wchar_t buff[260]; - ListView_GetSubItemRect(hList, lvhti.iItem, lvhti.iSubItem, LVIR_BOUNDS, &rc); - ListView_GetItemText(hList, lvhti.iItem, lvhti.iSubItem, buff, _countof(buff)); - HWND hEditor = CreateWindow(TEXT("EDIT"), buff, WS_CHILD | ES_AUTOHSCROLL, rc.left + 3, rc.top + 2, rc.right - rc.left - 3, rc.bottom - rc.top - 3, hList, nullptr, g_plugin.getInst(), nullptr); - SendMessage(hEditor, WM_SETFONT, (WPARAM)SendMessage(hList, WM_GETFONT, 0, 0), 0); - ShowWindow(hEditor, SW_SHOW); - SetWindowText(hEditor, buff); - ClientToScreen(hList, &pt); - ScreenToClient(hEditor, &pt); - mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); - mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); - - ROSTEREDITDAT * edat = (ROSTEREDITDAT *)malloc(sizeof(ROSTEREDITDAT)); - mir_subclassWindow(hEditor, _RosterItemNewEditProc); - edat->hList = hList; - edat->index = lvhti.iItem; - edat->subindex = lvhti.iSubItem; - SetWindowLongPtr(hEditor, GWLP_USERDATA, (LONG_PTR)edat); - } - } - return mir_callNextSubclass(hList, _RosterNewListProc, msg, wParam, lParam); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// JabberRosterOptDlgProc - advanced options dialog procedure - -class CRosterEditorDlg : public CJabberDlgBase -{ - friend struct CJabberProto; - typedef CJabberDlgBase CSuper; - - uint8_t m_bRRAction; - BOOL m_bReadyToDownload = true; - BOOL m_bReadyToUpload = false; - - void _RosterSendRequest(uint8_t rrAction) - { - m_bRRAction = rrAction; - - m_proto->m_ThreadInfo->send( - XmlNodeIq(m_proto->AddIQ(&CJabberProto::_RosterHandleGetRequest, JABBER_IQ_TYPE_GET)) - << XCHILDNS("query", JABBER_FEAT_IQ_ROSTER)); - } - - int _RosterInsertListItem(const char *jid, const char *nick, const char *group, const char *subscr, bool bChecked) - { - Utf2T wszJid(jid); - LVITEM item = { 0 }; - item.mask = LVIF_TEXT | LVIF_STATE; - item.iItem = m_list.GetItemCount(); - item.pszText = wszJid; - - int index = m_list.InsertItem(&item); - if (index < 0) - return index; - - m_list.SetCheckState(index, bChecked); - - m_list.SetItemText(index, 1, Utf2T(nick)); - m_list.SetItemText(index, 2, Utf2T(group)); - m_list.SetItemText(index, 3, TranslateW(Utf2T(subscr))); - return index; - } - - void _RosterListClear() - { - m_list.DeleteAllItems(); - while (m_list.GetColumnWidth(0) > 0) - m_list.DeleteColumn(0); - - LV_COLUMN column = { 0 }; - column.mask = LVCF_TEXT; - - column.pszText = TranslateT("JID"); - m_list.InsertColumn(1, &column); - - column.pszText = TranslateT("Nickname"); - m_list.InsertColumn(2, &column); - - column.pszText = TranslateT("Group"); - m_list.InsertColumn(3, &column); - - column.pszText = TranslateT("Subscription"); - m_list.InsertColumn(4, &column); - - RECT rc; - GetClientRect(m_list.GetHwnd(), &rc); - ResizeColumns(rc.right - rc.left); - } - - void ResizeColumns(int width) - { - m_list.SetColumnWidth(0, width * 40 / 100); - m_list.SetColumnWidth(1, width * 25 / 100); - m_list.SetColumnWidth(2, width * 20 / 100); - m_list.SetColumnWidth(3, width * 12 / 100); - } - - void OnChangeStatus() - { - int count = m_list.GetItemCount(); - btnDownload.Enable(m_proto->m_bJabberOnline); - btnUpload.Enable(count && m_proto->m_bJabberOnline); - btnExport.Enable(count > 0); - } - - CCtrlButton btnDownload, btnUpload, btnExport, btnImport; - CCtrlListView m_list; - -public: - CRosterEditorDlg(CJabberProto *m_proto) : - CSuper(m_proto, IDD_ROSTER_EDITOR), - m_list(this, IDC_ROSTER), - btnExport(this, IDC_EXPORT), - btnImport(this, IDC_IMPORT), - btnUpload(this, IDC_UPLOAD), - btnDownload(this, IDC_DOWNLOAD) - { - SetMinSize(550, 390); - - btnExport.OnClick = Callback(this, &CRosterEditorDlg::onClick_Export); - btnImport.OnClick = Callback(this, &CRosterEditorDlg::onClick_Import); - btnUpload.OnClick = Callback(this, &CRosterEditorDlg::onClick_Upload); - btnDownload.OnClick = Callback(this, &CRosterEditorDlg::onClick_Download); - } - - bool OnInitDialog() override - { - SetWindowTextW(m_hwnd, CMStringW(FORMAT, L"%s: %s", TranslateT("Roster Editor"), m_proto->m_tszUserName)); - - Window_SetIcon_IcoLib(m_hwnd, g_plugin.getIconHandle(IDI_AGENTS)); - - Utils_RestoreWindowPosition(m_hwnd, 0, m_proto->m_szModuleName, "rosterCtrlWnd_"); - - m_list.SetExtendedListViewStyle(LVS_EX_CHECKBOXES | LVS_EX_BORDERSELECT | LVS_EX_GRIDLINES); - mir_subclassWindow(m_list.GetHwnd(), _RosterNewListProc); - _RosterListClear(); - OnChangeStatus(); - return true; - } - - void OnDestroy() override - { - m_proto->m_hwndRosterEditor = nullptr; - Utils_SaveWindowPosition(m_hwnd, 0, m_proto->m_szModuleName, "rosterCtrlWnd_"); - Window_FreeIcon_IcoLib(m_hwnd); - } - - int Resizer(UTILRESIZECONTROL *urc) override - { - switch (urc->wId) { - case IDC_HEADERBAR: - return RD_ANCHORX_LEFT | RD_ANCHORY_TOP | RD_ANCHORX_WIDTH; - case IDC_ROSTER: - ResizeColumns(urc->rcItem.right - urc->rcItem.left + urc->dlgNewSize.cx - urc->dlgOriginalSize.cx); - return RD_ANCHORX_LEFT | RD_ANCHORY_TOP | RD_ANCHORY_HEIGHT | RD_ANCHORX_WIDTH; - case IDC_DOWNLOAD: - case IDC_UPLOAD: - return RD_ANCHORX_LEFT | RD_ANCHORY_BOTTOM; - case IDC_EXPORT: - case IDC_IMPORT: - return RD_ANCHORX_RIGHT | RD_ANCHORY_BOTTOM; - } - return RD_ANCHORX_LEFT | RD_ANCHORY_TOP; - } - - void HandleNode(const TiXmlElement *node) - { - if (m_bRRAction == RRA_FILLLIST) { - _RosterListClear(); - auto *query = XmlFirstChild(node, "query"); - if (query == nullptr) return; - - for (auto *item : TiXmlFilter(query, "item")) { - const char *jid = XmlGetAttr(item, "jid"); - if (jid == nullptr) - continue; - - const char *name = XmlGetAttr(item, "name"); - const char *subscription = XmlGetAttr(item, "subscription"); - const char *group = XmlGetChildText(item, "group"); - _RosterInsertListItem(jid, name, group, subscription, true); - } - - // now it is require to process whole contact list to add not in roster contacts - for (auto &hContact : m_proto->AccContacts()) { - ptrW tszJid(m_proto->getWStringA(hContact, "jid")); - if (tszJid == nullptr) - continue; - - LVFINDINFO lvfi = { 0 }; - lvfi.flags = LVFI_STRING; - lvfi.psz = tszJid; - if (m_list.FindItem(-1, &lvfi) == -1) { - ptrA tszName(db_get_utfa(hContact, "CList", "MyHandle")); - ptrA tszGroup(db_get_utfa(hContact, "CList", "Group")); - _RosterInsertListItem(T2Utf(tszJid), tszName, tszGroup, nullptr, false); - } - } - m_bReadyToDownload = false; - m_bReadyToUpload = true; - btnDownload.SetText(TranslateT("Download")); - btnUpload.SetText(TranslateT("Upload")); - OnChangeStatus(); - return; - } - - if (m_bRRAction == RRA_SYNCROSTER) { - btnUpload.SetText(TranslateT("Uploading...")); - auto *queryRoster = XmlFirstChild(node, "query"); - if (!queryRoster) - return; - - int ListItemCount = m_list.GetItemCount(); - for (int index = 0; index < ListItemCount; index++) { - wchar_t jid[JABBER_MAX_JID_LEN] = L""; - wchar_t name[260]; - wchar_t group[260]; - wchar_t subscr[260]; - m_list.GetItemText(index, 0, jid, _countof(jid)); - m_list.GetItemText(index, 1, name, _countof(name)); - m_list.GetItemText(index, 2, group, _countof(group)); - m_list.GetItemText(index, 3, subscr, _countof(subscr)); - - T2Utf szJid(jid), szName(name), szGroup(group), szSubscr(subscr); - auto *itemRoster = XmlGetChildByTag(queryRoster, "item", "jid", szJid); - bool bRemove = !m_list.GetCheckState(index); - if (itemRoster && bRemove) { // delete item - XmlNodeIq iq(m_proto->AddIQ(&CJabberProto::_RosterHandleGetRequest, JABBER_IQ_TYPE_SET)); - iq << XCHILDNS("query", JABBER_FEAT_IQ_ROSTER) << XCHILD("item") << XATTR("jid", szJid) << XATTR("subscription", "remove"); - m_proto->m_ThreadInfo->send(iq); - } - else if (!bRemove) { - bool bPushed = itemRoster != 0; - const char *rosterName = 0, *rosterGroup = 0; - if (!bPushed) { - rosterName = XmlGetAttr(itemRoster, "name"); - if ((rosterName != nullptr || name[0] != 0) && mir_strcmpi(rosterName, szName)) - bPushed = true; - if (!bPushed) { - auto *szSub = XmlGetAttr(itemRoster, "subscription"); - if ((szSub != nullptr || subscr[0] != 0) && mir_strcmpi(szSub, szSubscr)) - bPushed = true; - } - if (!bPushed) { - rosterGroup = XmlGetChildText(itemRoster, "group"); - if (rosterGroup != nullptr && mir_strcmpi(rosterGroup, szGroup)) - bPushed = true; - } - } - if (bPushed) { - XmlNodeIq iq(m_proto->AddIQ(&CJabberProto::_RosterHandleGetRequest, JABBER_IQ_TYPE_SET)); - auto *item = iq << XCHILDNS("query", JABBER_FEAT_IQ_ROSTER) << XCHILD("item"); - if (rosterGroup || mir_strlen(szGroup)) - item << XCHILD("group", szGroup); - if (rosterName || mir_strlen(szName)) - item << XATTR("name", szName); - item << XATTR("jid", szJid) << XATTR("subscription", subscr[0] ? szSubscr : "none"); - m_proto->m_ThreadInfo->send(iq); - } - } - } - m_bRRAction = RRA_SYNCDONE; - _RosterSendRequest(RRA_FILLLIST); - return; - } - - btnUpload.SetText(TranslateT("Upload")); - onClick_Download(nullptr); - }; - - void onClick_Download(CCtrlButton*) - { - m_bReadyToUpload = m_bReadyToDownload = false; - OnChangeStatus(); - btnDownload.SetText(TranslateT("Downloading...")); - _RosterSendRequest(RRA_FILLLIST); - } - - void onClick_Upload(CCtrlButton*) - { - m_bReadyToUpload = false; - OnChangeStatus(); - btnUpload.SetText(TranslateT("Connecting...")); - _RosterSendRequest(RRA_SYNCROSTER); - } - - void onClick_Export(CCtrlButton*) - { - wchar_t filename[MAX_PATH] = { 0 }; - - wchar_t filter[MAX_PATH]; - mir_snwprintf(filter, L"%s (*.xml)%c*.xml%c%c", TranslateT("XML (UTF-8 encoded)"), 0, 0, 0); - OPENFILENAME ofn = {}; - ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400; - ofn.hwndOwner = m_hwnd; - ofn.lpstrFilter = filter; - ofn.lpstrFile = filename; - ofn.Flags = OFN_HIDEREADONLY; - ofn.nMaxFile = _countof(filename); - ofn.nMaxFileTitle = MAX_PATH; - ofn.lpstrDefExt = L"xml"; - if (!GetSaveFileNameW(&ofn)) - return; - - FILE * fp = _wfopen(filename, L"wb"); - if (!fp) - return; - - int ListItemCount = m_list.GetItemCount(); - - XmlNode root("Roster"); - - for (int index = 0; index < ListItemCount; index++) { - wchar_t jid[JABBER_MAX_JID_LEN] = L""; - wchar_t name[260] = L""; - wchar_t group[260] = L""; - wchar_t subscr[260] = L""; - m_list.GetItemText(index, 0, jid, _countof(jid)); - m_list.GetItemText(index, 1, name, _countof(name)); - m_list.GetItemText(index, 2, group, _countof(group)); - m_list.GetItemText(index, 3, subscr, _countof(subscr)); - root << XCHILD("Item") << XATTR("jid", T2Utf(jid)) << XATTR("name", T2Utf(name)) << XATTR("group", T2Utf(group)) << XATTR("subscription", T2Utf(subscr)); - } - - tinyxml2::XMLPrinter printer(fp); - root.Print(&printer); - fclose(fp); - } - - void onClick_Import(CCtrlButton*) - { - wchar_t filename[MAX_PATH] = {}; - - OPENFILENAME ofn = { 0 }; - ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400; - ofn.hwndOwner = m_hwnd; - ofn.hInstance = nullptr; - ofn.lpstrFilter = L"XML (UTF-8 encoded)(*.xml)\0*.xml\0\0"; - ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; - ofn.lpstrFile = filename; - ofn.nMaxFile = _countof(filename); - ofn.nMaxFileTitle = MAX_PATH; - ofn.lpstrDefExt = L"xml"; - if (!GetOpenFileNameW(&ofn)) - return; - - FILE * fp = _wfopen(filename, L"rb"); - if (!fp) - return; - - TiXmlDocument doc; - int ret = doc.LoadFile(fp); - fclose(fp); - if (ret != 0) - return; - - _RosterListClear(); - - const TiXmlElement *Table = TiXmlConst(&doc)["Workbook"]["Worksheet"]["Table"].ToElement(); - if (Table) { - for (auto *Row : TiXmlFilter(Table, "Row")) { - bool bAdd = false; - const char *jid = nullptr; - const char *name = nullptr; - const char *group = nullptr; - const char *subscr = nullptr; - auto *Cell = XmlFirstChild(Row, "Cell"); - auto *Data = XmlFirstChild(Cell, "Data"); - if (Data) { - if (!mir_strcmpi(Data->GetText(), "+")) - bAdd = true; - else if (mir_strcmpi(Data->GetText(), "-")) - continue; - - Cell = Cell->NextSiblingElement("Cell"); - if (Cell) Data = XmlFirstChild(Cell, "Data"); - else Data = nullptr; - if (Data) { - jid = Data->GetText(); - if (!jid || mir_strlen(jid) == 0) - continue; - } - - Cell = Cell->NextSiblingElement("Cell"); - if (Cell) Data = XmlFirstChild(Cell, "Data"); - else Data = nullptr; - if (Data) name = Data->GetText(); - - Cell = Cell->NextSiblingElement("Cell"); - if (Cell) Data = XmlFirstChild(Cell, "Data"); - else Data = nullptr; - if (Data) group = Data->GetText(); - - Cell = Cell->NextSiblingElement("Cell"); - if (Cell) Data = XmlFirstChild(Cell, "Data"); - else Data = nullptr; - if (Data) subscr = Data->GetText(); - } - _RosterInsertListItem(jid, name, group, subscr, bAdd); - } - } - else if (Table = TiXmlConst(&doc)["Roster"].ToElement()) { - for (auto *Row : TiXmlFilter(Table, "Item")) { - auto *jid = Row->Attribute("jid"); - auto *name = Row->Attribute("name"); - auto *group = Row->Attribute("group"); - auto *subscr = Row->Attribute("subscription"); - _RosterInsertListItem(jid, name, group, subscr, true); - } - } - - OnChangeStatus(); - } -}; - -INT_PTR __cdecl CJabberProto::OnMenuHandleRosterControl(WPARAM, LPARAM) -{ - if (m_hwndRosterEditor) - SetForegroundWindow(m_hwndRosterEditor->GetHwnd()); - else { - m_hwndRosterEditor = new CRosterEditorDlg(this); - m_hwndRosterEditor->Show(); - } - - return 0; -} - -void CJabberProto::_RosterHandleGetRequest(const TiXmlElement *node, CJabberIqInfo*) -{ - if (m_hwndRosterEditor) - m_hwndRosterEditor->HandleNode(node); -} - -void CJabberProto::JabberUpdateDialogs(BOOL) -{ - if (m_hwndRosterEditor) - m_hwndRosterEditor->OnChangeStatus(); -} +/* + +Jabber Protocol Plugin for Miranda NG + +Copyright (c) 2002-04 Santithorn Bunchua +Copyright (c) 2005-12 George Hazan +Copyright (C) 2012-23 Miranda NG team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "stdafx.h" + +#include + +////////////////////////////////////////////////////////////////////////// +// roster editor +// + +enum +{ + RRA_FILLLIST = 0, + RRA_SYNCROSTER, + RRA_SYNCDONE +}; + +struct ROSTEREDITDAT +{ + HWND hList; + int index; + int subindex; +}; + +static void _RosterItemEditEnd(HWND hEditor, ROSTEREDITDAT *edat, BOOL bCancel) +{ + if (!bCancel) { + int len = GetWindowTextLength(hEditor) + 1; + wchar_t *buff = (wchar_t*)mir_alloc(len*sizeof(wchar_t)); + if (buff) { + GetWindowText(hEditor, buff, len); + ListView_SetItemText(edat->hList, edat->index, edat->subindex, buff); + } + mir_free(buff); + } + DestroyWindow(hEditor); +} + +static LRESULT CALLBACK _RosterItemNewEditProc(HWND hEditor, UINT msg, WPARAM wParam, LPARAM lParam) +{ + ROSTEREDITDAT * edat = (ROSTEREDITDAT *)GetWindowLongPtr(hEditor, GWLP_USERDATA); + if (!edat) return 0; + switch (msg) { + case WM_KEYDOWN: + switch (wParam) { + case VK_RETURN: + _RosterItemEditEnd(hEditor, edat, FALSE); + return 0; + case VK_ESCAPE: + _RosterItemEditEnd(hEditor, edat, TRUE); + return 0; + } + break; + + case WM_GETDLGCODE: + if (lParam) { + MSG *msg2 = (MSG*)lParam; + if (msg2->message == WM_KEYDOWN && msg2->wParam == VK_TAB) return 0; + if (msg2->message == WM_CHAR && msg2->wParam == '\t') return 0; + } + return DLGC_WANTMESSAGE; + + case WM_KILLFOCUS: + _RosterItemEditEnd(hEditor, edat, FALSE); + return 0; + + case WM_DESTROY: + SetWindowLongPtr(hEditor, GWLP_USERDATA, (LONG_PTR)0); + free(edat); + return 0; + } + + return mir_callNextSubclass(hEditor, _RosterItemNewEditProc, msg, wParam, lParam); +} + +static LRESULT CALLBACK _RosterNewListProc(HWND hList, UINT msg, WPARAM wParam, LPARAM lParam) +{ + if (msg == WM_MOUSEWHEEL || msg == WM_NCLBUTTONDOWN || msg == WM_NCRBUTTONDOWN) + SetFocus(hList); + + if (msg == WM_LBUTTONDOWN) { + POINT pt; + GetCursorPos(&pt); + ScreenToClient(hList, &pt); + + LVHITTESTINFO lvhti = { 0 }; + lvhti.pt = pt; + ListView_SubItemHitTest(hList, &lvhti); + if (lvhti.flags&LVHT_ONITEM && lvhti.iSubItem != 0) { + RECT rc; + wchar_t buff[260]; + ListView_GetSubItemRect(hList, lvhti.iItem, lvhti.iSubItem, LVIR_BOUNDS, &rc); + ListView_GetItemText(hList, lvhti.iItem, lvhti.iSubItem, buff, _countof(buff)); + HWND hEditor = CreateWindow(TEXT("EDIT"), buff, WS_CHILD | ES_AUTOHSCROLL, rc.left + 3, rc.top + 2, rc.right - rc.left - 3, rc.bottom - rc.top - 3, hList, nullptr, g_plugin.getInst(), nullptr); + SendMessage(hEditor, WM_SETFONT, (WPARAM)SendMessage(hList, WM_GETFONT, 0, 0), 0); + ShowWindow(hEditor, SW_SHOW); + SetWindowText(hEditor, buff); + ClientToScreen(hList, &pt); + ScreenToClient(hEditor, &pt); + mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); + mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); + + ROSTEREDITDAT * edat = (ROSTEREDITDAT *)malloc(sizeof(ROSTEREDITDAT)); + mir_subclassWindow(hEditor, _RosterItemNewEditProc); + edat->hList = hList; + edat->index = lvhti.iItem; + edat->subindex = lvhti.iSubItem; + SetWindowLongPtr(hEditor, GWLP_USERDATA, (LONG_PTR)edat); + } + } + return mir_callNextSubclass(hList, _RosterNewListProc, msg, wParam, lParam); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// JabberRosterOptDlgProc - advanced options dialog procedure + +class CRosterEditorDlg : public CJabberDlgBase +{ + friend struct CJabberProto; + typedef CJabberDlgBase CSuper; + + uint8_t m_bRRAction; + BOOL m_bReadyToDownload = true; + BOOL m_bReadyToUpload = false; + + void _RosterSendRequest(uint8_t rrAction) + { + m_bRRAction = rrAction; + + m_proto->m_ThreadInfo->send( + XmlNodeIq(m_proto->AddIQ(&CJabberProto::_RosterHandleGetRequest, JABBER_IQ_TYPE_GET)) + << XCHILDNS("query", JABBER_FEAT_IQ_ROSTER)); + } + + int _RosterInsertListItem(const char *jid, const char *nick, const char *group, const char *subscr, bool bChecked) + { + Utf2T wszJid(jid); + LVITEM item = { 0 }; + item.mask = LVIF_TEXT | LVIF_STATE; + item.iItem = m_list.GetItemCount(); + item.pszText = wszJid; + + int index = m_list.InsertItem(&item); + if (index < 0) + return index; + + m_list.SetCheckState(index, bChecked); + + m_list.SetItemText(index, 1, Utf2T(nick)); + m_list.SetItemText(index, 2, Utf2T(group)); + m_list.SetItemText(index, 3, TranslateW(Utf2T(subscr))); + return index; + } + + void _RosterListClear() + { + m_list.DeleteAllItems(); + while (m_list.GetColumnWidth(0) > 0) + m_list.DeleteColumn(0); + + LV_COLUMN column = { 0 }; + column.mask = LVCF_TEXT; + + column.pszText = TranslateT("JID"); + m_list.InsertColumn(1, &column); + + column.pszText = TranslateT("Nickname"); + m_list.InsertColumn(2, &column); + + column.pszText = TranslateT("Group"); + m_list.InsertColumn(3, &column); + + column.pszText = TranslateT("Subscription"); + m_list.InsertColumn(4, &column); + + RECT rc; + GetClientRect(m_list.GetHwnd(), &rc); + ResizeColumns(rc.right - rc.left); + } + + void ResizeColumns(int width) + { + m_list.SetColumnWidth(0, width * 40 / 100); + m_list.SetColumnWidth(1, width * 25 / 100); + m_list.SetColumnWidth(2, width * 20 / 100); + m_list.SetColumnWidth(3, width * 12 / 100); + } + + void OnChangeStatus() + { + int count = m_list.GetItemCount(); + btnDownload.Enable(m_proto->m_bJabberOnline); + btnUpload.Enable(count && m_proto->m_bJabberOnline); + btnExport.Enable(count > 0); + } + + CCtrlButton btnDownload, btnUpload, btnExport, btnImport; + CCtrlListView m_list; + +public: + CRosterEditorDlg(CJabberProto *m_proto) : + CSuper(m_proto, IDD_ROSTER_EDITOR), + m_list(this, IDC_ROSTER), + btnExport(this, IDC_EXPORT), + btnImport(this, IDC_IMPORT), + btnUpload(this, IDC_UPLOAD), + btnDownload(this, IDC_DOWNLOAD) + { + SetMinSize(550, 390); + + btnExport.OnClick = Callback(this, &CRosterEditorDlg::onClick_Export); + btnImport.OnClick = Callback(this, &CRosterEditorDlg::onClick_Import); + btnUpload.OnClick = Callback(this, &CRosterEditorDlg::onClick_Upload); + btnDownload.OnClick = Callback(this, &CRosterEditorDlg::onClick_Download); + } + + bool OnInitDialog() override + { + SetWindowTextW(m_hwnd, CMStringW(FORMAT, L"%s: %s", TranslateT("Roster Editor"), m_proto->m_tszUserName)); + + Window_SetIcon_IcoLib(m_hwnd, g_plugin.getIconHandle(IDI_AGENTS)); + + Utils_RestoreWindowPosition(m_hwnd, 0, m_proto->m_szModuleName, "rosterCtrlWnd_"); + + m_list.SetExtendedListViewStyle(LVS_EX_CHECKBOXES | LVS_EX_BORDERSELECT | LVS_EX_GRIDLINES); + mir_subclassWindow(m_list.GetHwnd(), _RosterNewListProc); + _RosterListClear(); + OnChangeStatus(); + return true; + } + + void OnDestroy() override + { + m_proto->m_hwndRosterEditor = nullptr; + Utils_SaveWindowPosition(m_hwnd, 0, m_proto->m_szModuleName, "rosterCtrlWnd_"); + Window_FreeIcon_IcoLib(m_hwnd); + } + + int Resizer(UTILRESIZECONTROL *urc) override + { + switch (urc->wId) { + case IDC_HEADERBAR: + return RD_ANCHORX_LEFT | RD_ANCHORY_TOP | RD_ANCHORX_WIDTH; + case IDC_ROSTER: + ResizeColumns(urc->rcItem.right - urc->rcItem.left + urc->dlgNewSize.cx - urc->dlgOriginalSize.cx); + return RD_ANCHORX_LEFT | RD_ANCHORY_TOP | RD_ANCHORY_HEIGHT | RD_ANCHORX_WIDTH; + case IDC_DOWNLOAD: + case IDC_UPLOAD: + return RD_ANCHORX_LEFT | RD_ANCHORY_BOTTOM; + case IDC_EXPORT: + case IDC_IMPORT: + return RD_ANCHORX_RIGHT | RD_ANCHORY_BOTTOM; + } + return RD_ANCHORX_LEFT | RD_ANCHORY_TOP; + } + + void HandleNode(const TiXmlElement *node) + { + if (m_bRRAction == RRA_FILLLIST) { + _RosterListClear(); + auto *query = XmlFirstChild(node, "query"); + if (query == nullptr) return; + + for (auto *item : TiXmlFilter(query, "item")) { + const char *jid = XmlGetAttr(item, "jid"); + if (jid == nullptr) + continue; + + const char *name = XmlGetAttr(item, "name"); + const char *subscription = XmlGetAttr(item, "subscription"); + const char *group = XmlGetChildText(item, "group"); + _RosterInsertListItem(jid, name, group, subscription, true); + } + + // now it is require to process whole contact list to add not in roster contacts + for (auto &hContact : m_proto->AccContacts()) { + ptrW tszJid(m_proto->getWStringA(hContact, "jid")); + if (tszJid == nullptr) + continue; + + LVFINDINFO lvfi = { 0 }; + lvfi.flags = LVFI_STRING; + lvfi.psz = tszJid; + if (m_list.FindItem(-1, &lvfi) == -1) { + ptrA tszName(db_get_utfa(hContact, "CList", "MyHandle")); + ptrA tszGroup(db_get_utfa(hContact, "CList", "Group")); + _RosterInsertListItem(T2Utf(tszJid), tszName, tszGroup, nullptr, false); + } + } + m_bReadyToDownload = false; + m_bReadyToUpload = true; + btnDownload.SetText(TranslateT("Download")); + btnUpload.SetText(TranslateT("Upload")); + OnChangeStatus(); + return; + } + + if (m_bRRAction == RRA_SYNCROSTER) { + btnUpload.SetText(TranslateT("Uploading...")); + auto *queryRoster = XmlFirstChild(node, "query"); + if (!queryRoster) + return; + + int ListItemCount = m_list.GetItemCount(); + for (int index = 0; index < ListItemCount; index++) { + wchar_t jid[JABBER_MAX_JID_LEN] = L""; + wchar_t name[260]; + wchar_t group[260]; + wchar_t subscr[260]; + m_list.GetItemText(index, 0, jid, _countof(jid)); + m_list.GetItemText(index, 1, name, _countof(name)); + m_list.GetItemText(index, 2, group, _countof(group)); + m_list.GetItemText(index, 3, subscr, _countof(subscr)); + + T2Utf szJid(jid), szName(name), szGroup(group), szSubscr(subscr); + auto *itemRoster = XmlGetChildByTag(queryRoster, "item", "jid", szJid); + bool bRemove = !m_list.GetCheckState(index); + if (itemRoster && bRemove) { // delete item + XmlNodeIq iq(m_proto->AddIQ(&CJabberProto::_RosterHandleGetRequest, JABBER_IQ_TYPE_SET)); + iq << XCHILDNS("query", JABBER_FEAT_IQ_ROSTER) << XCHILD("item") << XATTR("jid", szJid) << XATTR("subscription", "remove"); + m_proto->m_ThreadInfo->send(iq); + } + else if (!bRemove) { + bool bPushed = itemRoster != 0; + const char *rosterName = 0, *rosterGroup = 0; + if (!bPushed) { + rosterName = XmlGetAttr(itemRoster, "name"); + if ((rosterName != nullptr || name[0] != 0) && mir_strcmpi(rosterName, szName)) + bPushed = true; + if (!bPushed) { + auto *szSub = XmlGetAttr(itemRoster, "subscription"); + if ((szSub != nullptr || subscr[0] != 0) && mir_strcmpi(szSub, szSubscr)) + bPushed = true; + } + if (!bPushed) { + rosterGroup = XmlGetChildText(itemRoster, "group"); + if (rosterGroup != nullptr && mir_strcmpi(rosterGroup, szGroup)) + bPushed = true; + } + } + if (bPushed) { + XmlNodeIq iq(m_proto->AddIQ(&CJabberProto::_RosterHandleGetRequest, JABBER_IQ_TYPE_SET)); + auto *item = iq << XCHILDNS("query", JABBER_FEAT_IQ_ROSTER) << XCHILD("item"); + if (rosterGroup || mir_strlen(szGroup)) + item << XCHILD("group", szGroup); + if (rosterName || mir_strlen(szName)) + item << XATTR("name", szName); + item << XATTR("jid", szJid) << XATTR("subscription", subscr[0] ? szSubscr : "none"); + m_proto->m_ThreadInfo->send(iq); + } + } + } + m_bRRAction = RRA_SYNCDONE; + _RosterSendRequest(RRA_FILLLIST); + return; + } + + btnUpload.SetText(TranslateT("Upload")); + onClick_Download(nullptr); + }; + + void onClick_Download(CCtrlButton*) + { + m_bReadyToUpload = m_bReadyToDownload = false; + OnChangeStatus(); + btnDownload.SetText(TranslateT("Downloading...")); + _RosterSendRequest(RRA_FILLLIST); + } + + void onClick_Upload(CCtrlButton*) + { + m_bReadyToUpload = false; + OnChangeStatus(); + btnUpload.SetText(TranslateT("Connecting...")); + _RosterSendRequest(RRA_SYNCROSTER); + } + + void onClick_Export(CCtrlButton*) + { + wchar_t filename[MAX_PATH] = { 0 }; + + wchar_t filter[MAX_PATH]; + mir_snwprintf(filter, L"%s (*.xml)%c*.xml%c%c", TranslateT("XML (UTF-8 encoded)"), 0, 0, 0); + OPENFILENAME ofn = {}; + ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400; + ofn.hwndOwner = m_hwnd; + ofn.lpstrFilter = filter; + ofn.lpstrFile = filename; + ofn.Flags = OFN_HIDEREADONLY; + ofn.nMaxFile = _countof(filename); + ofn.nMaxFileTitle = MAX_PATH; + ofn.lpstrDefExt = L"xml"; + if (!GetSaveFileNameW(&ofn)) + return; + + FILE * fp = _wfopen(filename, L"wb"); + if (!fp) + return; + + int ListItemCount = m_list.GetItemCount(); + + XmlNode root("Roster"); + + for (int index = 0; index < ListItemCount; index++) { + wchar_t jid[JABBER_MAX_JID_LEN] = L""; + wchar_t name[260] = L""; + wchar_t group[260] = L""; + wchar_t subscr[260] = L""; + m_list.GetItemText(index, 0, jid, _countof(jid)); + m_list.GetItemText(index, 1, name, _countof(name)); + m_list.GetItemText(index, 2, group, _countof(group)); + m_list.GetItemText(index, 3, subscr, _countof(subscr)); + root << XCHILD("Item") << XATTR("jid", T2Utf(jid)) << XATTR("name", T2Utf(name)) << XATTR("group", T2Utf(group)) << XATTR("subscription", T2Utf(subscr)); + } + + tinyxml2::XMLPrinter printer(fp); + root.Print(&printer); + fclose(fp); + } + + void onClick_Import(CCtrlButton*) + { + wchar_t filename[MAX_PATH] = {}; + + OPENFILENAME ofn = { 0 }; + ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400; + ofn.hwndOwner = m_hwnd; + ofn.hInstance = nullptr; + ofn.lpstrFilter = L"XML (UTF-8 encoded)(*.xml)\0*.xml\0\0"; + ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; + ofn.lpstrFile = filename; + ofn.nMaxFile = _countof(filename); + ofn.nMaxFileTitle = MAX_PATH; + ofn.lpstrDefExt = L"xml"; + if (!GetOpenFileNameW(&ofn)) + return; + + FILE * fp = _wfopen(filename, L"rb"); + if (!fp) + return; + + TiXmlDocument doc; + int ret = doc.LoadFile(fp); + fclose(fp); + if (ret != 0) + return; + + _RosterListClear(); + + const TiXmlElement *Table = TiXmlConst(&doc)["Workbook"]["Worksheet"]["Table"].ToElement(); + if (Table) { + for (auto *Row : TiXmlFilter(Table, "Row")) { + bool bAdd = false; + const char *jid = nullptr; + const char *name = nullptr; + const char *group = nullptr; + const char *subscr = nullptr; + auto *Cell = XmlFirstChild(Row, "Cell"); + auto *Data = XmlFirstChild(Cell, "Data"); + if (Data) { + if (!mir_strcmpi(Data->GetText(), "+")) + bAdd = true; + else if (mir_strcmpi(Data->GetText(), "-")) + continue; + + Cell = Cell->NextSiblingElement("Cell"); + if (Cell) Data = XmlFirstChild(Cell, "Data"); + else Data = nullptr; + if (Data) { + jid = Data->GetText(); + if (!jid || mir_strlen(jid) == 0) + continue; + } + + Cell = Cell->NextSiblingElement("Cell"); + if (Cell) Data = XmlFirstChild(Cell, "Data"); + else Data = nullptr; + if (Data) name = Data->GetText(); + + Cell = Cell->NextSiblingElement("Cell"); + if (Cell) Data = XmlFirstChild(Cell, "Data"); + else Data = nullptr; + if (Data) group = Data->GetText(); + + Cell = Cell->NextSiblingElement("Cell"); + if (Cell) Data = XmlFirstChild(Cell, "Data"); + else Data = nullptr; + if (Data) subscr = Data->GetText(); + } + _RosterInsertListItem(jid, name, group, subscr, bAdd); + } + } + else if (Table = TiXmlConst(&doc)["Roster"].ToElement()) { + for (auto *Row : TiXmlFilter(Table, "Item")) { + auto *jid = Row->Attribute("jid"); + auto *name = Row->Attribute("name"); + auto *group = Row->Attribute("group"); + auto *subscr = Row->Attribute("subscription"); + _RosterInsertListItem(jid, name, group, subscr, true); + } + } + + OnChangeStatus(); + } +}; + +INT_PTR __cdecl CJabberProto::OnMenuHandleRosterControl(WPARAM, LPARAM) +{ + if (m_hwndRosterEditor) + SetForegroundWindow(m_hwndRosterEditor->GetHwnd()); + else { + m_hwndRosterEditor = new CRosterEditorDlg(this); + m_hwndRosterEditor->Show(); + } + + return 0; +} + +void CJabberProto::_RosterHandleGetRequest(const TiXmlElement *node, CJabberIqInfo*) +{ + if (m_hwndRosterEditor) + m_hwndRosterEditor->HandleNode(node); +} + +void CJabberProto::JabberUpdateDialogs(BOOL) +{ + if (m_hwndRosterEditor) + m_hwndRosterEditor->OnChangeStatus(); +} diff --git a/protocols/JabberG/src/jabber_search.cpp b/protocols/JabberG/src/jabber_search.cpp index 8beaf9fb7b..5102211399 100644 --- a/protocols/JabberG/src/jabber_search.cpp +++ b/protocols/JabberG/src/jabber_search.cpp @@ -1,765 +1,765 @@ -/* - -Jabber Protocol Plugin for Miranda NG - -Copyright (c) 2002-04 Santithorn Bunchua -Copyright (c) 2005-12 George Hazan -Copyright (c) 2007 Artem Shpynov -Copyright (C) 2012-22 Miranda NG team - -Module implements a search according to XEP-0055: Jabber Search -http://www.xmpp.org/extensions/xep-0055.html - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -*/ - -#include "stdafx.h" -#include -#include "jabber_iq.h" -#include "jabber_caps.h" - -/////////////////////////////////////////////////////////////////////////////// -// Subclassing of IDC_FRAME to implement more user-friendly fields scrolling - -static int JabberSearchFrameProc(HWND hwnd, int msg, WPARAM wParam, LPARAM lParam) -{ - if (msg == WM_COMMAND && lParam != 0) { - HWND hwndDlg = GetParent(hwnd); - JabberSearchData *dat = (JabberSearchData *)GetWindowLongPtr(hwndDlg, GWLP_USERDATA); - if (dat && lParam) { - int pos = dat->curPos; - RECT MineRect; - RECT FrameRect; - GetWindowRect(GetDlgItem(hwndDlg, IDC_FRAME), &FrameRect); - GetWindowRect((HWND)lParam, &MineRect); - if (MineRect.top - 10 < FrameRect.top) { - pos = dat->curPos + (MineRect.top - 14 - FrameRect.top); - if (pos < 0) pos = 0; - } - else if (MineRect.bottom > FrameRect.bottom) { - pos = dat->curPos + (MineRect.bottom - FrameRect.bottom); - if (dat->frameHeight + pos > dat->CurrentHeight) - pos = dat->CurrentHeight - dat->frameHeight; - } - if (pos != dat->curPos) { - ScrollWindow(GetDlgItem(hwndDlg, IDC_FRAME), 0, dat->curPos - pos, nullptr, &(dat->frameRect)); - SetScrollPos(GetDlgItem(hwndDlg, IDC_VSCROLL), SB_CTL, pos, TRUE); - RECT Invalid = dat->frameRect; - if (dat->curPos - pos > 0) - Invalid.bottom = Invalid.top + (dat->curPos - pos); - else - Invalid.top = Invalid.bottom + (dat->curPos - pos); - - RedrawWindow(GetDlgItem(hwndDlg, IDC_FRAME), nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN); - dat->curPos = pos; - } - } - - // Transmit focus set notification to parent window - if (HIWORD(wParam) == EN_SETFOCUS) - PostMessage(GetParent(hwndDlg), WM_COMMAND, MAKEWPARAM(0, EN_SETFOCUS), (LPARAM)hwndDlg); - } - - if (msg == WM_PAINT) { - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - FillRect(hdc, &(ps.rcPaint), GetSysColorBrush(COLOR_BTNFACE)); - EndPaint(hwnd, &ps); - } - - return DefWindowProc(hwnd, msg, wParam, lParam); -} - -/////////////////////////////////////////////////////////////////////////////// -// Add Search field to form - -static int JabberSearchAddField(HWND hwndDlg, Data* FieldDat) -{ - if (!FieldDat || !FieldDat->Label || !FieldDat->Var) - return FALSE; - - HFONT hFont = (HFONT)SendMessage(hwndDlg, WM_GETFONT, 0, 0); - HWND hwndParent = GetDlgItem(hwndDlg, IDC_FRAME); - LONG_PTR frameExStyle = GetWindowLongPtr(hwndParent, GWL_EXSTYLE); - frameExStyle |= WS_EX_CONTROLPARENT; - SetWindowLongPtr(hwndParent, GWL_EXSTYLE, frameExStyle); - SetWindowLongPtr(GetDlgItem(hwndDlg, IDC_FRAME), GWLP_WNDPROC, (LONG_PTR)JabberSearchFrameProc); - - int CornerX = 1; - int CornerY = 1; - RECT rect; - GetClientRect(hwndParent, &rect); - int width = rect.right - 5 - CornerX; - - int Order = (FieldDat->bHidden) ? -1 : FieldDat->Order; - - HWND hwndLabel = CreateWindowEx(0, L"STATIC", (const wchar_t *)TranslateW(FieldDat->Label), WS_CHILD, CornerX, CornerY + Order * 40, width, 13, hwndParent, nullptr, g_plugin.getInst(), nullptr); - HWND hwndVar = CreateWindowEx(0 | WS_EX_CLIENTEDGE, L"EDIT", FieldDat->defValue, WS_CHILD | WS_TABSTOP, CornerX + 5, CornerY + Order * 40 + 14, width, 20, hwndParent, nullptr, g_plugin.getInst(), nullptr); - SendMessage(hwndLabel, WM_SETFONT, (WPARAM)hFont, 0); - SendMessage(hwndVar, WM_SETFONT, (WPARAM)hFont, 0); - if (!FieldDat->bHidden) { - ShowWindow(hwndLabel, SW_SHOW); - ShowWindow(hwndVar, SW_SHOW); - EnableWindow(hwndLabel, !FieldDat->bReadOnly); - SendMessage(hwndVar, EM_SETREADONLY, (WPARAM)FieldDat->bReadOnly, 0); - } - - // remade list - // reallocation - JabberSearchData *dat = (JabberSearchData *)GetWindowLongPtr(hwndDlg, GWLP_USERDATA); - if (dat) { - dat->pJSInf = (JabberSearchFieldsInfo*)realloc(dat->pJSInf, sizeof(JabberSearchFieldsInfo)*(dat->nJSInfCount + 1)); - dat->pJSInf[dat->nJSInfCount].hwndCaptionItem = hwndLabel; - dat->pJSInf[dat->nJSInfCount].hwndValueItem = hwndVar; - dat->pJSInf[dat->nJSInfCount].szFieldCaption = wcsdup(FieldDat->Label); - dat->pJSInf[dat->nJSInfCount].szFieldName = wcsdup(FieldDat->Var); - dat->nJSInfCount++; - } - return CornerY + Order * 40 + 14 + 20; -} - -//////////////////////////////////////////////////////////////////////////////// -// Available search field request result handler (XEP-0055. Examples 2, 7) - -void CJabberProto::OnIqResultGetSearchFields(const TiXmlElement *iqNode, CJabberIqInfo*) -{ - if (!searchHandleDlg) - return; - - const char *type = XmlGetAttr(iqNode, "type"); - if (type == nullptr) - return; - - if (!mir_strcmp(type, "result")) { - auto *queryNode = XmlFirstChild(iqNode, "query"); - auto *xNode = XmlGetChildByTag(queryNode, "x", "xmlns", JABBER_FEAT_DATA_FORMS); - - ShowWindow(searchHandleDlg, SW_HIDE); - if (xNode) { - // 1. Form - PostMessage(searchHandleDlg, WM_USER + 11, (WPARAM)xNode, 0); - auto *xcNode = XmlFirstChild(xNode, "instructions"); - if (xcNode) - SetDlgItemTextUtf(searchHandleDlg, IDC_INSTRUCTIONS, xcNode->GetText()); - } - else { - int Order = 0; - for (auto *chNode : TiXmlEnum(queryNode)) { - if (!mir_strcmpi(chNode->Name(), "instructions") && chNode->GetText()) - SetDlgItemText(searchHandleDlg, IDC_INSTRUCTIONS, TranslateW(Utf2T(chNode->GetText()))); - else if (chNode->Name()) { - Data *MyData = (Data*)malloc(sizeof(Data)); - memset(MyData, 0, sizeof(Data)); - - MyData->Label = mir_utf8decodeW(chNode->Name()); - MyData->Var = mir_utf8decodeW(chNode->Name()); - MyData->defValue = mir_utf8decodeW(chNode->GetText()); - MyData->Order = Order; - if (MyData->defValue) - MyData->bReadOnly = true; - PostMessage(searchHandleDlg, WM_USER + 10, FALSE, (LPARAM)MyData); - Order++; - } - } - } - - const char *szFrom = XmlGetAttr(iqNode, "from"); - if (szFrom) - SearchAddToRecent(szFrom, searchHandleDlg); - PostMessage(searchHandleDlg, WM_USER + 10, 0, 0); - ShowWindow(searchHandleDlg, SW_SHOW); - } - else if (!mir_strcmp(type, "error")) { - const char *code = ""; - const char *description = ""; - auto *errorNode = XmlFirstChild(iqNode, "error"); - if (errorNode) { - code = XmlGetAttr(errorNode, "code"); - description = errorNode->GetText(); - } - - char buff[255]; - mir_snprintf(buff, TranslateU("Error %s %s\r\nPlease select other server"), code, description); - SetDlgItemTextUtf(searchHandleDlg, IDC_INSTRUCTIONS, buff); - } - else SetDlgItemText(searchHandleDlg, IDC_INSTRUCTIONS, TranslateT("Error: unknown reply received\r\nPlease select other server")); -} - -////////////////////////////////////////////////////////////////////////////////////////// -// Return results to search dialog -// The pmFields is the pointer to map of Not unical but ordered -// This can help to made result parser routines more simple - -static char *nickfields[] = { "nick", "nickname", "fullname", "name", "given", "first", "jid", nullptr }; - -static int TCharKeyCmp(const char *p1, const char *p2) -{ - return mir_strcmpi(p1, p2); -} - -static void SearchReturnResults(CJabberProto *ppro, HANDLE id, LIST &plUsersInfo, UNIQUE_MAP &pmAllFields) -{ - LIST ListOfNonEmptyFields(20, TCharKeyCmp); - LIST ListOfFields(20); - - // lets fill the ListOfNonEmptyFields but in users order - for (auto &pmUserData : plUsersInfo) { - int nUserFields = pmUserData->getCount(); - for (int j = 0; j < nUserFields; j++) { - char *var = pmUserData->getKeyName(j); - if (var && ListOfNonEmptyFields.getIndex(var) < 0) - ListOfNonEmptyFields.insert(var); - } - } - - // now fill the ListOfFields but order is from pmAllFields - int nAllCount = pmAllFields.getCount(); - for (int i = 0; i < nAllCount; i++) { - char *var = pmAllFields.getUnOrderedKeyName(i); - if (var && ListOfNonEmptyFields.getIndex(var) < 0) - continue; - ListOfFields.insert(var); - } - - // now lets transfer field names - int nFieldCount = ListOfFields.getCount(); - - CUSTOMSEARCHRESULTS Results = { 0 }; - Results.nSize = sizeof(Results); - Results.pszFields = (wchar_t**)mir_alloc(sizeof(wchar_t*)*nFieldCount); - Results.nFieldCount = nFieldCount; - - // Sending Columns Titles - for (int i = 0; i < nFieldCount; i++) { - char *var = ListOfFields[i]; - if (var) - Results.pszFields[i] = mir_utf8decodeW(pmAllFields[var]); - } - - Results.psr.cbSize = 0; // sending column names - ppro->ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SEARCHRESULT, id, (LPARAM)&Results); - for (int i = 0; i < nFieldCount; i++) - replaceStrW(Results.pszFields[i], nullptr); - - // Sending Users Data - Results.psr.cbSize = sizeof(Results.psr); - - for (auto &pmUserData : plUsersInfo) { - for (int j = 0; j < nFieldCount; j++) { - char *var = ListOfFields[j]; - char *value = pmUserData->operator [](var); - Results.pszFields[j] = value ? mir_utf8decodeW(value) : mir_wstrdup(L" "); - if (!mir_strcmpi(var, "jid") && value) - Results.psr.id.w = Results.pszFields[j]; - } - - const char *nick = nullptr; - for (int k = 0; k < _countof(nickfields) && !nick; k++) - nick = pmUserData->operator [](nickfields[k]); - - if (nick) { - Utf2T wszNick(nick); - wchar_t buff[200]; - if (mir_wstrcmpi(wszNick, Results.psr.id.w)) - mir_snwprintf(buff, L"%s (%s)", wszNick.get(), Results.psr.id.w); - else - wcsncpy_s(buff, wszNick, _TRUNCATE); - - Results.psr.nick.w = buff; - } - else Results.psr.nick.w = L""; - Results.psr.flags = PSR_UNICODE; - - ppro->ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SEARCHRESULT, id, (LPARAM)&Results); - for (int i = 0; i < nFieldCount; i++) - replaceStrW(Results.pszFields[i], nullptr); - } - - mir_free(Results.pszFields); -} - -//////////////////////////////////////////////////////////////////////////////// -// Search field request result handler (XEP-0055. Examples 3, 8) - -void CJabberProto::OnIqResultAdvancedSearch(const TiXmlElement *iqNode, CJabberIqInfo*) -{ - const char *type; - int id; - - UNIQUE_MAP mColumnsNames(10); - LIST SearchResults(2); - - if (((id = JabberGetPacketID(iqNode)) == -1) || ((type = XmlGetAttr(iqNode, "type")) == nullptr)) { - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)id); - return; - } - - if (!mir_strcmp(type, "result")) { - auto *queryNode = XmlFirstChild(iqNode, "query"); - auto *xNode = XmlGetChildByTag(queryNode, "x", "xmlns", JABBER_FEAT_DATA_FORMS); - if (xNode) { - // 1. Form search results info - for (auto *fieldNode : TiXmlFilter(XmlFirstChild(xNode, "reported"), "field")) { - auto *var = XmlGetAttr(fieldNode, "var"); - if (var) { - auto *label = XmlGetAttr(fieldNode, "label"); - mColumnsNames.insert(var, (label != nullptr) ? label : var); - } - } - - for (auto *itemNode : TiXmlFilter(xNode, "item")) { - UNIQUE_MAP *pUserColumn = new UNIQUE_MAP(10); - for (auto *fieldNode : TiXmlFilter(itemNode, "field")) { - if (auto *var = XmlGetAttr(fieldNode, "var")) { - if (auto *textNode = XmlFirstChild(fieldNode, "value")) { - if (!mColumnsNames[var]) - mColumnsNames.insert(var, var); - pUserColumn->insert(var, textNode->GetText()); - } - } - } - - SearchResults.insert(pUserColumn); - } - } - else { - // 2. Field list search results info - for (auto *itemNode : TiXmlFilter(queryNode, "item")) { - UNIQUE_MAP *pUserColumn = new UNIQUE_MAP(10); - - auto *jid = XmlGetAttr(itemNode, "jid"); - char *keyReturned; - mColumnsNames.insertCopyKey("jid", "jid", &keyReturned); - mColumnsNames.insert("jid", keyReturned); - pUserColumn->insertCopyKey("jid", jid, nullptr); - - for (auto *child : TiXmlEnum(itemNode)) { - const char *szColumnName = child->Name(); - if (szColumnName) { - const char *pszChild = child->GetText(); - if (pszChild && *pszChild) { - mColumnsNames.insertCopyKey(szColumnName, "", &keyReturned); - mColumnsNames.insert(szColumnName, keyReturned); - pUserColumn->insertCopyKey(szColumnName, pszChild, nullptr); - } - } - } - - SearchResults.insert(pUserColumn); - } - } - } - else if (!mir_strcmp(type, "error")) { - const char *code = ""; - const char *description = ""; - auto *errorNode = XmlFirstChild(iqNode, "error"); - if (errorNode) { - code = XmlGetAttr(errorNode, "code"); - description = errorNode->GetText(); - } - - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)id); - - char buff[255]; - mir_snprintf(buff, TranslateU("Error %s %s\r\nTry to specify more detailed"), code, description); - if (searchHandleDlg) - SetDlgItemTextUtf(searchHandleDlg, IDC_INSTRUCTIONS, buff); - else - MessageBox(nullptr, Utf2T(buff), TranslateT("Search error"), MB_OK | MB_ICONSTOP); - return; - } - - SearchReturnResults(this, (HANDLE)id, SearchResults, mColumnsNames); - - for (auto &it : SearchResults) - delete ((UNIQUE_MAP*)it); - - //send success to finish searching - ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)id); -} - -static BOOL CALLBACK DeleteChildWindowsProc(HWND hwnd, LPARAM) -{ - DestroyWindow(hwnd); - return TRUE; -} - -static void JabberSearchFreeData(HWND hwndDlg, JabberSearchData * dat) -{ - if (!dat->fSearchRequestIsXForm && dat->nJSInfCount && dat->pJSInf) { - for (int i = 0; i < dat->nJSInfCount; i++) { - if (dat->pJSInf[i].hwndValueItem) - DestroyWindow(dat->pJSInf[i].hwndValueItem); - if (dat->pJSInf[i].hwndCaptionItem) - DestroyWindow(dat->pJSInf[i].hwndCaptionItem); - if (dat->pJSInf[i].szFieldCaption) - free(dat->pJSInf[i].szFieldCaption); - if (dat->pJSInf[i].szFieldName) - free(dat->pJSInf[i].szFieldName); - } - free(dat->pJSInf); - dat->pJSInf = nullptr; - } - else EnumChildWindows(GetDlgItem(hwndDlg, IDC_FRAME), DeleteChildWindowsProc, 0); - - SendDlgItemMessage(hwndDlg, IDC_FRAME, WM_SETFONT, (WPARAM)SendMessage(hwndDlg, WM_GETFONT, 0, 0), 0); - dat->nJSInfCount = 0; - ShowWindow(GetDlgItem(hwndDlg, IDC_VSCROLL), SW_HIDE); - SetDlgItemText(hwndDlg, IDC_INSTRUCTIONS, TranslateT("Select/type search service URL above and press ")); -} - -static void JabberSearchRefreshFrameScroll(HWND hwndDlg, JabberSearchData *dat) -{ - HWND hFrame = GetDlgItem(hwndDlg, IDC_FRAME); - HWND hwndScroll = GetDlgItem(hwndDlg, IDC_VSCROLL); - RECT rc; - GetClientRect(hFrame, &rc); - GetClientRect(hFrame, &dat->frameRect); - dat->frameHeight = rc.bottom - rc.top; - if (dat->frameHeight < dat->CurrentHeight) { - ShowWindow(hwndScroll, SW_SHOW); - EnableWindow(hwndScroll, TRUE); - } - else ShowWindow(hwndScroll, SW_HIDE); - - SetScrollRange(hwndScroll, SB_CTL, 0, dat->CurrentHeight - dat->frameHeight, FALSE); -} - -int CJabberProto::SearchRenewFields(HWND hwndDlg, JabberSearchData *dat) -{ - wchar_t szServerName[100]; - EnableWindow(GetDlgItem(hwndDlg, IDC_GO), FALSE); - GetDlgItemText(hwndDlg, IDC_SERVER, szServerName, _countof(szServerName)); - dat->CurrentHeight = 0; - dat->curPos = 0; - SetScrollPos(GetDlgItem(hwndDlg, IDC_VSCROLL), SB_CTL, 0, FALSE); - - JabberSearchFreeData(hwndDlg, dat); - JabberSearchRefreshFrameScroll(hwndDlg, dat); - - SetDlgItemText(hwndDlg, IDC_INSTRUCTIONS, m_bJabberOnline ? TranslateT("Please wait...\r\nConnecting search server...") : TranslateT("You have to be connected to server")); - - if (!m_bJabberOnline) - return 0; - - searchHandleDlg = hwndDlg; - - CJabberIqInfo *pInfo = AddIQ(&CJabberProto::OnIqResultGetSearchFields, JABBER_IQ_TYPE_GET, T2Utf(szServerName)); - m_ThreadInfo->send(XmlNodeIq(pInfo) << XQUERY(JABBER_FEAT_JUD)); - return pInfo->GetIqId(); -} - -static void JabberSearchAddUrlToRecentCombo(HWND hwndDlg, const wchar_t *szAddr) -{ - int lResult = SendDlgItemMessage(hwndDlg, IDC_SERVER, (UINT)CB_FINDSTRING, 0, (LPARAM)szAddr); - if (lResult == -1) - SendDlgItemMessage(hwndDlg, IDC_SERVER, CB_ADDSTRING, 0, (LPARAM)szAddr); -} - -void CJabberProto::SearchDeleteFromRecent(const char *szAddr, bool deleteLastFromDB) -{ - // search in recent - for (int i = 0; i < 10; i++) { - char key[30]; - mir_snprintf(key, "RecentlySearched_%d", i); - ptrA szValue(getUStringA(key)); - if (szValue == nullptr || mir_strcmpi(szAddr, szValue)) - continue; - - for (int j = i; j < 10; j++) { - mir_snprintf(key, "RecentlySearched_%d", j + 1); - szValue = getUStringA(key); - if (szValue != nullptr) { - mir_snprintf(key, "RecentlySearched_%d", j); - setUString(0, key, szValue); - } - else { - if (deleteLastFromDB) { - mir_snprintf(key, "RecentlySearched_%d", j); - delSetting(0, key); - } - break; - } - } - break; - } -} - -void CJabberProto::SearchAddToRecent(const char *szAddr, HWND hwndDialog) -{ - char key[30]; - SearchDeleteFromRecent(szAddr, true); - - for (int j = 9; j > 0; j--) { - mir_snprintf(key, "RecentlySearched_%d", j - 1); - ptrW szValue(getWStringA(key)); - if (szValue != nullptr) { - mir_snprintf(key, "RecentlySearched_%d", j); - setWString(0, key, szValue); - } - } - - mir_snprintf(key, "RecentlySearched_%d", 0); - setUString(key, szAddr); - if (hwndDialog) - JabberSearchAddUrlToRecentCombo(hwndDialog, Utf2T(szAddr)); -} - -static INT_PTR CALLBACK JabberSearchAdvancedDlgProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam) -{ - JabberSearchData *dat = (JabberSearchData *)GetWindowLongPtr(hwndDlg, GWLP_USERDATA); - switch (msg) { - case WM_INITDIALOG: - TranslateDialogDefault(hwndDlg); - { - dat = new JabberSearchData(); - dat->ppro = (CJabberProto *)lParam; - SetWindowLongPtr(hwndDlg, GWLP_USERDATA, (LONG_PTR)dat); - - /* Server Combo box */ - ptrA jud(dat->ppro->getStringA("Jud")); - if (jud != nullptr) { - SetDlgItemTextA(hwndDlg, IDC_SERVER, jud); - SendDlgItemMessageA(hwndDlg, IDC_SERVER, CB_ADDSTRING, 0, jud); - } - - //TO DO: Add Transports here - for (auto &it : dat->ppro->m_lstTransports) - if (it != nullptr) - JabberSearchAddUrlToRecentCombo(hwndDlg, Utf2T(it)); - - for (int i = 0; i < 10; i++) { - char key[30]; - mir_snprintf(key, "RecentlySearched_%d", i); - ptrW szValue(dat->ppro->getWStringA(key)); - if (szValue != nullptr) - JabberSearchAddUrlToRecentCombo(hwndDlg, szValue); - } - - //TO DO: Add 4 recently used - dat->lastRequestIq = dat->ppro->SearchRenewFields(hwndDlg, dat); - } - return TRUE; - - case WM_COMMAND: - if (LOWORD(wParam) == IDC_SERVER) { - switch (HIWORD(wParam)) { - case CBN_SETFOCUS: - PostMessage(GetParent(hwndDlg), WM_COMMAND, MAKEWPARAM(0, EN_SETFOCUS), (LPARAM)hwndDlg); - return TRUE; - - case CBN_EDITCHANGE: - EnableWindow(GetDlgItem(hwndDlg, IDC_GO), TRUE); - return TRUE; - - case CBN_EDITUPDATE: - JabberSearchFreeData(hwndDlg, dat); - EnableWindow(GetDlgItem(hwndDlg, IDC_GO), TRUE); - return TRUE; - - case CBN_SELENDOK: - EnableWindow(GetDlgItem(hwndDlg, IDC_GO), TRUE); - PostMessage(hwndDlg, WM_COMMAND, MAKEWPARAM(IDC_GO, BN_CLICKED), 0); - return TRUE; - } - } - else if (LOWORD(wParam) == IDC_GO && HIWORD(wParam) == BN_CLICKED) { - dat->ppro->SearchRenewFields(hwndDlg, dat); - return TRUE; - } - break; - - case WM_SIZE: - { - //Resize IDC_FRAME to take full size - RECT rcForm; - GetWindowRect(hwndDlg, &rcForm); - RECT rcFrame; - GetWindowRect(GetDlgItem(hwndDlg, IDC_FRAME), &rcFrame); - rcFrame.bottom = rcForm.bottom; - SetWindowPos(GetDlgItem(hwndDlg, IDC_FRAME), nullptr, 0, 0, rcFrame.right - rcFrame.left, rcFrame.bottom - rcFrame.top, SWP_NOZORDER | SWP_NOMOVE); - GetWindowRect(GetDlgItem(hwndDlg, IDC_VSCROLL), &rcForm); - SetWindowPos(GetDlgItem(hwndDlg, IDC_VSCROLL), nullptr, 0, 0, rcForm.right - rcForm.left, rcFrame.bottom - rcFrame.top, SWP_NOZORDER | SWP_NOMOVE); - JabberSearchRefreshFrameScroll(hwndDlg, dat); - } - return TRUE; - - case WM_USER + 11: - { - dat->fSearchRequestIsXForm = TRUE; - if (dat->xNode) { - dat->doc.DeleteNode(dat->xNode); - dat->xNode = nullptr; - } - TiXmlElement *pNode = (TiXmlElement *)wParam; - if (pNode) { - dat->xNode = pNode->DeepClone(&dat->doc)->ToElement(); - JabberFormCreateUI(GetDlgItem(hwndDlg, IDC_FRAME), dat->xNode, &dat->CurrentHeight, TRUE); - } - ShowWindow(GetDlgItem(hwndDlg, IDC_FRAME), SW_SHOW); - dat->nJSInfCount = 1; - } - return TRUE; - - case WM_USER + 10: - { - Data *MyDat = (Data *)lParam; - if (MyDat) { - dat->fSearchRequestIsXForm = (BOOL)wParam; - dat->CurrentHeight = JabberSearchAddField(hwndDlg, MyDat); - mir_free(MyDat->Label); - mir_free(MyDat->Var); - mir_free(MyDat->defValue); - free(MyDat); - } - else { - JabberSearchRefreshFrameScroll(hwndDlg, dat); - ScrollWindow(GetDlgItem(hwndDlg, IDC_FRAME), 0, dat->curPos - 0, nullptr, &(dat->frameRect)); - SetScrollPos(GetDlgItem(hwndDlg, IDC_VSCROLL), SB_CTL, 0, FALSE); - dat->curPos = 0; - } - } - return TRUE; - - case WM_MOUSEWHEEL: - { - short zDelta = GET_WHEEL_DELTA_WPARAM(wParam); - if (zDelta) { - int nScrollLines = 0; - SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, (void *)&nScrollLines, 0); - for (int i = 0; i < (nScrollLines + 1) / 2; i++) - SendMessage(hwndDlg, WM_VSCROLL, (zDelta < 0) ? SB_LINEDOWN : SB_LINEUP, 0); - } - } - return TRUE; - - case WM_VSCROLL: - { - int pos; - if (dat != nullptr) { - pos = dat->curPos; - switch (LOWORD(wParam)) { - case SB_LINEDOWN: - pos += 10; - break; - case SB_LINEUP: - pos -= 10; - break; - case SB_PAGEDOWN: - pos += (dat->CurrentHeight - 10); - break; - case SB_PAGEUP: - pos -= (dat->CurrentHeight - 10); - break; - case SB_THUMBTRACK: - pos = HIWORD(wParam); - break; - } - if (pos > (dat->CurrentHeight - dat->frameHeight)) - pos = dat->CurrentHeight - dat->frameHeight; - if (pos < 0) - pos = 0; - if (dat->curPos != pos) { - ScrollWindow(GetDlgItem(hwndDlg, IDC_FRAME), 0, dat->curPos - pos, nullptr, &(dat->frameRect)); - SetScrollPos(GetDlgItem(hwndDlg, IDC_VSCROLL), SB_CTL, pos, TRUE); - RECT Invalid = dat->frameRect; - if (dat->curPos - pos > 0) - Invalid.bottom = Invalid.top + (dat->curPos - pos); - else - Invalid.top = Invalid.bottom + (dat->curPos - pos); - - RedrawWindow(GetDlgItem(hwndDlg, IDC_FRAME), nullptr, nullptr, RDW_UPDATENOW | RDW_ALLCHILDREN); - dat->curPos = pos; - } - } - } - return TRUE; - - case WM_DESTROY: - JabberSearchFreeData(hwndDlg, dat); - JabberFormDestroyUI(GetDlgItem(hwndDlg, IDC_FRAME)); - delete dat; - SetWindowLongPtr(hwndDlg, GWLP_USERDATA, 0); - return TRUE; - } - return FALSE; -} - -HWND CJabberProto::CreateExtendedSearchUI(HWND parent) -{ - if (parent && g_plugin.getInst()) { - ptrW szServer(getWStringA("LoginServer")); - if (szServer == nullptr || mir_wstrcmpi(szServer, L"S.ms")) - return CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_SEARCHUSER), parent, JabberSearchAdvancedDlgProc, (LPARAM)this); - } - - return nullptr; // Failure -} - -////////////////////////////////////////////////////////////////////////// -// The function formats request to server - -HWND CJabberProto::SearchAdvanced(HWND hwndDlg) -{ - if (!m_bJabberOnline || !hwndDlg) - return nullptr; //error - - JabberSearchData *dat = (JabberSearchData *)GetWindowLongPtr(hwndDlg, GWLP_USERDATA); - if (!dat) - return nullptr; //error - - // check if server connected (at least one field exists) - if (dat->nJSInfCount == 0) - return nullptr; - - // formating request - bool fRequestNotEmpty = false; - - // get server name - wchar_t szServerName[100]; - GetDlgItemText(hwndDlg, IDC_SERVER, szServerName, _countof(szServerName)); - - // formating query - CJabberIqInfo *pInfo = AddIQ(&CJabberProto::OnIqResultAdvancedSearch, JABBER_IQ_TYPE_SET, T2Utf(szServerName)); - XmlNodeIq iq(pInfo); - TiXmlElement *query = iq << XQUERY(JABBER_FEAT_JUD); - - if (m_tszSelectedLang) - iq << XATTR("xml:lang", m_tszSelectedLang); // i'm sure :) - - // next can be 2 cases: - // Forms: XEP-0055 Example 7 - if (dat->fSearchRequestIsXForm) { - fRequestNotEmpty = true; - JabberFormGetData(GetDlgItem(hwndDlg, IDC_FRAME), query, dat->xNode); - } - else { //and Simple fields: XEP-0055 Example 3 - for (int i = 0; i < dat->nJSInfCount; i++) { - wchar_t szFieldValue[100]; - GetWindowText(dat->pJSInf[i].hwndValueItem, szFieldValue, _countof(szFieldValue)); - if (szFieldValue[0] != 0) { - XmlAddChildA(query, T2Utf(dat->pJSInf[i].szFieldName).get(), T2Utf(szFieldValue).get()); - fRequestNotEmpty = true; - } - } - } - - if (fRequestNotEmpty) { - m_ThreadInfo->send(iq); - return (HWND)pInfo->GetIqId(); - } - return nullptr; -} +/* + +Jabber Protocol Plugin for Miranda NG + +Copyright (c) 2002-04 Santithorn Bunchua +Copyright (c) 2005-12 George Hazan +Copyright (c) 2007 Artem Shpynov +Copyright (C) 2012-23 Miranda NG team + +Module implements a search according to XEP-0055: Jabber Search +http://www.xmpp.org/extensions/xep-0055.html + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "stdafx.h" +#include +#include "jabber_iq.h" +#include "jabber_caps.h" + +/////////////////////////////////////////////////////////////////////////////// +// Subclassing of IDC_FRAME to implement more user-friendly fields scrolling + +static int JabberSearchFrameProc(HWND hwnd, int msg, WPARAM wParam, LPARAM lParam) +{ + if (msg == WM_COMMAND && lParam != 0) { + HWND hwndDlg = GetParent(hwnd); + JabberSearchData *dat = (JabberSearchData *)GetWindowLongPtr(hwndDlg, GWLP_USERDATA); + if (dat && lParam) { + int pos = dat->curPos; + RECT MineRect; + RECT FrameRect; + GetWindowRect(GetDlgItem(hwndDlg, IDC_FRAME), &FrameRect); + GetWindowRect((HWND)lParam, &MineRect); + if (MineRect.top - 10 < FrameRect.top) { + pos = dat->curPos + (MineRect.top - 14 - FrameRect.top); + if (pos < 0) pos = 0; + } + else if (MineRect.bottom > FrameRect.bottom) { + pos = dat->curPos + (MineRect.bottom - FrameRect.bottom); + if (dat->frameHeight + pos > dat->CurrentHeight) + pos = dat->CurrentHeight - dat->frameHeight; + } + if (pos != dat->curPos) { + ScrollWindow(GetDlgItem(hwndDlg, IDC_FRAME), 0, dat->curPos - pos, nullptr, &(dat->frameRect)); + SetScrollPos(GetDlgItem(hwndDlg, IDC_VSCROLL), SB_CTL, pos, TRUE); + RECT Invalid = dat->frameRect; + if (dat->curPos - pos > 0) + Invalid.bottom = Invalid.top + (dat->curPos - pos); + else + Invalid.top = Invalid.bottom + (dat->curPos - pos); + + RedrawWindow(GetDlgItem(hwndDlg, IDC_FRAME), nullptr, nullptr, RDW_ERASE | RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN); + dat->curPos = pos; + } + } + + // Transmit focus set notification to parent window + if (HIWORD(wParam) == EN_SETFOCUS) + PostMessage(GetParent(hwndDlg), WM_COMMAND, MAKEWPARAM(0, EN_SETFOCUS), (LPARAM)hwndDlg); + } + + if (msg == WM_PAINT) { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + FillRect(hdc, &(ps.rcPaint), GetSysColorBrush(COLOR_BTNFACE)); + EndPaint(hwnd, &ps); + } + + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +/////////////////////////////////////////////////////////////////////////////// +// Add Search field to form + +static int JabberSearchAddField(HWND hwndDlg, Data* FieldDat) +{ + if (!FieldDat || !FieldDat->Label || !FieldDat->Var) + return FALSE; + + HFONT hFont = (HFONT)SendMessage(hwndDlg, WM_GETFONT, 0, 0); + HWND hwndParent = GetDlgItem(hwndDlg, IDC_FRAME); + LONG_PTR frameExStyle = GetWindowLongPtr(hwndParent, GWL_EXSTYLE); + frameExStyle |= WS_EX_CONTROLPARENT; + SetWindowLongPtr(hwndParent, GWL_EXSTYLE, frameExStyle); + SetWindowLongPtr(GetDlgItem(hwndDlg, IDC_FRAME), GWLP_WNDPROC, (LONG_PTR)JabberSearchFrameProc); + + int CornerX = 1; + int CornerY = 1; + RECT rect; + GetClientRect(hwndParent, &rect); + int width = rect.right - 5 - CornerX; + + int Order = (FieldDat->bHidden) ? -1 : FieldDat->Order; + + HWND hwndLabel = CreateWindowEx(0, L"STATIC", (const wchar_t *)TranslateW(FieldDat->Label), WS_CHILD, CornerX, CornerY + Order * 40, width, 13, hwndParent, nullptr, g_plugin.getInst(), nullptr); + HWND hwndVar = CreateWindowEx(0 | WS_EX_CLIENTEDGE, L"EDIT", FieldDat->defValue, WS_CHILD | WS_TABSTOP, CornerX + 5, CornerY + Order * 40 + 14, width, 20, hwndParent, nullptr, g_plugin.getInst(), nullptr); + SendMessage(hwndLabel, WM_SETFONT, (WPARAM)hFont, 0); + SendMessage(hwndVar, WM_SETFONT, (WPARAM)hFont, 0); + if (!FieldDat->bHidden) { + ShowWindow(hwndLabel, SW_SHOW); + ShowWindow(hwndVar, SW_SHOW); + EnableWindow(hwndLabel, !FieldDat->bReadOnly); + SendMessage(hwndVar, EM_SETREADONLY, (WPARAM)FieldDat->bReadOnly, 0); + } + + // remade list + // reallocation + JabberSearchData *dat = (JabberSearchData *)GetWindowLongPtr(hwndDlg, GWLP_USERDATA); + if (dat) { + dat->pJSInf = (JabberSearchFieldsInfo*)realloc(dat->pJSInf, sizeof(JabberSearchFieldsInfo)*(dat->nJSInfCount + 1)); + dat->pJSInf[dat->nJSInfCount].hwndCaptionItem = hwndLabel; + dat->pJSInf[dat->nJSInfCount].hwndValueItem = hwndVar; + dat->pJSInf[dat->nJSInfCount].szFieldCaption = wcsdup(FieldDat->Label); + dat->pJSInf[dat->nJSInfCount].szFieldName = wcsdup(FieldDat->Var); + dat->nJSInfCount++; + } + return CornerY + Order * 40 + 14 + 20; +} + +//////////////////////////////////////////////////////////////////////////////// +// Available search field request result handler (XEP-0055. Examples 2, 7) + +void CJabberProto::OnIqResultGetSearchFields(const TiXmlElement *iqNode, CJabberIqInfo*) +{ + if (!searchHandleDlg) + return; + + const char *type = XmlGetAttr(iqNode, "type"); + if (type == nullptr) + return; + + if (!mir_strcmp(type, "result")) { + auto *queryNode = XmlFirstChild(iqNode, "query"); + auto *xNode = XmlGetChildByTag(queryNode, "x", "xmlns", JABBER_FEAT_DATA_FORMS); + + ShowWindow(searchHandleDlg, SW_HIDE); + if (xNode) { + // 1. Form + PostMessage(searchHandleDlg, WM_USER + 11, (WPARAM)xNode, 0); + auto *xcNode = XmlFirstChild(xNode, "instructions"); + if (xcNode) + SetDlgItemTextUtf(searchHandleDlg, IDC_INSTRUCTIONS, xcNode->GetText()); + } + else { + int Order = 0; + for (auto *chNode : TiXmlEnum(queryNode)) { + if (!mir_strcmpi(chNode->Name(), "instructions") && chNode->GetText()) + SetDlgItemText(searchHandleDlg, IDC_INSTRUCTIONS, TranslateW(Utf2T(chNode->GetText()))); + else if (chNode->Name()) { + Data *MyData = (Data*)malloc(sizeof(Data)); + memset(MyData, 0, sizeof(Data)); + + MyData->Label = mir_utf8decodeW(chNode->Name()); + MyData->Var = mir_utf8decodeW(chNode->Name()); + MyData->defValue = mir_utf8decodeW(chNode->GetText()); + MyData->Order = Order; + if (MyData->defValue) + MyData->bReadOnly = true; + PostMessage(searchHandleDlg, WM_USER + 10, FALSE, (LPARAM)MyData); + Order++; + } + } + } + + const char *szFrom = XmlGetAttr(iqNode, "from"); + if (szFrom) + SearchAddToRecent(szFrom, searchHandleDlg); + PostMessage(searchHandleDlg, WM_USER + 10, 0, 0); + ShowWindow(searchHandleDlg, SW_SHOW); + } + else if (!mir_strcmp(type, "error")) { + const char *code = ""; + const char *description = ""; + auto *errorNode = XmlFirstChild(iqNode, "error"); + if (errorNode) { + code = XmlGetAttr(errorNode, "code"); + description = errorNode->GetText(); + } + + char buff[255]; + mir_snprintf(buff, TranslateU("Error %s %s\r\nPlease select other server"), code, description); + SetDlgItemTextUtf(searchHandleDlg, IDC_INSTRUCTIONS, buff); + } + else SetDlgItemText(searchHandleDlg, IDC_INSTRUCTIONS, TranslateT("Error: unknown reply received\r\nPlease select other server")); +} + +////////////////////////////////////////////////////////////////////////////////////////// +// Return results to search dialog +// The pmFields is the pointer to map of Not unical but ordered +// This can help to made result parser routines more simple + +static char *nickfields[] = { "nick", "nickname", "fullname", "name", "given", "first", "jid", nullptr }; + +static int TCharKeyCmp(const char *p1, const char *p2) +{ + return mir_strcmpi(p1, p2); +} + +static void SearchReturnResults(CJabberProto *ppro, HANDLE id, LIST &plUsersInfo, UNIQUE_MAP &pmAllFields) +{ + LIST ListOfNonEmptyFields(20, TCharKeyCmp); + LIST ListOfFields(20); + + // lets fill the ListOfNonEmptyFields but in users order + for (auto &pmUserData : plUsersInfo) { + int nUserFields = pmUserData->getCount(); + for (int j = 0; j < nUserFields; j++) { + char *var = pmUserData->getKeyName(j); + if (var && ListOfNonEmptyFields.getIndex(var) < 0) + ListOfNonEmptyFields.insert(var); + } + } + + // now fill the ListOfFields but order is from pmAllFields + int nAllCount = pmAllFields.getCount(); + for (int i = 0; i < nAllCount; i++) { + char *var = pmAllFields.getUnOrderedKeyName(i); + if (var && ListOfNonEmptyFields.getIndex(var) < 0) + continue; + ListOfFields.insert(var); + } + + // now lets transfer field names + int nFieldCount = ListOfFields.getCount(); + + CUSTOMSEARCHRESULTS Results = { 0 }; + Results.nSize = sizeof(Results); + Results.pszFields = (wchar_t**)mir_alloc(sizeof(wchar_t*)*nFieldCount); + Results.nFieldCount = nFieldCount; + + // Sending Columns Titles + for (int i = 0; i < nFieldCount; i++) { + char *var = ListOfFields[i]; + if (var) + Results.pszFields[i] = mir_utf8decodeW(pmAllFields[var]); + } + + Results.psr.cbSize = 0; // sending column names + ppro->ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SEARCHRESULT, id, (LPARAM)&Results); + for (int i = 0; i < nFieldCount; i++) + replaceStrW(Results.pszFields[i], nullptr); + + // Sending Users Data + Results.psr.cbSize = sizeof(Results.psr); + + for (auto &pmUserData : plUsersInfo) { + for (int j = 0; j < nFieldCount; j++) { + char *var = ListOfFields[j]; + char *value = pmUserData->operator [](var); + Results.pszFields[j] = value ? mir_utf8decodeW(value) : mir_wstrdup(L" "); + if (!mir_strcmpi(var, "jid") && value) + Results.psr.id.w = Results.pszFields[j]; + } + + const char *nick = nullptr; + for (int k = 0; k < _countof(nickfields) && !nick; k++) + nick = pmUserData->operator [](nickfields[k]); + + if (nick) { + Utf2T wszNick(nick); + wchar_t buff[200]; + if (mir_wstrcmpi(wszNick, Results.psr.id.w)) + mir_snwprintf(buff, L"%s (%s)", wszNick.get(), Results.psr.id.w); + else + wcsncpy_s(buff, wszNick, _TRUNCATE); + + Results.psr.nick.w = buff; + } + else Results.psr.nick.w = L""; + Results.psr.flags = PSR_UNICODE; + + ppro->ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SEARCHRESULT, id, (LPARAM)&Results); + for (int i = 0; i < nFieldCount; i++) + replaceStrW(Results.pszFields[i], nullptr); + } + + mir_free(Results.pszFields); +} + +//////////////////////////////////////////////////////////////////////////////// +// Search field request result handler (XEP-0055. Examples 3, 8) + +void CJabberProto::OnIqResultAdvancedSearch(const TiXmlElement *iqNode, CJabberIqInfo*) +{ + const char *type; + int id; + + UNIQUE_MAP mColumnsNames(10); + LIST SearchResults(2); + + if (((id = JabberGetPacketID(iqNode)) == -1) || ((type = XmlGetAttr(iqNode, "type")) == nullptr)) { + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)id); + return; + } + + if (!mir_strcmp(type, "result")) { + auto *queryNode = XmlFirstChild(iqNode, "query"); + auto *xNode = XmlGetChildByTag(queryNode, "x", "xmlns", JABBER_FEAT_DATA_FORMS); + if (xNode) { + // 1. Form search results info + for (auto *fieldNode : TiXmlFilter(XmlFirstChild(xNode, "reported"), "field")) { + auto *var = XmlGetAttr(fieldNode, "var"); + if (var) { + auto *label = XmlGetAttr(fieldNode, "label"); + mColumnsNames.insert(var, (label != nullptr) ? label : var); + } + } + + for (auto *itemNode : TiXmlFilter(xNode, "item")) { + UNIQUE_MAP *pUserColumn = new UNIQUE_MAP(10); + for (auto *fieldNode : TiXmlFilter(itemNode, "field")) { + if (auto *var = XmlGetAttr(fieldNode, "var")) { + if (auto *textNode = XmlFirstChild(fieldNode, "value")) { + if (!mColumnsNames[var]) + mColumnsNames.insert(var, var); + pUserColumn->insert(var, textNode->GetText()); + } + } + } + + SearchResults.insert(pUserColumn); + } + } + else { + // 2. Field list search results info + for (auto *itemNode : TiXmlFilter(queryNode, "item")) { + UNIQUE_MAP *pUserColumn = new UNIQUE_MAP(10); + + auto *jid = XmlGetAttr(itemNode, "jid"); + char *keyReturned; + mColumnsNames.insertCopyKey("jid", "jid", &keyReturned); + mColumnsNames.insert("jid", keyReturned); + pUserColumn->insertCopyKey("jid", jid, nullptr); + + for (auto *child : TiXmlEnum(itemNode)) { + const char *szColumnName = child->Name(); + if (szColumnName) { + const char *pszChild = child->GetText(); + if (pszChild && *pszChild) { + mColumnsNames.insertCopyKey(szColumnName, "", &keyReturned); + mColumnsNames.insert(szColumnName, keyReturned); + pUserColumn->insertCopyKey(szColumnName, pszChild, nullptr); + } + } + } + + SearchResults.insert(pUserColumn); + } + } + } + else if (!mir_strcmp(type, "error")) { + const char *code = ""; + const char *description = ""; + auto *errorNode = XmlFirstChild(iqNode, "error"); + if (errorNode) { + code = XmlGetAttr(errorNode, "code"); + description = errorNode->GetText(); + } + + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)id); + + char buff[255]; + mir_snprintf(buff, TranslateU("Error %s %s\r\nTry to specify more detailed"), code, description); + if (searchHandleDlg) + SetDlgItemTextUtf(searchHandleDlg, IDC_INSTRUCTIONS, buff); + else + MessageBox(nullptr, Utf2T(buff), TranslateT("Search error"), MB_OK | MB_ICONSTOP); + return; + } + + SearchReturnResults(this, (HANDLE)id, SearchResults, mColumnsNames); + + for (auto &it : SearchResults) + delete ((UNIQUE_MAP*)it); + + //send success to finish searching + ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, (HANDLE)id); +} + +static BOOL CALLBACK DeleteChildWindowsProc(HWND hwnd, LPARAM) +{ + DestroyWindow(hwnd); + return TRUE; +} + +static void JabberSearchFreeData(HWND hwndDlg, JabberSearchData * dat) +{ + if (!dat->fSearchRequestIsXForm && dat->nJSInfCount && dat->pJSInf) { + for (int i = 0; i < dat->nJSInfCount; i++) { + if (dat->pJSInf[i].hwndValueItem) + DestroyWindow(dat->pJSInf[i].hwndValueItem); + if (dat->pJSInf[i].hwndCaptionItem) + DestroyWindow(dat->pJSInf[i].hwndCaptionItem); + if (dat->pJSInf[i].szFieldCaption) + free(dat->pJSInf[i].szFieldCaption); + if (dat->pJSInf[i].szFieldName) + free(dat->pJSInf[i].szFieldName); + } + free(dat->pJSInf); + dat->pJSInf = nullptr; + } + else EnumChildWindows(GetDlgItem(hwndDlg, IDC_FRAME), DeleteChildWindowsProc, 0); + + SendDlgItemMessage(hwndDlg, IDC_FRAME, WM_SETFONT, (WPARAM)SendMessage(hwndDlg, WM_GETFONT, 0, 0), 0); + dat->nJSInfCount = 0; + ShowWindow(GetDlgItem(hwndDlg, IDC_VSCROLL), SW_HIDE); + SetDlgItemText(hwndDlg, IDC_INSTRUCTIONS, TranslateT("Select/type search service URL above and press ")); +} + +static void JabberSearchRefreshFrameScroll(HWND hwndDlg, JabberSearchData *dat) +{ + HWND hFrame = GetDlgItem(hwndDlg, IDC_FRAME); + HWND hwndScroll = GetDlgItem(hwndDlg, IDC_VSCROLL); + RECT rc; + GetClientRect(hFrame, &rc); + GetClientRect(hFrame, &dat->frameRect); + dat->frameHeight = rc.bottom - rc.top; + if (dat->frameHeight < dat->CurrentHeight) { + ShowWindow(hwndScroll, SW_SHOW); + EnableWindow(hwndScroll, TRUE); + } + else ShowWindow(hwndScroll, SW_HIDE); + + SetScrollRange(hwndScroll, SB_CTL, 0, dat->CurrentHeight - dat->frameHeight, FALSE); +} + +int CJabberProto::SearchRenewFields(HWND hwndDlg, JabberSearchData *dat) +{ + wchar_t szServerName[100]; + EnableWindow(GetDlgItem(hwndDlg, IDC_GO), FALSE); + GetDlgItemText(hwndDlg, IDC_SERVER, szServerName, _countof(szServerName)); + dat->CurrentHeight = 0; + dat->curPos = 0; + SetScrollPos(GetDlgItem(hwndDlg, IDC_VSCROLL), SB_CTL, 0, FALSE); + + JabberSearchFreeData(hwndDlg, dat); + JabberSearchRefreshFrameScroll(hwndDlg, dat); + + SetDlgItemText(hwndDlg, IDC_INSTRUCTIONS, m_bJabberOnline ? TranslateT("Please wait...\r\nConnecting search server...") : TranslateT("You have to be connected to server")); + + if (!m_bJabberOnline) + return 0; + + searchHandleDlg = hwndDlg; + + CJabberIqInfo *pInfo = AddIQ(&CJabberProto::OnIqResultGetSearchFields, JABBER_IQ_TYPE_GET, T2Utf(szServerName)); + m_ThreadInfo->send(XmlNodeIq(pInfo) << XQUERY(JABBER_FEAT_JUD)); + return pInfo->GetIqId(); +} + +static void JabberSearchAddUrlToRecentCombo(HWND hwndDlg, const wchar_t *szAddr) +{ + int lResult = SendDlgItemMessage(hwndDlg, IDC_SERVER, (UINT)CB_FINDSTRING, 0, (LPARAM)szAddr); + if (lResult == -1) + SendDlgItemMessage(hwndDlg, IDC_SERVER, CB_ADDSTRING, 0, (LPARAM)szAddr); +} + +void CJabberProto::SearchDeleteFromRecent(const char *szAddr, bool deleteLastFromDB) +{ + // search in recent + for (int i = 0; i < 10; i++) { + char key[30]; + mir_snprintf(key, "RecentlySearched_%d", i); + ptrA szValue(getUStringA(key)); + if (szValue == nullptr || mir_strcmpi(szAddr, szValue)) + continue; + + for (int j = i; j < 10; j++) { + mir_snprintf(key, "RecentlySearched_%d", j + 1); + szValue = getUStringA(key); + if (szValue != nullptr) { + mir_snprintf(key, "RecentlySearched_%d", j); + setUString(0, key, szValue); + } + else { + if (deleteLastFromDB) { + mir_snprintf(key, "RecentlySearched_%d", j); + delSetting(0, key); + } + break; + } + } + break; + } +} + +void CJabberProto::SearchAddToRecent(const char *szAddr, HWND hwndDialog) +{ + char key[30]; + SearchDeleteFromRecent(szAddr, true); + + for (int j = 9; j > 0; j--) { + mir_snprintf(key, "RecentlySearched_%d", j - 1); + ptrW szValue(getWStringA(key)); + if (szValue != nullptr) { + mir_snprintf(key, "RecentlySearched_%d", j); + setWString(0, key, szValue); + } + } + + mir_snprintf(key, "RecentlySearched_%d", 0); + setUString(key, szAddr); + if (hwndDialog) + JabberSearchAddUrlToRecentCombo(hwndDialog, Utf2T(szAddr)); +} + +static INT_PTR CALLBACK JabberSearchAdvancedDlgProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam) +{ + JabberSearchData *dat = (JabberSearchData *)GetWindowLongPtr(hwndDlg, GWLP_USERDATA); + switch (msg) { + case WM_INITDIALOG: + TranslateDialogDefault(hwndDlg); + { + dat = new JabberSearchData(); + dat->ppro = (CJabberProto *)lParam; + SetWindowLongPtr(hwndDlg, GWLP_USERDATA, (LONG_PTR)dat); + + /* Server Combo box */ + ptrA jud(dat->ppro->getStringA("Jud")); + if (jud != nullptr) { + SetDlgItemTextA(hwndDlg, IDC_SERVER, jud); + SendDlgItemMessageA(hwndDlg, IDC_SERVER, CB_ADDSTRING, 0, jud); + } + + //TO DO: Add Transports here + for (auto &it : dat->ppro->m_lstTransports) + if (it != nullptr) + JabberSearchAddUrlToRecentCombo(hwndDlg, Utf2T(it)); + + for (int i = 0; i < 10; i++) { + char key[30]; + mir_snprintf(key, "RecentlySearched_%d", i); + ptrW szValue(dat->ppro->getWStringA(key)); + if (szValue != nullptr) + JabberSearchAddUrlToRecentCombo(hwndDlg, szValue); + } + + //TO DO: Add 4 recently used + dat->lastRequestIq = dat->ppro->SearchRenewFields(hwndDlg, dat); + } + return TRUE; + + case WM_COMMAND: + if (LOWORD(wParam) == IDC_SERVER) { + switch (HIWORD(wParam)) { + case CBN_SETFOCUS: + PostMessage(GetParent(hwndDlg), WM_COMMAND, MAKEWPARAM(0, EN_SETFOCUS), (LPARAM)hwndDlg); + return TRUE; + + case CBN_EDITCHANGE: + EnableWindow(GetDlgItem(hwndDlg, IDC_GO), TRUE); + return TRUE; + + case CBN_EDITUPDATE: + JabberSearchFreeData(hwndDlg, dat); + EnableWindow(GetDlgItem(hwndDlg, IDC_GO), TRUE); + return TRUE; + + case CBN_SELENDOK: + EnableWindow(GetDlgItem(hwndDlg, IDC_GO), TRUE); + PostMessage(hwndDlg, WM_COMMAND, MAKEWPARAM(IDC_GO, BN_CLICKED), 0); + return TRUE; + } + } + else if (LOWORD(wParam) == IDC_GO && HIWORD(wParam) == BN_CLICKED) { + dat->ppro->SearchRenewFields(hwndDlg, dat); + return TRUE; + } + break; + + case WM_SIZE: + { + //Resize IDC_FRAME to take full size + RECT rcForm; + GetWindowRect(hwndDlg, &rcForm); + RECT rcFrame; + GetWindowRect(GetDlgItem(hwndDlg, IDC_FRAME), &rcFrame); + rcFrame.bottom = rcForm.bottom; + SetWindowPos(GetDlgItem(hwndDlg, IDC_FRAME), nullptr, 0, 0, rcFrame.right - rcFrame.left, rcFrame.bottom - rcFrame.top, SWP_NOZORDER | SWP_NOMOVE); + GetWindowRect(GetDlgItem(hwndDlg, IDC_VSCROLL), &rcForm); + SetWindowPos(GetDlgItem(hwndDlg, IDC_VSCROLL), nullptr, 0, 0, rcForm.right - rcForm.left, rcFrame.bottom - rcFrame.top, SWP_NOZORDER | SWP_NOMOVE); + JabberSearchRefreshFrameScroll(hwndDlg, dat); + } + return TRUE; + + case WM_USER + 11: + { + dat->fSearchRequestIsXForm = TRUE; + if (dat->xNode) { + dat->doc.DeleteNode(dat->xNode); + dat->xNode = nullptr; + } + TiXmlElement *pNode = (TiXmlElement *)wParam; + if (pNode) { + dat->xNode = pNode->DeepClone(&dat->doc)->ToElement(); + JabberFormCreateUI(GetDlgItem(hwndDlg, IDC_FRAME), dat->xNode, &dat->CurrentHeight, TRUE); + } + ShowWindow(GetDlgItem(hwndDlg, IDC_FRAME), SW_SHOW); + dat->nJSInfCount = 1; + } + return TRUE; + + case WM_USER + 10: + { + Data *MyDat = (Data *)lParam; + if (MyDat) { + dat->fSearchRequestIsXForm = (BOOL)wParam; + dat->CurrentHeight = JabberSearchAddField(hwndDlg, MyDat); + mir_free(MyDat->Label); + mir_free(MyDat->Var); + mir_free(MyDat->defValue); + free(MyDat); + } + else { + JabberSearchRefreshFrameScroll(hwndDlg, dat); + ScrollWindow(GetDlgItem(hwndDlg, IDC_FRAME), 0, dat->curPos - 0, nullptr, &(dat->frameRect)); + SetScrollPos(GetDlgItem(hwndDlg, IDC_VSCROLL), SB_CTL, 0, FALSE); + dat->curPos = 0; + } + } + return TRUE; + + case WM_MOUSEWHEEL: + { + short zDelta = GET_WHEEL_DELTA_WPARAM(wParam); + if (zDelta) { + int nScrollLines = 0; + SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, (void *)&nScrollLines, 0); + for (int i = 0; i < (nScrollLines + 1) / 2; i++) + SendMessage(hwndDlg, WM_VSCROLL, (zDelta < 0) ? SB_LINEDOWN : SB_LINEUP, 0); + } + } + return TRUE; + + case WM_VSCROLL: + { + int pos; + if (dat != nullptr) { + pos = dat->curPos; + switch (LOWORD(wParam)) { + case SB_LINEDOWN: + pos += 10; + break; + case SB_LINEUP: + pos -= 10; + break; + case SB_PAGEDOWN: + pos += (dat->CurrentHeight - 10); + break; + case SB_PAGEUP: + pos -= (dat->CurrentHeight - 10); + break; + case SB_THUMBTRACK: + pos = HIWORD(wParam); + break; + } + if (pos > (dat->CurrentHeight - dat->frameHeight)) + pos = dat->CurrentHeight - dat->frameHeight; + if (pos < 0) + pos = 0; + if (dat->curPos != pos) { + ScrollWindow(GetDlgItem(hwndDlg, IDC_FRAME), 0, dat->curPos - pos, nullptr, &(dat->frameRect)); + SetScrollPos(GetDlgItem(hwndDlg, IDC_VSCROLL), SB_CTL, pos, TRUE); + RECT Invalid = dat->frameRect; + if (dat->curPos - pos > 0) + Invalid.bottom = Invalid.top + (dat->curPos - pos); + else + Invalid.top = Invalid.bottom + (dat->curPos - pos); + + RedrawWindow(GetDlgItem(hwndDlg, IDC_FRAME), nullptr, nullptr, RDW_UPDATENOW | RDW_ALLCHILDREN); + dat->curPos = pos; + } + } + } + return TRUE; + + case WM_DESTROY: + JabberSearchFreeData(hwndDlg, dat); + JabberFormDestroyUI(GetDlgItem(hwndDlg, IDC_FRAME)); + delete dat; + SetWindowLongPtr(hwndDlg, GWLP_USERDATA, 0); + return TRUE; + } + return FALSE; +} + +HWND CJabberProto::CreateExtendedSearchUI(HWND parent) +{ + if (parent && g_plugin.getInst()) { + ptrW szServer(getWStringA("LoginServer")); + if (szServer == nullptr || mir_wstrcmpi(szServer, L"S.ms")) + return CreateDialogParam(g_plugin.getInst(), MAKEINTRESOURCE(IDD_SEARCHUSER), parent, JabberSearchAdvancedDlgProc, (LPARAM)this); + } + + return nullptr; // Failure +} + +////////////////////////////////////////////////////////////////////////// +// The function formats request to server + +HWND CJabberProto::SearchAdvanced(HWND hwndDlg) +{ + if (!m_bJabberOnline || !hwndDlg) + return nullptr; //error + + JabberSearchData *dat = (JabberSearchData *)GetWindowLongPtr(hwndDlg, GWLP_USERDATA); + if (!dat) + return nullptr; //error + + // check if server connected (at least one field exists) + if (dat->nJSInfCount == 0) + return nullptr; + + // formating request + bool fRequestNotEmpty = false; + + // get server name + wchar_t szServerName[100]; + GetDlgItemText(hwndDlg, IDC_SERVER, szServerName, _countof(szServerName)); + + // formating query + CJabberIqInfo *pInfo = AddIQ(&CJabberProto::OnIqResultAdvancedSearch, JABBER_IQ_TYPE_SET, T2Utf(szServerName)); + XmlNodeIq iq(pInfo); + TiXmlElement *query = iq << XQUERY(JABBER_FEAT_JUD); + + if (m_tszSelectedLang) + iq << XATTR("xml:lang", m_tszSelectedLang); // i'm sure :) + + // next can be 2 cases: + // Forms: XEP-0055 Example 7 + if (dat->fSearchRequestIsXForm) { + fRequestNotEmpty = true; + JabberFormGetData(GetDlgItem(hwndDlg, IDC_FRAME), query, dat->xNode); + } + else { //and Simple fields: XEP-0055 Example 3 + for (int i = 0; i < dat->nJSInfCount; i++) { + wchar_t szFieldValue[100]; + GetWindowText(dat->pJSInf[i].hwndValueItem, szFieldValue, _countof(szFieldValue)); + if (szFieldValue[0] != 0) { + XmlAddChildA(query, T2Utf(dat->pJSInf[i].szFieldName).get(), T2Utf(szFieldValue).get()); + fRequestNotEmpty = true; + } + } + } + + if (fRequestNotEmpty) { + m_ThreadInfo->send(iq); + return (HWND)pInfo->GetIqId(); + } + return nullptr; +} diff --git a/protocols/JabberG/src/jabber_search.h b/protocols/JabberG/src/jabber_search.h index 4916afb29a..3bfaa2df8a 100644 --- a/protocols/JabberG/src/jabber_search.h +++ b/protocols/JabberG/src/jabber_search.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Artem Shpynov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team Module implements a search according to XEP-0055: Jabber Search http://www.xmpp.org/extensions/xep-0055.html diff --git a/protocols/JabberG/src/jabber_secur.cpp b/protocols/JabberG/src/jabber_secur.cpp index a1cf3faccc..853016823d 100644 --- a/protocols/JabberG/src/jabber_secur.cpp +++ b/protocols/JabberG/src/jabber_secur.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_secur.h b/protocols/JabberG/src/jabber_secur.h index 133d55e840..90ad39bd60 100644 --- a/protocols/JabberG/src/jabber_secur.h +++ b/protocols/JabberG/src/jabber_secur.h @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_send_manager.cpp b/protocols/JabberG/src/jabber_send_manager.cpp index c70d4017af..67031c0567 100644 --- a/protocols/JabberG/src/jabber_send_manager.cpp +++ b/protocols/JabberG/src/jabber_send_manager.cpp @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-08 George Hazan Copyright (c) 2007 Maxim Mluhov Copyright (c) 2008-09 Dmitriy Chervov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_send_manager.h b/protocols/JabberG/src/jabber_send_manager.h index d223782694..9b98db0458 100644 --- a/protocols/JabberG/src/jabber_send_manager.h +++ b/protocols/JabberG/src/jabber_send_manager.h @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-08 George Hazan Copyright (c) 2007 Maxim Mluhov Copyright (c) 2008-09 Dmitriy Chervov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_strm_mgmt.cpp b/protocols/JabberG/src/jabber_strm_mgmt.cpp index e0f95eb2a8..a081570cfe 100644 --- a/protocols/JabberG/src/jabber_strm_mgmt.cpp +++ b/protocols/JabberG/src/jabber_strm_mgmt.cpp @@ -2,7 +2,7 @@ Jabber Protocol Plugin for Miranda NG -Copyright (c) 2018-22 Miranda NG team +Copyright (c) 2018-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_strm_mgmt.h b/protocols/JabberG/src/jabber_strm_mgmt.h index 412127d1d2..158c72fefb 100644 --- a/protocols/JabberG/src/jabber_strm_mgmt.h +++ b/protocols/JabberG/src/jabber_strm_mgmt.h @@ -2,7 +2,7 @@ Jabber Protocol Plugin for Miranda NG -Copyright (c) 2018-22 Miranda NG team +Copyright (c) 2018-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_svc.cpp b/protocols/JabberG/src/jabber_svc.cpp index 63fbd74e8b..5252c0865f 100644 --- a/protocols/JabberG/src/jabber_svc.cpp +++ b/protocols/JabberG/src/jabber_svc.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_thread.cpp b/protocols/JabberG/src/jabber_thread.cpp index 212c6bdd6a..0ad67e79dd 100644 --- a/protocols/JabberG/src/jabber_thread.cpp +++ b/protocols/JabberG/src/jabber_thread.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_treelist.cpp b/protocols/JabberG/src/jabber_treelist.cpp index 21fcb1fbc5..35467cbd1c 100644 --- a/protocols/JabberG/src/jabber_treelist.cpp +++ b/protocols/JabberG/src/jabber_treelist.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Victor Pavlychko -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_userinfo.cpp b/protocols/JabberG/src/jabber_userinfo.cpp index f164252fbd..0f9271481a 100644 --- a/protocols/JabberG/src/jabber_userinfo.cpp +++ b/protocols/JabberG/src/jabber_userinfo.cpp @@ -1,912 +1,912 @@ -/* - -Jabber Protocol Plugin for Miranda NG - -Copyright (c) 2002-04 Santithorn Bunchua -Copyright (c) 2005-12 George Hazan -Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team - -This program is free software; you can redistribute it and/or -modify it under the terms of the GNU General Public License -as published by the Free Software Foundation; either version 2 -of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - -*/ - -#include "stdafx.h" - -#include -#include -#include - -#include "jabber_list.h" - -static MWindowList hUserInfoList = nullptr; - -class JabberBaseUserInfoDlg : public CUserInfoPageDlg -{ - INT_PTR DoRefresh(UINT, WPARAM, LPARAM) - { - OnRefresh(); - return 0; - } - -protected: - UI_MESSAGE_MAP(JabberBaseUserInfoDlg, CUserInfoPageDlg); - UI_MESSAGE(WM_PROTO_REFRESH, DoRefresh); - UI_MESSAGE(WM_JABBER_REFRESH_VCARD, DoRefresh); - UI_MESSAGE_MAP_END(); - - CJabberProto *ppro; - - JabberBaseUserInfoDlg(CJabberProto *_ppro, int dlgId) : - CUserInfoPageDlg(g_plugin, dlgId), - ppro(_ppro) - {} -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// JabberUserInfoDlgProc - main user info dialog - -enum -{ - INFOLINE_DELETE = 0x80000000, - INFOLINE_MASK = 0x7fffffff, - INFOLINE_BAD_ID = 0x7fffffff, - - INFOLINE_NAME = 1, - INFOLINE_MOOD, - INFOLINE_ACTIVITY, - INFOLINE_TUNE, - INFOLINE_OFFLINE, - INFOLINE_MESSAGE, - INFOLINE_SOFTWARE, - INFOLINE_VERSION, - INFOLINE_SYSTEM, - INFOLINE_PRIORITY, - INFOLINE_IDLE, - INFOLINE_CAPS, - INFOLINE_SOFTWARE_INFORMATION, - INFOLINE_SUBSCRIPTION, - INFOLINE_LOGOFF, - INFOLINE_LOGOFF_MSG, - INFOLINE_LASTACTIVE, -}; - -__forceinline uint32_t sttInfoLineId(uint32_t res, uint32_t type, uint32_t line = 0) -{ - return - (type << 24) & 0x7f000000 | - (res << 12) & 0x00fff000 | - (line) & 0x00000fff; -} - -class JabberUserInfoDlg : public JabberBaseUserInfoDlg -{ - JABBER_LIST_ITEM *item = nullptr; - int resourcesCount = -1; - - CCtrlTreeView m_tree; - - UI_MESSAGE_MAP(JabberUserInfoDlg, JabberBaseUserInfoDlg); - UI_MESSAGE(WM_PROTO_CHECK_ONLINE, OnCheckOnline); - UI_MESSAGE_MAP_END(); - - INT_PTR OnCheckOnline(UINT, WPARAM, LPARAM) - { - if (!ppro->m_bJabberOnline) - item = nullptr; - else - OnRefresh(); - return 0; - } - - //////////////////////////////////////////////////////////////////////////////////////// - // User information block - - void CleanupInfo(int stage) - { - HTREEITEM hItem = m_tree.GetRoot(); - while (hItem) { - TVITEMEX tvi = { 0 }; - tvi.mask = TVIF_HANDLE | TVIF_PARAM; - tvi.hItem = hItem; - m_tree.GetItem(&tvi); - - switch (stage) { - case 0: - tvi.lParam |= INFOLINE_DELETE; - m_tree.SetItem(&tvi); - break; - - case 1: - if (tvi.lParam & INFOLINE_DELETE) { - hItem = m_tree.GetNextSibling(hItem); - m_tree.DeleteItem(tvi.hItem); - continue; - } - break; - } - - HTREEITEM hItemTmp = nullptr; - if (hItemTmp = m_tree.GetChild(hItem)) - hItem = hItemTmp; - else if (hItemTmp = m_tree.GetNextSibling(hItem)) - hItem = hItemTmp; - else { - while (true) { - if (!(hItem = m_tree.GetParent(hItem))) break; - if (hItemTmp = m_tree.GetNextSibling(hItem)) { - hItem = hItemTmp; - break; - } - } - } - } - } - - HTREEITEM FindInfoLine(HTREEITEM htiRoot, LPARAM id = INFOLINE_BAD_ID) - { - if (id == INFOLINE_BAD_ID) return nullptr; - for (HTREEITEM hti = m_tree.GetChild(htiRoot); hti; hti = m_tree.GetNextSibling(hti)) { - TVITEMEX tvi = { 0 }; - tvi.mask = TVIF_HANDLE | TVIF_PARAM; - tvi.hItem = hti; - m_tree.GetItem(&tvi); - if ((tvi.lParam&INFOLINE_MASK) == (id&INFOLINE_MASK)) - return hti; - } - return nullptr; - } - - HTREEITEM FillInfoLine(HTREEITEM htiRoot, HICON hIcon, const wchar_t *title, const char *value, LPARAM id = INFOLINE_BAD_ID, bool expand = false) - { - HTREEITEM hti = FindInfoLine(htiRoot, id); - - Utf2T wszValue(value); - const wchar_t *pwszValue = (value == nullptr) ? TranslateT("") : wszValue; - wchar_t buf[256]; - if (title) - mir_snwprintf(buf, L"%s: %s", title, pwszValue); - else - mir_wstrncpy(buf, pwszValue, _countof(buf)); - - TVINSERTSTRUCT tvis = {}; - tvis.hParent = htiRoot; - tvis.hInsertAfter = TVI_LAST; - tvis.itemex.mask = TVIF_TEXT | TVIF_PARAM; - tvis.itemex.pszText = buf; - tvis.itemex.lParam = id; - - if (hIcon) { - HIMAGELIST himl = m_tree.GetImageList(TVSIL_NORMAL); - tvis.itemex.mask |= TVIF_IMAGE | TVIF_SELECTEDIMAGE; - tvis.itemex.iImage = - tvis.itemex.iSelectedImage = ImageList_AddIcon(himl, hIcon); - IcoLib_ReleaseIcon(hIcon); - } - - if (hti) { - tvis.itemex.mask |= TVIF_HANDLE; - tvis.itemex.hItem = hti; - m_tree.SetItem(&tvis.itemex); - } - else { - tvis.itemex.mask |= TVIF_STATE; - tvis.itemex.stateMask = TVIS_EXPANDED; - tvis.itemex.state = expand ? TVIS_EXPANDED : 0; - hti = m_tree.InsertItem(&tvis); - } - - return hti; - } - - void FillAdvStatusInfo(HTREEITEM htiRoot, uint32_t dwInfoLine, MCONTACT hContact, wchar_t *szTitle, char *pszSlot) - { - ptrA szAdvStatusIcon(ppro->ReadAdvStatusA(hContact, pszSlot, ADVSTATUS_VAL_ICON)); - ptrW szAdvStatusTitle(ppro->ReadAdvStatusT(hContact, pszSlot, ADVSTATUS_VAL_TITLE)); - ptrW szAdvStatusText(ppro->ReadAdvStatusT(hContact, pszSlot, ADVSTATUS_VAL_TEXT)); - - if (szAdvStatusIcon && szAdvStatusTitle && *szAdvStatusTitle) { - wchar_t szText[2048]; - if (szAdvStatusText && *szAdvStatusText) - mir_snwprintf(szText, L"%s (%s)", TranslateW(szAdvStatusTitle), szAdvStatusText.get()); - else - wcsncpy_s(szText, TranslateW(szAdvStatusTitle), _TRUNCATE); - FillInfoLine(htiRoot, IcoLib_GetIcon(szAdvStatusIcon), szTitle, T2Utf(szText), dwInfoLine); - } - } - - void FillResourceInfo(HTREEITEM htiRoot, int resource) - { - HTREEITEM htiResource = htiRoot; - pResourceStatus r = resource ? item->arResources[resource - 1] : item->getTemp(); - - if (r->m_szResourceName && *r->m_szResourceName) - htiResource = FillInfoLine(htiRoot, Skin_LoadProtoIcon(ppro->m_szModuleName, r->m_iStatus), - TranslateT("Resource"), r->m_szResourceName, sttInfoLineId(resource, INFOLINE_NAME), true); - - // StatusMsg - FillInfoLine(htiResource, nullptr /*Skin_LoadIcon(SKINICON_EVENT_MESSAGE)*/, - TranslateT("Message"), r->m_szStatusMessage, - sttInfoLineId(resource, INFOLINE_MESSAGE)); - - // Software - if (CJabberClientPartialCaps *pCaps = r->m_pCaps) { - HICON hIcon = nullptr; - - if (ServiceExists(MS_FP_GETCLIENTICONT)) { - if (pCaps->GetSoft()) { - wchar_t buf[256]; - mir_snwprintf(buf, L"%s %s", pCaps->GetSoft(), pCaps->GetSoftVer()); - hIcon = Finger_GetClientIcon(buf, 0); - } - } - - FillInfoLine(htiResource, hIcon, TranslateT("Software"), pCaps->GetSoft(), sttInfoLineId(resource, INFOLINE_SOFTWARE)); - - // Version - FillInfoLine(htiResource, nullptr, TranslateT("Version"), pCaps->GetSoftMir() ? pCaps->GetSoftMir() : pCaps->GetSoftVer(), sttInfoLineId(resource, INFOLINE_VERSION)); - - // System - FillInfoLine(htiResource, nullptr, TranslateT("System"), pCaps->GetOsVer() ? pCaps->GetOsVer() : pCaps->GetOs(), sttInfoLineId(resource, INFOLINE_SYSTEM)); - - if (hIcon) - DestroyIcon(hIcon); - } - - // Resource priority - char buf[256]; - itoa(r->m_iPriority, buf, 10); - FillInfoLine(htiResource, nullptr, TranslateT("Resource priority"), buf, sttInfoLineId(resource, INFOLINE_PRIORITY)); - - // Idle - if (r->m_dwIdleStartTime != -1) { - if (r->m_dwIdleStartTime != 0) { - mir_strncpy(buf, ctime(&r->m_dwIdleStartTime), _countof(buf)); - size_t len = mir_strlen(buf); - if (len > 0) - buf[len - 1] = 0; - } - else mir_strncpy(buf, TranslateU(""), _countof(buf)); - - FillInfoLine(htiResource, nullptr, TranslateT("Last activity"), buf, sttInfoLineId(resource, INFOLINE_IDLE)); - } - - // caps - JabberCapsBits jcb = ppro->GetResourceCapabilities(MakeJid(item->jid, r->m_szResourceName), r); - if (!(jcb & JABBER_RESOURCE_CAPS_ERROR)) { - HTREEITEM htiCaps = FillInfoLine(htiResource, IcoLib_GetIconByHandle(ppro->m_hProtoIcon), nullptr, TranslateU("Client capabilities"), sttInfoLineId(resource, INFOLINE_CAPS)); - int i; - for (i = 0; i < g_cJabberFeatCapPairs; i++) - if (jcb & g_JabberFeatCapPairs[i].jcbCap) { - char szDescription[1024]; - if (g_JabberFeatCapPairs[i].tszDescription) - mir_snprintf(szDescription, "%s (%s)", TranslateU(g_JabberFeatCapPairs[i].tszDescription), g_JabberFeatCapPairs[i].szFeature); - else - strncpy_s(szDescription, g_JabberFeatCapPairs[i].szFeature, _TRUNCATE); - FillInfoLine(htiCaps, nullptr, nullptr, szDescription, sttInfoLineId(resource, INFOLINE_CAPS, i)); - } - - for (auto &it : ppro->m_lstJabberFeatCapPairsDynamic) { - if (jcb & it->jcbCap) { - char szDescription[1024]; - if (it->szDescription) - mir_snprintf(szDescription, "%s (%s)", TranslateU(it->szDescription), it->szFeature); - else - strncpy_s(szDescription, it->szFeature, _TRUNCATE); - FillInfoLine(htiCaps, nullptr, nullptr, szDescription, sttInfoLineId(resource, INFOLINE_CAPS, i++)); - } - } - } - - // Software info - HTREEITEM htiSoftwareInfo = FillInfoLine(htiResource, IcoLib_GetIconByHandle(ppro->m_hProtoIcon), nullptr, TranslateU("Software information"), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION)); - int nLineId = 0; - if (CJabberClientPartialCaps *pCaps = r->m_pCaps) { - if (pCaps->GetOs()) - FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Operating system"), pCaps->GetOs(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); - if (pCaps->GetOsVer()) - FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Operating system version"), pCaps->GetOsVer(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); - if (pCaps->GetSoft()) - FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Software"), pCaps->GetSoft(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); - if (pCaps->GetSoftVer()) - FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Software version"), pCaps->GetSoftVer(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); - if (pCaps->GetSoftMir()) - FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Miranda core version"), pCaps->GetSoftMir(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); - } - } - - void FillUserInfo() - { - m_tree.SendMsg(WM_SETREDRAW, FALSE, 0); - - CleanupInfo(0); - - HTREEITEM htiRoot = FillInfoLine(nullptr, IcoLib_GetIconByHandle(ppro->m_hProtoIcon), L"JID", item->jid, sttInfoLineId(0, INFOLINE_NAME), true); - - if (MCONTACT hContact = ppro->HContactFromJID(item->jid)) { - FillAdvStatusInfo(htiRoot, sttInfoLineId(0, INFOLINE_MOOD), hContact, TranslateT("Mood"), ADVSTATUS_MOOD); - FillAdvStatusInfo(htiRoot, sttInfoLineId(0, INFOLINE_ACTIVITY), hContact, TranslateT("Activity"), ADVSTATUS_ACTIVITY); - FillAdvStatusInfo(htiRoot, sttInfoLineId(0, INFOLINE_TUNE), hContact, TranslateT("Tune"), ADVSTATUS_TUNE); - } - - // subscription - switch (item->subscription) { - case SUB_BOTH: - FillInfoLine(htiRoot, nullptr, TranslateT("Subscription"), TranslateU("both"), sttInfoLineId(0, INFOLINE_SUBSCRIPTION)); - break; - case SUB_TO: - FillInfoLine(htiRoot, nullptr, TranslateT("Subscription"), TranslateU("to"), sttInfoLineId(0, INFOLINE_SUBSCRIPTION)); - break; - case SUB_FROM: - FillInfoLine(htiRoot, nullptr, TranslateT("Subscription"), TranslateU("from"), sttInfoLineId(0, INFOLINE_SUBSCRIPTION)); - break; - default: - FillInfoLine(htiRoot, nullptr, TranslateT("Subscription"), TranslateU("none"), sttInfoLineId(0, INFOLINE_SUBSCRIPTION)); - break; - } - - // logoff - char buf[256]; - JABBER_RESOURCE_STATUS *r = item->getTemp(); - if (r->m_dwIdleStartTime != -1) { - if (r->m_dwIdleStartTime > 0) { - mir_strncpy(buf, ctime(&r->m_dwIdleStartTime), _countof(buf)); - size_t len = mir_strlen(buf); - if (len > 0) - buf[len - 1] = 0; - } - else mir_strncpy(buf, TranslateU(""), _countof(buf)); - - FillInfoLine(htiRoot, nullptr, - (item->jid && strchr(item->jid, '@')) ? TranslateT("Last logoff time") : TranslateT("Uptime"), buf, - sttInfoLineId(0, INFOLINE_LOGOFF)); - } - - if (r->m_szStatusMessage) - FillInfoLine(htiRoot, nullptr, TranslateT("Logoff message"), r->m_szStatusMessage, sttInfoLineId(0, INFOLINE_LOGOFF_MSG)); - - // activity - if (item->m_pLastSeenResource) - mir_strncpy(buf, item->m_pLastSeenResource->m_szResourceName, _countof(buf)); - else - mir_strncpy(buf, TranslateU(""), _countof(buf)); - FillInfoLine(htiRoot, nullptr, TranslateT("Last active resource"), buf, sttInfoLineId(0, INFOLINE_LASTACTIVE)); - - // resources - if (item->arResources.getCount()) { - for (int i = 0; i < item->arResources.getCount(); i++) - FillResourceInfo(htiRoot, i + 1); - } - else if (!strchr(item->jid, '@') || (r->m_iStatus != ID_STATUS_OFFLINE)) - FillResourceInfo(htiRoot, 0); - - CleanupInfo(1); - m_tree.SendMsg(WM_SETREDRAW, TRUE, 0); - - RedrawWindow(m_tree.GetHwnd(), nullptr, nullptr, RDW_INVALIDATE); - } - -public: - JabberUserInfoDlg(CJabberProto *_ppro) : - JabberBaseUserInfoDlg(_ppro, IDD_INFO_JABBER), - m_tree(this, IDC_TV_INFO) - { - m_tree.OnBuildMenu = Callback(this, &JabberUserInfoDlg::onMenu_Tree); - } - - bool OnInitDialog() override - { - ppro->WindowSubscribe(m_hwnd); - Window_SetSkinIcon_IcoLib(m_hwnd, SKINICON_OTHER_USERDETAILS); - - RECT rc; - GetClientRect(m_hwnd, &rc); - MoveWindow(m_tree.GetHwnd(), 5, 5, rc.right - 10, rc.bottom - 10, TRUE); - - HIMAGELIST himl = ImageList_Create(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), ILC_COLOR | ILC_COLOR32 | ILC_MASK, 5, 1); - ImageList_AddSkinIcon(himl, SKINICON_OTHER_SMALLDOT); - TreeView_SetImageList(m_tree.GetHwnd(), himl, TVSIL_NORMAL); - - WindowList_Add(hUserInfoList, m_hwnd, m_hContact); - return true; - } - - void OnDestroy() override - { - ppro->WindowUnsubscribe(m_hwnd); - WindowList_Remove(hUserInfoList, m_hwnd); - ImageList_Destroy(m_tree.SetImageList(nullptr, TVSIL_NORMAL)); - Window_FreeIcon_IcoLib(m_hwnd); - } - - int Resizer(UTILRESIZECONTROL*) override - { - return RD_ANCHORX_WIDTH | RD_ANCHORY_HEIGHT; - } - - bool OnRefresh() override - { - if (item == nullptr) { - ptrA jid(ppro->getUStringA(m_hContact, "jid")); - if (jid == nullptr) - return false; - - if (ppro->m_bJabberOnline) - if (!(item = ppro->ListGetItemPtr(LIST_VCARD_TEMP, jid))) - item = ppro->ListGetItemPtr(LIST_ROSTER, jid); - - if (item == nullptr) { - m_tree.DeleteAllItems(); - HTREEITEM htiRoot = FillInfoLine(nullptr, IcoLib_GetIconByHandle(ppro->m_hProtoIcon), L"JID", jid, sttInfoLineId(0, INFOLINE_NAME), true); - FillInfoLine(htiRoot, g_plugin.getIcon(IDI_VCARD), nullptr, TranslateU("Please switch online to see more details.")); - return false; - } - } - FillUserInfo(); - return false; - } - - //////////////////////////////////////////////////////////////////////////////////////// - // Context menu - - void GetNodeText(HTREEITEM hti, CMStringW &buf, int indent = 0) - { - for (int i = 0; i < indent; i++) - buf.AppendChar('\t'); - - wchar_t wszText[256]; - TVITEMEX tvi = {}; - tvi.mask = TVIF_HANDLE | TVIF_TEXT | TVIF_STATE; - tvi.hItem = hti; - tvi.cchTextMax = _countof(wszText); - tvi.pszText = wszText; - if (!m_tree.GetItem(&tvi)) // failure, maybe item was removed... - return; - - buf.Append(wszText); - buf.Append(L"\r\n"); - - if (tvi.state & TVIS_EXPANDED) - for (hti = m_tree.GetChild(hti); hti; hti = m_tree.GetNextSibling(hti)) - GetNodeText(hti, buf, indent + 1); - } - - void onMenu_Tree(CContextMenuPos *pos) - { - if (!pos->hItem) - return; - - HMENU hMenu = CreatePopupMenu(); - AppendMenu(hMenu, MF_STRING, (UINT_PTR)1, TranslateT("Copy")); - AppendMenu(hMenu, MF_STRING, (UINT_PTR)2, TranslateT("Copy only this value")); - AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr); - AppendMenu(hMenu, MF_STRING, (UINT_PTR)0, TranslateT("Cancel")); - int nReturnCmd = TrackPopupMenu(hMenu, TPM_RETURNCMD, pos->pt.x, pos->pt.y, 0, m_hwnd, nullptr); - if (nReturnCmd == 1) { - CMStringW buf; - GetNodeText(pos->hItem, buf); - Utils_ClipboardCopy(buf); - } - else if (nReturnCmd == 2) { - wchar_t szBuffer[1024]; - TVITEMEX tvi = { 0 }; - tvi.mask = TVIF_HANDLE | TVIF_TEXT | TVIF_STATE; - tvi.hItem = pos->hItem; - tvi.cchTextMax = _countof(szBuffer); - tvi.pszText = szBuffer; - if (m_tree.GetItem(&tvi)) { - if (wchar_t *str = wcsstr(szBuffer, L": ")) - Utils_ClipboardCopy(str + 2); - else - Utils_ClipboardCopy(szBuffer); - } - } - DestroyMenu(hMenu); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// JabberUserPhotoDlgProc - Jabber photo dialog - -class JabberUserPhotoDlg : public JabberBaseUserInfoDlg -{ - HBITMAP hBitmap = nullptr; - - CCtrlMButton btnSave; - - UI_MESSAGE_MAP(JabberUserInfoDlg, JabberBaseUserInfoDlg); - UI_MESSAGE(WM_PAINT, OnPaint); - UI_MESSAGE_MAP_END(); - - char *GetFileName() const - { - ptrA jid(ppro->getUStringA(m_hContact, "jid")); - if (jid != nullptr) { - JABBER_LIST_ITEM *item = ppro->ListGetItemPtr(LIST_VCARD_TEMP, jid); - if (item == nullptr) - item = ppro->ListGetItemPtr(LIST_ROSTER, jid); - if (item != nullptr) - return item->photoFileName; - } - - return nullptr; - } - -public: - JabberUserPhotoDlg(CJabberProto *_ppro) : - JabberBaseUserInfoDlg(_ppro, IDD_VCARD_PHOTO), - btnSave(this, IDC_SAVE, g_plugin.getIcon(IDI_SAVE), LPGEN("Save")) - { - btnSave.OnClick = Callback(this, &JabberUserPhotoDlg::onClick_Save); - } - - bool OnInitDialog() override - { - ShowWindow(GetDlgItem(m_hwnd, IDC_LOAD), SW_HIDE); - ShowWindow(GetDlgItem(m_hwnd, IDC_DELETE), SW_HIDE); - return true; - } - - void OnDestroy() override - { - if (hBitmap) { - ppro->debugLogA("Delete bitmap"); - DeleteObject(hBitmap); - } - } - - bool IsEmpty() const override - { - return mir_strlen(GetFileName()) == 0; - } - - bool OnRefresh() override - { - if (hBitmap) { - DeleteObject(hBitmap); - hBitmap = nullptr; - } - btnSave.Hide(); - - char *pszFileName = GetFileName(); - if (mir_strlen(pszFileName)) { - ppro->debugLogA("Showing picture from %s", pszFileName); - hBitmap = Bitmap_Load(Utf2T(pszFileName)); - FreeImage_Premultiply(hBitmap); - btnSave.Show(); - } - - InvalidateRect(m_hwnd, nullptr, TRUE); - UpdateWindow(m_hwnd); - return true; - } - - void onClick_Save(CCtrlButton *) - { - wchar_t szFilter[512]; - - ptrA jid(ppro->getUStringA(m_hContact, "jid")); - if (jid == nullptr) - return; - - JABBER_LIST_ITEM *item = ppro->ListGetItemPtr(LIST_VCARD_TEMP, jid); - if (item == nullptr) - if ((item = ppro->ListGetItemPtr(LIST_ROSTER, jid)) == nullptr) - return; - - switch (ProtoGetAvatarFileFormat(Utf2T(item->photoFileName))) { - case PA_FORMAT_BMP: - mir_snwprintf(szFilter, L"BMP %s (*.bmp)%c*.BMP", TranslateT("format"), 0); - break; - - case PA_FORMAT_GIF: - mir_snwprintf(szFilter, L"GIF %s (*.gif)%c*.GIF", TranslateT("format"), 0); - break; - - case PA_FORMAT_JPEG: - mir_snwprintf(szFilter, L"JPEG %s (*.jpg;*.jpeg)%c*.JPG;*.JPEG", TranslateT("format"), 0); - break; - - default: - mir_snwprintf(szFilter, L"%s (*.*)%c*.*", TranslateT("Unknown format"), 0); - } - - wchar_t szFileName[MAX_PATH]; szFileName[0] = '\0'; - OPENFILENAME ofn = { 0 }; - ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400; - ofn.hwndOwner = m_hwnd; - ofn.lpstrFilter = szFilter; - ofn.lpstrFile = szFileName; - ofn.nMaxFile = _MAX_PATH; - ofn.Flags = OFN_OVERWRITEPROMPT; - if (GetSaveFileName(&ofn)) { - ppro->debugLogW(L"File selected is %s", szFileName); - CopyFile(Utf2T(item->photoFileName), szFileName, FALSE); - } - } - - INT_PTR OnPaint(UINT, WPARAM, LPARAM) - { - if (!ppro->m_bJabberOnline) - SetDlgItemText(m_hwnd, IDC_CANVAS, TranslateT("")); - else if (!hBitmap) - SetDlgItemText(m_hwnd, IDC_CANVAS, TranslateT("")); - else { - BITMAP bm; - POINT ptSize, ptOrg, pt, ptFitSize; - RECT rect; - - SetDlgItemTextA(m_hwnd, IDC_CANVAS, ""); - HWND hwndCanvas = GetDlgItem(m_hwnd, IDC_CANVAS); - HDC hdcCanvas = GetDC(hwndCanvas); - HDC hdcMem = CreateCompatibleDC(hdcCanvas); - SelectObject(hdcMem, hBitmap); - SetMapMode(hdcMem, GetMapMode(hdcCanvas)); - GetObject(hBitmap, sizeof(BITMAP), (LPVOID)&bm); - ptSize.x = bm.bmWidth; - ptSize.y = bm.bmHeight; - DPtoLP(hdcCanvas, &ptSize, 1); - ptOrg.x = ptOrg.y = 0; - DPtoLP(hdcMem, &ptOrg, 1); - GetClientRect(hwndCanvas, &rect); - InvalidateRect(hwndCanvas, nullptr, TRUE); - UpdateWindow(hwndCanvas); - if (ptSize.x <= rect.right && ptSize.y <= rect.bottom) { - pt.x = (rect.right - ptSize.x) / 2; - pt.y = (rect.bottom - ptSize.y) / 2; - ptFitSize = ptSize; - } - else { - if (((float)(ptSize.x - rect.right)) / ptSize.x > ((float)(ptSize.y - rect.bottom)) / ptSize.y) { - ptFitSize.x = rect.right; - ptFitSize.y = (ptSize.y * rect.right) / ptSize.x; - pt.x = 0; - pt.y = (rect.bottom - ptFitSize.y) / 2; - } - else { - ptFitSize.x = (ptSize.x * rect.bottom) / ptSize.y; - ptFitSize.y = rect.bottom; - pt.x = (rect.right - ptFitSize.x) / 2; - pt.y = 0; - } - } - - RECT rc; - if (IsThemeActive()) { - GetClientRect(hwndCanvas, &rc); - DrawThemeParentBackground(hwndCanvas, hdcCanvas, &rc); - } - else { - GetClientRect(hwndCanvas, &rc); - FillRect(hdcCanvas, &rc, (HBRUSH)GetSysColorBrush(COLOR_BTNFACE)); - } - - if (bm.bmBitsPixel == 32) { - BLENDFUNCTION bf = { 0 }; - bf.AlphaFormat = AC_SRC_ALPHA; - bf.BlendOp = AC_SRC_OVER; - bf.SourceConstantAlpha = 255; - GdiAlphaBlend(hdcCanvas, pt.x, pt.y, ptFitSize.x, ptFitSize.y, hdcMem, ptOrg.x, ptOrg.y, ptSize.x, ptSize.y, bf); - } - else { - SetStretchBltMode(hdcCanvas, COLORONCOLOR); - StretchBlt(hdcCanvas, pt.x, pt.y, ptFitSize.x, ptFitSize.y, hdcMem, ptOrg.x, ptOrg.y, ptSize.x, ptSize.y, SRCCOPY); - } - - DeleteDC(hdcMem); - } - return FALSE; - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// JabberUserPhotoDlgProc - Jabber photo dialog - -static int EnumOwnSessions(const char *szSetting, void *param) -{ - auto *pList = (LIST*)param; - - if (!memcmp(szSetting, omemo::DevicePrefix, sizeof(omemo::DevicePrefix)-1)) - if (szSetting[sizeof(omemo::DevicePrefix)] != 0) - pList->insert(mir_strdup(szSetting)); - - return 0; -} - -static int EnumOmemoSessions(const char *szSetting, void *param) -{ - auto *pList = (LIST*)param; - - if (!memcmp(szSetting, omemo::IdentityPrefix, sizeof(omemo::IdentityPrefix) - 1)) - pList->insert(mir_strdup(szSetting + sizeof(omemo::IdentityPrefix))); - - return 0; -} - -class JabberUserOmemoDlg : public JabberBaseUserInfoDlg -{ - CCtrlListView m_list; - - void AddListItem(const CMStringA &pszStr1, const wchar_t *pszStr2, const CMStringA &pszStr3) - { - LVITEM lvi = {}; - lvi.mask = LVIF_TEXT; - lvi.pszText = mir_a2u(pszStr1); - lvi.iItem = 100500; - int idx = m_list.InsertItem(&lvi); - mir_free(lvi.pszText); - - m_list.SetItemText(idx, 1, (wchar_t *)pszStr2); - CMStringW fp = omemo::FormatFingerprint(pszStr3); - m_list.SetItemText(idx, 2, fp.GetBuffer()); - } - -public: - JabberUserOmemoDlg(CJabberProto *_ppro) : - JabberBaseUserInfoDlg(_ppro, IDD_INFO_OMEMO), - m_list(this, IDC_LIST) - { - } - - bool OnInitDialog() override - { - LV_COLUMN lvc = {}; - lvc.mask = LVCF_TEXT | LVCF_WIDTH; - - lvc.cx = 90; - lvc.pszText = TranslateT("Device ID"); - m_list.InsertColumn(1, &lvc); - - lvc.cx = 80; - lvc.pszText = TranslateT("Status"); - m_list.InsertColumn(2, &lvc); - - lvc.cx = 550; - lvc.pszText = TranslateT("Fingerprint"); - m_list.InsertColumn(3, &lvc); - - if (m_hContact == 0) - OnRefresh(); - return true; - } - - int Resizer(UTILRESIZECONTROL*) override - { - return RD_ANCHORX_WIDTH | RD_ANCHORY_HEIGHT; - } - - bool OnRefresh() override - { - m_list.DeleteAllItems(); - - if (m_hContact == 0) { - // GetOwnDeviceId() creates own keys if they don't exist - CMStringA str1(FORMAT, "%d", ppro->m_omemo.GetOwnDeviceId()); - CMStringA str2(ppro->getMStringA("OmemoFingerprintOwn")); - AddListItem(str1, TranslateT("Own device"), str2); - } - - for (int i = 0;; i++) { - CMStringA szSetting(FORMAT, "%s%d", omemo::DevicePrefix, i); - uint32_t device_id = ppro->getDword(m_hContact, szSetting, 0); - if (device_id == 0) - break; - - char *jiddev = ppro->getStringA(m_hContact, "jid"); - if (jiddev == 0) - continue; - - size_t len = strlen(jiddev); - jiddev = (char *)mir_realloc(jiddev, len + sizeof(int32_t)); - memcpy(jiddev + len, &device_id, sizeof(int32_t)); - - szSetting = omemo::IdentityPrefix; - szSetting.Append(ptrA(mir_base64_encode(jiddev, len + sizeof(int32_t)))); - mir_free(jiddev); - - const wchar_t *pwszStatus = L""; - CMStringA fp_hex; - DBVARIANT dbv = { 0 }; - dbv.type = DBVT_BLOB; - db_get(m_hContact, ppro->m_szModuleName, szSetting, &dbv); - if (dbv.cpbVal == 33) { - fp_hex.Truncate(33 * 2); - bin2hex(dbv.pbVal, 33, fp_hex.GetBuffer()); - uint8_t trusted = ppro->getByte(m_hContact, "OmemoFingerprintTrusted_" + fp_hex); - pwszStatus = trusted ? TranslateT("Trusted") : TranslateT("UNTRUSTED"); - //TODO: 3 states Trusted, Untrusted, TOFU - } - else if (dbv.cpbVal) - pwszStatus = TranslateT("Unknown"); - - db_free(&dbv); - - AddListItem(CMStringA(FORMAT, "%d", device_id), pwszStatus, fp_hex); - } - return false; - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// OnInfoInit - initializes user info option dialogs - -int CJabberProto::OnUserInfoInit(WPARAM wParam, LPARAM hContact) -{ - if (!Proto_GetAccount(m_szModuleName)) - return 0; - - if (hContact == 0) { - // Show our vcard - OnUserInfoInit_VCard(wParam, hContact); - return 0; - } - - char *szProto = Proto_GetBaseAccountName(hContact); - if (szProto != nullptr && !mir_strcmp(szProto, m_szModuleName)) { - USERINFOPAGE uip = {}; - uip.dwInitParam = (LPARAM)this; - uip.flags = ODPF_UNICODE | ODPF_USERINFOTAB | ODPF_ICON; - uip.szGroup.w = m_tszUserName; - uip.dwInitParam = (LPARAM)Skin_GetProtoIcon(m_szModuleName, ID_STATUS_ONLINE); - - uip.pDialog = new JabberUserInfoDlg(this); - uip.position = -2000000000; - uip.szTitle.w = LPGENW("Account"); - g_plugin.addUserInfo(wParam, &uip); - - uip.pDialog = new JabberUserPhotoDlg(this); - uip.position = 2000000000; - uip.szTitle.w = LPGENW("Photo"); - g_plugin.addUserInfo(wParam, &uip); - - CheckOmemoUserInfo(wParam, uip); - } - - return 0; -} - -void CJabberProto::CheckOmemoUserInfo(WPARAM wParam, USERINFOPAGE &uip) -{ - if (m_bUseOMEMO) { - uip.pDialog = new JabberUserOmemoDlg(this); - uip.position = 2000000001; - uip.szTitle.w = L"OMEMO"; - g_plugin.addUserInfo(wParam, &uip); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// -// JabberUserInfoUpdate - -void JabberUserInfoInit() -{ - hUserInfoList = WindowList_Create(); -} - -void JabberUserInfoUninit() -{ - WindowList_Destroy(hUserInfoList); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// JabberUserInfoUpdate - -void JabberUserInfoUpdate(MCONTACT hContact) -{ - if (!hContact) - WindowList_BroadcastAsync(hUserInfoList, WM_PROTO_REFRESH, 0, 0); - else if (HWND hwnd = WindowList_Find(hUserInfoList, hContact)) - PostMessage(hwnd, WM_PROTO_REFRESH, 0, 0); -} +/* + +Jabber Protocol Plugin for Miranda NG + +Copyright (c) 2002-04 Santithorn Bunchua +Copyright (c) 2005-12 George Hazan +Copyright (c) 2007 Maxim Mluhov +Copyright (C) 2012-23 Miranda NG team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +*/ + +#include "stdafx.h" + +#include +#include +#include + +#include "jabber_list.h" + +static MWindowList hUserInfoList = nullptr; + +class JabberBaseUserInfoDlg : public CUserInfoPageDlg +{ + INT_PTR DoRefresh(UINT, WPARAM, LPARAM) + { + OnRefresh(); + return 0; + } + +protected: + UI_MESSAGE_MAP(JabberBaseUserInfoDlg, CUserInfoPageDlg); + UI_MESSAGE(WM_PROTO_REFRESH, DoRefresh); + UI_MESSAGE(WM_JABBER_REFRESH_VCARD, DoRefresh); + UI_MESSAGE_MAP_END(); + + CJabberProto *ppro; + + JabberBaseUserInfoDlg(CJabberProto *_ppro, int dlgId) : + CUserInfoPageDlg(g_plugin, dlgId), + ppro(_ppro) + {} +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// JabberUserInfoDlgProc - main user info dialog + +enum +{ + INFOLINE_DELETE = 0x80000000, + INFOLINE_MASK = 0x7fffffff, + INFOLINE_BAD_ID = 0x7fffffff, + + INFOLINE_NAME = 1, + INFOLINE_MOOD, + INFOLINE_ACTIVITY, + INFOLINE_TUNE, + INFOLINE_OFFLINE, + INFOLINE_MESSAGE, + INFOLINE_SOFTWARE, + INFOLINE_VERSION, + INFOLINE_SYSTEM, + INFOLINE_PRIORITY, + INFOLINE_IDLE, + INFOLINE_CAPS, + INFOLINE_SOFTWARE_INFORMATION, + INFOLINE_SUBSCRIPTION, + INFOLINE_LOGOFF, + INFOLINE_LOGOFF_MSG, + INFOLINE_LASTACTIVE, +}; + +__forceinline uint32_t sttInfoLineId(uint32_t res, uint32_t type, uint32_t line = 0) +{ + return + (type << 24) & 0x7f000000 | + (res << 12) & 0x00fff000 | + (line) & 0x00000fff; +} + +class JabberUserInfoDlg : public JabberBaseUserInfoDlg +{ + JABBER_LIST_ITEM *item = nullptr; + int resourcesCount = -1; + + CCtrlTreeView m_tree; + + UI_MESSAGE_MAP(JabberUserInfoDlg, JabberBaseUserInfoDlg); + UI_MESSAGE(WM_PROTO_CHECK_ONLINE, OnCheckOnline); + UI_MESSAGE_MAP_END(); + + INT_PTR OnCheckOnline(UINT, WPARAM, LPARAM) + { + if (!ppro->m_bJabberOnline) + item = nullptr; + else + OnRefresh(); + return 0; + } + + //////////////////////////////////////////////////////////////////////////////////////// + // User information block + + void CleanupInfo(int stage) + { + HTREEITEM hItem = m_tree.GetRoot(); + while (hItem) { + TVITEMEX tvi = { 0 }; + tvi.mask = TVIF_HANDLE | TVIF_PARAM; + tvi.hItem = hItem; + m_tree.GetItem(&tvi); + + switch (stage) { + case 0: + tvi.lParam |= INFOLINE_DELETE; + m_tree.SetItem(&tvi); + break; + + case 1: + if (tvi.lParam & INFOLINE_DELETE) { + hItem = m_tree.GetNextSibling(hItem); + m_tree.DeleteItem(tvi.hItem); + continue; + } + break; + } + + HTREEITEM hItemTmp = nullptr; + if (hItemTmp = m_tree.GetChild(hItem)) + hItem = hItemTmp; + else if (hItemTmp = m_tree.GetNextSibling(hItem)) + hItem = hItemTmp; + else { + while (true) { + if (!(hItem = m_tree.GetParent(hItem))) break; + if (hItemTmp = m_tree.GetNextSibling(hItem)) { + hItem = hItemTmp; + break; + } + } + } + } + } + + HTREEITEM FindInfoLine(HTREEITEM htiRoot, LPARAM id = INFOLINE_BAD_ID) + { + if (id == INFOLINE_BAD_ID) return nullptr; + for (HTREEITEM hti = m_tree.GetChild(htiRoot); hti; hti = m_tree.GetNextSibling(hti)) { + TVITEMEX tvi = { 0 }; + tvi.mask = TVIF_HANDLE | TVIF_PARAM; + tvi.hItem = hti; + m_tree.GetItem(&tvi); + if ((tvi.lParam&INFOLINE_MASK) == (id&INFOLINE_MASK)) + return hti; + } + return nullptr; + } + + HTREEITEM FillInfoLine(HTREEITEM htiRoot, HICON hIcon, const wchar_t *title, const char *value, LPARAM id = INFOLINE_BAD_ID, bool expand = false) + { + HTREEITEM hti = FindInfoLine(htiRoot, id); + + Utf2T wszValue(value); + const wchar_t *pwszValue = (value == nullptr) ? TranslateT("") : wszValue; + wchar_t buf[256]; + if (title) + mir_snwprintf(buf, L"%s: %s", title, pwszValue); + else + mir_wstrncpy(buf, pwszValue, _countof(buf)); + + TVINSERTSTRUCT tvis = {}; + tvis.hParent = htiRoot; + tvis.hInsertAfter = TVI_LAST; + tvis.itemex.mask = TVIF_TEXT | TVIF_PARAM; + tvis.itemex.pszText = buf; + tvis.itemex.lParam = id; + + if (hIcon) { + HIMAGELIST himl = m_tree.GetImageList(TVSIL_NORMAL); + tvis.itemex.mask |= TVIF_IMAGE | TVIF_SELECTEDIMAGE; + tvis.itemex.iImage = + tvis.itemex.iSelectedImage = ImageList_AddIcon(himl, hIcon); + IcoLib_ReleaseIcon(hIcon); + } + + if (hti) { + tvis.itemex.mask |= TVIF_HANDLE; + tvis.itemex.hItem = hti; + m_tree.SetItem(&tvis.itemex); + } + else { + tvis.itemex.mask |= TVIF_STATE; + tvis.itemex.stateMask = TVIS_EXPANDED; + tvis.itemex.state = expand ? TVIS_EXPANDED : 0; + hti = m_tree.InsertItem(&tvis); + } + + return hti; + } + + void FillAdvStatusInfo(HTREEITEM htiRoot, uint32_t dwInfoLine, MCONTACT hContact, wchar_t *szTitle, char *pszSlot) + { + ptrA szAdvStatusIcon(ppro->ReadAdvStatusA(hContact, pszSlot, ADVSTATUS_VAL_ICON)); + ptrW szAdvStatusTitle(ppro->ReadAdvStatusT(hContact, pszSlot, ADVSTATUS_VAL_TITLE)); + ptrW szAdvStatusText(ppro->ReadAdvStatusT(hContact, pszSlot, ADVSTATUS_VAL_TEXT)); + + if (szAdvStatusIcon && szAdvStatusTitle && *szAdvStatusTitle) { + wchar_t szText[2048]; + if (szAdvStatusText && *szAdvStatusText) + mir_snwprintf(szText, L"%s (%s)", TranslateW(szAdvStatusTitle), szAdvStatusText.get()); + else + wcsncpy_s(szText, TranslateW(szAdvStatusTitle), _TRUNCATE); + FillInfoLine(htiRoot, IcoLib_GetIcon(szAdvStatusIcon), szTitle, T2Utf(szText), dwInfoLine); + } + } + + void FillResourceInfo(HTREEITEM htiRoot, int resource) + { + HTREEITEM htiResource = htiRoot; + pResourceStatus r = resource ? item->arResources[resource - 1] : item->getTemp(); + + if (r->m_szResourceName && *r->m_szResourceName) + htiResource = FillInfoLine(htiRoot, Skin_LoadProtoIcon(ppro->m_szModuleName, r->m_iStatus), + TranslateT("Resource"), r->m_szResourceName, sttInfoLineId(resource, INFOLINE_NAME), true); + + // StatusMsg + FillInfoLine(htiResource, nullptr /*Skin_LoadIcon(SKINICON_EVENT_MESSAGE)*/, + TranslateT("Message"), r->m_szStatusMessage, + sttInfoLineId(resource, INFOLINE_MESSAGE)); + + // Software + if (CJabberClientPartialCaps *pCaps = r->m_pCaps) { + HICON hIcon = nullptr; + + if (ServiceExists(MS_FP_GETCLIENTICONT)) { + if (pCaps->GetSoft()) { + wchar_t buf[256]; + mir_snwprintf(buf, L"%s %s", pCaps->GetSoft(), pCaps->GetSoftVer()); + hIcon = Finger_GetClientIcon(buf, 0); + } + } + + FillInfoLine(htiResource, hIcon, TranslateT("Software"), pCaps->GetSoft(), sttInfoLineId(resource, INFOLINE_SOFTWARE)); + + // Version + FillInfoLine(htiResource, nullptr, TranslateT("Version"), pCaps->GetSoftMir() ? pCaps->GetSoftMir() : pCaps->GetSoftVer(), sttInfoLineId(resource, INFOLINE_VERSION)); + + // System + FillInfoLine(htiResource, nullptr, TranslateT("System"), pCaps->GetOsVer() ? pCaps->GetOsVer() : pCaps->GetOs(), sttInfoLineId(resource, INFOLINE_SYSTEM)); + + if (hIcon) + DestroyIcon(hIcon); + } + + // Resource priority + char buf[256]; + itoa(r->m_iPriority, buf, 10); + FillInfoLine(htiResource, nullptr, TranslateT("Resource priority"), buf, sttInfoLineId(resource, INFOLINE_PRIORITY)); + + // Idle + if (r->m_dwIdleStartTime != -1) { + if (r->m_dwIdleStartTime != 0) { + mir_strncpy(buf, ctime(&r->m_dwIdleStartTime), _countof(buf)); + size_t len = mir_strlen(buf); + if (len > 0) + buf[len - 1] = 0; + } + else mir_strncpy(buf, TranslateU(""), _countof(buf)); + + FillInfoLine(htiResource, nullptr, TranslateT("Last activity"), buf, sttInfoLineId(resource, INFOLINE_IDLE)); + } + + // caps + JabberCapsBits jcb = ppro->GetResourceCapabilities(MakeJid(item->jid, r->m_szResourceName), r); + if (!(jcb & JABBER_RESOURCE_CAPS_ERROR)) { + HTREEITEM htiCaps = FillInfoLine(htiResource, IcoLib_GetIconByHandle(ppro->m_hProtoIcon), nullptr, TranslateU("Client capabilities"), sttInfoLineId(resource, INFOLINE_CAPS)); + int i; + for (i = 0; i < g_cJabberFeatCapPairs; i++) + if (jcb & g_JabberFeatCapPairs[i].jcbCap) { + char szDescription[1024]; + if (g_JabberFeatCapPairs[i].tszDescription) + mir_snprintf(szDescription, "%s (%s)", TranslateU(g_JabberFeatCapPairs[i].tszDescription), g_JabberFeatCapPairs[i].szFeature); + else + strncpy_s(szDescription, g_JabberFeatCapPairs[i].szFeature, _TRUNCATE); + FillInfoLine(htiCaps, nullptr, nullptr, szDescription, sttInfoLineId(resource, INFOLINE_CAPS, i)); + } + + for (auto &it : ppro->m_lstJabberFeatCapPairsDynamic) { + if (jcb & it->jcbCap) { + char szDescription[1024]; + if (it->szDescription) + mir_snprintf(szDescription, "%s (%s)", TranslateU(it->szDescription), it->szFeature); + else + strncpy_s(szDescription, it->szFeature, _TRUNCATE); + FillInfoLine(htiCaps, nullptr, nullptr, szDescription, sttInfoLineId(resource, INFOLINE_CAPS, i++)); + } + } + } + + // Software info + HTREEITEM htiSoftwareInfo = FillInfoLine(htiResource, IcoLib_GetIconByHandle(ppro->m_hProtoIcon), nullptr, TranslateU("Software information"), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION)); + int nLineId = 0; + if (CJabberClientPartialCaps *pCaps = r->m_pCaps) { + if (pCaps->GetOs()) + FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Operating system"), pCaps->GetOs(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); + if (pCaps->GetOsVer()) + FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Operating system version"), pCaps->GetOsVer(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); + if (pCaps->GetSoft()) + FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Software"), pCaps->GetSoft(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); + if (pCaps->GetSoftVer()) + FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Software version"), pCaps->GetSoftVer(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); + if (pCaps->GetSoftMir()) + FillInfoLine(htiSoftwareInfo, nullptr, TranslateT("Miranda core version"), pCaps->GetSoftMir(), sttInfoLineId(resource, INFOLINE_SOFTWARE_INFORMATION, nLineId++)); + } + } + + void FillUserInfo() + { + m_tree.SendMsg(WM_SETREDRAW, FALSE, 0); + + CleanupInfo(0); + + HTREEITEM htiRoot = FillInfoLine(nullptr, IcoLib_GetIconByHandle(ppro->m_hProtoIcon), L"JID", item->jid, sttInfoLineId(0, INFOLINE_NAME), true); + + if (MCONTACT hContact = ppro->HContactFromJID(item->jid)) { + FillAdvStatusInfo(htiRoot, sttInfoLineId(0, INFOLINE_MOOD), hContact, TranslateT("Mood"), ADVSTATUS_MOOD); + FillAdvStatusInfo(htiRoot, sttInfoLineId(0, INFOLINE_ACTIVITY), hContact, TranslateT("Activity"), ADVSTATUS_ACTIVITY); + FillAdvStatusInfo(htiRoot, sttInfoLineId(0, INFOLINE_TUNE), hContact, TranslateT("Tune"), ADVSTATUS_TUNE); + } + + // subscription + switch (item->subscription) { + case SUB_BOTH: + FillInfoLine(htiRoot, nullptr, TranslateT("Subscription"), TranslateU("both"), sttInfoLineId(0, INFOLINE_SUBSCRIPTION)); + break; + case SUB_TO: + FillInfoLine(htiRoot, nullptr, TranslateT("Subscription"), TranslateU("to"), sttInfoLineId(0, INFOLINE_SUBSCRIPTION)); + break; + case SUB_FROM: + FillInfoLine(htiRoot, nullptr, TranslateT("Subscription"), TranslateU("from"), sttInfoLineId(0, INFOLINE_SUBSCRIPTION)); + break; + default: + FillInfoLine(htiRoot, nullptr, TranslateT("Subscription"), TranslateU("none"), sttInfoLineId(0, INFOLINE_SUBSCRIPTION)); + break; + } + + // logoff + char buf[256]; + JABBER_RESOURCE_STATUS *r = item->getTemp(); + if (r->m_dwIdleStartTime != -1) { + if (r->m_dwIdleStartTime > 0) { + mir_strncpy(buf, ctime(&r->m_dwIdleStartTime), _countof(buf)); + size_t len = mir_strlen(buf); + if (len > 0) + buf[len - 1] = 0; + } + else mir_strncpy(buf, TranslateU(""), _countof(buf)); + + FillInfoLine(htiRoot, nullptr, + (item->jid && strchr(item->jid, '@')) ? TranslateT("Last logoff time") : TranslateT("Uptime"), buf, + sttInfoLineId(0, INFOLINE_LOGOFF)); + } + + if (r->m_szStatusMessage) + FillInfoLine(htiRoot, nullptr, TranslateT("Logoff message"), r->m_szStatusMessage, sttInfoLineId(0, INFOLINE_LOGOFF_MSG)); + + // activity + if (item->m_pLastSeenResource) + mir_strncpy(buf, item->m_pLastSeenResource->m_szResourceName, _countof(buf)); + else + mir_strncpy(buf, TranslateU(""), _countof(buf)); + FillInfoLine(htiRoot, nullptr, TranslateT("Last active resource"), buf, sttInfoLineId(0, INFOLINE_LASTACTIVE)); + + // resources + if (item->arResources.getCount()) { + for (int i = 0; i < item->arResources.getCount(); i++) + FillResourceInfo(htiRoot, i + 1); + } + else if (!strchr(item->jid, '@') || (r->m_iStatus != ID_STATUS_OFFLINE)) + FillResourceInfo(htiRoot, 0); + + CleanupInfo(1); + m_tree.SendMsg(WM_SETREDRAW, TRUE, 0); + + RedrawWindow(m_tree.GetHwnd(), nullptr, nullptr, RDW_INVALIDATE); + } + +public: + JabberUserInfoDlg(CJabberProto *_ppro) : + JabberBaseUserInfoDlg(_ppro, IDD_INFO_JABBER), + m_tree(this, IDC_TV_INFO) + { + m_tree.OnBuildMenu = Callback(this, &JabberUserInfoDlg::onMenu_Tree); + } + + bool OnInitDialog() override + { + ppro->WindowSubscribe(m_hwnd); + Window_SetSkinIcon_IcoLib(m_hwnd, SKINICON_OTHER_USERDETAILS); + + RECT rc; + GetClientRect(m_hwnd, &rc); + MoveWindow(m_tree.GetHwnd(), 5, 5, rc.right - 10, rc.bottom - 10, TRUE); + + HIMAGELIST himl = ImageList_Create(GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), ILC_COLOR | ILC_COLOR32 | ILC_MASK, 5, 1); + ImageList_AddSkinIcon(himl, SKINICON_OTHER_SMALLDOT); + TreeView_SetImageList(m_tree.GetHwnd(), himl, TVSIL_NORMAL); + + WindowList_Add(hUserInfoList, m_hwnd, m_hContact); + return true; + } + + void OnDestroy() override + { + ppro->WindowUnsubscribe(m_hwnd); + WindowList_Remove(hUserInfoList, m_hwnd); + ImageList_Destroy(m_tree.SetImageList(nullptr, TVSIL_NORMAL)); + Window_FreeIcon_IcoLib(m_hwnd); + } + + int Resizer(UTILRESIZECONTROL*) override + { + return RD_ANCHORX_WIDTH | RD_ANCHORY_HEIGHT; + } + + bool OnRefresh() override + { + if (item == nullptr) { + ptrA jid(ppro->getUStringA(m_hContact, "jid")); + if (jid == nullptr) + return false; + + if (ppro->m_bJabberOnline) + if (!(item = ppro->ListGetItemPtr(LIST_VCARD_TEMP, jid))) + item = ppro->ListGetItemPtr(LIST_ROSTER, jid); + + if (item == nullptr) { + m_tree.DeleteAllItems(); + HTREEITEM htiRoot = FillInfoLine(nullptr, IcoLib_GetIconByHandle(ppro->m_hProtoIcon), L"JID", jid, sttInfoLineId(0, INFOLINE_NAME), true); + FillInfoLine(htiRoot, g_plugin.getIcon(IDI_VCARD), nullptr, TranslateU("Please switch online to see more details.")); + return false; + } + } + FillUserInfo(); + return false; + } + + //////////////////////////////////////////////////////////////////////////////////////// + // Context menu + + void GetNodeText(HTREEITEM hti, CMStringW &buf, int indent = 0) + { + for (int i = 0; i < indent; i++) + buf.AppendChar('\t'); + + wchar_t wszText[256]; + TVITEMEX tvi = {}; + tvi.mask = TVIF_HANDLE | TVIF_TEXT | TVIF_STATE; + tvi.hItem = hti; + tvi.cchTextMax = _countof(wszText); + tvi.pszText = wszText; + if (!m_tree.GetItem(&tvi)) // failure, maybe item was removed... + return; + + buf.Append(wszText); + buf.Append(L"\r\n"); + + if (tvi.state & TVIS_EXPANDED) + for (hti = m_tree.GetChild(hti); hti; hti = m_tree.GetNextSibling(hti)) + GetNodeText(hti, buf, indent + 1); + } + + void onMenu_Tree(CContextMenuPos *pos) + { + if (!pos->hItem) + return; + + HMENU hMenu = CreatePopupMenu(); + AppendMenu(hMenu, MF_STRING, (UINT_PTR)1, TranslateT("Copy")); + AppendMenu(hMenu, MF_STRING, (UINT_PTR)2, TranslateT("Copy only this value")); + AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr); + AppendMenu(hMenu, MF_STRING, (UINT_PTR)0, TranslateT("Cancel")); + int nReturnCmd = TrackPopupMenu(hMenu, TPM_RETURNCMD, pos->pt.x, pos->pt.y, 0, m_hwnd, nullptr); + if (nReturnCmd == 1) { + CMStringW buf; + GetNodeText(pos->hItem, buf); + Utils_ClipboardCopy(buf); + } + else if (nReturnCmd == 2) { + wchar_t szBuffer[1024]; + TVITEMEX tvi = { 0 }; + tvi.mask = TVIF_HANDLE | TVIF_TEXT | TVIF_STATE; + tvi.hItem = pos->hItem; + tvi.cchTextMax = _countof(szBuffer); + tvi.pszText = szBuffer; + if (m_tree.GetItem(&tvi)) { + if (wchar_t *str = wcsstr(szBuffer, L": ")) + Utils_ClipboardCopy(str + 2); + else + Utils_ClipboardCopy(szBuffer); + } + } + DestroyMenu(hMenu); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// JabberUserPhotoDlgProc - Jabber photo dialog + +class JabberUserPhotoDlg : public JabberBaseUserInfoDlg +{ + HBITMAP hBitmap = nullptr; + + CCtrlMButton btnSave; + + UI_MESSAGE_MAP(JabberUserInfoDlg, JabberBaseUserInfoDlg); + UI_MESSAGE(WM_PAINT, OnPaint); + UI_MESSAGE_MAP_END(); + + char *GetFileName() const + { + ptrA jid(ppro->getUStringA(m_hContact, "jid")); + if (jid != nullptr) { + JABBER_LIST_ITEM *item = ppro->ListGetItemPtr(LIST_VCARD_TEMP, jid); + if (item == nullptr) + item = ppro->ListGetItemPtr(LIST_ROSTER, jid); + if (item != nullptr) + return item->photoFileName; + } + + return nullptr; + } + +public: + JabberUserPhotoDlg(CJabberProto *_ppro) : + JabberBaseUserInfoDlg(_ppro, IDD_VCARD_PHOTO), + btnSave(this, IDC_SAVE, g_plugin.getIcon(IDI_SAVE), LPGEN("Save")) + { + btnSave.OnClick = Callback(this, &JabberUserPhotoDlg::onClick_Save); + } + + bool OnInitDialog() override + { + ShowWindow(GetDlgItem(m_hwnd, IDC_LOAD), SW_HIDE); + ShowWindow(GetDlgItem(m_hwnd, IDC_DELETE), SW_HIDE); + return true; + } + + void OnDestroy() override + { + if (hBitmap) { + ppro->debugLogA("Delete bitmap"); + DeleteObject(hBitmap); + } + } + + bool IsEmpty() const override + { + return mir_strlen(GetFileName()) == 0; + } + + bool OnRefresh() override + { + if (hBitmap) { + DeleteObject(hBitmap); + hBitmap = nullptr; + } + btnSave.Hide(); + + char *pszFileName = GetFileName(); + if (mir_strlen(pszFileName)) { + ppro->debugLogA("Showing picture from %s", pszFileName); + hBitmap = Bitmap_Load(Utf2T(pszFileName)); + FreeImage_Premultiply(hBitmap); + btnSave.Show(); + } + + InvalidateRect(m_hwnd, nullptr, TRUE); + UpdateWindow(m_hwnd); + return true; + } + + void onClick_Save(CCtrlButton *) + { + wchar_t szFilter[512]; + + ptrA jid(ppro->getUStringA(m_hContact, "jid")); + if (jid == nullptr) + return; + + JABBER_LIST_ITEM *item = ppro->ListGetItemPtr(LIST_VCARD_TEMP, jid); + if (item == nullptr) + if ((item = ppro->ListGetItemPtr(LIST_ROSTER, jid)) == nullptr) + return; + + switch (ProtoGetAvatarFileFormat(Utf2T(item->photoFileName))) { + case PA_FORMAT_BMP: + mir_snwprintf(szFilter, L"BMP %s (*.bmp)%c*.BMP", TranslateT("format"), 0); + break; + + case PA_FORMAT_GIF: + mir_snwprintf(szFilter, L"GIF %s (*.gif)%c*.GIF", TranslateT("format"), 0); + break; + + case PA_FORMAT_JPEG: + mir_snwprintf(szFilter, L"JPEG %s (*.jpg;*.jpeg)%c*.JPG;*.JPEG", TranslateT("format"), 0); + break; + + default: + mir_snwprintf(szFilter, L"%s (*.*)%c*.*", TranslateT("Unknown format"), 0); + } + + wchar_t szFileName[MAX_PATH]; szFileName[0] = '\0'; + OPENFILENAME ofn = { 0 }; + ofn.lStructSize = OPENFILENAME_SIZE_VERSION_400; + ofn.hwndOwner = m_hwnd; + ofn.lpstrFilter = szFilter; + ofn.lpstrFile = szFileName; + ofn.nMaxFile = _MAX_PATH; + ofn.Flags = OFN_OVERWRITEPROMPT; + if (GetSaveFileName(&ofn)) { + ppro->debugLogW(L"File selected is %s", szFileName); + CopyFile(Utf2T(item->photoFileName), szFileName, FALSE); + } + } + + INT_PTR OnPaint(UINT, WPARAM, LPARAM) + { + if (!ppro->m_bJabberOnline) + SetDlgItemText(m_hwnd, IDC_CANVAS, TranslateT("")); + else if (!hBitmap) + SetDlgItemText(m_hwnd, IDC_CANVAS, TranslateT("")); + else { + BITMAP bm; + POINT ptSize, ptOrg, pt, ptFitSize; + RECT rect; + + SetDlgItemTextA(m_hwnd, IDC_CANVAS, ""); + HWND hwndCanvas = GetDlgItem(m_hwnd, IDC_CANVAS); + HDC hdcCanvas = GetDC(hwndCanvas); + HDC hdcMem = CreateCompatibleDC(hdcCanvas); + SelectObject(hdcMem, hBitmap); + SetMapMode(hdcMem, GetMapMode(hdcCanvas)); + GetObject(hBitmap, sizeof(BITMAP), (LPVOID)&bm); + ptSize.x = bm.bmWidth; + ptSize.y = bm.bmHeight; + DPtoLP(hdcCanvas, &ptSize, 1); + ptOrg.x = ptOrg.y = 0; + DPtoLP(hdcMem, &ptOrg, 1); + GetClientRect(hwndCanvas, &rect); + InvalidateRect(hwndCanvas, nullptr, TRUE); + UpdateWindow(hwndCanvas); + if (ptSize.x <= rect.right && ptSize.y <= rect.bottom) { + pt.x = (rect.right - ptSize.x) / 2; + pt.y = (rect.bottom - ptSize.y) / 2; + ptFitSize = ptSize; + } + else { + if (((float)(ptSize.x - rect.right)) / ptSize.x > ((float)(ptSize.y - rect.bottom)) / ptSize.y) { + ptFitSize.x = rect.right; + ptFitSize.y = (ptSize.y * rect.right) / ptSize.x; + pt.x = 0; + pt.y = (rect.bottom - ptFitSize.y) / 2; + } + else { + ptFitSize.x = (ptSize.x * rect.bottom) / ptSize.y; + ptFitSize.y = rect.bottom; + pt.x = (rect.right - ptFitSize.x) / 2; + pt.y = 0; + } + } + + RECT rc; + if (IsThemeActive()) { + GetClientRect(hwndCanvas, &rc); + DrawThemeParentBackground(hwndCanvas, hdcCanvas, &rc); + } + else { + GetClientRect(hwndCanvas, &rc); + FillRect(hdcCanvas, &rc, (HBRUSH)GetSysColorBrush(COLOR_BTNFACE)); + } + + if (bm.bmBitsPixel == 32) { + BLENDFUNCTION bf = { 0 }; + bf.AlphaFormat = AC_SRC_ALPHA; + bf.BlendOp = AC_SRC_OVER; + bf.SourceConstantAlpha = 255; + GdiAlphaBlend(hdcCanvas, pt.x, pt.y, ptFitSize.x, ptFitSize.y, hdcMem, ptOrg.x, ptOrg.y, ptSize.x, ptSize.y, bf); + } + else { + SetStretchBltMode(hdcCanvas, COLORONCOLOR); + StretchBlt(hdcCanvas, pt.x, pt.y, ptFitSize.x, ptFitSize.y, hdcMem, ptOrg.x, ptOrg.y, ptSize.x, ptSize.y, SRCCOPY); + } + + DeleteDC(hdcMem); + } + return FALSE; + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// JabberUserPhotoDlgProc - Jabber photo dialog + +static int EnumOwnSessions(const char *szSetting, void *param) +{ + auto *pList = (LIST*)param; + + if (!memcmp(szSetting, omemo::DevicePrefix, sizeof(omemo::DevicePrefix)-1)) + if (szSetting[sizeof(omemo::DevicePrefix)] != 0) + pList->insert(mir_strdup(szSetting)); + + return 0; +} + +static int EnumOmemoSessions(const char *szSetting, void *param) +{ + auto *pList = (LIST*)param; + + if (!memcmp(szSetting, omemo::IdentityPrefix, sizeof(omemo::IdentityPrefix) - 1)) + pList->insert(mir_strdup(szSetting + sizeof(omemo::IdentityPrefix))); + + return 0; +} + +class JabberUserOmemoDlg : public JabberBaseUserInfoDlg +{ + CCtrlListView m_list; + + void AddListItem(const CMStringA &pszStr1, const wchar_t *pszStr2, const CMStringA &pszStr3) + { + LVITEM lvi = {}; + lvi.mask = LVIF_TEXT; + lvi.pszText = mir_a2u(pszStr1); + lvi.iItem = 100500; + int idx = m_list.InsertItem(&lvi); + mir_free(lvi.pszText); + + m_list.SetItemText(idx, 1, (wchar_t *)pszStr2); + CMStringW fp = omemo::FormatFingerprint(pszStr3); + m_list.SetItemText(idx, 2, fp.GetBuffer()); + } + +public: + JabberUserOmemoDlg(CJabberProto *_ppro) : + JabberBaseUserInfoDlg(_ppro, IDD_INFO_OMEMO), + m_list(this, IDC_LIST) + { + } + + bool OnInitDialog() override + { + LV_COLUMN lvc = {}; + lvc.mask = LVCF_TEXT | LVCF_WIDTH; + + lvc.cx = 90; + lvc.pszText = TranslateT("Device ID"); + m_list.InsertColumn(1, &lvc); + + lvc.cx = 80; + lvc.pszText = TranslateT("Status"); + m_list.InsertColumn(2, &lvc); + + lvc.cx = 550; + lvc.pszText = TranslateT("Fingerprint"); + m_list.InsertColumn(3, &lvc); + + if (m_hContact == 0) + OnRefresh(); + return true; + } + + int Resizer(UTILRESIZECONTROL*) override + { + return RD_ANCHORX_WIDTH | RD_ANCHORY_HEIGHT; + } + + bool OnRefresh() override + { + m_list.DeleteAllItems(); + + if (m_hContact == 0) { + // GetOwnDeviceId() creates own keys if they don't exist + CMStringA str1(FORMAT, "%d", ppro->m_omemo.GetOwnDeviceId()); + CMStringA str2(ppro->getMStringA("OmemoFingerprintOwn")); + AddListItem(str1, TranslateT("Own device"), str2); + } + + for (int i = 0;; i++) { + CMStringA szSetting(FORMAT, "%s%d", omemo::DevicePrefix, i); + uint32_t device_id = ppro->getDword(m_hContact, szSetting, 0); + if (device_id == 0) + break; + + char *jiddev = ppro->getStringA(m_hContact, "jid"); + if (jiddev == 0) + continue; + + size_t len = strlen(jiddev); + jiddev = (char *)mir_realloc(jiddev, len + sizeof(int32_t)); + memcpy(jiddev + len, &device_id, sizeof(int32_t)); + + szSetting = omemo::IdentityPrefix; + szSetting.Append(ptrA(mir_base64_encode(jiddev, len + sizeof(int32_t)))); + mir_free(jiddev); + + const wchar_t *pwszStatus = L""; + CMStringA fp_hex; + DBVARIANT dbv = { 0 }; + dbv.type = DBVT_BLOB; + db_get(m_hContact, ppro->m_szModuleName, szSetting, &dbv); + if (dbv.cpbVal == 33) { + fp_hex.Truncate(33 * 2); + bin2hex(dbv.pbVal, 33, fp_hex.GetBuffer()); + uint8_t trusted = ppro->getByte(m_hContact, "OmemoFingerprintTrusted_" + fp_hex); + pwszStatus = trusted ? TranslateT("Trusted") : TranslateT("UNTRUSTED"); + //TODO: 3 states Trusted, Untrusted, TOFU + } + else if (dbv.cpbVal) + pwszStatus = TranslateT("Unknown"); + + db_free(&dbv); + + AddListItem(CMStringA(FORMAT, "%d", device_id), pwszStatus, fp_hex); + } + return false; + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// OnInfoInit - initializes user info option dialogs + +int CJabberProto::OnUserInfoInit(WPARAM wParam, LPARAM hContact) +{ + if (!Proto_GetAccount(m_szModuleName)) + return 0; + + if (hContact == 0) { + // Show our vcard + OnUserInfoInit_VCard(wParam, hContact); + return 0; + } + + char *szProto = Proto_GetBaseAccountName(hContact); + if (szProto != nullptr && !mir_strcmp(szProto, m_szModuleName)) { + USERINFOPAGE uip = {}; + uip.dwInitParam = (LPARAM)this; + uip.flags = ODPF_UNICODE | ODPF_USERINFOTAB | ODPF_ICON; + uip.szGroup.w = m_tszUserName; + uip.dwInitParam = (LPARAM)Skin_GetProtoIcon(m_szModuleName, ID_STATUS_ONLINE); + + uip.pDialog = new JabberUserInfoDlg(this); + uip.position = -2000000000; + uip.szTitle.w = LPGENW("Account"); + g_plugin.addUserInfo(wParam, &uip); + + uip.pDialog = new JabberUserPhotoDlg(this); + uip.position = 2000000000; + uip.szTitle.w = LPGENW("Photo"); + g_plugin.addUserInfo(wParam, &uip); + + CheckOmemoUserInfo(wParam, uip); + } + + return 0; +} + +void CJabberProto::CheckOmemoUserInfo(WPARAM wParam, USERINFOPAGE &uip) +{ + if (m_bUseOMEMO) { + uip.pDialog = new JabberUserOmemoDlg(this); + uip.position = 2000000001; + uip.szTitle.w = L"OMEMO"; + g_plugin.addUserInfo(wParam, &uip); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +// JabberUserInfoUpdate + +void JabberUserInfoInit() +{ + hUserInfoList = WindowList_Create(); +} + +void JabberUserInfoUninit() +{ + WindowList_Destroy(hUserInfoList); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// JabberUserInfoUpdate + +void JabberUserInfoUpdate(MCONTACT hContact) +{ + if (!hContact) + WindowList_BroadcastAsync(hUserInfoList, WM_PROTO_REFRESH, 0, 0); + else if (HWND hwnd = WindowList_Find(hUserInfoList, hContact)) + PostMessage(hwnd, WM_PROTO_REFRESH, 0, 0); +} diff --git a/protocols/JabberG/src/jabber_util.cpp b/protocols/JabberG/src/jabber_util.cpp index 3eebd4e2c7..a3e1c856f6 100644 --- a/protocols/JabberG/src/jabber_util.cpp +++ b/protocols/JabberG/src/jabber_util.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_vcard.cpp b/protocols/JabberG/src/jabber_vcard.cpp index 0d722aac37..aad0f8d9be 100644 --- a/protocols/JabberG/src/jabber_vcard.cpp +++ b/protocols/JabberG/src/jabber_vcard.cpp @@ -4,7 +4,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_xml.cpp b/protocols/JabberG/src/jabber_xml.cpp index b53ca07d45..c3a166017a 100644 --- a/protocols/JabberG/src/jabber_xml.cpp +++ b/protocols/JabberG/src/jabber_xml.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_xml.h b/protocols/JabberG/src/jabber_xml.h index 16f2626b1c..4ecdde53ea 100644 --- a/protocols/JabberG/src/jabber_xml.h +++ b/protocols/JabberG/src/jabber_xml.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_xstatus.cpp b/protocols/JabberG/src/jabber_xstatus.cpp index b67347f7e7..0a989d9dd6 100644 --- a/protocols/JabberG/src/jabber_xstatus.cpp +++ b/protocols/JabberG/src/jabber_xstatus.cpp @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_xstatus.h b/protocols/JabberG/src/jabber_xstatus.h index 0d2bbb266a..80358daca6 100644 --- a/protocols/JabberG/src/jabber_xstatus.h +++ b/protocols/JabberG/src/jabber_xstatus.h @@ -6,7 +6,7 @@ Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007-09 Maxim Mluhov Copyright (c) 2007-09 Victor Pavlychko -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/jabber_zstream.cpp b/protocols/JabberG/src/jabber_zstream.cpp index 0931e4fc34..87f21d44aa 100644 --- a/protocols/JabberG/src/jabber_zstream.cpp +++ b/protocols/JabberG/src/jabber_zstream.cpp @@ -6,7 +6,7 @@ XEP-0138 (Stream Compression) implementation Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Kostya Chukavin, Taras Zackrepa -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/stdafx.cxx b/protocols/JabberG/src/stdafx.cxx index f64d25234b..ebbde0ade1 100644 --- a/protocols/JabberG/src/stdafx.cxx +++ b/protocols/JabberG/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/JabberG/src/stdafx.h b/protocols/JabberG/src/stdafx.h index da81c661c9..d848e5a215 100644 --- a/protocols/JabberG/src/stdafx.h +++ b/protocols/JabberG/src/stdafx.h @@ -5,7 +5,7 @@ Jabber Protocol Plugin for Miranda NG Copyright (c) 2002-04 Santithorn Bunchua Copyright (c) 2005-12 George Hazan Copyright (c) 2007 Maxim Mluhov -Copyright (C) 2012-22 Miranda NG team +Copyright (C) 2012-23 Miranda NG team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License diff --git a/protocols/JabberG/src/version.h b/protocols/JabberG/src/version.h index 29a9e971eb..1308d51095 100644 --- a/protocols/JabberG/src/version.h +++ b/protocols/JabberG/src/version.h @@ -9,5 +9,5 @@ #define __FILENAME "Jabber.dll" #define __DESCRIPTION "Jabber (XMPP) protocol support for Miranda NG." #define __AUTHOR "George Hazan, Maxim Mluhov, Victor Pavlychko, Artem Shpynov, Michael Stepura" -#define __COPYRIGHT "© 2005-22 George Hazan, Maxim Mluhov, Victor Pavlychko, Artem Shpynov, Michael Stepura" +#define __COPYRIGHT "© 2005-23 George Hazan, Maxim Mluhov, Victor Pavlychko, Artem Shpynov, Michael Stepura" #define __AUTHORWEB "https://miranda-ng.org/p/Jabber" diff --git a/protocols/LotusNotify/src/stdafx.cxx b/protocols/LotusNotify/src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/LotusNotify/src/stdafx.cxx +++ b/protocols/LotusNotify/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/MinecraftDynmap/src/chat.cpp b/protocols/MinecraftDynmap/src/chat.cpp index a6f15cc115..c0c1f80542 100644 --- a/protocols/MinecraftDynmap/src/chat.cpp +++ b/protocols/MinecraftDynmap/src/chat.cpp @@ -1,185 +1,185 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -void MinecraftDynmapProto::UpdateChat(const char *name, const char *message, const time_t timestamp, bool addtolog) -{ - // replace % to %% to not interfere with chat color codes - CMStringA szMessage(message); - szMessage.Replace("%", "%%"); - - GCEVENT gce = { m_szModuleName, szRoomName, GC_EVENT_MESSAGE }; - gce.dwFlags = GCEF_UTF8; - gce.time = timestamp; - gce.pszText.a = szMessage.c_str(); - - if (name == NULL) { - gce.iType = GC_EVENT_INFORMATION; - name = TranslateU("Server"); - gce.bIsMe = false; - } - else gce.bIsMe = (m_nick == name); - - if (addtolog) - gce.dwFlags |= GCEF_ADDTOLOG; - - gce.pszUID.a = gce.pszNick.a = name; - Chat_Event(&gce); -} - -int MinecraftDynmapProto::OnChatEvent(WPARAM, LPARAM lParam) -{ - GCHOOK *hook = reinterpret_cast(lParam); - if(strcmp(hook->si->pszModule,m_szModuleName)) - return 0; - - switch(hook->iType) { - case GC_USER_MESSAGE: - { - CMStringA szText(ptrA(mir_utf8encodeW(hook->ptszText))); - szText.Replace("%%", "%"); - if (szText.IsEmpty()) - break; - - // Outgoing message - debugLogA("**Chat - Outgoing message: %s", szText.c_str()); - ForkThread(&MinecraftDynmapProto::SendMsgWorker, new std::string(szText)); - } - break; - - case GC_SESSION_TERMINATE: - m_nick.clear(); - SetStatus(ID_STATUS_OFFLINE); - break; - } - - return 0; -} - -void MinecraftDynmapProto::AddChatContact(const char *name) -{ - GCEVENT gce = { m_szModuleName, szRoomName, GC_EVENT_JOIN }; - gce.time = uint32_t(time(0)); - gce.dwFlags = GCEF_UTF8 + GCEF_ADDTOLOG; - gce.pszUID.a = gce.pszNick.a = name; - gce.bIsMe = (m_nick == name); - - if (gce.bIsMe) - gce.pszStatus.a = "Admin"; - else - gce.pszStatus.a = "Normal"; - - Chat_Event(&gce); -} - -void MinecraftDynmapProto::DeleteChatContact(const char *name) -{ - GCEVENT gce = { m_szModuleName, szRoomName, GC_EVENT_PART }; - gce.dwFlags = GCEF_UTF8 + GCEF_ADDTOLOG; - gce.pszUID.a = gce.pszNick.a = name; - gce.time = uint32_t(time(0)); - gce.bIsMe = (m_nick == name); - Chat_Event(&gce); -} - -INT_PTR MinecraftDynmapProto::OnJoinChat(WPARAM,LPARAM suppress) -{ - ptrW tszTitle(mir_a2u_cp(m_title.c_str(), CP_UTF8)); - - // Create the group chat session - SESSION_INFO *si = Chat_NewSession(GCW_PRIVMESS, m_szModuleName, m_tszUserName, tszTitle); - if (!si || m_iStatus == ID_STATUS_OFFLINE) - return 0; - - // Create a group - Chat_AddGroup(si, TranslateT("Admin")); - Chat_AddGroup(si, TranslateT("Normal")); - - // Note: Initialization will finish up in SetChatStatus, called separately - if (!suppress) - SetChatStatus(m_iStatus); - - return 0; -} - -void MinecraftDynmapProto::SetTopic(const char *topic) -{ - GCEVENT gce = { m_szModuleName, szRoomName, GC_EVENT_TOPIC }; - gce.dwFlags = GCEF_UTF8; - gce.time = ::time(0); - gce.pszText.a = topic; - Chat_Event( &gce); -} - -INT_PTR MinecraftDynmapProto::OnLeaveChat(WPARAM,LPARAM) -{ - Chat_Control(m_szModuleName, m_tszUserName, SESSION_OFFLINE); - Chat_Terminate(m_szModuleName, m_tszUserName); - return 0; -} - -void MinecraftDynmapProto::SetChatStatus(int status) -{ - if (status == ID_STATUS_ONLINE) - { - // Load actual name from database - ptrA nick(db_get_sa(0, m_szModuleName, MINECRAFTDYNMAP_KEY_NAME, Translate("You"))); - m_nick = nick; - - // Add self contact - AddChatContact(m_nick.c_str()); - - Chat_Control(m_szModuleName, m_tszUserName, SESSION_INITDONE); - Chat_Control(m_szModuleName, m_tszUserName, SESSION_ONLINE); - } - else Chat_Control(m_szModuleName, m_tszUserName, SESSION_OFFLINE); -} - -void MinecraftDynmapProto::ClearChat() -{ - Chat_Control(m_szModuleName, m_tszUserName, WINDOW_CLEARLOG); -} - -// TODO: Could this be done better? -MCONTACT MinecraftDynmapProto::GetChatHandle() -{ - /*if (chatHandle_ != NULL) - return chatHandle_; - - for (auto &hContact : AccContacts()) { - if (db_get_b(hContact, m_szModuleName, "ChatRoom", 0) > 0) { - ptrA id = db_get_sa(hContact, m_szModuleName, "ChatRoomId"); - if (id != NULL && !strcmp(id, m_szModuleName)) - return hContact; - } - } - - return NULL;*/ - - GC_INFO gci = {0}; - gci.Flags = GCF_HCONTACT; - gci.pszModule = m_szModuleName; - gci.pszID = m_tszUserName; - Chat_GetInfo(&gci); - - return gci.hContact; +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +void MinecraftDynmapProto::UpdateChat(const char *name, const char *message, const time_t timestamp, bool addtolog) +{ + // replace % to %% to not interfere with chat color codes + CMStringA szMessage(message); + szMessage.Replace("%", "%%"); + + GCEVENT gce = { m_szModuleName, szRoomName, GC_EVENT_MESSAGE }; + gce.dwFlags = GCEF_UTF8; + gce.time = timestamp; + gce.pszText.a = szMessage.c_str(); + + if (name == NULL) { + gce.iType = GC_EVENT_INFORMATION; + name = TranslateU("Server"); + gce.bIsMe = false; + } + else gce.bIsMe = (m_nick == name); + + if (addtolog) + gce.dwFlags |= GCEF_ADDTOLOG; + + gce.pszUID.a = gce.pszNick.a = name; + Chat_Event(&gce); +} + +int MinecraftDynmapProto::OnChatEvent(WPARAM, LPARAM lParam) +{ + GCHOOK *hook = reinterpret_cast(lParam); + if(strcmp(hook->si->pszModule,m_szModuleName)) + return 0; + + switch(hook->iType) { + case GC_USER_MESSAGE: + { + CMStringA szText(ptrA(mir_utf8encodeW(hook->ptszText))); + szText.Replace("%%", "%"); + if (szText.IsEmpty()) + break; + + // Outgoing message + debugLogA("**Chat - Outgoing message: %s", szText.c_str()); + ForkThread(&MinecraftDynmapProto::SendMsgWorker, new std::string(szText)); + } + break; + + case GC_SESSION_TERMINATE: + m_nick.clear(); + SetStatus(ID_STATUS_OFFLINE); + break; + } + + return 0; +} + +void MinecraftDynmapProto::AddChatContact(const char *name) +{ + GCEVENT gce = { m_szModuleName, szRoomName, GC_EVENT_JOIN }; + gce.time = uint32_t(time(0)); + gce.dwFlags = GCEF_UTF8 + GCEF_ADDTOLOG; + gce.pszUID.a = gce.pszNick.a = name; + gce.bIsMe = (m_nick == name); + + if (gce.bIsMe) + gce.pszStatus.a = "Admin"; + else + gce.pszStatus.a = "Normal"; + + Chat_Event(&gce); +} + +void MinecraftDynmapProto::DeleteChatContact(const char *name) +{ + GCEVENT gce = { m_szModuleName, szRoomName, GC_EVENT_PART }; + gce.dwFlags = GCEF_UTF8 + GCEF_ADDTOLOG; + gce.pszUID.a = gce.pszNick.a = name; + gce.time = uint32_t(time(0)); + gce.bIsMe = (m_nick == name); + Chat_Event(&gce); +} + +INT_PTR MinecraftDynmapProto::OnJoinChat(WPARAM,LPARAM suppress) +{ + ptrW tszTitle(mir_a2u_cp(m_title.c_str(), CP_UTF8)); + + // Create the group chat session + SESSION_INFO *si = Chat_NewSession(GCW_PRIVMESS, m_szModuleName, m_tszUserName, tszTitle); + if (!si || m_iStatus == ID_STATUS_OFFLINE) + return 0; + + // Create a group + Chat_AddGroup(si, TranslateT("Admin")); + Chat_AddGroup(si, TranslateT("Normal")); + + // Note: Initialization will finish up in SetChatStatus, called separately + if (!suppress) + SetChatStatus(m_iStatus); + + return 0; +} + +void MinecraftDynmapProto::SetTopic(const char *topic) +{ + GCEVENT gce = { m_szModuleName, szRoomName, GC_EVENT_TOPIC }; + gce.dwFlags = GCEF_UTF8; + gce.time = ::time(0); + gce.pszText.a = topic; + Chat_Event( &gce); +} + +INT_PTR MinecraftDynmapProto::OnLeaveChat(WPARAM,LPARAM) +{ + Chat_Control(m_szModuleName, m_tszUserName, SESSION_OFFLINE); + Chat_Terminate(m_szModuleName, m_tszUserName); + return 0; +} + +void MinecraftDynmapProto::SetChatStatus(int status) +{ + if (status == ID_STATUS_ONLINE) + { + // Load actual name from database + ptrA nick(db_get_sa(0, m_szModuleName, MINECRAFTDYNMAP_KEY_NAME, Translate("You"))); + m_nick = nick; + + // Add self contact + AddChatContact(m_nick.c_str()); + + Chat_Control(m_szModuleName, m_tszUserName, SESSION_INITDONE); + Chat_Control(m_szModuleName, m_tszUserName, SESSION_ONLINE); + } + else Chat_Control(m_szModuleName, m_tszUserName, SESSION_OFFLINE); +} + +void MinecraftDynmapProto::ClearChat() +{ + Chat_Control(m_szModuleName, m_tszUserName, WINDOW_CLEARLOG); +} + +// TODO: Could this be done better? +MCONTACT MinecraftDynmapProto::GetChatHandle() +{ + /*if (chatHandle_ != NULL) + return chatHandle_; + + for (auto &hContact : AccContacts()) { + if (db_get_b(hContact, m_szModuleName, "ChatRoom", 0) > 0) { + ptrA id = db_get_sa(hContact, m_szModuleName, "ChatRoomId"); + if (id != NULL && !strcmp(id, m_szModuleName)) + return hContact; + } + } + + return NULL;*/ + + GC_INFO gci = {0}; + gci.Flags = GCF_HCONTACT; + gci.pszModule = m_szModuleName; + gci.pszID = m_tszUserName; + Chat_GetInfo(&gci); + + return gci.hContact; } \ No newline at end of file diff --git a/protocols/MinecraftDynmap/src/communication.cpp b/protocols/MinecraftDynmap/src/communication.cpp index 7ca5f0a732..47e6ec4480 100644 --- a/protocols/MinecraftDynmap/src/communication.cpp +++ b/protocols/MinecraftDynmap/src/communication.cpp @@ -1,442 +1,442 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -http::response MinecraftDynmapProto::sendRequest(const int request_type, std::string *post_data, std::string *get_data) -{ - http::response resp; - - // Prepare the request - NETLIBHTTPREQUEST nlhr = { sizeof(NETLIBHTTPREQUEST) }; - - // FIXME: get server - - // Set request URL - std::string url = m_server + chooseAction(request_type, get_data); - nlhr.szUrl = (char*)url.c_str(); - - // Set timeout (bigger for channel request) - nlhr.timeout = 1000 * ((request_type == MINECRAFTDYNMAP_REQUEST_EVENTS) ? 65 : 20); - - // Set request type (GET/POST) and eventually also POST data - if (post_data != nullptr) { - nlhr.requestType = REQUEST_POST; - nlhr.pData = (char*)(*post_data).c_str(); - nlhr.dataLength = (int)post_data->length(); - } - else { - nlhr.requestType = REQUEST_GET; - } - - // Set headers - it depends on requestType so it must be after setting that - nlhr.headers = get_request_headers(nlhr.requestType, &nlhr.headersCount); - - // Set flags - nlhr.flags = NLHRF_HTTP11; - -#ifdef _DEBUG - nlhr.flags |= NLHRF_DUMPASTEXT; -#else - nlhr.flags |= NLHRF_NODUMP; -#endif - - // Set persistent connection (or not) - switch (request_type) - { - case MINECRAFTDYNMAP_REQUEST_HOME: - nlhr.nlc = nullptr; - break; - - case MINECRAFTDYNMAP_REQUEST_EVENTS: - nlhr.nlc = hEventsConnection; - nlhr.flags |= NLHRF_PERSISTENT; - break; - - default: - WaitForSingleObject(connection_lock_, INFINITE); - nlhr.nlc = hConnection; - nlhr.flags |= NLHRF_PERSISTENT; - break; - } - - debugLogA("@@@@@ Sending request to '%s'", nlhr.szUrl); - - // Send the request - NLHR_PTR pnlhr(Netlib_HttpTransaction(m_hNetlibUser, &nlhr)); - - mir_free(nlhr.headers); - - // Remember the persistent connection handle (or not) - switch (request_type) { - case MINECRAFTDYNMAP_REQUEST_HOME: - break; - - case MINECRAFTDYNMAP_REQUEST_EVENTS: - hEventsConnection = pnlhr ? pnlhr->nlc : nullptr; - break; - - default: - ReleaseMutex(connection_lock_); - hConnection = pnlhr ? pnlhr->nlc : nullptr; - break; - } - - // Check and copy response data - if (pnlhr != nullptr) - { - debugLogA("@@@@@ Got response with code %d", pnlhr->resultCode); - store_headers(&resp, pnlhr->headers, pnlhr->headersCount); - resp.code = pnlhr->resultCode; - resp.data = pnlhr->pData ? pnlhr->pData : ""; - - // debugLogA("&&&&& Got response: %s", resp.data.c_str()); - } else { - debugLogA("!!!!! No response from server (time-out)"); - resp.code = HTTP_CODE_FAKE_DISCONNECTED; - // Better to have something set explicitely as this value is compaired in all communication requests - } - - return resp; -} - -////////////////////////////////////////////////////////////////////////////// - -std::string MinecraftDynmapProto::chooseAction(int request_type, std::string *get_data) -{ - switch (request_type) { - case MINECRAFTDYNMAP_REQUEST_MESSAGE: - return "/up/sendmessage"; - - case MINECRAFTDYNMAP_REQUEST_CONFIGURATION: - return "/up/configuration"; - - case MINECRAFTDYNMAP_REQUEST_EVENTS: - return std::string("/up/world/world/") + (!m_timestamp.empty() ? m_timestamp : "0"); - - //case MINECRAFTDYNMAP_REQUEST_HOME: - default: - return "/" + *get_data; - } -} - - -NETLIBHTTPHEADER* MinecraftDynmapProto::get_request_headers(int request_type, int* headers_count) -{ - if (request_type == REQUEST_POST) - *headers_count = 5; - else - *headers_count = 4; - - NETLIBHTTPHEADER *headers = (NETLIBHTTPHEADER*)mir_calloc(sizeof(NETLIBHTTPHEADER)*(*headers_count)); - - if (request_type == REQUEST_POST) { - headers[4].szName = "Content-Type"; - headers[4].szValue = "application/json; charset=utf-8"; - } - - headers[3].szName = "Cookie"; - headers[3].szValue = (char *)m_cookie.c_str(); - headers[2].szName = "User-Agent"; - headers[2].szValue = (char *)g_strUserAgent.c_str(); - headers[1].szName = "Accept"; - headers[1].szValue = "*/*"; - headers[0].szName = "Accept-Language"; - headers[0].szValue = "en,en-US;q=0.9"; - - return headers; -} - -void MinecraftDynmapProto::store_headers(http::response* resp, NETLIBHTTPHEADER* headers, int headersCount) -{ - for (size_t i = 0; i < (size_t)headersCount; i++) { - std::string header_name = headers[i].szName; - std::string header_value = headers[i].szValue; - - resp->headers[header_name] = header_value; - } -} - -////////////////////////////////////////////////////////////////////////////// - -bool MinecraftDynmapProto::doSignOn() -{ - handleEntry(__FUNCTION__); - - http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_CONFIGURATION); - - if (resp.code != HTTP_CODE_OK) { - return handleError(__FUNCTION__, "Can't load configuration", true); - } - - JSONNode root = JSONNode::parse(resp.data.c_str()); - if (!root) - return false; - - /* - const JSONNode &allowchat_ = root["allowchat"]; // boolean - const JSONNode &allowwebchat_ = root["allowwebchat"]; // boolean - const JSONNode &loggedin_ = root["loggedin"]; // boolean - const JSONNode &loginEnabled_ = root["login-enabled"]; // boolean - const JSONNode &loginRequired_ = root["webchat-requires-login"]; // boolean - */ - - const JSONNode &title_ = root["title"]; // name of server - const JSONNode &interval_ = root["webchat-interval"]; // limit in seconds for sending messages - const JSONNode &rate_ = root["updaterate"]; // probably update rate for events request - - if (!title_ || !interval_ || !rate_) { - return handleError(__FUNCTION__, "No title, interval or rate in configuration", true); - } - - m_title = title_.as_string(); - m_interval = interval_.as_int(); - m_updateRate = rate_.as_int(); - m_cookie.clear(); - - if (resp.headers.find("Set-Cookie") != resp.headers.end()) { - // Load Session identifier - std::string cookies = resp.headers["Set-Cookie"]; - - const char *findStr = "JSESSIONID="; - std::string::size_type start = cookies.find(findStr); - - if (start != std::string::npos) { - m_cookie = cookies.substr(start, cookies.find(";") - start); - } - } - - if (m_cookie.empty()) { - return handleError(__FUNCTION__, "Empty session id", true); - } - - return handleSuccess(__FUNCTION__); -} - -bool MinecraftDynmapProto::doEvents() -{ - handleEntry(__FUNCTION__); - - // Get update - http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_EVENTS); - - if (resp.code != HTTP_CODE_OK) - return handleError(__FUNCTION__, "Response is not code 200"); - - JSONNode root = JSONNode::parse(resp.data.c_str()); - if (!root) - return handleError(__FUNCTION__, "Invalid JSON response"); - - const JSONNode ×tamp_ = root["timestamp"]; - if (!timestamp_) - return handleError(__FUNCTION__, "Received no timestamp node"); - - m_timestamp = timestamp_.as_string(); - - const JSONNode &updates_ = root["updates"]; - if (!updates_) - return handleError(__FUNCTION__, "Received no updates node"); - - for (auto it = updates_.begin(); it != updates_.end(); ++it) { - const JSONNode &type_ = (*it)["type"]; - if (type_ && type_.as_string() == "chat") { - const JSONNode &time_ = (*it)["timestamp"]; - // const JSONNode &source_ = (*it)["source"]; // e.g. "web" - const JSONNode &playerName_ = (*it)["playerName"]; - const JSONNode &message_ = (*it)["message"]; - // TODO: there are also "channel" and "account" elements - - if (!time_ || !playerName_ || !message_) { - debugLogW(L"Error: No player name, time or text for message"); - continue; - } - - time_t timestamp = atoi(time_.as_string().c_str()); - std::string name = playerName_.as_string(); - std::string message = message_.as_string(); - - debugLogW(L"Received message: [%d] %s -> %s", timestamp, name.c_str(), message.c_str()); - UpdateChat(name.c_str(), message.c_str(), timestamp); - } - } - - return handleSuccess(__FUNCTION__); -} - -bool MinecraftDynmapProto::doSendMessage(const std::string &message_text) -{ - handleEntry(__FUNCTION__); - - JSONNode json(JSON_NODE); - json.push_back(JSONNode("name", m_nick.c_str())); - json.push_back(JSONNode("message", message_text.c_str())); - std::string data = json.write(); - - http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_MESSAGE, &data); - - if (resp.code == HTTP_CODE_OK) { - JSONNode root = JSONNode::parse(resp.data.c_str()); - if (root) { - const JSONNode &error_ = root["error"]; - if (error_) { - std::string error = error_.as_string(); - if (error == "none") { - return handleSuccess(__FUNCTION__); - } - else if (error == "not-allowed") { - UpdateChat(nullptr, Translate("Message was not sent. Probably you are sending them too fast or chat is disabled completely.")); - } - } - } - } - - return handleError(__FUNCTION__); -} - -std::string MinecraftDynmapProto::doGetPage(const int request_type) -{ - handleEntry(__FUNCTION__); - - http::response resp = sendRequest(request_type); - - if (resp.code == HTTP_CODE_OK) { - handleSuccess(__FUNCTION__); - } else { - handleError(__FUNCTION__); - } - - return resp.data; -} - -void MinecraftDynmapProto::SignOnWorker(void*) -{ - SYSTEMTIME t; - GetLocalTime(&t); - debugLogA("[%d.%d.%d] Using Omegle Protocol %s", t.wDay, t.wMonth, t.wYear, __VERSION_STRING_DOTS); - - ScopedLock s(signon_lock_); - - int old_status = m_iStatus; - - // Load server from database - ptrA str(db_get_sa(0, m_szModuleName, MINECRAFTDYNMAP_KEY_SERVER)); - if (!str || !str[0]) { - MessageBox(nullptr, TranslateT("Set server address to connect."), m_tszUserName, MB_OK); - SetStatus(ID_STATUS_OFFLINE); - return; - } - m_server = str; - - // Fix format of given server - if (m_server.substr(0, 7) != "http://" && m_server.substr(0, 8) != "https://") - m_server = "http://" + m_server; - if (m_server.substr(m_server.length() - 1, 1) == "/") - m_server = m_server.substr(0, m_server.length() -1); - - if (doSignOn()) { - // Signed in, switch to online, create chatroom and start events loop - m_iStatus = m_iDesiredStatus; - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus); - - setDword("LogonTS", (uint32_t)time(0)); - ClearChat(); - OnJoinChat(0, false); - - ResetEvent(events_loop_event_); - - ForkThread(&MinecraftDynmapProto::EventsLoop, this); - } - else { - // Some error - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_FAILED, (HANDLE)old_status, m_iStatus); - } - -} - -void MinecraftDynmapProto::SignOffWorker(void*) -{ - ScopedLock s(signon_lock_); - - SetEvent(events_loop_event_); - - m_cookie.clear(); - m_title.clear(); - m_server.clear(); - m_timestamp.clear(); - - int old_status = m_iStatus; - m_iStatus = ID_STATUS_OFFLINE; - - Netlib_Shutdown(hEventsConnection); - - OnLeaveChat(NULL, NULL); - - delSetting("LogonTS"); - - ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus); - - if (hConnection) - Netlib_CloseHandle(hConnection); - hConnection = nullptr; - - if (hEventsConnection) - Netlib_CloseHandle(hEventsConnection); - hEventsConnection = nullptr; -} - -void MinecraftDynmapProto::EventsLoop(void *) -{ - ScopedLock s(events_loop_lock_); - - time_t tim = ::time(0); - debugLogA(">>>>> Entering %s[%d]", __FUNCTION__, tim); - - while (doEvents()) - { - if (!isOnline()) - break; - - if (WaitForSingleObjectEx(events_loop_event_, m_updateRate, true) != WAIT_TIMEOUT) // FIXME: correct timeout - break; - - debugLogA("***** %s[%d] refreshing...", __FUNCTION__, tim); - } - - ResetEvent(events_loop_event_); - ResetEvent(events_loop_lock_); - debugLogA("<<<<< Exiting %s[%d]", __FUNCTION__, tim); -} - -void MinecraftDynmapProto::SendMsgWorker(void *p) -{ - if (p == nullptr) - return; - - ScopedLock s(send_message_lock_); - - CMStringA data = ((std::string*)p)->c_str(); - delete (std::string*)p; - - data.TrimRight(); - - if (isOnline() && data.GetLength()) - doSendMessage(data.c_str()); -} +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +http::response MinecraftDynmapProto::sendRequest(const int request_type, std::string *post_data, std::string *get_data) +{ + http::response resp; + + // Prepare the request + NETLIBHTTPREQUEST nlhr = { sizeof(NETLIBHTTPREQUEST) }; + + // FIXME: get server + + // Set request URL + std::string url = m_server + chooseAction(request_type, get_data); + nlhr.szUrl = (char*)url.c_str(); + + // Set timeout (bigger for channel request) + nlhr.timeout = 1000 * ((request_type == MINECRAFTDYNMAP_REQUEST_EVENTS) ? 65 : 20); + + // Set request type (GET/POST) and eventually also POST data + if (post_data != nullptr) { + nlhr.requestType = REQUEST_POST; + nlhr.pData = (char*)(*post_data).c_str(); + nlhr.dataLength = (int)post_data->length(); + } + else { + nlhr.requestType = REQUEST_GET; + } + + // Set headers - it depends on requestType so it must be after setting that + nlhr.headers = get_request_headers(nlhr.requestType, &nlhr.headersCount); + + // Set flags + nlhr.flags = NLHRF_HTTP11; + +#ifdef _DEBUG + nlhr.flags |= NLHRF_DUMPASTEXT; +#else + nlhr.flags |= NLHRF_NODUMP; +#endif + + // Set persistent connection (or not) + switch (request_type) + { + case MINECRAFTDYNMAP_REQUEST_HOME: + nlhr.nlc = nullptr; + break; + + case MINECRAFTDYNMAP_REQUEST_EVENTS: + nlhr.nlc = hEventsConnection; + nlhr.flags |= NLHRF_PERSISTENT; + break; + + default: + WaitForSingleObject(connection_lock_, INFINITE); + nlhr.nlc = hConnection; + nlhr.flags |= NLHRF_PERSISTENT; + break; + } + + debugLogA("@@@@@ Sending request to '%s'", nlhr.szUrl); + + // Send the request + NLHR_PTR pnlhr(Netlib_HttpTransaction(m_hNetlibUser, &nlhr)); + + mir_free(nlhr.headers); + + // Remember the persistent connection handle (or not) + switch (request_type) { + case MINECRAFTDYNMAP_REQUEST_HOME: + break; + + case MINECRAFTDYNMAP_REQUEST_EVENTS: + hEventsConnection = pnlhr ? pnlhr->nlc : nullptr; + break; + + default: + ReleaseMutex(connection_lock_); + hConnection = pnlhr ? pnlhr->nlc : nullptr; + break; + } + + // Check and copy response data + if (pnlhr != nullptr) + { + debugLogA("@@@@@ Got response with code %d", pnlhr->resultCode); + store_headers(&resp, pnlhr->headers, pnlhr->headersCount); + resp.code = pnlhr->resultCode; + resp.data = pnlhr->pData ? pnlhr->pData : ""; + + // debugLogA("&&&&& Got response: %s", resp.data.c_str()); + } else { + debugLogA("!!!!! No response from server (time-out)"); + resp.code = HTTP_CODE_FAKE_DISCONNECTED; + // Better to have something set explicitely as this value is compaired in all communication requests + } + + return resp; +} + +////////////////////////////////////////////////////////////////////////////// + +std::string MinecraftDynmapProto::chooseAction(int request_type, std::string *get_data) +{ + switch (request_type) { + case MINECRAFTDYNMAP_REQUEST_MESSAGE: + return "/up/sendmessage"; + + case MINECRAFTDYNMAP_REQUEST_CONFIGURATION: + return "/up/configuration"; + + case MINECRAFTDYNMAP_REQUEST_EVENTS: + return std::string("/up/world/world/") + (!m_timestamp.empty() ? m_timestamp : "0"); + + //case MINECRAFTDYNMAP_REQUEST_HOME: + default: + return "/" + *get_data; + } +} + + +NETLIBHTTPHEADER* MinecraftDynmapProto::get_request_headers(int request_type, int* headers_count) +{ + if (request_type == REQUEST_POST) + *headers_count = 5; + else + *headers_count = 4; + + NETLIBHTTPHEADER *headers = (NETLIBHTTPHEADER*)mir_calloc(sizeof(NETLIBHTTPHEADER)*(*headers_count)); + + if (request_type == REQUEST_POST) { + headers[4].szName = "Content-Type"; + headers[4].szValue = "application/json; charset=utf-8"; + } + + headers[3].szName = "Cookie"; + headers[3].szValue = (char *)m_cookie.c_str(); + headers[2].szName = "User-Agent"; + headers[2].szValue = (char *)g_strUserAgent.c_str(); + headers[1].szName = "Accept"; + headers[1].szValue = "*/*"; + headers[0].szName = "Accept-Language"; + headers[0].szValue = "en,en-US;q=0.9"; + + return headers; +} + +void MinecraftDynmapProto::store_headers(http::response* resp, NETLIBHTTPHEADER* headers, int headersCount) +{ + for (size_t i = 0; i < (size_t)headersCount; i++) { + std::string header_name = headers[i].szName; + std::string header_value = headers[i].szValue; + + resp->headers[header_name] = header_value; + } +} + +////////////////////////////////////////////////////////////////////////////// + +bool MinecraftDynmapProto::doSignOn() +{ + handleEntry(__FUNCTION__); + + http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_CONFIGURATION); + + if (resp.code != HTTP_CODE_OK) { + return handleError(__FUNCTION__, "Can't load configuration", true); + } + + JSONNode root = JSONNode::parse(resp.data.c_str()); + if (!root) + return false; + + /* + const JSONNode &allowchat_ = root["allowchat"]; // boolean + const JSONNode &allowwebchat_ = root["allowwebchat"]; // boolean + const JSONNode &loggedin_ = root["loggedin"]; // boolean + const JSONNode &loginEnabled_ = root["login-enabled"]; // boolean + const JSONNode &loginRequired_ = root["webchat-requires-login"]; // boolean + */ + + const JSONNode &title_ = root["title"]; // name of server + const JSONNode &interval_ = root["webchat-interval"]; // limit in seconds for sending messages + const JSONNode &rate_ = root["updaterate"]; // probably update rate for events request + + if (!title_ || !interval_ || !rate_) { + return handleError(__FUNCTION__, "No title, interval or rate in configuration", true); + } + + m_title = title_.as_string(); + m_interval = interval_.as_int(); + m_updateRate = rate_.as_int(); + m_cookie.clear(); + + if (resp.headers.find("Set-Cookie") != resp.headers.end()) { + // Load Session identifier + std::string cookies = resp.headers["Set-Cookie"]; + + const char *findStr = "JSESSIONID="; + std::string::size_type start = cookies.find(findStr); + + if (start != std::string::npos) { + m_cookie = cookies.substr(start, cookies.find(";") - start); + } + } + + if (m_cookie.empty()) { + return handleError(__FUNCTION__, "Empty session id", true); + } + + return handleSuccess(__FUNCTION__); +} + +bool MinecraftDynmapProto::doEvents() +{ + handleEntry(__FUNCTION__); + + // Get update + http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_EVENTS); + + if (resp.code != HTTP_CODE_OK) + return handleError(__FUNCTION__, "Response is not code 200"); + + JSONNode root = JSONNode::parse(resp.data.c_str()); + if (!root) + return handleError(__FUNCTION__, "Invalid JSON response"); + + const JSONNode ×tamp_ = root["timestamp"]; + if (!timestamp_) + return handleError(__FUNCTION__, "Received no timestamp node"); + + m_timestamp = timestamp_.as_string(); + + const JSONNode &updates_ = root["updates"]; + if (!updates_) + return handleError(__FUNCTION__, "Received no updates node"); + + for (auto it = updates_.begin(); it != updates_.end(); ++it) { + const JSONNode &type_ = (*it)["type"]; + if (type_ && type_.as_string() == "chat") { + const JSONNode &time_ = (*it)["timestamp"]; + // const JSONNode &source_ = (*it)["source"]; // e.g. "web" + const JSONNode &playerName_ = (*it)["playerName"]; + const JSONNode &message_ = (*it)["message"]; + // TODO: there are also "channel" and "account" elements + + if (!time_ || !playerName_ || !message_) { + debugLogW(L"Error: No player name, time or text for message"); + continue; + } + + time_t timestamp = atoi(time_.as_string().c_str()); + std::string name = playerName_.as_string(); + std::string message = message_.as_string(); + + debugLogW(L"Received message: [%d] %s -> %s", timestamp, name.c_str(), message.c_str()); + UpdateChat(name.c_str(), message.c_str(), timestamp); + } + } + + return handleSuccess(__FUNCTION__); +} + +bool MinecraftDynmapProto::doSendMessage(const std::string &message_text) +{ + handleEntry(__FUNCTION__); + + JSONNode json(JSON_NODE); + json.push_back(JSONNode("name", m_nick.c_str())); + json.push_back(JSONNode("message", message_text.c_str())); + std::string data = json.write(); + + http::response resp = sendRequest(MINECRAFTDYNMAP_REQUEST_MESSAGE, &data); + + if (resp.code == HTTP_CODE_OK) { + JSONNode root = JSONNode::parse(resp.data.c_str()); + if (root) { + const JSONNode &error_ = root["error"]; + if (error_) { + std::string error = error_.as_string(); + if (error == "none") { + return handleSuccess(__FUNCTION__); + } + else if (error == "not-allowed") { + UpdateChat(nullptr, Translate("Message was not sent. Probably you are sending them too fast or chat is disabled completely.")); + } + } + } + } + + return handleError(__FUNCTION__); +} + +std::string MinecraftDynmapProto::doGetPage(const int request_type) +{ + handleEntry(__FUNCTION__); + + http::response resp = sendRequest(request_type); + + if (resp.code == HTTP_CODE_OK) { + handleSuccess(__FUNCTION__); + } else { + handleError(__FUNCTION__); + } + + return resp.data; +} + +void MinecraftDynmapProto::SignOnWorker(void*) +{ + SYSTEMTIME t; + GetLocalTime(&t); + debugLogA("[%d.%d.%d] Using Omegle Protocol %s", t.wDay, t.wMonth, t.wYear, __VERSION_STRING_DOTS); + + ScopedLock s(signon_lock_); + + int old_status = m_iStatus; + + // Load server from database + ptrA str(db_get_sa(0, m_szModuleName, MINECRAFTDYNMAP_KEY_SERVER)); + if (!str || !str[0]) { + MessageBox(nullptr, TranslateT("Set server address to connect."), m_tszUserName, MB_OK); + SetStatus(ID_STATUS_OFFLINE); + return; + } + m_server = str; + + // Fix format of given server + if (m_server.substr(0, 7) != "http://" && m_server.substr(0, 8) != "https://") + m_server = "http://" + m_server; + if (m_server.substr(m_server.length() - 1, 1) == "/") + m_server = m_server.substr(0, m_server.length() -1); + + if (doSignOn()) { + // Signed in, switch to online, create chatroom and start events loop + m_iStatus = m_iDesiredStatus; + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus); + + setDword("LogonTS", (uint32_t)time(0)); + ClearChat(); + OnJoinChat(0, false); + + ResetEvent(events_loop_event_); + + ForkThread(&MinecraftDynmapProto::EventsLoop, this); + } + else { + // Some error + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_FAILED, (HANDLE)old_status, m_iStatus); + } + +} + +void MinecraftDynmapProto::SignOffWorker(void*) +{ + ScopedLock s(signon_lock_); + + SetEvent(events_loop_event_); + + m_cookie.clear(); + m_title.clear(); + m_server.clear(); + m_timestamp.clear(); + + int old_status = m_iStatus; + m_iStatus = ID_STATUS_OFFLINE; + + Netlib_Shutdown(hEventsConnection); + + OnLeaveChat(NULL, NULL); + + delSetting("LogonTS"); + + ProtoBroadcastAck(0, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus); + + if (hConnection) + Netlib_CloseHandle(hConnection); + hConnection = nullptr; + + if (hEventsConnection) + Netlib_CloseHandle(hEventsConnection); + hEventsConnection = nullptr; +} + +void MinecraftDynmapProto::EventsLoop(void *) +{ + ScopedLock s(events_loop_lock_); + + time_t tim = ::time(0); + debugLogA(">>>>> Entering %s[%d]", __FUNCTION__, tim); + + while (doEvents()) + { + if (!isOnline()) + break; + + if (WaitForSingleObjectEx(events_loop_event_, m_updateRate, true) != WAIT_TIMEOUT) // FIXME: correct timeout + break; + + debugLogA("***** %s[%d] refreshing...", __FUNCTION__, tim); + } + + ResetEvent(events_loop_event_); + ResetEvent(events_loop_lock_); + debugLogA("<<<<< Exiting %s[%d]", __FUNCTION__, tim); +} + +void MinecraftDynmapProto::SendMsgWorker(void *p) +{ + if (p == nullptr) + return; + + ScopedLock s(send_message_lock_); + + CMStringA data = ((std::string*)p)->c_str(); + delete (std::string*)p; + + data.TrimRight(); + + if (isOnline() && data.GetLength()) + doSendMessage(data.c_str()); +} diff --git a/protocols/MinecraftDynmap/src/constants.h b/protocols/MinecraftDynmap/src/constants.h index 42d1d2c33c..b8f454fe0f 100644 --- a/protocols/MinecraftDynmap/src/constants.h +++ b/protocols/MinecraftDynmap/src/constants.h @@ -1,46 +1,46 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -// Product management -#define MINECRAFTDYNMAP_NAME "Minecraft Dynmap" - -// Limits -#define MINECRAFTDYNMAP_TIMEOUTS_LIMIT 6 -// This is just guessed some wise limit -#define MINECRAFTDYNMAP_MESSAGE_LIMIT 256 -#define MINECRAFTDYNMAP_MESSAGE_LIMIT_TEXT "256" - -#define MINECRAFTDYNMAP_QUESTION_MIN_LENGTH 10 - -// Request types -#define MINECRAFTDYNMAP_REQUEST_HOME 100 // getting server homepage -#define MINECRAFTDYNMAP_REQUEST_CONFIGURATION 101 // getting server configuration -#define MINECRAFTDYNMAP_REQUEST_EVENTS 102 // receiving events -#define MINECRAFTDYNMAP_REQUEST_MESSAGE 103 // sending messages - -// DB settings -#define MINECRAFTDYNMAP_KEY_TIMEOUTS_LIMIT "TimeoutsLimit" // [HIDDEN] - -#define MINECRAFTDYNMAP_KEY_NAME "Nick" -#define MINECRAFTDYNMAP_KEY_SERVER "Server" +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +// Product management +#define MINECRAFTDYNMAP_NAME "Minecraft Dynmap" + +// Limits +#define MINECRAFTDYNMAP_TIMEOUTS_LIMIT 6 +// This is just guessed some wise limit +#define MINECRAFTDYNMAP_MESSAGE_LIMIT 256 +#define MINECRAFTDYNMAP_MESSAGE_LIMIT_TEXT "256" + +#define MINECRAFTDYNMAP_QUESTION_MIN_LENGTH 10 + +// Request types +#define MINECRAFTDYNMAP_REQUEST_HOME 100 // getting server homepage +#define MINECRAFTDYNMAP_REQUEST_CONFIGURATION 101 // getting server configuration +#define MINECRAFTDYNMAP_REQUEST_EVENTS 102 // receiving events +#define MINECRAFTDYNMAP_REQUEST_MESSAGE 103 // sending messages + +// DB settings +#define MINECRAFTDYNMAP_KEY_TIMEOUTS_LIMIT "TimeoutsLimit" // [HIDDEN] + +#define MINECRAFTDYNMAP_KEY_NAME "Nick" +#define MINECRAFTDYNMAP_KEY_SERVER "Server" diff --git a/protocols/MinecraftDynmap/src/dialogs.cpp b/protocols/MinecraftDynmap/src/dialogs.cpp index 54ea975fae..38707c6870 100644 --- a/protocols/MinecraftDynmap/src/dialogs.cpp +++ b/protocols/MinecraftDynmap/src/dialogs.cpp @@ -1,98 +1,98 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -// Icons - -static IconItem iconList[] = -{ - { "proto", LPGEN("Protocol icon"), IDI_PROTO }, -}; - -static HANDLE hIconLibItem[_countof(iconList)]; - -void InitIcons(void) -{ - g_plugin.registerIcon("Protocols/MinecraftDynmap", iconList, "MinecraftDynmap"); -} - -// Dialogs - -static void LoadDBText(MinecraftDynmapProto* ppro, HWND hwnd, int idCtrl, const char* szSetting) -{ - ptrW tstr(db_get_wsa(0, ppro->m_szModuleName, szSetting)); - if (tstr) - SetDlgItemText(hwnd, idCtrl, tstr); -} - -static void StoreDBText(MinecraftDynmapProto* ppro, HWND hwnd, int idCtrl, const char* szSetting) -{ - wchar_t tstr[250 + 1]; - - GetDlgItemText(hwnd, idCtrl, tstr, _countof(tstr)); - if (tstr[0] != '\0') - db_set_ws(0, ppro->m_szModuleName, szSetting, tstr); - else - db_unset(0, ppro->m_szModuleName, szSetting); -} - - -INT_PTR CALLBACK MinecraftDynmapAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) -{ - MinecraftDynmapProto *proto; - - switch (message) { - case WM_INITDIALOG: - TranslateDialogDefault(hwnd); - - proto = reinterpret_cast(lparam); - SetWindowLongPtr(hwnd, GWLP_USERDATA, lparam); - - LoadDBText(proto, hwnd, IDC_SERVER, MINECRAFTDYNMAP_KEY_SERVER); - LoadDBText(proto, hwnd, IDC_NAME, MINECRAFTDYNMAP_KEY_NAME); - return TRUE; - - case WM_COMMAND: - switch (LOWORD(wparam)) { - case IDC_NAME: - if (HIWORD(wparam) != EN_CHANGE || (HWND)lparam != GetFocus()) - return TRUE; - - SendMessage(GetParent(hwnd), PSM_CHANGED, 0, 0); - break; - } - break; - - case WM_NOTIFY: - if (reinterpret_cast(lparam)->code == PSN_APPLY) { - proto = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); - - StoreDBText(proto, hwnd, IDC_SERVER, MINECRAFTDYNMAP_KEY_SERVER); - StoreDBText(proto, hwnd, IDC_NAME, MINECRAFTDYNMAP_KEY_NAME); - - return TRUE; - } - break; - } - return FALSE; -} +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +// Icons + +static IconItem iconList[] = +{ + { "proto", LPGEN("Protocol icon"), IDI_PROTO }, +}; + +static HANDLE hIconLibItem[_countof(iconList)]; + +void InitIcons(void) +{ + g_plugin.registerIcon("Protocols/MinecraftDynmap", iconList, "MinecraftDynmap"); +} + +// Dialogs + +static void LoadDBText(MinecraftDynmapProto* ppro, HWND hwnd, int idCtrl, const char* szSetting) +{ + ptrW tstr(db_get_wsa(0, ppro->m_szModuleName, szSetting)); + if (tstr) + SetDlgItemText(hwnd, idCtrl, tstr); +} + +static void StoreDBText(MinecraftDynmapProto* ppro, HWND hwnd, int idCtrl, const char* szSetting) +{ + wchar_t tstr[250 + 1]; + + GetDlgItemText(hwnd, idCtrl, tstr, _countof(tstr)); + if (tstr[0] != '\0') + db_set_ws(0, ppro->m_szModuleName, szSetting, tstr); + else + db_unset(0, ppro->m_szModuleName, szSetting); +} + + +INT_PTR CALLBACK MinecraftDynmapAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) +{ + MinecraftDynmapProto *proto; + + switch (message) { + case WM_INITDIALOG: + TranslateDialogDefault(hwnd); + + proto = reinterpret_cast(lparam); + SetWindowLongPtr(hwnd, GWLP_USERDATA, lparam); + + LoadDBText(proto, hwnd, IDC_SERVER, MINECRAFTDYNMAP_KEY_SERVER); + LoadDBText(proto, hwnd, IDC_NAME, MINECRAFTDYNMAP_KEY_NAME); + return TRUE; + + case WM_COMMAND: + switch (LOWORD(wparam)) { + case IDC_NAME: + if (HIWORD(wparam) != EN_CHANGE || (HWND)lparam != GetFocus()) + return TRUE; + + SendMessage(GetParent(hwnd), PSM_CHANGED, 0, 0); + break; + } + break; + + case WM_NOTIFY: + if (reinterpret_cast(lparam)->code == PSN_APPLY) { + proto = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + StoreDBText(proto, hwnd, IDC_SERVER, MINECRAFTDYNMAP_KEY_SERVER); + StoreDBText(proto, hwnd, IDC_NAME, MINECRAFTDYNMAP_KEY_NAME); + + return TRUE; + } + break; + } + return FALSE; +} diff --git a/protocols/MinecraftDynmap/src/dialogs.h b/protocols/MinecraftDynmap/src/dialogs.h index 0af8617bae..f9d4586590 100644 --- a/protocols/MinecraftDynmap/src/dialogs.h +++ b/protocols/MinecraftDynmap/src/dialogs.h @@ -1,27 +1,27 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -void InitIcons(void); - -INT_PTR CALLBACK MinecraftDynmapAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +void InitIcons(void); + +INT_PTR CALLBACK MinecraftDynmapAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam); diff --git a/protocols/MinecraftDynmap/src/main.cpp b/protocols/MinecraftDynmap/src/main.cpp index 5aa5c93460..a108fc6244 100644 --- a/protocols/MinecraftDynmap/src/main.cpp +++ b/protocols/MinecraftDynmap/src/main.cpp @@ -1,90 +1,90 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -CMPlugin g_plugin; - -std::string g_strUserAgent; - -PLUGININFOEX pluginInfoEx = { - sizeof(PLUGININFOEX), - __PLUGIN_NAME, - PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), - __DESCRIPTION, - __AUTHOR, - __COPYRIGHT, - __AUTHORWEB, - UNICODE_AWARE, - // {40DA5EBD-4F2D-4BEA-841C-EAB77BEE6F4F} - { 0x40da5ebd, 0x4f2d, 0x4bea, 0x84, 0x1c, 0xea, 0xb7, 0x7b, 0xee, 0x6f, 0x4f } -}; - -CMPlugin::CMPlugin() : - ACCPROTOPLUGIN("MinecraftDynmap", pluginInfoEx) -{ - SetUniqueId("Nick"); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Interface information - -extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = {MIID_PROTOCOL, MIID_LAST}; - -///////////////////////////////////////////////////////////////////////////////////////// -// Load - -static HANDLE g_hEvents[1]; - -int CMPlugin::Load() -{ - InitIcons(); - - // Init native User-Agent - { - MFileVersion w; - Miranda_GetFileVersion(&w); - std::stringstream agent; - agent << "Miranda NG/" << w[0] << "." << w[1] << "." << w[2] << "." << w[3]; - #ifdef _WIN64 - agent << " Minecraft Dynmap Protocol x64/"; - #else - agent << " Minecraft Dynmap Protocol/"; - #endif - agent << __VERSION_STRING_DOTS; - g_strUserAgent = agent.str(); - } - - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Unload - -int CMPlugin::Unload() -{ - for (size_t i=0; i < _countof(g_hEvents); i++) - UnhookEvent(g_hEvents[i]); - - return 0; -} - +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +CMPlugin g_plugin; + +std::string g_strUserAgent; + +PLUGININFOEX pluginInfoEx = { + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + // {40DA5EBD-4F2D-4BEA-841C-EAB77BEE6F4F} + { 0x40da5ebd, 0x4f2d, 0x4bea, 0x84, 0x1c, 0xea, 0xb7, 0x7b, 0xee, 0x6f, 0x4f } +}; + +CMPlugin::CMPlugin() : + ACCPROTOPLUGIN("MinecraftDynmap", pluginInfoEx) +{ + SetUniqueId("Nick"); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Interface information + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = {MIID_PROTOCOL, MIID_LAST}; + +///////////////////////////////////////////////////////////////////////////////////////// +// Load + +static HANDLE g_hEvents[1]; + +int CMPlugin::Load() +{ + InitIcons(); + + // Init native User-Agent + { + MFileVersion w; + Miranda_GetFileVersion(&w); + std::stringstream agent; + agent << "Miranda NG/" << w[0] << "." << w[1] << "." << w[2] << "." << w[3]; + #ifdef _WIN64 + agent << " Minecraft Dynmap Protocol x64/"; + #else + agent << " Minecraft Dynmap Protocol/"; + #endif + agent << __VERSION_STRING_DOTS; + g_strUserAgent = agent.str(); + } + + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Unload + +int CMPlugin::Unload() +{ + for (size_t i=0; i < _countof(g_hEvents); i++) + UnhookEvent(g_hEvents[i]); + + return 0; +} + diff --git a/protocols/MinecraftDynmap/src/proto.cpp b/protocols/MinecraftDynmap/src/proto.cpp index e787b831da..47c05d5105 100644 --- a/protocols/MinecraftDynmap/src/proto.cpp +++ b/protocols/MinecraftDynmap/src/proto.cpp @@ -1,180 +1,180 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -MinecraftDynmapProto::MinecraftDynmapProto(const char* proto_name, const wchar_t* username) : - PROTO(proto_name, username), m_interval(0), hConnection(nullptr), hEventsConnection(nullptr), - m_updateRate(5000), m_nick("") -{ - this->signon_lock_ = CreateMutex(nullptr, FALSE, nullptr); - this->send_message_lock_ = CreateMutex(nullptr, FALSE, nullptr); - this->connection_lock_ = CreateMutex(nullptr, FALSE, nullptr); - this->events_loop_lock_ = CreateMutex(nullptr, FALSE, nullptr); - this->events_loop_event_ = CreateEvent(nullptr, FALSE, FALSE, nullptr); - - // Group chats - CreateProtoService(PS_JOINCHAT, &MinecraftDynmapProto::OnJoinChat); - CreateProtoService(PS_LEAVECHAT, &MinecraftDynmapProto::OnLeaveChat); - - CreateProtoService(PS_CREATEACCMGRUI, &MinecraftDynmapProto::SvcCreateAccMgrUI); - - HookProtoEvent(ME_GC_EVENT, &MinecraftDynmapProto::OnChatEvent); - - // Create standard network connection - NETLIBUSER nlu = {}; - nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; - nlu.szSettingsModule = m_szModuleName; - nlu.szDescriptiveName.w = m_tszUserName; - m_hNetlibUser = Netlib_RegisterUser(&nlu); - if (m_hNetlibUser == nullptr) { - wchar_t error[200]; - mir_snwprintf(error, TranslateT("Unable to initialize Netlib for %s."), m_tszUserName); - MessageBox(nullptr, error, L"Miranda NG", MB_OK | MB_ICONERROR); - } - - // Register group chat - GCREGISTER gcr = {}; - gcr.pszModule = m_szModuleName; - gcr.ptszDispName = m_tszUserName; - gcr.iMaxText = MINECRAFTDYNMAP_MESSAGE_LIMIT; - Chat_Register(&gcr); - - // Client instantiation - this->error_count_ = 0; - this->chatHandle_ = nullptr; - this->szRoomName = mir_utf8encodeW(username); -} - -MinecraftDynmapProto::~MinecraftDynmapProto() -{ - Netlib_CloseHandle(m_hNetlibUser); - - WaitForSingleObject(this->signon_lock_, IGNORE); - WaitForSingleObject(this->send_message_lock_, IGNORE); - WaitForSingleObject(this->connection_lock_, IGNORE); - WaitForSingleObject(this->events_loop_lock_, IGNORE); - - CloseHandle(this->signon_lock_); - CloseHandle(this->send_message_lock_); - CloseHandle(this->connection_lock_); - CloseHandle(this->events_loop_lock_); - CloseHandle(this->events_loop_event_); -} - -////////////////////////////////////////////////////////////////////////////// - -INT_PTR MinecraftDynmapProto::GetCaps(int type, MCONTACT) -{ - switch(type) { - case PFLAGNUM_1: - return PF1_CHAT; - case PFLAGNUM_2: - return PF2_ONLINE; - case PFLAG_MAXLENOFMESSAGE: - return MINECRAFTDYNMAP_MESSAGE_LIMIT; - case PFLAG_UNIQUEIDTEXT: - return (INT_PTR) TranslateT("Visible name"); - } - return 0; -} - -////////////////////////////////////////////////////////////////////////////// - -int MinecraftDynmapProto::SetStatus(int new_status) -{ - // Routing not supported statuses - switch (new_status) { - case ID_STATUS_OFFLINE: - case ID_STATUS_CONNECTING: - new_status = ID_STATUS_OFFLINE; - break; - default: - new_status = ID_STATUS_ONLINE; - break; - } - - m_iDesiredStatus = new_status; - - if (new_status == m_iStatus) { - return 0; - } - - if (m_iStatus == ID_STATUS_CONNECTING && new_status != ID_STATUS_OFFLINE) { - return 0; - } - - if (new_status == ID_STATUS_OFFLINE) { - ForkThread(&MinecraftDynmapProto::SignOffWorker, this); - } else { - ForkThread(&MinecraftDynmapProto::SignOnWorker, this); - } - return 0; -} - -////////////////////////////////////////////////////////////////////////////// -// EVENTS - -INT_PTR MinecraftDynmapProto::SvcCreateAccMgrUI(WPARAM, LPARAM lParam) -{ - return (INT_PTR)CreateDialogParam(g_plugin.getInst(),MAKEINTRESOURCE(IDD_MinecraftDynmapACCOUNT), (HWND)lParam, MinecraftDynmapAccountProc, (LPARAM)this); -} - -void MinecraftDynmapProto::OnShutdown() -{ - SetStatus(ID_STATUS_OFFLINE); -} - -void MinecraftDynmapProto::OnContactDeleted(MCONTACT) -{ - OnLeaveChat(NULL, NULL); -} - -////////////////////////////////////////////////////////////////////////////// -// HELPERS - -bool MinecraftDynmapProto::handleEntry(const std::string &method) -{ - debugLogA(" >> Entering %s()", method.c_str()); - return true; -} - -bool MinecraftDynmapProto::handleSuccess(const std::string &method) -{ - debugLogA(" << Quitting %s()", method.c_str()); - reset_error(); - return true; -} - -bool MinecraftDynmapProto::handleError(const std::string &method, const std::string &error, bool force_disconnect) -{ - increment_error(); - debugLogA("!!!!! Quitting %s() with error: %s", method.c_str(), !error.empty() ? error.c_str() : "Something went wrong"); - - if (!force_disconnect && error_count_ <= (UINT)db_get_b(0, m_szModuleName, MINECRAFTDYNMAP_KEY_TIMEOUTS_LIMIT, MINECRAFTDYNMAP_TIMEOUTS_LIMIT)) { - return true; - } - - reset_error(); - SetStatus(ID_STATUS_OFFLINE); - return false; -} +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +MinecraftDynmapProto::MinecraftDynmapProto(const char* proto_name, const wchar_t* username) : + PROTO(proto_name, username), m_interval(0), hConnection(nullptr), hEventsConnection(nullptr), + m_updateRate(5000), m_nick("") +{ + this->signon_lock_ = CreateMutex(nullptr, FALSE, nullptr); + this->send_message_lock_ = CreateMutex(nullptr, FALSE, nullptr); + this->connection_lock_ = CreateMutex(nullptr, FALSE, nullptr); + this->events_loop_lock_ = CreateMutex(nullptr, FALSE, nullptr); + this->events_loop_event_ = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + // Group chats + CreateProtoService(PS_JOINCHAT, &MinecraftDynmapProto::OnJoinChat); + CreateProtoService(PS_LEAVECHAT, &MinecraftDynmapProto::OnLeaveChat); + + CreateProtoService(PS_CREATEACCMGRUI, &MinecraftDynmapProto::SvcCreateAccMgrUI); + + HookProtoEvent(ME_GC_EVENT, &MinecraftDynmapProto::OnChatEvent); + + // Create standard network connection + NETLIBUSER nlu = {}; + nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szSettingsModule = m_szModuleName; + nlu.szDescriptiveName.w = m_tszUserName; + m_hNetlibUser = Netlib_RegisterUser(&nlu); + if (m_hNetlibUser == nullptr) { + wchar_t error[200]; + mir_snwprintf(error, TranslateT("Unable to initialize Netlib for %s."), m_tszUserName); + MessageBox(nullptr, error, L"Miranda NG", MB_OK | MB_ICONERROR); + } + + // Register group chat + GCREGISTER gcr = {}; + gcr.pszModule = m_szModuleName; + gcr.ptszDispName = m_tszUserName; + gcr.iMaxText = MINECRAFTDYNMAP_MESSAGE_LIMIT; + Chat_Register(&gcr); + + // Client instantiation + this->error_count_ = 0; + this->chatHandle_ = nullptr; + this->szRoomName = mir_utf8encodeW(username); +} + +MinecraftDynmapProto::~MinecraftDynmapProto() +{ + Netlib_CloseHandle(m_hNetlibUser); + + WaitForSingleObject(this->signon_lock_, IGNORE); + WaitForSingleObject(this->send_message_lock_, IGNORE); + WaitForSingleObject(this->connection_lock_, IGNORE); + WaitForSingleObject(this->events_loop_lock_, IGNORE); + + CloseHandle(this->signon_lock_); + CloseHandle(this->send_message_lock_); + CloseHandle(this->connection_lock_); + CloseHandle(this->events_loop_lock_); + CloseHandle(this->events_loop_event_); +} + +////////////////////////////////////////////////////////////////////////////// + +INT_PTR MinecraftDynmapProto::GetCaps(int type, MCONTACT) +{ + switch(type) { + case PFLAGNUM_1: + return PF1_CHAT; + case PFLAGNUM_2: + return PF2_ONLINE; + case PFLAG_MAXLENOFMESSAGE: + return MINECRAFTDYNMAP_MESSAGE_LIMIT; + case PFLAG_UNIQUEIDTEXT: + return (INT_PTR) TranslateT("Visible name"); + } + return 0; +} + +////////////////////////////////////////////////////////////////////////////// + +int MinecraftDynmapProto::SetStatus(int new_status) +{ + // Routing not supported statuses + switch (new_status) { + case ID_STATUS_OFFLINE: + case ID_STATUS_CONNECTING: + new_status = ID_STATUS_OFFLINE; + break; + default: + new_status = ID_STATUS_ONLINE; + break; + } + + m_iDesiredStatus = new_status; + + if (new_status == m_iStatus) { + return 0; + } + + if (m_iStatus == ID_STATUS_CONNECTING && new_status != ID_STATUS_OFFLINE) { + return 0; + } + + if (new_status == ID_STATUS_OFFLINE) { + ForkThread(&MinecraftDynmapProto::SignOffWorker, this); + } else { + ForkThread(&MinecraftDynmapProto::SignOnWorker, this); + } + return 0; +} + +////////////////////////////////////////////////////////////////////////////// +// EVENTS + +INT_PTR MinecraftDynmapProto::SvcCreateAccMgrUI(WPARAM, LPARAM lParam) +{ + return (INT_PTR)CreateDialogParam(g_plugin.getInst(),MAKEINTRESOURCE(IDD_MinecraftDynmapACCOUNT), (HWND)lParam, MinecraftDynmapAccountProc, (LPARAM)this); +} + +void MinecraftDynmapProto::OnShutdown() +{ + SetStatus(ID_STATUS_OFFLINE); +} + +void MinecraftDynmapProto::OnContactDeleted(MCONTACT) +{ + OnLeaveChat(NULL, NULL); +} + +////////////////////////////////////////////////////////////////////////////// +// HELPERS + +bool MinecraftDynmapProto::handleEntry(const std::string &method) +{ + debugLogA(" >> Entering %s()", method.c_str()); + return true; +} + +bool MinecraftDynmapProto::handleSuccess(const std::string &method) +{ + debugLogA(" << Quitting %s()", method.c_str()); + reset_error(); + return true; +} + +bool MinecraftDynmapProto::handleError(const std::string &method, const std::string &error, bool force_disconnect) +{ + increment_error(); + debugLogA("!!!!! Quitting %s() with error: %s", method.c_str(), !error.empty() ? error.c_str() : "Something went wrong"); + + if (!force_disconnect && error_count_ <= (UINT)db_get_b(0, m_szModuleName, MINECRAFTDYNMAP_KEY_TIMEOUTS_LIMIT, MINECRAFTDYNMAP_TIMEOUTS_LIMIT)) { + return true; + } + + reset_error(); + SetStatus(ID_STATUS_OFFLINE); + return false; +} diff --git a/protocols/MinecraftDynmap/src/proto.h b/protocols/MinecraftDynmap/src/proto.h index b863b2547c..852edaef74 100644 --- a/protocols/MinecraftDynmap/src/proto.h +++ b/protocols/MinecraftDynmap/src/proto.h @@ -1,136 +1,136 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -class MinecraftDynmapProto : public PROTO -{ - ptrA szRoomName; - -public: - MinecraftDynmapProto(const char *proto_name, const wchar_t *username); - ~MinecraftDynmapProto(); - - inline const char* ModuleName() const { - return m_szModuleName; - } - - inline bool isOnline() { - return (m_iStatus != ID_STATUS_OFFLINE && m_iStatus != ID_STATUS_CONNECTING); - } - - inline bool isOffline() { - return (m_iStatus == ID_STATUS_OFFLINE); - } - - // PROTO_INTERFACE - INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; - int SetStatus(int iNewStatus) override; - - void OnContactDeleted(MCONTACT) override; - void OnShutdown() override; - - // Services - INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); - - // Chat handling - int __cdecl OnChatEvent(WPARAM,LPARAM); - INT_PTR __cdecl OnJoinChat(WPARAM,LPARAM); - INT_PTR __cdecl OnLeaveChat(WPARAM,LPARAM); - - // Loops - void __cdecl EventsLoop(void*); - - // Worker threads - void __cdecl SignOnWorker(void*); - void __cdecl SignOffWorker(void*); - void __cdecl SendMsgWorker(void*); - - // Chat handling - void UpdateChat(const char *name, const char *message, const time_t timestamp = time(0), bool addtochat = true); - void AddChatContact(const char *nick); - void DeleteChatContact(const char *name); - void SetChatStatus(int); - void ClearChat(); - void SetTopic(const char *topic); - MCONTACT GetChatHandle(); - - // Locks - HANDLE signon_lock_; - HANDLE connection_lock_; - HANDLE send_message_lock_; - HANDLE events_loop_lock_; - - HANDLE events_loop_event_; - - // Handles - HNETLIBCONN hConnection; - HNETLIBCONN hEventsConnection; - HANDLE chatHandle_; - - // Data storage - void store_headers(http::response *resp, NETLIBHTTPHEADER *headers, int headers_count); - - std::string get_server(bool not_last = false); - std::string get_language(); - - // Connection handling - unsigned int error_count_; - - // Helpers - bool handleEntry(const std::string &method); - bool handleSuccess(const std::string &method); - bool handleError(const std::string &method, const std::string &error = "", bool force_disconnect = false); - - void __inline increment_error() { error_count_++; } - void __inline decrement_error() { if (error_count_ > 0) error_count_--; } - void __inline reset_error() { error_count_ = 0; } - - // HTTP communication - http::response sendRequest(const int request_type, std::string *post_data = nullptr, std::string *get_data = nullptr); - std::string chooseAction(int, std::string *get_data = nullptr); - NETLIBHTTPHEADER *get_request_headers(int request_type, int *headers_count); - - // Requests and processing - bool doSignOn(); - bool doEvents(); - bool doSendMessage(const std::string &message_text); - - std::string doGetPage(int); - - // Configuration - std::string m_nick; - std::string m_cookie; - std::string m_server; - std::string m_title; - std::string m_timestamp; - int m_interval; - int m_updateRate; -}; - -struct CMPlugin : public ACCPROTOPLUGIN -{ - CMPlugin(); - - int Load() override; - int Unload() override; -}; +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +class MinecraftDynmapProto : public PROTO +{ + ptrA szRoomName; + +public: + MinecraftDynmapProto(const char *proto_name, const wchar_t *username); + ~MinecraftDynmapProto(); + + inline const char* ModuleName() const { + return m_szModuleName; + } + + inline bool isOnline() { + return (m_iStatus != ID_STATUS_OFFLINE && m_iStatus != ID_STATUS_CONNECTING); + } + + inline bool isOffline() { + return (m_iStatus == ID_STATUS_OFFLINE); + } + + // PROTO_INTERFACE + INT_PTR GetCaps(int type, MCONTACT hContact = NULL) override; + int SetStatus(int iNewStatus) override; + + void OnContactDeleted(MCONTACT) override; + void OnShutdown() override; + + // Services + INT_PTR __cdecl SvcCreateAccMgrUI(WPARAM, LPARAM); + + // Chat handling + int __cdecl OnChatEvent(WPARAM,LPARAM); + INT_PTR __cdecl OnJoinChat(WPARAM,LPARAM); + INT_PTR __cdecl OnLeaveChat(WPARAM,LPARAM); + + // Loops + void __cdecl EventsLoop(void*); + + // Worker threads + void __cdecl SignOnWorker(void*); + void __cdecl SignOffWorker(void*); + void __cdecl SendMsgWorker(void*); + + // Chat handling + void UpdateChat(const char *name, const char *message, const time_t timestamp = time(0), bool addtochat = true); + void AddChatContact(const char *nick); + void DeleteChatContact(const char *name); + void SetChatStatus(int); + void ClearChat(); + void SetTopic(const char *topic); + MCONTACT GetChatHandle(); + + // Locks + HANDLE signon_lock_; + HANDLE connection_lock_; + HANDLE send_message_lock_; + HANDLE events_loop_lock_; + + HANDLE events_loop_event_; + + // Handles + HNETLIBCONN hConnection; + HNETLIBCONN hEventsConnection; + HANDLE chatHandle_; + + // Data storage + void store_headers(http::response *resp, NETLIBHTTPHEADER *headers, int headers_count); + + std::string get_server(bool not_last = false); + std::string get_language(); + + // Connection handling + unsigned int error_count_; + + // Helpers + bool handleEntry(const std::string &method); + bool handleSuccess(const std::string &method); + bool handleError(const std::string &method, const std::string &error = "", bool force_disconnect = false); + + void __inline increment_error() { error_count_++; } + void __inline decrement_error() { if (error_count_ > 0) error_count_--; } + void __inline reset_error() { error_count_ = 0; } + + // HTTP communication + http::response sendRequest(const int request_type, std::string *post_data = nullptr, std::string *get_data = nullptr); + std::string chooseAction(int, std::string *get_data = nullptr); + NETLIBHTTPHEADER *get_request_headers(int request_type, int *headers_count); + + // Requests and processing + bool doSignOn(); + bool doEvents(); + bool doSendMessage(const std::string &message_text); + + std::string doGetPage(int); + + // Configuration + std::string m_nick; + std::string m_cookie; + std::string m_server; + std::string m_title; + std::string m_timestamp; + int m_interval; + int m_updateRate; +}; + +struct CMPlugin : public ACCPROTOPLUGIN +{ + CMPlugin(); + + int Load() override; + int Unload() override; +}; diff --git a/protocols/MinecraftDynmap/src/stdafx.cxx b/protocols/MinecraftDynmap/src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/MinecraftDynmap/src/stdafx.cxx +++ b/protocols/MinecraftDynmap/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/MinecraftDynmap/src/stdafx.h b/protocols/MinecraftDynmap/src/stdafx.h index 929267ae60..f5cc740bca 100644 --- a/protocols/MinecraftDynmap/src/stdafx.h +++ b/protocols/MinecraftDynmap/src/stdafx.h @@ -1,70 +1,70 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -#pragma warning(disable:4996) - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "version.h" - -class MinecraftDynmapProto; - -#include "utils.h" -#include "proto.h" -#include "constants.h" -#include "dialogs.h" -#include "resource.h" - -extern std::string g_strUserAgent; +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +#pragma warning(disable:4996) + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "version.h" + +class MinecraftDynmapProto; + +#include "utils.h" +#include "proto.h" +#include "constants.h" +#include "dialogs.h" +#include "resource.h" + +extern std::string g_strUserAgent; diff --git a/protocols/MinecraftDynmap/src/utils.h b/protocols/MinecraftDynmap/src/utils.h index e4f3d3ff37..c42fb86d9e 100644 --- a/protocols/MinecraftDynmap/src/utils.h +++ b/protocols/MinecraftDynmap/src/utils.h @@ -1,63 +1,63 @@ -/* - -Minecraft Dynmap plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2015-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#pragma once - -// DB macros -#define getU8String(setting, dest) db_get_utf(NULL, m_szModuleName, setting, dest) -#define setU8String(setting, value) db_set_utf(NULL, m_szModuleName, setting, value) - -// HTTP constants and object -#define HTTP_CODE_FAKE_DISCONNECTED 0 -#define HTTP_CODE_FAKE_ERROR 1 - -namespace http -{ - struct response - { - response() : code(0) {} - int code; - std::map< std::string, std::string > headers; - std::string data; - }; -} - -class ScopedLock -{ -public: - ScopedLock(HANDLE h) : handle_(h) - { - WaitForSingleObject(handle_,INFINITE); - } - ~ScopedLock() - { - if (handle_) - ReleaseMutex(handle_); - } - void Unlock() - { - ReleaseMutex(handle_); - handle_ = nullptr; - } -private: - HANDLE handle_; -}; +/* + +Minecraft Dynmap plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2015-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#pragma once + +// DB macros +#define getU8String(setting, dest) db_get_utf(NULL, m_szModuleName, setting, dest) +#define setU8String(setting, value) db_set_utf(NULL, m_szModuleName, setting, value) + +// HTTP constants and object +#define HTTP_CODE_FAKE_DISCONNECTED 0 +#define HTTP_CODE_FAKE_ERROR 1 + +namespace http +{ + struct response + { + response() : code(0) {} + int code; + std::map< std::string, std::string > headers; + std::string data; + }; +} + +class ScopedLock +{ +public: + ScopedLock(HANDLE h) : handle_(h) + { + WaitForSingleObject(handle_,INFINITE); + } + ~ScopedLock() + { + if (handle_) + ReleaseMutex(handle_); + } + void Unlock() + { + ReleaseMutex(handle_); + handle_ = nullptr; + } +private: + HANDLE handle_; +}; diff --git a/protocols/MinecraftDynmap/src/version.h b/protocols/MinecraftDynmap/src/version.h index fa021b8cae..1e71a749da 100644 --- a/protocols/MinecraftDynmap/src/version.h +++ b/protocols/MinecraftDynmap/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Minecraft Dynmap support for Miranda NG." #define __AUTHOR "Robert Pösel" #define __AUTHORWEB "https://miranda-ng.org/p/MinecraftDynmap" -#define __COPYRIGHT "© 2015-17 Robert Pösel, 2017-22 Miranda NG team" +#define __COPYRIGHT "© 2015-17 Robert Pösel, 2017-23 Miranda NG team" diff --git a/protocols/NewsAggregator/Src/stdafx.cxx b/protocols/NewsAggregator/Src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/NewsAggregator/Src/stdafx.cxx +++ b/protocols/NewsAggregator/Src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/NewsAggregator/Src/version.h b/protocols/NewsAggregator/Src/version.h index 8258430e90..e9d19559e7 100644 --- a/protocols/NewsAggregator/Src/version.h +++ b/protocols/NewsAggregator/Src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "RSS/Atom news aggregator." #define __AUTHOR "Mataes, FREAK_THEMIGHTY" #define __AUTHORWEB "https://miranda-ng.org/p/NewsAggregator" -#define __COPYRIGHT "© 2012-22 Mataes, FREAK_THEMIGHTY" +#define __COPYRIGHT "© 2012-23 Mataes, FREAK_THEMIGHTY" diff --git a/protocols/Non-IM Contact/src/stdafx.cxx b/protocols/Non-IM Contact/src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/Non-IM Contact/src/stdafx.cxx +++ b/protocols/Non-IM Contact/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/Omegle/src/chat.cpp b/protocols/Omegle/src/chat.cpp index 1e732b8004..484d7b0e34 100644 --- a/protocols/Omegle/src/chat.cpp +++ b/protocols/Omegle/src/chat.cpp @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/client.h b/protocols/Omegle/src/client.h index ae3da3ca3a..1a34b20629 100644 --- a/protocols/Omegle/src/client.h +++ b/protocols/Omegle/src/client.h @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/communication.cpp b/protocols/Omegle/src/communication.cpp index f033cd93a6..dfd1dc4dd4 100644 --- a/protocols/Omegle/src/communication.cpp +++ b/protocols/Omegle/src/communication.cpp @@ -1,762 +1,762 @@ -/* - -Omegle plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "stdafx.h" - -http::response Omegle_client::flap(const int request_type, std::string *post_data, std::string *get_data) -{ - http::response resp; - - // Prepare the request - NETLIBHTTPREQUEST nlhr = { sizeof(NETLIBHTTPREQUEST) }; - - // Set request URL - std::string url = choose_server(request_type) + choose_action(request_type, get_data); - nlhr.szUrl = (char*)url.c_str(); - - // Set timeout (bigger for channel request) - nlhr.timeout = 1000 * ((request_type == OMEGLE_REQUEST_EVENTS) ? 65 : 20); - - // Set request type (GET/POST) and eventually also POST data - if (post_data != nullptr) { - nlhr.requestType = REQUEST_POST; - nlhr.pData = (char*)(*post_data).c_str(); - nlhr.dataLength = (int)post_data->length(); - } - else { - nlhr.requestType = REQUEST_GET; - } - - // Set headers - it depends on requestType so it must be after setting that - nlhr.headers = get_request_headers(nlhr.requestType, &nlhr.headersCount); - - // Set flags - nlhr.flags = NLHRF_HTTP11; - -#ifdef _DEBUG - nlhr.flags |= NLHRF_DUMPASTEXT; -#else - nlhr.flags |= NLHRF_NODUMP; -#endif - - // Set persistent connection (or not) - switch (request_type) - { - case OMEGLE_REQUEST_HOME: - nlhr.nlc = nullptr; - break; - - case OMEGLE_REQUEST_EVENTS: - nlhr.nlc = hEventsConnection; - nlhr.flags |= NLHRF_PERSISTENT; - break; - - default: - WaitForSingleObject(connection_lock_, INFINITE); - nlhr.nlc = hConnection; - nlhr.flags |= NLHRF_PERSISTENT; - break; - } - - parent->debugLogA("@@@@@ Sending request to '%s'", nlhr.szUrl); - - // Send the request - NLHR_PTR pnlhr(Netlib_HttpTransaction(handle_, &nlhr)); - - mir_free(nlhr.headers); - - // Remember the persistent connection handle (or not) - switch (request_type) - { - case OMEGLE_REQUEST_HOME: - break; - - case OMEGLE_REQUEST_EVENTS: - hEventsConnection = pnlhr ? pnlhr->nlc : nullptr; - break; - - default: - ReleaseMutex(connection_lock_); - hConnection = pnlhr ? pnlhr->nlc : nullptr; - break; - } - - // Check and copy response data - if (pnlhr != nullptr) - { - parent->debugLogA("@@@@@ Got response with code %d", pnlhr->resultCode); - store_headers(&resp, pnlhr->headers, pnlhr->headersCount); - resp.code = pnlhr->resultCode; - resp.data = pnlhr->pData ? pnlhr->pData : ""; - - // This error code gives us some error page, but we don't need that, so clear the response - if (pnlhr->resultCode == HTTP_CODE_WEB_SERVER_IS_DOWN) { - resp.data = ""; - } - - parent->debugLogA("&&&&& Got response: %s", resp.data.c_str()); - } - else { - parent->debugLogA("!!!!! No response from server (time-out)"); - resp.code = HTTP_CODE_FAKE_DISCONNECTED; - // Better to have something set explicitely as this value is compaired in all communication requests - } - - return resp; -} - -bool Omegle_client::handle_entry(const std::string &method) -{ - parent->debugLogA(" >> Entering %s()", method.c_str()); - return true; -} - -bool Omegle_client::handle_success(const std::string &method) -{ - parent->debugLogA(" << Quitting %s()", method.c_str()); - reset_error(); - return true; -} - -bool Omegle_client::handle_error(const std::string &method, bool force_disconnect) -{ - bool result; - increment_error(); - parent->debugLogA("!!!!! %s(): Something with Omegle went wrong", method.c_str()); - - if (force_disconnect) - result = false; - else if (error_count_ <= (UINT)db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_TIMEOUTS_LIMIT, OMEGLE_TIMEOUTS_LIMIT)) - result = true; - else - result = false; - - if (result == false) - { - reset_error(); - parent->UpdateChat(nullptr, TranslateT("Connection error.")); - parent->StopChat(false); - } - - return result; -} - -////////////////////////////////////////////////////////////////////////////// - -std::string Omegle_client::get_server(bool not_last) -{ - int q = not_last ? 1 : 0; - - int server = db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_SERVER, 0); - if (server < 0 || server >= (int)(_countof(servers) - q)) - server = 0; - - if (server == 0) { - srand(::time(0)); - server = (rand() % (_countof(servers) - 1 - q)) + 1; - } - - return servers[server]; -} - -std::string Omegle_client::get_language() -{ - int language = db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_LANGUAGE, 0); - if (language < 0 || language >= (_countof(languages))) - language = 0; - - return language > 0 ? languages[language].id : "en"; -} - -std::string Omegle_client::choose_server(int request_type) -{ - switch (request_type) - { - case OMEGLE_REQUEST_HOME: - return OMEGLE_SERVER_REGULAR; - - /* case OMEGLE_REQUEST_START: - case OMEGLE_REQUEST_STOP: - case OMEGLE_REQUEST_SEND: - case OMEGLE_REQUEST_EVENTS: - case OMEGLE_REQUEST_TYPING_START: - case OMEGLE_REQUEST_TYPING_STOP: - case OMEGLE_REQUEST_RECAPTCHA: - case OMEGLE_REQUEST_COUNT: - */ default: - std::string server = OMEGLE_SERVER_CHAT; - utils::text::replace_first(&server, "%s", this->server_); - return server; - } -} - -std::string Omegle_client::choose_action(int request_type, std::string* get_data) -{ - switch (request_type) - { - case OMEGLE_REQUEST_START: - { - std::string action = "/start?caps=recaptcha2,t&rcs=1&spid=&lang="; - action += get_language(); - if (get_data != nullptr) - action += (*get_data); - - return action; - } - - case OMEGLE_REQUEST_STOP: - return "/disconnect"; - - case OMEGLE_REQUEST_SEND: - return "/send"; - - case OMEGLE_REQUEST_EVENTS: - return "/events"; - - case OMEGLE_REQUEST_TYPING_START: - return "/typing"; - - case OMEGLE_REQUEST_TYPING_STOP: - return "/stoppedtyping"; - - case OMEGLE_REQUEST_RECAPTCHA: - return "/recaptcha"; - - case OMEGLE_REQUEST_COUNT: - return "/count"; - - // "/stoplookingforcommonlikes" - - /* case OMEGLE_REQUEST_HOME: - */ default: - return "/"; - } -} - - -NETLIBHTTPHEADER* Omegle_client::get_request_headers(int request_type, int* headers_count) -{ - if (request_type == REQUEST_POST) - *headers_count = 4; - else - *headers_count = 3; - - NETLIBHTTPHEADER *headers = (NETLIBHTTPHEADER*)mir_calloc(sizeof(NETLIBHTTPHEADER)*(*headers_count)); - - if (request_type == REQUEST_POST) { - headers[3].szName = "Content-Type"; - headers[3].szValue = "application/x-www-form-urlencoded; charset=utf-8"; - } - - headers[2].szName = "User-Agent"; - headers[2].szValue = Netlib_GetUserAgent(); - headers[1].szName = "Accept"; - headers[1].szValue = "*/*"; - headers[0].szName = "Accept-Language"; - headers[0].szValue = "en,en-US;q=0.9"; - - return headers; -} - -void Omegle_client::store_headers(http::response* resp, NETLIBHTTPHEADER* headers, int headersCount) -{ - for (size_t i = 0; i < (size_t)headersCount; i++) - { - std::string header_name = headers[i].szName; - std::string header_value = headers[i].szValue; - - // TODO RM: (un)comment - //parent->debugLogA("----- Got header '%s': %s", header_name.c_str(), header_value.c_str()); - resp->headers[header_name] = header_value; - } -} - -////////////////////////////////////////////////////////////////////////////// - -bool Omegle_client::start() -{ - HANDLE_ENTRY; - - this->server_ = get_server(); - //parent->debugLogA("Chosing server %s", this->server_.c_str()); - //std::string log = Translate("Chosing server: ") + this->server_; - //parent->UpdateChat(NULL, log.c_str()); - - std::string data; - - if (this->spy_mode_) { - //// get last server from list, which is for spy mode - //this->server_ = servers[_countof(servers)-1]; - - if (this->question_.empty()) { - data = "&wantsspy=1"; - } - else { - data = "&ask=" + utils::url::encode(this->question_); - data += "&cansavequestion="; - data += db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_REUSE_QUESTION, 0) ? "1" : "0"; - } - } - else if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_MEET_COMMON, 0)) - { - DBVARIANT dbv; - if (!db_get_utf(NULL, parent->m_szModuleName, OMEGLE_KEY_INTERESTS, &dbv)) - { - std::string topics = dbv.pszVal; - std::string topic; - - db_free(&dbv); - - std::string::size_type pos = 0; - std::string::size_type pos2 = 0; - while ((pos2 = topics.find(",", pos)) != std::string::npos) { - topic = topics.substr(pos, pos2 - pos); - topic = utils::text::trim(topic); - - if (!topic.empty()) { - if (pos > 0) - data += ","; - - data += "\"" + topic + "\""; - } - - pos = pos2 + 1; - } - - topic = topics.substr(pos); - topic = utils::text::trim(topic); - if (!topic.empty()) { - if (pos > 0) - data += ","; - data += "\"" + topic + "\""; - } - - parent->debugLogA("TOPICS: %s", data.c_str()); - - if (!data.empty()) { - data = "[" + data + "]"; - data = "&topics=" + utils::url::encode(data); - } - - //// get any server but last, which is for spy mode - //this->server_ = get_server(true); - } - } - - if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_SERVER_INFO, 0)) - { - std::string count = get_page(OMEGLE_REQUEST_COUNT); - if (!count.empty()) { - char str[255]; - mir_snprintf(str, Translate("Connected to server %s. There are %s users online now."), server_.c_str(), count.c_str()); - - wchar_t *msg = mir_a2u(str); - parent->UpdateChat(nullptr, msg); - mir_free(msg); - } - } - else { - char str[255]; - mir_snprintf(str, Translate("Connected to server %s."), server_.c_str()); - - wchar_t *msg = mir_a2u(str); - parent->UpdateChat(nullptr, msg); - mir_free(msg); - } - - // Send validation - http::response resp = flap(OMEGLE_REQUEST_START, nullptr, &data); - - switch (resp.code) - { - case HTTP_CODE_FAKE_DISCONNECTED: - { - // If is is only timeout error, try login once more - if (HANDLE_ERROR(false)) - return start(); - else - return false; - } - - case HTTP_CODE_OK: - { - if (!resp.data.empty()) { - this->chat_id_ = resp.data.substr(1, resp.data.length() - 2); - this->state_ = STATE_WAITING; - - return HANDLE_SUCCESS; - } - else { - return HANDLE_ERROR(FORCE_DISCONNECT); - } - } - - default: - return HANDLE_ERROR(FORCE_DISCONNECT); - } -} - -bool Omegle_client::stop() -{ - if (parent->isOffline()) - return true; - - HANDLE_ENTRY; - - std::string data = "id=" + this->chat_id_; - - http::response resp = flap(OMEGLE_REQUEST_STOP, &data); - - Netlib_Shutdown(hConnection); - Netlib_Shutdown(hEventsConnection); - - return (resp.data == "win") ? HANDLE_SUCCESS : HANDLE_ERROR(false); -} - -bool Omegle_client::events() -{ - HANDLE_ENTRY; - - std::string post_data = "id=" + this->chat_id_; - - // Get update - http::response resp = flap(OMEGLE_REQUEST_EVENTS, &post_data); - - // Return - switch (resp.code) - { - case HTTP_CODE_OK: - { - if (resp.data == "null") { - // Everything is OK, no new message received -- OR it is a problem - // TODO: if we are waiting for Stranger with common likes, then we should try standard Stranger if this takes too long - return HANDLE_ERROR(false); - } - else if (resp.data == "fail") { - // Something went wrong - return HANDLE_ERROR(false); - } - - JSONROOT root(resp.data.c_str()); - if (root == nullptr) - return HANDLE_ERROR(false); - - bool newStranger = false; - bool waiting = false; - - for (size_t i = 0; i < json_size(root); i++) { - JSONNode *item = json_at(root, i); - if (item == nullptr) - continue; - - std::string name = _T2A(json_as_string(json_at(item, 0))); - - if (name == "waiting") { - // We are just waiting for new Stranger - waiting = true; - } - else if (name == "identDigests") { - // We get some comma separated hashes, I'm not sure what for - } - else if (name == "statusInfo") { - JSONNode *data = json_at(item, 1); - - // We got some object as second parameter - //data["antinudepercent"]; // probably 1 by default - //data["antinudeservers"]; // array of server names, like "waw3.omegle.com" - //data["rtmfp"]; // some rtmfp protocol address - //data["servers"]; // array of server names, like "front5.omegle.com" - //data["spyeeQueueTime"]; // some float number, e.g. 0.0701999903 - //data["spyQueueTime"]; // some float number, e.g. 4.7505000114 - //data["timestamp"]; // e.g. 1445336566.0196209 - - // We got info about count of connected people there - ptrW count(json_as_string(json_get(data, "count"))); - wchar_t strT[255]; - mir_snwprintf(strT, TranslateT("On whole Omegle are %s strangers online now."), count.get()); - - parent->UpdateChat(nullptr, strT); - } - else if (name == "serverMessage") { - ptrW message(json_as_string(json_at(item, 1))); - parent->UpdateChat(nullptr, TranslateW(message)); - } - else if (name == "connected") { - // Stranger connected - if (this->spy_mode_ && !this->question_.empty()) { - parent->AddChatContact(TranslateT("Stranger 1")); - parent->AddChatContact(TranslateT("Stranger 2")); - this->state_ = STATE_SPY; - } - else { - parent->AddChatContact(TranslateT("Stranger")); - this->state_ = STATE_ACTIVE; - } - - newStranger = true; - waiting = false; - } - else if (name == "commonLikes") { - std::wstring likes = TranslateT("You and the Stranger both like: "); - - JSONNode *items = json_at(item, 1); - size_t size = json_size(items); - for (size_t j = 0; j < size; j++) { - likes += ptrW(json_as_string(json_at(items, j))); - if (j < size - 1) - likes += L", "; - } - - parent->debugLogW(L"Got common likes: '%s'", likes.c_str()); - parent->SetTopic(likes.c_str()); - } - else if (name == "question") { - ptrW question(json_as_string(json_at(item, 1))); - parent->SetTopic(question); - } - else if (name == "typing" || name == "spyTyping") { - // Stranger is typing, not supported by chat module yet - Skin_PlaySound("StrangerTyp"); - - ptrW who(name == "spyTyping" ? json_as_string(json_at(item, 1)) : mir_wstrdup(L"Stranger")); - Srmm_SetStatusText(parent->GetChatHandle(), CMStringW(FORMAT, TranslateT("%s is typing."), TranslateW(who)), g_plugin.getIcon(IDI_TYPING_ON)); - } - else if (name == "stoppedTyping" || name == "spyStoppedTyping") { - // Stranger stopped typing, not supported by chat module yet - Skin_PlaySound("StrangerTypStop"); - - ptrW who(name == "spyTyping" ? json_as_string(json_at(item, 1)) : mir_wstrdup(L"Stranger")); - Srmm_SetStatusText(parent->GetChatHandle(), CMStringW(FORMAT, TranslateT("%s stopped typing."), TranslateW(who)), g_plugin.getIcon(IDI_TYPING_OFF)); - } - else if (name == "gotMessage") { - Srmm_SetStatusText(parent->GetChatHandle(), nullptr); - - // Play sound as we received message - Skin_PlaySound("StrangerMessage"); - - if (state_ == STATE_ACTIVE) { - ptrW msg(json_as_string(json_at(item, 1))); - parent->UpdateChat(TranslateT("Stranger"), msg); - } - } - else if (name == "spyMessage") { - Srmm_SetStatusText(parent->GetChatHandle(), nullptr); - - // Play sound as we received message - Skin_PlaySound("StrangerMessage"); - - if (state_ == STATE_SPY) { - ptrW stranger(json_as_string(json_at(item, 1))); - ptrW msg(json_as_string(json_at(item, 2))); - parent->UpdateChat(stranger, msg); - } - } - else if (name == "strangerDisconnected") { - Srmm_SetStatusText(parent->GetChatHandle(), nullptr); - - // Stranger disconnected - if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_DONT_STOP, 0)) - { - Skin_PlaySound("StrangerChange"); - parent->NewChat(); - } - else - parent->StopChat(false); - } - else if (name == "spyDisconnected") { - Srmm_SetStatusText(parent->GetChatHandle(), nullptr); - - ptrW stranger(json_as_string(json_at(item, 1))); - - wchar_t strT[255]; - mir_snwprintf(strT, TranslateT("%s disconnected."), TranslateW(stranger)); - parent->UpdateChat(nullptr, strT); - - // Stranger disconnected - if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_DONT_STOP, 0)) - { - Skin_PlaySound("StrangerChange"); - parent->NewChat(); - } - else - parent->StopChat(false); - } - else if (name == "recaptchaRequired") { - // Nothing to do with recaptcha - parent->UpdateChat(nullptr, TranslateT("Recaptcha is required.\nOpen http://omegle.com , solve Recaptcha and try again.")); - parent->StopChat(false); - } - else if (name == "recaptchaRejected") { - // Nothing to do with recaptcha - parent->StopChat(false); - } - else if (name == "error") { - ptrW error(json_as_string(json_at(item, 1))); - - wchar_t strT[255]; - mir_snwprintf(strT, TranslateT("Error: %s"), TranslateW(error)); - parent->UpdateChat(nullptr, strT); - } - } - - if (newStranger && !spy_mode_) { - // We got new stranger in this event, lets say him "Hi message" if enabled - if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_HI_ENABLED, 0)) { - DBVARIANT dbv; - if (!db_get_utf(NULL, parent->m_szModuleName, OMEGLE_KEY_HI, &dbv)) { - std::vector messages; - utils::text::explode(std::string(dbv.pszVal), "\r\n", &messages); - db_free(&dbv); - - int pos = rand() % messages.size(); - std::string *message = new std::string(messages.at(pos)); - - parent->debugLogA("**Chat - saying Hi! message"); - parent->ForkThread(&OmegleProto::SendMsgWorker, message); - } - else parent->debugLogA("**Chat - Hi message is enabled but not used"); - } - } - - if (waiting) { - // If we are only waiting in this event... - parent->UpdateChat(nullptr, TranslateT("We are still waiting...")); - } - - return HANDLE_SUCCESS; - } - - case HTTP_CODE_FAKE_DISCONNECTED: - // timeout - return HANDLE_SUCCESS; - - default: - return HANDLE_ERROR(false); - } -} - -bool Omegle_client::send_message(const std::string &message_text) -{ - HANDLE_ENTRY; - - std::string data = "msg=" + utils::url::encode(message_text); - data += "&id=" + this->chat_id_; - - http::response resp = flap(OMEGLE_REQUEST_SEND, &data); - - switch (resp.code) - { - case HTTP_CODE_OK: - if (resp.data == "win") { - return HANDLE_SUCCESS; - } - - case HTTP_CODE_FAKE_DISCONNECTED: - default: - return HANDLE_ERROR(false); - } -} - -bool Omegle_client::typing_start() -{ - HANDLE_ENTRY; - - std::string data = "id=" + this->chat_id_; - - http::response resp = flap(OMEGLE_REQUEST_TYPING_START, &data); - - switch (resp.code) - { - case HTTP_CODE_OK: - if (resp.data == "win") { - return HANDLE_SUCCESS; - } - - case HTTP_CODE_FAKE_DISCONNECTED: - default: - return HANDLE_ERROR(false); - } -} - -bool Omegle_client::typing_stop() -{ - HANDLE_ENTRY; - - std::string data = "id=" + this->chat_id_; - - http::response resp = flap(OMEGLE_REQUEST_TYPING_STOP, &data); - - switch (resp.code) - { - case HTTP_CODE_OK: - if (resp.data == "win") { - return HANDLE_SUCCESS; - } - - case HTTP_CODE_FAKE_DISCONNECTED: - default: - return HANDLE_ERROR(false); - } -} - -bool Omegle_client::recaptcha() -{ - // TODO: Implement! - - HANDLE_ENTRY; - - // data:{id:this.clientID,challenge:b,response:a}} - //std::string data = "?id=...&challenge= ..., &response= ..."; - - http::response resp = flap(OMEGLE_REQUEST_RECAPTCHA); - - switch (resp.code) - { - case HTTP_CODE_OK: - /* if (resp.data == "win") { - return handle_success( "typing_start" ); - }*/ - - case HTTP_CODE_FAKE_DISCONNECTED: - default: - return HANDLE_ERROR(false); - } -} - -std::string Omegle_client::get_page(const int request_type) -{ - HANDLE_ENTRY; - - http::response resp = flap(request_type); - - switch (resp.code) - { - case HTTP_CODE_OK: - HANDLE_SUCCESS; - break; - - case HTTP_CODE_FAKE_DISCONNECTED: - default: - HANDLE_ERROR(false); - } - - return resp.data; -} +/* + +Omegle plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "stdafx.h" + +http::response Omegle_client::flap(const int request_type, std::string *post_data, std::string *get_data) +{ + http::response resp; + + // Prepare the request + NETLIBHTTPREQUEST nlhr = { sizeof(NETLIBHTTPREQUEST) }; + + // Set request URL + std::string url = choose_server(request_type) + choose_action(request_type, get_data); + nlhr.szUrl = (char*)url.c_str(); + + // Set timeout (bigger for channel request) + nlhr.timeout = 1000 * ((request_type == OMEGLE_REQUEST_EVENTS) ? 65 : 20); + + // Set request type (GET/POST) and eventually also POST data + if (post_data != nullptr) { + nlhr.requestType = REQUEST_POST; + nlhr.pData = (char*)(*post_data).c_str(); + nlhr.dataLength = (int)post_data->length(); + } + else { + nlhr.requestType = REQUEST_GET; + } + + // Set headers - it depends on requestType so it must be after setting that + nlhr.headers = get_request_headers(nlhr.requestType, &nlhr.headersCount); + + // Set flags + nlhr.flags = NLHRF_HTTP11; + +#ifdef _DEBUG + nlhr.flags |= NLHRF_DUMPASTEXT; +#else + nlhr.flags |= NLHRF_NODUMP; +#endif + + // Set persistent connection (or not) + switch (request_type) + { + case OMEGLE_REQUEST_HOME: + nlhr.nlc = nullptr; + break; + + case OMEGLE_REQUEST_EVENTS: + nlhr.nlc = hEventsConnection; + nlhr.flags |= NLHRF_PERSISTENT; + break; + + default: + WaitForSingleObject(connection_lock_, INFINITE); + nlhr.nlc = hConnection; + nlhr.flags |= NLHRF_PERSISTENT; + break; + } + + parent->debugLogA("@@@@@ Sending request to '%s'", nlhr.szUrl); + + // Send the request + NLHR_PTR pnlhr(Netlib_HttpTransaction(handle_, &nlhr)); + + mir_free(nlhr.headers); + + // Remember the persistent connection handle (or not) + switch (request_type) + { + case OMEGLE_REQUEST_HOME: + break; + + case OMEGLE_REQUEST_EVENTS: + hEventsConnection = pnlhr ? pnlhr->nlc : nullptr; + break; + + default: + ReleaseMutex(connection_lock_); + hConnection = pnlhr ? pnlhr->nlc : nullptr; + break; + } + + // Check and copy response data + if (pnlhr != nullptr) + { + parent->debugLogA("@@@@@ Got response with code %d", pnlhr->resultCode); + store_headers(&resp, pnlhr->headers, pnlhr->headersCount); + resp.code = pnlhr->resultCode; + resp.data = pnlhr->pData ? pnlhr->pData : ""; + + // This error code gives us some error page, but we don't need that, so clear the response + if (pnlhr->resultCode == HTTP_CODE_WEB_SERVER_IS_DOWN) { + resp.data = ""; + } + + parent->debugLogA("&&&&& Got response: %s", resp.data.c_str()); + } + else { + parent->debugLogA("!!!!! No response from server (time-out)"); + resp.code = HTTP_CODE_FAKE_DISCONNECTED; + // Better to have something set explicitely as this value is compaired in all communication requests + } + + return resp; +} + +bool Omegle_client::handle_entry(const std::string &method) +{ + parent->debugLogA(" >> Entering %s()", method.c_str()); + return true; +} + +bool Omegle_client::handle_success(const std::string &method) +{ + parent->debugLogA(" << Quitting %s()", method.c_str()); + reset_error(); + return true; +} + +bool Omegle_client::handle_error(const std::string &method, bool force_disconnect) +{ + bool result; + increment_error(); + parent->debugLogA("!!!!! %s(): Something with Omegle went wrong", method.c_str()); + + if (force_disconnect) + result = false; + else if (error_count_ <= (UINT)db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_TIMEOUTS_LIMIT, OMEGLE_TIMEOUTS_LIMIT)) + result = true; + else + result = false; + + if (result == false) + { + reset_error(); + parent->UpdateChat(nullptr, TranslateT("Connection error.")); + parent->StopChat(false); + } + + return result; +} + +////////////////////////////////////////////////////////////////////////////// + +std::string Omegle_client::get_server(bool not_last) +{ + int q = not_last ? 1 : 0; + + int server = db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_SERVER, 0); + if (server < 0 || server >= (int)(_countof(servers) - q)) + server = 0; + + if (server == 0) { + srand(::time(0)); + server = (rand() % (_countof(servers) - 1 - q)) + 1; + } + + return servers[server]; +} + +std::string Omegle_client::get_language() +{ + int language = db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_LANGUAGE, 0); + if (language < 0 || language >= (_countof(languages))) + language = 0; + + return language > 0 ? languages[language].id : "en"; +} + +std::string Omegle_client::choose_server(int request_type) +{ + switch (request_type) + { + case OMEGLE_REQUEST_HOME: + return OMEGLE_SERVER_REGULAR; + + /* case OMEGLE_REQUEST_START: + case OMEGLE_REQUEST_STOP: + case OMEGLE_REQUEST_SEND: + case OMEGLE_REQUEST_EVENTS: + case OMEGLE_REQUEST_TYPING_START: + case OMEGLE_REQUEST_TYPING_STOP: + case OMEGLE_REQUEST_RECAPTCHA: + case OMEGLE_REQUEST_COUNT: + */ default: + std::string server = OMEGLE_SERVER_CHAT; + utils::text::replace_first(&server, "%s", this->server_); + return server; + } +} + +std::string Omegle_client::choose_action(int request_type, std::string* get_data) +{ + switch (request_type) + { + case OMEGLE_REQUEST_START: + { + std::string action = "/start?caps=recaptcha2,t&rcs=1&spid=&lang="; + action += get_language(); + if (get_data != nullptr) + action += (*get_data); + + return action; + } + + case OMEGLE_REQUEST_STOP: + return "/disconnect"; + + case OMEGLE_REQUEST_SEND: + return "/send"; + + case OMEGLE_REQUEST_EVENTS: + return "/events"; + + case OMEGLE_REQUEST_TYPING_START: + return "/typing"; + + case OMEGLE_REQUEST_TYPING_STOP: + return "/stoppedtyping"; + + case OMEGLE_REQUEST_RECAPTCHA: + return "/recaptcha"; + + case OMEGLE_REQUEST_COUNT: + return "/count"; + + // "/stoplookingforcommonlikes" + + /* case OMEGLE_REQUEST_HOME: + */ default: + return "/"; + } +} + + +NETLIBHTTPHEADER* Omegle_client::get_request_headers(int request_type, int* headers_count) +{ + if (request_type == REQUEST_POST) + *headers_count = 4; + else + *headers_count = 3; + + NETLIBHTTPHEADER *headers = (NETLIBHTTPHEADER*)mir_calloc(sizeof(NETLIBHTTPHEADER)*(*headers_count)); + + if (request_type == REQUEST_POST) { + headers[3].szName = "Content-Type"; + headers[3].szValue = "application/x-www-form-urlencoded; charset=utf-8"; + } + + headers[2].szName = "User-Agent"; + headers[2].szValue = Netlib_GetUserAgent(); + headers[1].szName = "Accept"; + headers[1].szValue = "*/*"; + headers[0].szName = "Accept-Language"; + headers[0].szValue = "en,en-US;q=0.9"; + + return headers; +} + +void Omegle_client::store_headers(http::response* resp, NETLIBHTTPHEADER* headers, int headersCount) +{ + for (size_t i = 0; i < (size_t)headersCount; i++) + { + std::string header_name = headers[i].szName; + std::string header_value = headers[i].szValue; + + // TODO RM: (un)comment + //parent->debugLogA("----- Got header '%s': %s", header_name.c_str(), header_value.c_str()); + resp->headers[header_name] = header_value; + } +} + +////////////////////////////////////////////////////////////////////////////// + +bool Omegle_client::start() +{ + HANDLE_ENTRY; + + this->server_ = get_server(); + //parent->debugLogA("Chosing server %s", this->server_.c_str()); + //std::string log = Translate("Chosing server: ") + this->server_; + //parent->UpdateChat(NULL, log.c_str()); + + std::string data; + + if (this->spy_mode_) { + //// get last server from list, which is for spy mode + //this->server_ = servers[_countof(servers)-1]; + + if (this->question_.empty()) { + data = "&wantsspy=1"; + } + else { + data = "&ask=" + utils::url::encode(this->question_); + data += "&cansavequestion="; + data += db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_REUSE_QUESTION, 0) ? "1" : "0"; + } + } + else if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_MEET_COMMON, 0)) + { + DBVARIANT dbv; + if (!db_get_utf(NULL, parent->m_szModuleName, OMEGLE_KEY_INTERESTS, &dbv)) + { + std::string topics = dbv.pszVal; + std::string topic; + + db_free(&dbv); + + std::string::size_type pos = 0; + std::string::size_type pos2 = 0; + while ((pos2 = topics.find(",", pos)) != std::string::npos) { + topic = topics.substr(pos, pos2 - pos); + topic = utils::text::trim(topic); + + if (!topic.empty()) { + if (pos > 0) + data += ","; + + data += "\"" + topic + "\""; + } + + pos = pos2 + 1; + } + + topic = topics.substr(pos); + topic = utils::text::trim(topic); + if (!topic.empty()) { + if (pos > 0) + data += ","; + data += "\"" + topic + "\""; + } + + parent->debugLogA("TOPICS: %s", data.c_str()); + + if (!data.empty()) { + data = "[" + data + "]"; + data = "&topics=" + utils::url::encode(data); + } + + //// get any server but last, which is for spy mode + //this->server_ = get_server(true); + } + } + + if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_SERVER_INFO, 0)) + { + std::string count = get_page(OMEGLE_REQUEST_COUNT); + if (!count.empty()) { + char str[255]; + mir_snprintf(str, Translate("Connected to server %s. There are %s users online now."), server_.c_str(), count.c_str()); + + wchar_t *msg = mir_a2u(str); + parent->UpdateChat(nullptr, msg); + mir_free(msg); + } + } + else { + char str[255]; + mir_snprintf(str, Translate("Connected to server %s."), server_.c_str()); + + wchar_t *msg = mir_a2u(str); + parent->UpdateChat(nullptr, msg); + mir_free(msg); + } + + // Send validation + http::response resp = flap(OMEGLE_REQUEST_START, nullptr, &data); + + switch (resp.code) + { + case HTTP_CODE_FAKE_DISCONNECTED: + { + // If is is only timeout error, try login once more + if (HANDLE_ERROR(false)) + return start(); + else + return false; + } + + case HTTP_CODE_OK: + { + if (!resp.data.empty()) { + this->chat_id_ = resp.data.substr(1, resp.data.length() - 2); + this->state_ = STATE_WAITING; + + return HANDLE_SUCCESS; + } + else { + return HANDLE_ERROR(FORCE_DISCONNECT); + } + } + + default: + return HANDLE_ERROR(FORCE_DISCONNECT); + } +} + +bool Omegle_client::stop() +{ + if (parent->isOffline()) + return true; + + HANDLE_ENTRY; + + std::string data = "id=" + this->chat_id_; + + http::response resp = flap(OMEGLE_REQUEST_STOP, &data); + + Netlib_Shutdown(hConnection); + Netlib_Shutdown(hEventsConnection); + + return (resp.data == "win") ? HANDLE_SUCCESS : HANDLE_ERROR(false); +} + +bool Omegle_client::events() +{ + HANDLE_ENTRY; + + std::string post_data = "id=" + this->chat_id_; + + // Get update + http::response resp = flap(OMEGLE_REQUEST_EVENTS, &post_data); + + // Return + switch (resp.code) + { + case HTTP_CODE_OK: + { + if (resp.data == "null") { + // Everything is OK, no new message received -- OR it is a problem + // TODO: if we are waiting for Stranger with common likes, then we should try standard Stranger if this takes too long + return HANDLE_ERROR(false); + } + else if (resp.data == "fail") { + // Something went wrong + return HANDLE_ERROR(false); + } + + JSONROOT root(resp.data.c_str()); + if (root == nullptr) + return HANDLE_ERROR(false); + + bool newStranger = false; + bool waiting = false; + + for (size_t i = 0; i < json_size(root); i++) { + JSONNode *item = json_at(root, i); + if (item == nullptr) + continue; + + std::string name = _T2A(json_as_string(json_at(item, 0))); + + if (name == "waiting") { + // We are just waiting for new Stranger + waiting = true; + } + else if (name == "identDigests") { + // We get some comma separated hashes, I'm not sure what for + } + else if (name == "statusInfo") { + JSONNode *data = json_at(item, 1); + + // We got some object as second parameter + //data["antinudepercent"]; // probably 1 by default + //data["antinudeservers"]; // array of server names, like "waw3.omegle.com" + //data["rtmfp"]; // some rtmfp protocol address + //data["servers"]; // array of server names, like "front5.omegle.com" + //data["spyeeQueueTime"]; // some float number, e.g. 0.0701999903 + //data["spyQueueTime"]; // some float number, e.g. 4.7505000114 + //data["timestamp"]; // e.g. 1445336566.0196209 + + // We got info about count of connected people there + ptrW count(json_as_string(json_get(data, "count"))); + wchar_t strT[255]; + mir_snwprintf(strT, TranslateT("On whole Omegle are %s strangers online now."), count.get()); + + parent->UpdateChat(nullptr, strT); + } + else if (name == "serverMessage") { + ptrW message(json_as_string(json_at(item, 1))); + parent->UpdateChat(nullptr, TranslateW(message)); + } + else if (name == "connected") { + // Stranger connected + if (this->spy_mode_ && !this->question_.empty()) { + parent->AddChatContact(TranslateT("Stranger 1")); + parent->AddChatContact(TranslateT("Stranger 2")); + this->state_ = STATE_SPY; + } + else { + parent->AddChatContact(TranslateT("Stranger")); + this->state_ = STATE_ACTIVE; + } + + newStranger = true; + waiting = false; + } + else if (name == "commonLikes") { + std::wstring likes = TranslateT("You and the Stranger both like: "); + + JSONNode *items = json_at(item, 1); + size_t size = json_size(items); + for (size_t j = 0; j < size; j++) { + likes += ptrW(json_as_string(json_at(items, j))); + if (j < size - 1) + likes += L", "; + } + + parent->debugLogW(L"Got common likes: '%s'", likes.c_str()); + parent->SetTopic(likes.c_str()); + } + else if (name == "question") { + ptrW question(json_as_string(json_at(item, 1))); + parent->SetTopic(question); + } + else if (name == "typing" || name == "spyTyping") { + // Stranger is typing, not supported by chat module yet + Skin_PlaySound("StrangerTyp"); + + ptrW who(name == "spyTyping" ? json_as_string(json_at(item, 1)) : mir_wstrdup(L"Stranger")); + Srmm_SetStatusText(parent->GetChatHandle(), CMStringW(FORMAT, TranslateT("%s is typing."), TranslateW(who)), g_plugin.getIcon(IDI_TYPING_ON)); + } + else if (name == "stoppedTyping" || name == "spyStoppedTyping") { + // Stranger stopped typing, not supported by chat module yet + Skin_PlaySound("StrangerTypStop"); + + ptrW who(name == "spyTyping" ? json_as_string(json_at(item, 1)) : mir_wstrdup(L"Stranger")); + Srmm_SetStatusText(parent->GetChatHandle(), CMStringW(FORMAT, TranslateT("%s stopped typing."), TranslateW(who)), g_plugin.getIcon(IDI_TYPING_OFF)); + } + else if (name == "gotMessage") { + Srmm_SetStatusText(parent->GetChatHandle(), nullptr); + + // Play sound as we received message + Skin_PlaySound("StrangerMessage"); + + if (state_ == STATE_ACTIVE) { + ptrW msg(json_as_string(json_at(item, 1))); + parent->UpdateChat(TranslateT("Stranger"), msg); + } + } + else if (name == "spyMessage") { + Srmm_SetStatusText(parent->GetChatHandle(), nullptr); + + // Play sound as we received message + Skin_PlaySound("StrangerMessage"); + + if (state_ == STATE_SPY) { + ptrW stranger(json_as_string(json_at(item, 1))); + ptrW msg(json_as_string(json_at(item, 2))); + parent->UpdateChat(stranger, msg); + } + } + else if (name == "strangerDisconnected") { + Srmm_SetStatusText(parent->GetChatHandle(), nullptr); + + // Stranger disconnected + if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_DONT_STOP, 0)) + { + Skin_PlaySound("StrangerChange"); + parent->NewChat(); + } + else + parent->StopChat(false); + } + else if (name == "spyDisconnected") { + Srmm_SetStatusText(parent->GetChatHandle(), nullptr); + + ptrW stranger(json_as_string(json_at(item, 1))); + + wchar_t strT[255]; + mir_snwprintf(strT, TranslateT("%s disconnected."), TranslateW(stranger)); + parent->UpdateChat(nullptr, strT); + + // Stranger disconnected + if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_DONT_STOP, 0)) + { + Skin_PlaySound("StrangerChange"); + parent->NewChat(); + } + else + parent->StopChat(false); + } + else if (name == "recaptchaRequired") { + // Nothing to do with recaptcha + parent->UpdateChat(nullptr, TranslateT("Recaptcha is required.\nOpen http://omegle.com , solve Recaptcha and try again.")); + parent->StopChat(false); + } + else if (name == "recaptchaRejected") { + // Nothing to do with recaptcha + parent->StopChat(false); + } + else if (name == "error") { + ptrW error(json_as_string(json_at(item, 1))); + + wchar_t strT[255]; + mir_snwprintf(strT, TranslateT("Error: %s"), TranslateW(error)); + parent->UpdateChat(nullptr, strT); + } + } + + if (newStranger && !spy_mode_) { + // We got new stranger in this event, lets say him "Hi message" if enabled + if (db_get_b(0, parent->m_szModuleName, OMEGLE_KEY_HI_ENABLED, 0)) { + DBVARIANT dbv; + if (!db_get_utf(NULL, parent->m_szModuleName, OMEGLE_KEY_HI, &dbv)) { + std::vector messages; + utils::text::explode(std::string(dbv.pszVal), "\r\n", &messages); + db_free(&dbv); + + int pos = rand() % messages.size(); + std::string *message = new std::string(messages.at(pos)); + + parent->debugLogA("**Chat - saying Hi! message"); + parent->ForkThread(&OmegleProto::SendMsgWorker, message); + } + else parent->debugLogA("**Chat - Hi message is enabled but not used"); + } + } + + if (waiting) { + // If we are only waiting in this event... + parent->UpdateChat(nullptr, TranslateT("We are still waiting...")); + } + + return HANDLE_SUCCESS; + } + + case HTTP_CODE_FAKE_DISCONNECTED: + // timeout + return HANDLE_SUCCESS; + + default: + return HANDLE_ERROR(false); + } +} + +bool Omegle_client::send_message(const std::string &message_text) +{ + HANDLE_ENTRY; + + std::string data = "msg=" + utils::url::encode(message_text); + data += "&id=" + this->chat_id_; + + http::response resp = flap(OMEGLE_REQUEST_SEND, &data); + + switch (resp.code) + { + case HTTP_CODE_OK: + if (resp.data == "win") { + return HANDLE_SUCCESS; + } + + case HTTP_CODE_FAKE_DISCONNECTED: + default: + return HANDLE_ERROR(false); + } +} + +bool Omegle_client::typing_start() +{ + HANDLE_ENTRY; + + std::string data = "id=" + this->chat_id_; + + http::response resp = flap(OMEGLE_REQUEST_TYPING_START, &data); + + switch (resp.code) + { + case HTTP_CODE_OK: + if (resp.data == "win") { + return HANDLE_SUCCESS; + } + + case HTTP_CODE_FAKE_DISCONNECTED: + default: + return HANDLE_ERROR(false); + } +} + +bool Omegle_client::typing_stop() +{ + HANDLE_ENTRY; + + std::string data = "id=" + this->chat_id_; + + http::response resp = flap(OMEGLE_REQUEST_TYPING_STOP, &data); + + switch (resp.code) + { + case HTTP_CODE_OK: + if (resp.data == "win") { + return HANDLE_SUCCESS; + } + + case HTTP_CODE_FAKE_DISCONNECTED: + default: + return HANDLE_ERROR(false); + } +} + +bool Omegle_client::recaptcha() +{ + // TODO: Implement! + + HANDLE_ENTRY; + + // data:{id:this.clientID,challenge:b,response:a}} + //std::string data = "?id=...&challenge= ..., &response= ..."; + + http::response resp = flap(OMEGLE_REQUEST_RECAPTCHA); + + switch (resp.code) + { + case HTTP_CODE_OK: + /* if (resp.data == "win") { + return handle_success( "typing_start" ); + }*/ + + case HTTP_CODE_FAKE_DISCONNECTED: + default: + return HANDLE_ERROR(false); + } +} + +std::string Omegle_client::get_page(const int request_type) +{ + HANDLE_ENTRY; + + http::response resp = flap(request_type); + + switch (resp.code) + { + case HTTP_CODE_OK: + HANDLE_SUCCESS; + break; + + case HTTP_CODE_FAKE_DISCONNECTED: + default: + HANDLE_ERROR(false); + } + + return resp.data; +} diff --git a/protocols/Omegle/src/connection.cpp b/protocols/Omegle/src/connection.cpp index 7750fe9038..db2b73970c 100644 --- a/protocols/Omegle/src/connection.cpp +++ b/protocols/Omegle/src/connection.cpp @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/constants.h b/protocols/Omegle/src/constants.h index 67ce350240..5bc923f4a3 100644 --- a/protocols/Omegle/src/constants.h +++ b/protocols/Omegle/src/constants.h @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/db.h b/protocols/Omegle/src/db.h index 3798d0af78..45d3478828 100644 --- a/protocols/Omegle/src/db.h +++ b/protocols/Omegle/src/db.h @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/dialogs.cpp b/protocols/Omegle/src/dialogs.cpp index e71b6aa542..ee243053c0 100644 --- a/protocols/Omegle/src/dialogs.cpp +++ b/protocols/Omegle/src/dialogs.cpp @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/dialogs.h b/protocols/Omegle/src/dialogs.h index 4685b9de4c..080b36c07a 100644 --- a/protocols/Omegle/src/dialogs.h +++ b/protocols/Omegle/src/dialogs.h @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/http.cpp b/protocols/Omegle/src/http.cpp index b21f5e8602..45bb2a62a2 100644 --- a/protocols/Omegle/src/http.cpp +++ b/protocols/Omegle/src/http.cpp @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/http.h b/protocols/Omegle/src/http.h index aeffb16f51..b107aaf3a0 100644 --- a/protocols/Omegle/src/http.h +++ b/protocols/Omegle/src/http.h @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/main.cpp b/protocols/Omegle/src/main.cpp index 20de90c123..72b1edc3cd 100644 --- a/protocols/Omegle/src/main.cpp +++ b/protocols/Omegle/src/main.cpp @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/messages.cpp b/protocols/Omegle/src/messages.cpp index cd92e02be9..fd7a79e4ae 100644 --- a/protocols/Omegle/src/messages.cpp +++ b/protocols/Omegle/src/messages.cpp @@ -1,74 +1,74 @@ -/* - -Omegle plugin for Miranda Instant Messenger -_____________________________________________ - -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . - -*/ - -#include "stdafx.h" - -void OmegleProto::SendMsgWorker(void *p) -{ - if (p == nullptr) - return; - - std::string data = *(std::string*)p; - delete (std::string*)p; - - data = utils::text::trim(data); - - if (facy.state_ == STATE_ACTIVE && data.length() && facy.send_message(data)) { - wchar_t *msg = mir_a2u_cp(data.c_str(), CP_UTF8); - UpdateChat(facy.nick_, msg); - mir_free(msg); - } -} - -void OmegleProto::SendTypingWorker(void *p) -{ - if (p == nullptr) - return; - - // Save typing info - bool typ = (*static_cast(p) == PROTOTYPE_SELFTYPING_ON); - delete (int*)p; - - // Ignore same typing info - if (facy.typing_ == typ) - return; - - if (facy.state_ != STATE_ACTIVE) - return; - - facy.typing_ = typ; - - if (typ) - facy.typing_start(); - else - facy.typing_stop(); -} - -void OmegleProto::NewChatWorker(void*) -{ - NewChat(); -} - -void OmegleProto::StopChatWorker(void*) -{ - StopChat(); -} +/* + +Omegle plugin for Miranda Instant Messenger +_____________________________________________ + +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "stdafx.h" + +void OmegleProto::SendMsgWorker(void *p) +{ + if (p == nullptr) + return; + + std::string data = *(std::string*)p; + delete (std::string*)p; + + data = utils::text::trim(data); + + if (facy.state_ == STATE_ACTIVE && data.length() && facy.send_message(data)) { + wchar_t *msg = mir_a2u_cp(data.c_str(), CP_UTF8); + UpdateChat(facy.nick_, msg); + mir_free(msg); + } +} + +void OmegleProto::SendTypingWorker(void *p) +{ + if (p == nullptr) + return; + + // Save typing info + bool typ = (*static_cast(p) == PROTOTYPE_SELFTYPING_ON); + delete (int*)p; + + // Ignore same typing info + if (facy.typing_ == typ) + return; + + if (facy.state_ != STATE_ACTIVE) + return; + + facy.typing_ = typ; + + if (typ) + facy.typing_start(); + else + facy.typing_stop(); +} + +void OmegleProto::NewChatWorker(void*) +{ + NewChat(); +} + +void OmegleProto::StopChatWorker(void*) +{ + StopChat(); +} diff --git a/protocols/Omegle/src/proto.cpp b/protocols/Omegle/src/proto.cpp index ccc0cb9173..036dc41d6f 100644 --- a/protocols/Omegle/src/proto.cpp +++ b/protocols/Omegle/src/proto.cpp @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/proto.h b/protocols/Omegle/src/proto.h index 388e94299a..839d6ecb25 100644 --- a/protocols/Omegle/src/proto.h +++ b/protocols/Omegle/src/proto.h @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/stdafx.cxx b/protocols/Omegle/src/stdafx.cxx index f64d25234b..ebbde0ade1 100644 --- a/protocols/Omegle/src/stdafx.cxx +++ b/protocols/Omegle/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Omegle/src/stdafx.h b/protocols/Omegle/src/stdafx.h index d10351511d..81425082c5 100644 --- a/protocols/Omegle/src/stdafx.h +++ b/protocols/Omegle/src/stdafx.h @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/theme.cpp b/protocols/Omegle/src/theme.cpp index ce96ff5224..f9899adbe9 100644 --- a/protocols/Omegle/src/theme.cpp +++ b/protocols/Omegle/src/theme.cpp @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/theme.h b/protocols/Omegle/src/theme.h index 5b50f4648e..0418ca4268 100644 --- a/protocols/Omegle/src/theme.h +++ b/protocols/Omegle/src/theme.h @@ -3,7 +3,7 @@ Omegle plugin for Miranda Instant Messenger _____________________________________________ -Copyright © 2011-17 Robert Pösel, 2017-22 Miranda NG team +Copyright © 2011-17 Robert Pösel, 2017-23 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/protocols/Omegle/src/version.h b/protocols/Omegle/src/version.h index d30f57170c..3780281020 100644 --- a/protocols/Omegle/src/version.h +++ b/protocols/Omegle/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Omegle protocol support for Miranda NG." #define __AUTHOR "Robert Pösel" #define __AUTHORWEB "https://miranda-ng.org/p/Omegle" -#define __COPYRIGHT "© 2011-17 Robert Pösel, 2017-22 Miranda NG team" +#define __COPYRIGHT "© 2011-17 Robert Pösel, 2017-23 Miranda NG team" diff --git a/protocols/Sametime/src/version.h b/protocols/Sametime/src/version.h index a09e825f76..87628ff6ab 100644 --- a/protocols/Sametime/src/version.h +++ b/protocols/Sametime/src/version.h @@ -9,5 +9,5 @@ #define __FILENAME "Sametime.dll" #define __DESCRIPTION "Implementation of instant messaging for the Lotus Sametime protocol." #define __AUTHOR "Scott Ellis, Szymon Tokarz" -#define __COPYRIGHT "© 2005 Scott Ellis, 2014-22 wsx22" +#define __COPYRIGHT "© 2005 Scott Ellis, 2014-23 wsx22" #define __AUTHORWEB "https://miranda-ng.org/p/Sametime" diff --git a/protocols/SkypeWeb/src/main.cpp b/protocols/SkypeWeb/src/main.cpp index 3e3c36210b..b1e3ddfb5b 100644 --- a/protocols/SkypeWeb/src/main.cpp +++ b/protocols/SkypeWeb/src/main.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/request_queue.cpp b/protocols/SkypeWeb/src/request_queue.cpp index c7ab39d344..d6240e7cee 100644 --- a/protocols/SkypeWeb/src/request_queue.cpp +++ b/protocols/SkypeWeb/src/request_queue.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/avatars.h b/protocols/SkypeWeb/src/requests/avatars.h index fd015e1bed..cdf5ac7b6e 100644 --- a/protocols/SkypeWeb/src/requests/avatars.h +++ b/protocols/SkypeWeb/src/requests/avatars.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/capabilities.h b/protocols/SkypeWeb/src/requests/capabilities.h index 17b823a575..3170ca6d66 100644 --- a/protocols/SkypeWeb/src/requests/capabilities.h +++ b/protocols/SkypeWeb/src/requests/capabilities.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/chatrooms.h b/protocols/SkypeWeb/src/requests/chatrooms.h index 2e12729862..2fc4e435ff 100644 --- a/protocols/SkypeWeb/src/requests/chatrooms.h +++ b/protocols/SkypeWeb/src/requests/chatrooms.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/contacts.h b/protocols/SkypeWeb/src/requests/contacts.h index 4a62c67441..f9a3c13fa0 100644 --- a/protocols/SkypeWeb/src/requests/contacts.h +++ b/protocols/SkypeWeb/src/requests/contacts.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/endpoint.h b/protocols/SkypeWeb/src/requests/endpoint.h index 8c351144c8..c0abe99a43 100644 --- a/protocols/SkypeWeb/src/requests/endpoint.h +++ b/protocols/SkypeWeb/src/requests/endpoint.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/history.h b/protocols/SkypeWeb/src/requests/history.h index 75b7dc93d4..4438288479 100644 --- a/protocols/SkypeWeb/src/requests/history.h +++ b/protocols/SkypeWeb/src/requests/history.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/login.h b/protocols/SkypeWeb/src/requests/login.h index e06cb28d40..e0053745d4 100644 --- a/protocols/SkypeWeb/src/requests/login.h +++ b/protocols/SkypeWeb/src/requests/login.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/messages.h b/protocols/SkypeWeb/src/requests/messages.h index 4a71b79445..4e41e38dd4 100644 --- a/protocols/SkypeWeb/src/requests/messages.h +++ b/protocols/SkypeWeb/src/requests/messages.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/oauth.h b/protocols/SkypeWeb/src/requests/oauth.h index 5d26982a8d..cc9842a0d2 100644 --- a/protocols/SkypeWeb/src/requests/oauth.h +++ b/protocols/SkypeWeb/src/requests/oauth.h @@ -1,74 +1,74 @@ -/* -Copyright (c) 2015-22 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 . -*/ - -#ifndef _SKYPE_REQUEST_OAUTH_H_ -#define _SKYPE_REQUEST_OAUTH_H_ - -struct OAuthRequest : public AsyncHttpRequest -{ - OAuthRequest() : - AsyncHttpRequest(REQUEST_GET, HOST_OTHER, "https://login.live.com/login.srf", &CSkypeProto::OnOAuthStart) - { - flags |= NLHRF_REDIRECT; - - this << CHAR_PARAM("wa", "wsignin1.0") << CHAR_PARAM("wp", "MBI_SSL") - << CHAR_PARAM("wreply", "https://lw.skype.com/login/oauth/proxy?site_name=lw.skype.com") - << CHAR_PARAM("cobrandid", "90010"); - } - - OAuthRequest(const char *login, const char *password, const char *cookies, const char *ppft) : - AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://login.live.com/ppsecure/post.srf", &CSkypeProto::OnOAuthConfirm) - { - this << CHAR_PARAM("wa", "wsignin1.0") << CHAR_PARAM("wp", "MBI_SSL") - << CHAR_PARAM("wreply", "https://lw.skype.com/login/oauth/proxy?site_name=lw.skype.com") - << CHAR_PARAM("cobrandid", "90010"); - m_szUrl.AppendFormat("?%s", m_szParam.c_str()); - m_szParam.Empty(); - - AddHeader("Cookie", cookies); - - if (auto *delim = strchr(login, ':')) - login = delim + 1; - - this << CHAR_PARAM("login", login) << CHAR_PARAM("passwd", password) << CHAR_PARAM("PPFT", ppft); - } - - OAuthRequest(const char *cookies, const char* ppft, const char* opid) : - AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://login.live.com/ppsecure/post.srf", &CSkypeProto::OnOAuthAuthorize) - { - this << CHAR_PARAM("wa", "wsignin1.0") << CHAR_PARAM("wp", "MBI_SSL") - << CHAR_PARAM("wreply", "https://lw.skype.com/login/oauth/proxy?site_name=lw.skype.com") - << CHAR_PARAM("cobrandid", "90010") - << CHAR_PARAM("id", "293290") - << CHAR_PARAM("opid", opid); - - m_szUrl.AppendFormat("?%s", m_szParam.c_str()); - m_szParam.Empty(); - - AddHeader("Cookie", cookies); - - this << CHAR_PARAM("type", "28") << CHAR_PARAM("PPFT", ppft); - } - - OAuthRequest(const char *t) : - AsyncHttpRequest(REQUEST_POST, HOST_LOGIN, "/login/microsoft", &CSkypeProto::OnOAuthEnd) - { - this << CHAR_PARAM ("t", t) << CHAR_PARAM("site_name", "lw.skype.com") << INT_PARAM ("oauthPartner", 999); - } -}; - -#endif //_SKYPE_REQUEST_OAUTH_H_ +/* +Copyright (c) 2015-23 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 . +*/ + +#ifndef _SKYPE_REQUEST_OAUTH_H_ +#define _SKYPE_REQUEST_OAUTH_H_ + +struct OAuthRequest : public AsyncHttpRequest +{ + OAuthRequest() : + AsyncHttpRequest(REQUEST_GET, HOST_OTHER, "https://login.live.com/login.srf", &CSkypeProto::OnOAuthStart) + { + flags |= NLHRF_REDIRECT; + + this << CHAR_PARAM("wa", "wsignin1.0") << CHAR_PARAM("wp", "MBI_SSL") + << CHAR_PARAM("wreply", "https://lw.skype.com/login/oauth/proxy?site_name=lw.skype.com") + << CHAR_PARAM("cobrandid", "90010"); + } + + OAuthRequest(const char *login, const char *password, const char *cookies, const char *ppft) : + AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://login.live.com/ppsecure/post.srf", &CSkypeProto::OnOAuthConfirm) + { + this << CHAR_PARAM("wa", "wsignin1.0") << CHAR_PARAM("wp", "MBI_SSL") + << CHAR_PARAM("wreply", "https://lw.skype.com/login/oauth/proxy?site_name=lw.skype.com") + << CHAR_PARAM("cobrandid", "90010"); + m_szUrl.AppendFormat("?%s", m_szParam.c_str()); + m_szParam.Empty(); + + AddHeader("Cookie", cookies); + + if (auto *delim = strchr(login, ':')) + login = delim + 1; + + this << CHAR_PARAM("login", login) << CHAR_PARAM("passwd", password) << CHAR_PARAM("PPFT", ppft); + } + + OAuthRequest(const char *cookies, const char* ppft, const char* opid) : + AsyncHttpRequest(REQUEST_POST, HOST_OTHER, "https://login.live.com/ppsecure/post.srf", &CSkypeProto::OnOAuthAuthorize) + { + this << CHAR_PARAM("wa", "wsignin1.0") << CHAR_PARAM("wp", "MBI_SSL") + << CHAR_PARAM("wreply", "https://lw.skype.com/login/oauth/proxy?site_name=lw.skype.com") + << CHAR_PARAM("cobrandid", "90010") + << CHAR_PARAM("id", "293290") + << CHAR_PARAM("opid", opid); + + m_szUrl.AppendFormat("?%s", m_szParam.c_str()); + m_szParam.Empty(); + + AddHeader("Cookie", cookies); + + this << CHAR_PARAM("type", "28") << CHAR_PARAM("PPFT", ppft); + } + + OAuthRequest(const char *t) : + AsyncHttpRequest(REQUEST_POST, HOST_LOGIN, "/login/microsoft", &CSkypeProto::OnOAuthEnd) + { + this << CHAR_PARAM ("t", t) << CHAR_PARAM("site_name", "lw.skype.com") << INT_PARAM ("oauthPartner", 999); + } +}; + +#endif //_SKYPE_REQUEST_OAUTH_H_ diff --git a/protocols/SkypeWeb/src/requests/poll.h b/protocols/SkypeWeb/src/requests/poll.h index ce4b32cb4c..4475ec041e 100644 --- a/protocols/SkypeWeb/src/requests/poll.h +++ b/protocols/SkypeWeb/src/requests/poll.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/profile.h b/protocols/SkypeWeb/src/requests/profile.h index 426087a63a..c255f9166b 100644 --- a/protocols/SkypeWeb/src/requests/profile.h +++ b/protocols/SkypeWeb/src/requests/profile.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/search.h b/protocols/SkypeWeb/src/requests/search.h index d47562ff9c..357ad470bc 100644 --- a/protocols/SkypeWeb/src/requests/search.h +++ b/protocols/SkypeWeb/src/requests/search.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/status.h b/protocols/SkypeWeb/src/requests/status.h index 58ebd23dcd..74e35960cd 100644 --- a/protocols/SkypeWeb/src/requests/status.h +++ b/protocols/SkypeWeb/src/requests/status.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/requests/subscriptions.h b/protocols/SkypeWeb/src/requests/subscriptions.h index b0f71046e8..b1ddc03a6e 100644 --- a/protocols/SkypeWeb/src/requests/subscriptions.h +++ b/protocols/SkypeWeb/src/requests/subscriptions.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_avatars.cpp b/protocols/SkypeWeb/src/skype_avatars.cpp index 934180e929..1ac07d6d33 100644 --- a/protocols/SkypeWeb/src/skype_avatars.cpp +++ b/protocols/SkypeWeb/src/skype_avatars.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_chatrooms.cpp b/protocols/SkypeWeb/src/skype_chatrooms.cpp index 3087321ea2..4f27b13c5c 100644 --- a/protocols/SkypeWeb/src/skype_chatrooms.cpp +++ b/protocols/SkypeWeb/src/skype_chatrooms.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_contacts.cpp b/protocols/SkypeWeb/src/skype_contacts.cpp index cd1a2a13a1..75827daaf6 100644 --- a/protocols/SkypeWeb/src/skype_contacts.cpp +++ b/protocols/SkypeWeb/src/skype_contacts.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_db.cpp b/protocols/SkypeWeb/src/skype_db.cpp index 3417b15c01..8d815088d1 100644 --- a/protocols/SkypeWeb/src/skype_db.cpp +++ b/protocols/SkypeWeb/src/skype_db.cpp @@ -1,119 +1,119 @@ -/* -Copyright (c) 2015-22 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 . -*/ - -#include "stdafx.h" - -struct { int type; char *name; uint32_t flags; } g_SkypeDBTypes[] = -{ - { SKYPE_DB_EVENT_TYPE_INCOMING_CALL, LPGEN("Incoming call"), DETF_NONOTIFY }, - { SKYPE_DB_EVENT_TYPE_EDITED_MESSAGE, LPGEN("Edited message"), 0 }, - { SKYPE_DB_EVENT_TYPE_ACTION, LPGEN("Action"), 0 }, - { SKYPE_DB_EVENT_TYPE_CALL_INFO, LPGEN("Call information"), 0 }, - { SKYPE_DB_EVENT_TYPE_FILETRANSFER_INFO, LPGEN("File transfer information"), 0 }, - { SKYPE_DB_EVENT_TYPE_URIOBJ, LPGEN("URI object"), 0 }, - { SKYPE_DB_EVENT_TYPE_MOJI, LPGEN("Moji"), 0 }, - { SKYPE_DB_EVENT_TYPE_FILE, LPGEN("File"), 0 }, - { SKYPE_DB_EVENT_TYPE_UNKNOWN, LPGEN("Unknown event"), 0 }, -}; - -MEVENT CSkypeProto::GetMessageFromDb(const char *messageId) -{ - if (messageId == nullptr) - return NULL; - - return db_event_getById(m_szModuleName, messageId); -} - -MEVENT CSkypeProto::AddDbEvent(uint16_t type, MCONTACT hContact, uint32_t timestamp, uint32_t flags, const CMStringW &content, const CMStringA &msgId) -{ - if (MEVENT hDbEvent = GetMessageFromDb(msgId)) - return hDbEvent; - - T2Utf szMsg(content); - DBEVENTINFO dbei = {}; - dbei.szModule = m_szModuleName; - dbei.timestamp = timestamp; - dbei.eventType = type; - dbei.cbBlob = (uint32_t)mir_strlen(szMsg) + 1; - dbei.pBlob = (uint8_t *)szMsg; - dbei.flags = flags; - dbei.szId = msgId; - return db_event_add(hContact, &dbei); -} - -void CSkypeProto::EditEvent(MCONTACT hContact, MEVENT hEvent, const CMStringW &szContent, time_t edit_time) -{ - mir_cslock lck(m_AppendMessageLock); - - DB::EventInfo dbei; - dbei.cbBlob = -1; - if (db_event_get(hEvent, &dbei)) - return; - - JSONNode jMsg = JSONNode::parse((char*)dbei.pBlob); - if (jMsg) { - JSONNode &jEdits = jMsg["edits"]; - if (jEdits) { - for (auto &it : jEdits) - if (it["time"].as_int() == edit_time) - return; - - JSONNode jEdit; - jEdit << INT_PARAM("time", (long)edit_time) << WCHAR_PARAM("text", szContent); - jEdits << jEdit; - } - } - else { - JSONNode jOriginalMsg; jOriginalMsg.set_name("original_message"); - jOriginalMsg << INT_PARAM("time", (long)dbei.timestamp) << CHAR_PARAM("text", (char *)dbei.pBlob); - - jMsg = JSONNode(); - jMsg << jOriginalMsg; - - JSONNode jEdit; - jEdit << INT_PARAM("time", (long)edit_time) << WCHAR_PARAM("text", szContent); - - JSONNode jEdits(JSON_ARRAY); jEdits.set_name("edits"); - jEdits << jEdit; - jMsg << jEdits; - } - - std::string newMsg = jMsg.write().c_str(); - dbei.cbBlob = int(newMsg.size() + 1); - dbei.pBlob = (uint8_t*)newMsg.c_str(); - db_event_edit(hContact, hEvent, &dbei); -} - -void CSkypeProto::InitDBEvents() -{ - // custom event - DBEVENTTYPEDESCR dbEventType = {}; - dbEventType.module = m_szModuleName; - dbEventType.flags = DETF_HISTORY | DETF_MSGWINDOW; - dbEventType.iconService = MODULE "/GetEventIcon"; - dbEventType.textService = MODULE "/GetEventText"; - - for (auto &cur : g_SkypeDBTypes) { - dbEventType.eventType = cur.type; - dbEventType.descr = Translate(cur.name); - dbEventType.flags |= cur.flags; - - DbEvent_RegisterType(&dbEventType); - - dbEventType.flags &= (~cur.flags); - } -} +/* +Copyright (c) 2015-23 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 . +*/ + +#include "stdafx.h" + +struct { int type; char *name; uint32_t flags; } g_SkypeDBTypes[] = +{ + { SKYPE_DB_EVENT_TYPE_INCOMING_CALL, LPGEN("Incoming call"), DETF_NONOTIFY }, + { SKYPE_DB_EVENT_TYPE_EDITED_MESSAGE, LPGEN("Edited message"), 0 }, + { SKYPE_DB_EVENT_TYPE_ACTION, LPGEN("Action"), 0 }, + { SKYPE_DB_EVENT_TYPE_CALL_INFO, LPGEN("Call information"), 0 }, + { SKYPE_DB_EVENT_TYPE_FILETRANSFER_INFO, LPGEN("File transfer information"), 0 }, + { SKYPE_DB_EVENT_TYPE_URIOBJ, LPGEN("URI object"), 0 }, + { SKYPE_DB_EVENT_TYPE_MOJI, LPGEN("Moji"), 0 }, + { SKYPE_DB_EVENT_TYPE_FILE, LPGEN("File"), 0 }, + { SKYPE_DB_EVENT_TYPE_UNKNOWN, LPGEN("Unknown event"), 0 }, +}; + +MEVENT CSkypeProto::GetMessageFromDb(const char *messageId) +{ + if (messageId == nullptr) + return NULL; + + return db_event_getById(m_szModuleName, messageId); +} + +MEVENT CSkypeProto::AddDbEvent(uint16_t type, MCONTACT hContact, uint32_t timestamp, uint32_t flags, const CMStringW &content, const CMStringA &msgId) +{ + if (MEVENT hDbEvent = GetMessageFromDb(msgId)) + return hDbEvent; + + T2Utf szMsg(content); + DBEVENTINFO dbei = {}; + dbei.szModule = m_szModuleName; + dbei.timestamp = timestamp; + dbei.eventType = type; + dbei.cbBlob = (uint32_t)mir_strlen(szMsg) + 1; + dbei.pBlob = (uint8_t *)szMsg; + dbei.flags = flags; + dbei.szId = msgId; + return db_event_add(hContact, &dbei); +} + +void CSkypeProto::EditEvent(MCONTACT hContact, MEVENT hEvent, const CMStringW &szContent, time_t edit_time) +{ + mir_cslock lck(m_AppendMessageLock); + + DB::EventInfo dbei; + dbei.cbBlob = -1; + if (db_event_get(hEvent, &dbei)) + return; + + JSONNode jMsg = JSONNode::parse((char*)dbei.pBlob); + if (jMsg) { + JSONNode &jEdits = jMsg["edits"]; + if (jEdits) { + for (auto &it : jEdits) + if (it["time"].as_int() == edit_time) + return; + + JSONNode jEdit; + jEdit << INT_PARAM("time", (long)edit_time) << WCHAR_PARAM("text", szContent); + jEdits << jEdit; + } + } + else { + JSONNode jOriginalMsg; jOriginalMsg.set_name("original_message"); + jOriginalMsg << INT_PARAM("time", (long)dbei.timestamp) << CHAR_PARAM("text", (char *)dbei.pBlob); + + jMsg = JSONNode(); + jMsg << jOriginalMsg; + + JSONNode jEdit; + jEdit << INT_PARAM("time", (long)edit_time) << WCHAR_PARAM("text", szContent); + + JSONNode jEdits(JSON_ARRAY); jEdits.set_name("edits"); + jEdits << jEdit; + jMsg << jEdits; + } + + std::string newMsg = jMsg.write().c_str(); + dbei.cbBlob = int(newMsg.size() + 1); + dbei.pBlob = (uint8_t*)newMsg.c_str(); + db_event_edit(hContact, hEvent, &dbei); +} + +void CSkypeProto::InitDBEvents() +{ + // custom event + DBEVENTTYPEDESCR dbEventType = {}; + dbEventType.module = m_szModuleName; + dbEventType.flags = DETF_HISTORY | DETF_MSGWINDOW; + dbEventType.iconService = MODULE "/GetEventIcon"; + dbEventType.textService = MODULE "/GetEventText"; + + for (auto &cur : g_SkypeDBTypes) { + dbEventType.eventType = cur.type; + dbEventType.descr = Translate(cur.name); + dbEventType.flags |= cur.flags; + + DbEvent_RegisterType(&dbEventType); + + dbEventType.flags &= (~cur.flags); + } +} diff --git a/protocols/SkypeWeb/src/skype_db.h b/protocols/SkypeWeb/src/skype_db.h index 8a0d58a049..ad9a7e2ff7 100644 --- a/protocols/SkypeWeb/src/skype_db.h +++ b/protocols/SkypeWeb/src/skype_db.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_events.cpp b/protocols/SkypeWeb/src/skype_events.cpp index a2f13e8187..49a93a53a0 100644 --- a/protocols/SkypeWeb/src/skype_events.cpp +++ b/protocols/SkypeWeb/src/skype_events.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_history_sync.cpp b/protocols/SkypeWeb/src/skype_history_sync.cpp index a034722bbd..ae72cdb920 100644 --- a/protocols/SkypeWeb/src/skype_history_sync.cpp +++ b/protocols/SkypeWeb/src/skype_history_sync.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_icons.cpp b/protocols/SkypeWeb/src/skype_icons.cpp index 8bd77f324a..15934a3d23 100644 --- a/protocols/SkypeWeb/src/skype_icons.cpp +++ b/protocols/SkypeWeb/src/skype_icons.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_login.cpp b/protocols/SkypeWeb/src/skype_login.cpp index d003c4ee71..94faff30d8 100644 --- a/protocols/SkypeWeb/src/skype_login.cpp +++ b/protocols/SkypeWeb/src/skype_login.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_menus.cpp b/protocols/SkypeWeb/src/skype_menus.cpp index 2d52b1a790..faf99da486 100644 --- a/protocols/SkypeWeb/src/skype_menus.cpp +++ b/protocols/SkypeWeb/src/skype_menus.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_menus.h b/protocols/SkypeWeb/src/skype_menus.h index 3e8c380d6f..b4e74aa6a3 100644 --- a/protocols/SkypeWeb/src/skype_menus.h +++ b/protocols/SkypeWeb/src/skype_menus.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_messages.cpp b/protocols/SkypeWeb/src/skype_messages.cpp index c6974a071c..01a0235caa 100644 --- a/protocols/SkypeWeb/src/skype_messages.cpp +++ b/protocols/SkypeWeb/src/skype_messages.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_oauth.cpp b/protocols/SkypeWeb/src/skype_oauth.cpp index dee5e92387..0a1f8908d3 100644 --- a/protocols/SkypeWeb/src/skype_oauth.cpp +++ b/protocols/SkypeWeb/src/skype_oauth.cpp @@ -1,165 +1,165 @@ -/* -Copyright (c) 2015-22 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 . -*/ - -#include "stdafx.h" - -static std::string sub(const std::string &str, const char *start, const char *end) -{ - size_t i1 = str.find(start); - if (i1 == -1) - return ""; - - i1 += strlen(start); - size_t i2 = str.find(end, i1); - return (i2 == -1) ? "" : str.substr(i1, i2 - i1); -} - -void CSkypeProto::OnOAuthStart(NETLIBHTTPREQUEST *response, AsyncHttpRequest*) -{ - if (response == nullptr || response->pData == nullptr) { - ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); - SetStatus(ID_STATUS_OFFLINE); - return; - } - - std::regex regex; - std::smatch match; - std::string content = response->pData; - - regex = ""; - - if (!std::regex_search(content, match, regex)) { - ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); - SetStatus(ID_STATUS_OFFLINE); - return; - } - std::string PPFT = match[1]; - - std::map scookies; - regex = "^(.+?)=(.*?);"; - - for (int i = 0; i < response->headersCount; i++) { - if (mir_strcmpi(response->headers[i].szName, "Set-Cookie")) - continue; - - content = response->headers[i].szValue; - if (std::regex_search(content, match, regex)) - scookies[match[1]] = match[2]; - } - - ptrA login(getStringA(SKYPE_SETTINGS_ID)); - ptrA password(getStringA(SKYPE_SETTINGS_PASSWORD)); - CMStringA mscookies(FORMAT, "MSPRequ=%s;MSPOK=%s;CkTst=G%lld;", scookies["MSPRequ"].c_str(), scookies["MSPOK"].c_str(), time(NULL)); - - cookies["MSPRequ"] = scookies["MSPRequ"]; - - PushRequest(new OAuthRequest(login, password, mscookies.c_str(), PPFT.c_str())); -} - -bool CSkypeProto::CheckOauth(const char *szResponse) -{ - std::string content = szResponse; - std::smatch match; - if (!std::regex_search(content, match, std::regex(""))) - if (!std::regex_search(content, match, std::regex(""))) - return false; - - std::string t = match[1]; - PushRequest(new OAuthRequest(t.c_str())); - return true; -} - -void CSkypeProto::OnOAuthConfirm(NETLIBHTTPREQUEST *response, AsyncHttpRequest *) -{ - if (response == nullptr || response->pData == nullptr) { - ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); - SetStatus(ID_STATUS_OFFLINE); - return; - } - - if (CheckOauth(response->pData)) - return; - - std::string content = response->pData; - std::string PPFT = sub(content, "sFT:'", "'"); - std::string opid = sub(content, "opid=", "&"); - if (PPFT.empty() || opid.empty()) { - ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); - SetStatus(ID_STATUS_OFFLINE); - return; - } - - std::regex regex("^(.+?=.*?;)"); - std::smatch match; - CMStringA mscookies; - - for (int i = 0; i < response->headersCount; i++) { - if (mir_strcmpi(response->headers[i].szName, "Set-Cookie")) - continue; - - content = response->headers[i].szValue; - if (std::regex_search(content, match, regex)) - mscookies.Append(match[1].str().c_str()); - } - - PushRequest(new OAuthRequest(mscookies.c_str(), PPFT.c_str(), opid.c_str())); -} - -void CSkypeProto::OnOAuthAuthorize(NETLIBHTTPREQUEST *response, AsyncHttpRequest*) -{ - if (response == nullptr || response->pData == nullptr) { - ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); - SetStatus(ID_STATUS_OFFLINE); - return; - } - - if (!CheckOauth(response->pData)) { - ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); - SetStatus(ID_STATUS_OFFLINE); - } -} - -void CSkypeProto::OnOAuthEnd(NETLIBHTTPREQUEST *response, AsyncHttpRequest*) -{ - if (response == nullptr || response->pData == nullptr) { - ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); - SetStatus(ID_STATUS_OFFLINE); - return; - } - - std::regex regex; - std::smatch match; - std::string content = response->pData; - - regex = ""; - if (!std::regex_search(content, match, regex)) { - ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); - SetStatus(ID_STATUS_OFFLINE); - return; - } - std::string token = match[1]; - setString("TokenSecret", token.c_str()); - regex = ""; - - if (std::regex_search(content, match, regex)) { - std::string expiresIn = match[1]; - int seconds = atoi(expiresIn.c_str()); - setDword("TokenExpiresIn", time(NULL) + seconds); - } - - OnLoginSuccess(); -} +/* +Copyright (c) 2015-23 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 . +*/ + +#include "stdafx.h" + +static std::string sub(const std::string &str, const char *start, const char *end) +{ + size_t i1 = str.find(start); + if (i1 == -1) + return ""; + + i1 += strlen(start); + size_t i2 = str.find(end, i1); + return (i2 == -1) ? "" : str.substr(i1, i2 - i1); +} + +void CSkypeProto::OnOAuthStart(NETLIBHTTPREQUEST *response, AsyncHttpRequest*) +{ + if (response == nullptr || response->pData == nullptr) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); + SetStatus(ID_STATUS_OFFLINE); + return; + } + + std::regex regex; + std::smatch match; + std::string content = response->pData; + + regex = ""; + + if (!std::regex_search(content, match, regex)) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); + SetStatus(ID_STATUS_OFFLINE); + return; + } + std::string PPFT = match[1]; + + std::map scookies; + regex = "^(.+?)=(.*?);"; + + for (int i = 0; i < response->headersCount; i++) { + if (mir_strcmpi(response->headers[i].szName, "Set-Cookie")) + continue; + + content = response->headers[i].szValue; + if (std::regex_search(content, match, regex)) + scookies[match[1]] = match[2]; + } + + ptrA login(getStringA(SKYPE_SETTINGS_ID)); + ptrA password(getStringA(SKYPE_SETTINGS_PASSWORD)); + CMStringA mscookies(FORMAT, "MSPRequ=%s;MSPOK=%s;CkTst=G%lld;", scookies["MSPRequ"].c_str(), scookies["MSPOK"].c_str(), time(NULL)); + + cookies["MSPRequ"] = scookies["MSPRequ"]; + + PushRequest(new OAuthRequest(login, password, mscookies.c_str(), PPFT.c_str())); +} + +bool CSkypeProto::CheckOauth(const char *szResponse) +{ + std::string content = szResponse; + std::smatch match; + if (!std::regex_search(content, match, std::regex(""))) + if (!std::regex_search(content, match, std::regex(""))) + return false; + + std::string t = match[1]; + PushRequest(new OAuthRequest(t.c_str())); + return true; +} + +void CSkypeProto::OnOAuthConfirm(NETLIBHTTPREQUEST *response, AsyncHttpRequest *) +{ + if (response == nullptr || response->pData == nullptr) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); + SetStatus(ID_STATUS_OFFLINE); + return; + } + + if (CheckOauth(response->pData)) + return; + + std::string content = response->pData; + std::string PPFT = sub(content, "sFT:'", "'"); + std::string opid = sub(content, "opid=", "&"); + if (PPFT.empty() || opid.empty()) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); + SetStatus(ID_STATUS_OFFLINE); + return; + } + + std::regex regex("^(.+?=.*?;)"); + std::smatch match; + CMStringA mscookies; + + for (int i = 0; i < response->headersCount; i++) { + if (mir_strcmpi(response->headers[i].szName, "Set-Cookie")) + continue; + + content = response->headers[i].szValue; + if (std::regex_search(content, match, regex)) + mscookies.Append(match[1].str().c_str()); + } + + PushRequest(new OAuthRequest(mscookies.c_str(), PPFT.c_str(), opid.c_str())); +} + +void CSkypeProto::OnOAuthAuthorize(NETLIBHTTPREQUEST *response, AsyncHttpRequest*) +{ + if (response == nullptr || response->pData == nullptr) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); + SetStatus(ID_STATUS_OFFLINE); + return; + } + + if (!CheckOauth(response->pData)) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); + SetStatus(ID_STATUS_OFFLINE); + } +} + +void CSkypeProto::OnOAuthEnd(NETLIBHTTPREQUEST *response, AsyncHttpRequest*) +{ + if (response == nullptr || response->pData == nullptr) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); + SetStatus(ID_STATUS_OFFLINE); + return; + } + + std::regex regex; + std::smatch match; + std::string content = response->pData; + + regex = ""; + if (!std::regex_search(content, match, regex)) { + ProtoBroadcastAck(NULL, ACKTYPE_LOGIN, ACKRESULT_FAILED, NULL, LOGIN_ERROR_UNKNOWN); + SetStatus(ID_STATUS_OFFLINE); + return; + } + std::string token = match[1]; + setString("TokenSecret", token.c_str()); + regex = ""; + + if (std::regex_search(content, match, regex)) { + std::string expiresIn = match[1]; + int seconds = atoi(expiresIn.c_str()); + setDword("TokenExpiresIn", time(NULL) + seconds); + } + + OnLoginSuccess(); +} diff --git a/protocols/SkypeWeb/src/skype_options.cpp b/protocols/SkypeWeb/src/skype_options.cpp index baaff8b7ab..07f15c215a 100644 --- a/protocols/SkypeWeb/src/skype_options.cpp +++ b/protocols/SkypeWeb/src/skype_options.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_polling.cpp b/protocols/SkypeWeb/src/skype_polling.cpp index b868e18bcd..0000de3b20 100644 --- a/protocols/SkypeWeb/src/skype_polling.cpp +++ b/protocols/SkypeWeb/src/skype_polling.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_profile.cpp b/protocols/SkypeWeb/src/skype_profile.cpp index 5612dc9619..f8fbaa2fbe 100644 --- a/protocols/SkypeWeb/src/skype_profile.cpp +++ b/protocols/SkypeWeb/src/skype_profile.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_proto.cpp b/protocols/SkypeWeb/src/skype_proto.cpp index d99fe64b61..5eb23d11ca 100644 --- a/protocols/SkypeWeb/src/skype_proto.cpp +++ b/protocols/SkypeWeb/src/skype_proto.cpp @@ -1,343 +1,343 @@ -/* -Copyright (c) 2015-22 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 . -*/ - -#include "stdafx.h" - -CSkypeProto::CSkypeProto(const char* protoName, const wchar_t* userName) : - PROTO(protoName, userName), - m_PopupClasses(1), - m_OutMessages(3, PtrKeySortT), - m_bThreadsTerminated(false), - m_impl(*this), - m_requests(1), - bAutoHistorySync(this, "AutoSync", true), - bMarkAllAsUnread(this, "MarkMesUnread", true), - bUseHostnameAsPlace(this, "UseHostName", true), - bUseBBCodes(this, "UseBBCodes", true), - bUseServerTime(this, "UseServerTime", false), - wstrCListGroup(this, SKYPE_SETTINGS_GROUP, L"Skype"), - wstrPlace(this, "Place", L"") -{ - 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_CREATEACCMGRUI, &CSkypeProto::OnAccountManagerInit); - CreateProtoService(PS_GETAVATARINFO, &CSkypeProto::SvcGetAvatarInfo); - CreateProtoService(PS_GETAVATARCAPS, &CSkypeProto::SvcGetAvatarCaps); - CreateProtoService(PS_GETMYAVATAR, &CSkypeProto::SvcGetMyAvatar); - CreateProtoService(PS_SETMYAVATAR, &CSkypeProto::SvcSetMyAvatar); - - CreateProtoService(PS_MENU_REQAUTH, &CSkypeProto::OnRequestAuth); - CreateProtoService(PS_MENU_GRANTAUTH, &CSkypeProto::OnGrantAuth); - CreateProtoService(PS_MENU_LOADHISTORY, &CSkypeProto::GetContactHistory); - - HookProtoEvent(ME_OPT_INITIALISE, &CSkypeProto::OnOptionsInit); - HookProtoEvent(ME_DB_EVENT_MARKED_READ, &CSkypeProto::OnDbEventRead); - - m_tszAvatarFolder = std::wstring(VARSW(L"%miranda_avatarcache%")) + L"\\" + m_tszUserName; - CreateDirectoryTreeW(m_tszAvatarFolder.c_str()); - - //sounds - g_plugin.addSound("skype_inc_call", L"SkypeWeb", LPGENW("Incoming call")); - g_plugin.addSound("skype_call_canceled", L"SkypeWeb", LPGENW("Incoming call canceled")); - - m_hPollingThread = ForkThreadEx(&CSkypeProto::PollingThread, NULL, NULL); - - 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"); - } - } - } - - InitGroupChatModule(); -} - -CSkypeProto::~CSkypeProto() -{ - StopQueue(); - if (m_hRequestQueueThread) { - WaitForSingleObject(m_hRequestQueueThread, INFINITE); - m_hRequestQueueThread = nullptr; - } - - UninitPopups(); - - if (m_hPollingThread) { - WaitForSingleObject(m_hPollingThread, INFINITE); - m_hPollingThread = nullptr; - } -} - -void CSkypeProto::OnModulesLoaded() -{ - setAllContactStatuses(ID_STATUS_OFFLINE, false); - - HookProtoEvent(ME_MSG_PRECREATEEVENT, &CSkypeProto::OnPreCreateMessage); - - InitDBEvents(); - InitPopups(); -} - -void CSkypeProto::OnShutdown() -{ - debugLogA(__FUNCTION__); - - StopQueue(); - - m_bThreadsTerminated = true; - - m_hPollingEvent.Set(); - m_hTrouterEvent.Set(); -} - -INT_PTR CSkypeProto::GetCaps(int type, MCONTACT) -{ - switch (type) { - case PFLAGNUM_1: - return PF1_IM | PF1_AUTHREQ | PF1_CHAT | PF1_BASICSEARCH | PF1_MODEMSG | PF1_FILE; - case PFLAGNUM_2: - return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_HEAVYDND; - case PFLAGNUM_3: - return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_HEAVYDND; - case PFLAGNUM_4: - return PF4_NOAUTHDENYREASON | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_IMSENDOFFLINE | PF4_OFFLINEFILES | PF4_SERVERMSGID; - case PFLAG_UNIQUEIDTEXT: - return (INT_PTR)TranslateT("Skypename"); - } - return 0; -} - -int CSkypeProto::SetAwayMsg(int, const wchar_t *msg) -{ - if (IsOnline()) - PushRequest(new SetStatusMsgRequest(msg ? T2Utf(msg) : "")); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void CSkypeProto::OnReceiveAwayMsg(NETLIBHTTPREQUEST *response, AsyncHttpRequest *pRequest) -{ - JsonReply reply(response); - if (reply.error()) - return; - - MCONTACT hContact = DWORD_PTR(pRequest->pUserInfo); - auto &root = reply.data(); - if (JSONNode &mood = root["mood"]) { - CMStringW str = mood.as_mstring(); - ProtoBroadcastAck(hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, (LPARAM)str.c_str()); - } - else { - ProtoBroadcastAck(hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, 0); - } -} - -HANDLE CSkypeProto::GetAwayMsg(MCONTACT hContact) -{ - auto *pReq = new GetProfileRequest(this, hContact); - pReq->m_pFunc = &CSkypeProto::OnReceiveAwayMsg; - return (HANDLE)1; -} - -MCONTACT CSkypeProto::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 CSkypeProto::AddToListByEvent(int, int, MEVENT hDbEvent) -{ - debugLogA(__FUNCTION__); - - DB::EventInfo dbei; - dbei.cbBlob = -1; - if (db_event_get(hDbEvent, &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 CSkypeProto::Authorize(MEVENT hDbEvent) -{ - MCONTACT hContact = GetContactFromAuthEvent(hDbEvent); - if (hContact == INVALID_CONTACT_ID) - return 1; - - PushRequest(new AuthAcceptRequest(getId(hContact))); - return 0; -} - -int CSkypeProto::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 CSkypeProto::AuthRecv(MCONTACT, PROTORECVEVENT* pre) -{ - return Proto_AuthRecv(m_szModuleName, pre); -} - -int CSkypeProto::AuthRequest(MCONTACT hContact, const wchar_t *szMessage) -{ - if (hContact == INVALID_CONTACT_ID) - return 1; - - PushRequest(new AddContactRequest(getId(hContact), T2Utf(szMessage))); - return 0; -} - -int CSkypeProto::GetInfo(MCONTACT hContact, int) -{ - if (isChatRoom(hContact)) - return 1; - - PushRequest(new GetProfileRequest(this, hContact)); - return 0; -} - -int CSkypeProto::SendMsg(MCONTACT hContact, int flags, const char *msg) -{ - return OnSendMessage(hContact, flags, msg); -} - -int CSkypeProto::SetStatus(int iNewStatus) -{ - if (iNewStatus == m_iDesiredStatus) - return 0; - - switch (iNewStatus) { - case ID_STATUS_FREECHAT: iNewStatus = ID_STATUS_ONLINE; break; - case ID_STATUS_NA: iNewStatus = ID_STATUS_AWAY; break; - case ID_STATUS_OCCUPIED: iNewStatus = ID_STATUS_DND; break; - } - - debugLogA(__FUNCTION__ ": changing status from %i to %i", m_iStatus, iNewStatus); - - int old_status = m_iStatus; - m_iDesiredStatus = iNewStatus; - - if (iNewStatus == ID_STATUS_OFFLINE) { - if (m_iStatus > ID_STATUS_CONNECTING + 1 && m_szId) - PushRequest(new DeleteEndpointRequest(this)); - - m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; - // logout - StopQueue(); - - ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, ID_STATUS_OFFLINE); - - m_impl.m_heartBeat.StopSafe(); - - if (!Miranda_IsTerminated()) - setAllContactStatuses(ID_STATUS_OFFLINE, false); - return 0; - } - else { - if (old_status == ID_STATUS_CONNECTING) - return 0; - - if (old_status == ID_STATUS_OFFLINE && m_iStatus == ID_STATUS_OFFLINE) - Login(); - else - PushRequest(new SetStatusRequest(MirandaToSkypeStatus(m_iDesiredStatus))); - } - - ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus); - return 0; -} - -int CSkypeProto::UserIsTyping(MCONTACT hContact, int type) -{ - PushRequest(new SendTypingRequest(getId(hContact), type)); - return 0; -} - -int CSkypeProto::RecvContacts(MCONTACT hContact, PROTORECVEVENT* pre) -{ - PROTOSEARCHRESULT **isrList = (PROTOSEARCHRESULT**)pre->szMessage; - - int nCount = *((LPARAM*)pre->lParam); - char* szMessageId = ((char*)pre->lParam + sizeof(LPARAM)); - - //if (GetMessageFromDb(hContact, szMessageId, pre->timestamp)) return 0; - - 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) + mir_strlen(szMessageId)); - - uint8_t *pBlob = (uint8_t*)mir_calloc(cbBlob); - uint8_t *pCurBlob = pBlob; - - for (int i = 0; i < nCount; i++) { - //mir_strcpy((char*)pCurBlob, _T2A(isrList[i]->nick.w)); - pCurBlob += mir_strlen((PCHAR)pCurBlob) + 1; - - mir_strcpy((char*)pCurBlob, _T2A(isrList[i]->id.w)); - pCurBlob += mir_strlen((char*)pCurBlob) + 1; - } - - DBEVENTINFO dbei = {}; - dbei.szModule = m_szModuleName; - dbei.timestamp = pre->timestamp; - dbei.eventType = EVENTTYPE_CONTACTS; - dbei.cbBlob = cbBlob; - dbei.pBlob = pBlob; - dbei.flags = (pre->flags & PREF_CREATEREAD) ? DBEF_READ : 0; - db_event_add(hContact, &dbei); - - mir_free(pBlob); - return 0; -} +/* +Copyright (c) 2015-23 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 . +*/ + +#include "stdafx.h" + +CSkypeProto::CSkypeProto(const char* protoName, const wchar_t* userName) : + PROTO(protoName, userName), + m_PopupClasses(1), + m_OutMessages(3, PtrKeySortT), + m_bThreadsTerminated(false), + m_impl(*this), + m_requests(1), + bAutoHistorySync(this, "AutoSync", true), + bMarkAllAsUnread(this, "MarkMesUnread", true), + bUseHostnameAsPlace(this, "UseHostName", true), + bUseBBCodes(this, "UseBBCodes", true), + bUseServerTime(this, "UseServerTime", false), + wstrCListGroup(this, SKYPE_SETTINGS_GROUP, L"Skype"), + wstrPlace(this, "Place", L"") +{ + 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_CREATEACCMGRUI, &CSkypeProto::OnAccountManagerInit); + CreateProtoService(PS_GETAVATARINFO, &CSkypeProto::SvcGetAvatarInfo); + CreateProtoService(PS_GETAVATARCAPS, &CSkypeProto::SvcGetAvatarCaps); + CreateProtoService(PS_GETMYAVATAR, &CSkypeProto::SvcGetMyAvatar); + CreateProtoService(PS_SETMYAVATAR, &CSkypeProto::SvcSetMyAvatar); + + CreateProtoService(PS_MENU_REQAUTH, &CSkypeProto::OnRequestAuth); + CreateProtoService(PS_MENU_GRANTAUTH, &CSkypeProto::OnGrantAuth); + CreateProtoService(PS_MENU_LOADHISTORY, &CSkypeProto::GetContactHistory); + + HookProtoEvent(ME_OPT_INITIALISE, &CSkypeProto::OnOptionsInit); + HookProtoEvent(ME_DB_EVENT_MARKED_READ, &CSkypeProto::OnDbEventRead); + + m_tszAvatarFolder = std::wstring(VARSW(L"%miranda_avatarcache%")) + L"\\" + m_tszUserName; + CreateDirectoryTreeW(m_tszAvatarFolder.c_str()); + + //sounds + g_plugin.addSound("skype_inc_call", L"SkypeWeb", LPGENW("Incoming call")); + g_plugin.addSound("skype_call_canceled", L"SkypeWeb", LPGENW("Incoming call canceled")); + + m_hPollingThread = ForkThreadEx(&CSkypeProto::PollingThread, NULL, NULL); + + 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"); + } + } + } + + InitGroupChatModule(); +} + +CSkypeProto::~CSkypeProto() +{ + StopQueue(); + if (m_hRequestQueueThread) { + WaitForSingleObject(m_hRequestQueueThread, INFINITE); + m_hRequestQueueThread = nullptr; + } + + UninitPopups(); + + if (m_hPollingThread) { + WaitForSingleObject(m_hPollingThread, INFINITE); + m_hPollingThread = nullptr; + } +} + +void CSkypeProto::OnModulesLoaded() +{ + setAllContactStatuses(ID_STATUS_OFFLINE, false); + + HookProtoEvent(ME_MSG_PRECREATEEVENT, &CSkypeProto::OnPreCreateMessage); + + InitDBEvents(); + InitPopups(); +} + +void CSkypeProto::OnShutdown() +{ + debugLogA(__FUNCTION__); + + StopQueue(); + + m_bThreadsTerminated = true; + + m_hPollingEvent.Set(); + m_hTrouterEvent.Set(); +} + +INT_PTR CSkypeProto::GetCaps(int type, MCONTACT) +{ + switch (type) { + case PFLAGNUM_1: + return PF1_IM | PF1_AUTHREQ | PF1_CHAT | PF1_BASICSEARCH | PF1_MODEMSG | PF1_FILE; + case PFLAGNUM_2: + return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_HEAVYDND; + case PFLAGNUM_3: + return PF2_ONLINE | PF2_INVISIBLE | PF2_SHORTAWAY | PF2_HEAVYDND; + case PFLAGNUM_4: + return PF4_NOAUTHDENYREASON | PF4_SUPPORTTYPING | PF4_AVATARS | PF4_IMSENDOFFLINE | PF4_OFFLINEFILES | PF4_SERVERMSGID; + case PFLAG_UNIQUEIDTEXT: + return (INT_PTR)TranslateT("Skypename"); + } + return 0; +} + +int CSkypeProto::SetAwayMsg(int, const wchar_t *msg) +{ + if (IsOnline()) + PushRequest(new SetStatusMsgRequest(msg ? T2Utf(msg) : "")); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void CSkypeProto::OnReceiveAwayMsg(NETLIBHTTPREQUEST *response, AsyncHttpRequest *pRequest) +{ + JsonReply reply(response); + if (reply.error()) + return; + + MCONTACT hContact = DWORD_PTR(pRequest->pUserInfo); + auto &root = reply.data(); + if (JSONNode &mood = root["mood"]) { + CMStringW str = mood.as_mstring(); + ProtoBroadcastAck(hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, (LPARAM)str.c_str()); + } + else { + ProtoBroadcastAck(hContact, ACKTYPE_AWAYMSG, ACKRESULT_SUCCESS, (HANDLE)1, 0); + } +} + +HANDLE CSkypeProto::GetAwayMsg(MCONTACT hContact) +{ + auto *pReq = new GetProfileRequest(this, hContact); + pReq->m_pFunc = &CSkypeProto::OnReceiveAwayMsg; + return (HANDLE)1; +} + +MCONTACT CSkypeProto::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 CSkypeProto::AddToListByEvent(int, int, MEVENT hDbEvent) +{ + debugLogA(__FUNCTION__); + + DB::EventInfo dbei; + dbei.cbBlob = -1; + if (db_event_get(hDbEvent, &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 CSkypeProto::Authorize(MEVENT hDbEvent) +{ + MCONTACT hContact = GetContactFromAuthEvent(hDbEvent); + if (hContact == INVALID_CONTACT_ID) + return 1; + + PushRequest(new AuthAcceptRequest(getId(hContact))); + return 0; +} + +int CSkypeProto::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 CSkypeProto::AuthRecv(MCONTACT, PROTORECVEVENT* pre) +{ + return Proto_AuthRecv(m_szModuleName, pre); +} + +int CSkypeProto::AuthRequest(MCONTACT hContact, const wchar_t *szMessage) +{ + if (hContact == INVALID_CONTACT_ID) + return 1; + + PushRequest(new AddContactRequest(getId(hContact), T2Utf(szMessage))); + return 0; +} + +int CSkypeProto::GetInfo(MCONTACT hContact, int) +{ + if (isChatRoom(hContact)) + return 1; + + PushRequest(new GetProfileRequest(this, hContact)); + return 0; +} + +int CSkypeProto::SendMsg(MCONTACT hContact, int flags, const char *msg) +{ + return OnSendMessage(hContact, flags, msg); +} + +int CSkypeProto::SetStatus(int iNewStatus) +{ + if (iNewStatus == m_iDesiredStatus) + return 0; + + switch (iNewStatus) { + case ID_STATUS_FREECHAT: iNewStatus = ID_STATUS_ONLINE; break; + case ID_STATUS_NA: iNewStatus = ID_STATUS_AWAY; break; + case ID_STATUS_OCCUPIED: iNewStatus = ID_STATUS_DND; break; + } + + debugLogA(__FUNCTION__ ": changing status from %i to %i", m_iStatus, iNewStatus); + + int old_status = m_iStatus; + m_iDesiredStatus = iNewStatus; + + if (iNewStatus == ID_STATUS_OFFLINE) { + if (m_iStatus > ID_STATUS_CONNECTING + 1 && m_szId) + PushRequest(new DeleteEndpointRequest(this)); + + m_iStatus = m_iDesiredStatus = ID_STATUS_OFFLINE; + // logout + StopQueue(); + + ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, ID_STATUS_OFFLINE); + + m_impl.m_heartBeat.StopSafe(); + + if (!Miranda_IsTerminated()) + setAllContactStatuses(ID_STATUS_OFFLINE, false); + return 0; + } + else { + if (old_status == ID_STATUS_CONNECTING) + return 0; + + if (old_status == ID_STATUS_OFFLINE && m_iStatus == ID_STATUS_OFFLINE) + Login(); + else + PushRequest(new SetStatusRequest(MirandaToSkypeStatus(m_iDesiredStatus))); + } + + ProtoBroadcastAck(NULL, ACKTYPE_STATUS, ACKRESULT_SUCCESS, (HANDLE)old_status, m_iStatus); + return 0; +} + +int CSkypeProto::UserIsTyping(MCONTACT hContact, int type) +{ + PushRequest(new SendTypingRequest(getId(hContact), type)); + return 0; +} + +int CSkypeProto::RecvContacts(MCONTACT hContact, PROTORECVEVENT* pre) +{ + PROTOSEARCHRESULT **isrList = (PROTOSEARCHRESULT**)pre->szMessage; + + int nCount = *((LPARAM*)pre->lParam); + char* szMessageId = ((char*)pre->lParam + sizeof(LPARAM)); + + //if (GetMessageFromDb(hContact, szMessageId, pre->timestamp)) return 0; + + 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) + mir_strlen(szMessageId)); + + uint8_t *pBlob = (uint8_t*)mir_calloc(cbBlob); + uint8_t *pCurBlob = pBlob; + + for (int i = 0; i < nCount; i++) { + //mir_strcpy((char*)pCurBlob, _T2A(isrList[i]->nick.w)); + pCurBlob += mir_strlen((PCHAR)pCurBlob) + 1; + + mir_strcpy((char*)pCurBlob, _T2A(isrList[i]->id.w)); + pCurBlob += mir_strlen((char*)pCurBlob) + 1; + } + + DBEVENTINFO dbei = {}; + dbei.szModule = m_szModuleName; + dbei.timestamp = pre->timestamp; + dbei.eventType = EVENTTYPE_CONTACTS; + dbei.cbBlob = cbBlob; + dbei.pBlob = pBlob; + dbei.flags = (pre->flags & PREF_CREATEREAD) ? DBEF_READ : 0; + db_event_add(hContact, &dbei); + + mir_free(pBlob); + return 0; +} diff --git a/protocols/SkypeWeb/src/skype_proto.h b/protocols/SkypeWeb/src/skype_proto.h index 4630cde68f..3517ae742a 100644 --- a/protocols/SkypeWeb/src/skype_proto.h +++ b/protocols/SkypeWeb/src/skype_proto.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_search.cpp b/protocols/SkypeWeb/src/skype_search.cpp index e47eba7243..51a4952021 100644 --- a/protocols/SkypeWeb/src/skype_search.cpp +++ b/protocols/SkypeWeb/src/skype_search.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_trouter.cpp b/protocols/SkypeWeb/src/skype_trouter.cpp index bb8a74385b..65e38d4d41 100644 --- a/protocols/SkypeWeb/src/skype_trouter.cpp +++ b/protocols/SkypeWeb/src/skype_trouter.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/skype_utils.cpp b/protocols/SkypeWeb/src/skype_utils.cpp index 903309054b..fc7a4bb6b1 100644 --- a/protocols/SkypeWeb/src/skype_utils.cpp +++ b/protocols/SkypeWeb/src/skype_utils.cpp @@ -1,679 +1,679 @@ -/* -Copyright (c) 2015-22 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 . -*/ - -#include "stdafx.h" - -#pragma warning(disable:4566) - -time_t CSkypeProto::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" } -}; - -CMStringW RemoveHtml(const CMStringW &data) -{ - CMStringW new_string; - - for (int i = 0; i < data.GetLength(); i++) { - wchar_t c = data[i]; - if (c == '<') { - 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; - } - } - - new_string.AppendChar(c); - } - - return new_string; -} - -const char* CSkypeProto::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 CSkypeProto::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 CSkypeProto::IsFileExists(std::wstring path) -{ - return _waccess(path.c_str(), 0) == 0; -} - -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; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// 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, 8, 19 }; - -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 CSkypeProto::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); - CallService(MS_FILE_SENDFILE, hContact, NULL); - return 1; - } - - if (!mir_wstrcmpi(szCommand, L"voicemail")) - return 1; - - return 1; -} - -INT_PTR CSkypeProto::GlobalParseSkypeUriService(WPARAM wParam, LPARAM lParam) -{ - for (auto &it : CMPlugin::g_arInstances) - if (it->IsOnline()) - return it->ParseSkypeUriService(wParam, lParam); - - return 1; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) -{ - if (pReply == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = pReply->resultCode; - if (m_errorCode != 200) - return; - - m_root = json_parse(pReply->pData); - if (m_root == nullptr) { - m_errorCode = 500; - return; - } - - m_errorCode = (*m_root)["status"]["code"].as_int(); -} - -JsonReply::~JsonReply() -{ - json_delete(m_root); -} +/* +Copyright (c) 2015-23 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 . +*/ + +#include "stdafx.h" + +#pragma warning(disable:4566) + +time_t CSkypeProto::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" } +}; + +CMStringW RemoveHtml(const CMStringW &data) +{ + CMStringW new_string; + + for (int i = 0; i < data.GetLength(); i++) { + wchar_t c = data[i]; + if (c == '<') { + 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; + } + } + + new_string.AppendChar(c); + } + + return new_string; +} + +const char* CSkypeProto::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 CSkypeProto::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 CSkypeProto::IsFileExists(std::wstring path) +{ + return _waccess(path.c_str(), 0) == 0; +} + +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; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// 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, 8, 19 }; + +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 CSkypeProto::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); + CallService(MS_FILE_SENDFILE, hContact, NULL); + return 1; + } + + if (!mir_wstrcmpi(szCommand, L"voicemail")) + return 1; + + return 1; +} + +INT_PTR CSkypeProto::GlobalParseSkypeUriService(WPARAM wParam, LPARAM lParam) +{ + for (auto &it : CMPlugin::g_arInstances) + if (it->IsOnline()) + return it->ParseSkypeUriService(wParam, lParam); + + return 1; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +JsonReply::JsonReply(NETLIBHTTPREQUEST *pReply) +{ + if (pReply == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = pReply->resultCode; + if (m_errorCode != 200) + return; + + m_root = json_parse(pReply->pData); + if (m_root == nullptr) { + m_errorCode = 500; + return; + } + + m_errorCode = (*m_root)["status"]["code"].as_int(); +} + +JsonReply::~JsonReply() +{ + json_delete(m_root); +} diff --git a/protocols/SkypeWeb/src/skype_utils.h b/protocols/SkypeWeb/src/skype_utils.h index 6234abf8b6..e8a1f05836 100644 --- a/protocols/SkypeWeb/src/skype_utils.h +++ b/protocols/SkypeWeb/src/skype_utils.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/stdafx.cxx b/protocols/SkypeWeb/src/stdafx.cxx index f64d25234b..ebbde0ade1 100644 --- a/protocols/SkypeWeb/src/stdafx.cxx +++ b/protocols/SkypeWeb/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/SkypeWeb/src/stdafx.h b/protocols/SkypeWeb/src/stdafx.h index 059d5cb9d6..6dbb1da1c3 100644 --- a/protocols/SkypeWeb/src/stdafx.h +++ b/protocols/SkypeWeb/src/stdafx.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2015-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2015-23 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 diff --git a/protocols/SkypeWeb/src/version.h b/protocols/SkypeWeb/src/version.h index b41d65df1e..991de1af6d 100644 --- a/protocols/SkypeWeb/src/version.h +++ b/protocols/SkypeWeb/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Skype protocol support for Miranda NG. Based on new Skype for Web." #define __AUTHOR "Miranda NG team" #define __AUTHORWEB "https://miranda-ng.org/p/SkypeWeb" -#define __COPYRIGHT "© 2015-22 Miranda NG team" +#define __COPYRIGHT "© 2015-23 Miranda NG team" diff --git a/protocols/Steam/src/stdafx.cxx b/protocols/Steam/src/stdafx.cxx index f64d25234b..ebbde0ade1 100644 --- a/protocols/Steam/src/stdafx.cxx +++ b/protocols/Steam/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Steam/src/version.h b/protocols/Steam/src/version.h index e9ed560cb2..43a5d81451 100644 --- a/protocols/Steam/src/version.h +++ b/protocols/Steam/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Steam protocol support for Miranda NG." #define __AUTHOR "Miranda NG team, Robert Pösel" #define __AUTHORWEB "https://miranda-ng.org/p/Steam" -#define __COPYRIGHT "© 2014-17 Robert Pösel, 2017-22 Miranda NG team" +#define __COPYRIGHT "© 2014-17 Robert Pösel, 2017-23 Miranda NG team" diff --git a/protocols/Telegram/src/auth.cpp b/protocols/Telegram/src/auth.cpp index 22254ea460..3778ff1039 100644 --- a/protocols/Telegram/src/auth.cpp +++ b/protocols/Telegram/src/auth.cpp @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Telegram/src/options.cpp b/protocols/Telegram/src/options.cpp index d830c2122a..9159d7304e 100644 --- a/protocols/Telegram/src/options.cpp +++ b/protocols/Telegram/src/options.cpp @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Telegram/src/server.cpp b/protocols/Telegram/src/server.cpp index 8792000d59..2dccbc0281 100644 --- a/protocols/Telegram/src/server.cpp +++ b/protocols/Telegram/src/server.cpp @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Telegram/src/stdafx.cxx b/protocols/Telegram/src/stdafx.cxx index d265a4c02e..b08670e67f 100644 --- a/protocols/Telegram/src/stdafx.cxx +++ b/protocols/Telegram/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Telegram/src/utils.cpp b/protocols/Telegram/src/utils.cpp index 3ea60b4c19..6113870bd3 100644 --- a/protocols/Telegram/src/utils.cpp +++ b/protocols/Telegram/src/utils.cpp @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Telegram/src/version.h b/protocols/Telegram/src/version.h index 46df8e9330..7e46257aab 100644 --- a/protocols/Telegram/src/version.h +++ b/protocols/Telegram/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Telegram protocol support for Miranda NG." #define __AUTHOR "Miranda NG team" #define __AUTHORWEB "https://miranda-ng.org/p/Telegram" -#define __COPYRIGHT "© 2018-22 Miranda NG team" +#define __COPYRIGHT "© 2018-23 Miranda NG team" diff --git a/protocols/Tox/src/stdafx.cxx b/protocols/Tox/src/stdafx.cxx index f64d25234b..ebbde0ade1 100644 --- a/protocols/Tox/src/stdafx.cxx +++ b/protocols/Tox/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (C) 2012-22 Miranda NG team (https://miranda-ng.org) +Copyright (C) 2012-23 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 diff --git a/protocols/Tox/src/version.h b/protocols/Tox/src/version.h index 837b6a836d..e4140a30e9 100644 --- a/protocols/Tox/src/version.h +++ b/protocols/Tox/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Tox protocol support for Miranda NG." #define __AUTHOR "Miranda NG team" #define __AUTHORWEB "https://miranda-ng.org/p/Tox" -#define __COPYRIGHT "© 2014-22 Miranda NG team" +#define __COPYRIGHT "© 2014-23 Miranda NG team" diff --git a/protocols/Twitter/src/StringUtil.cpp b/protocols/Twitter/src/StringUtil.cpp index f2e0bf7189..39c65c5319 100644 --- a/protocols/Twitter/src/StringUtil.cpp +++ b/protocols/Twitter/src/StringUtil.cpp @@ -1,5 +1,5 @@ /* -Copyright 2012-22 Miranda NG team +Copyright 2012-23 Miranda NG team Copyright 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/chat.cpp b/protocols/Twitter/src/chat.cpp index 0d0426c0f4..40b344ada0 100644 --- a/protocols/Twitter/src/chat.cpp +++ b/protocols/Twitter/src/chat.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/connection.cpp b/protocols/Twitter/src/connection.cpp index f2622b29fa..d25dbcb919 100644 --- a/protocols/Twitter/src/connection.cpp +++ b/protocols/Twitter/src/connection.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/contacts.cpp b/protocols/Twitter/src/contacts.cpp index 2a15a4bd48..d17263cccf 100644 --- a/protocols/Twitter/src/contacts.cpp +++ b/protocols/Twitter/src/contacts.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/http.h b/protocols/Twitter/src/http.h index ed1732f082..db0f2204b0 100644 --- a/protocols/Twitter/src/http.h +++ b/protocols/Twitter/src/http.h @@ -1,6 +1,6 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/main.cpp b/protocols/Twitter/src/main.cpp index 0726ac15ac..5862b8a667 100644 --- a/protocols/Twitter/src/main.cpp +++ b/protocols/Twitter/src/main.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/oauth.cpp b/protocols/Twitter/src/oauth.cpp index 584284ad9f..16fc13e609 100644 --- a/protocols/Twitter/src/oauth.cpp +++ b/protocols/Twitter/src/oauth.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/proto.cpp b/protocols/Twitter/src/proto.cpp index df7fa2f46b..ab569e9d5b 100644 --- a/protocols/Twitter/src/proto.cpp +++ b/protocols/Twitter/src/proto.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/proto.h b/protocols/Twitter/src/proto.h index 4ea8b0241a..69fca0130a 100644 --- a/protocols/Twitter/src/proto.h +++ b/protocols/Twitter/src/proto.h @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/stdafx.cxx b/protocols/Twitter/src/stdafx.cxx index a8bf7b4e5f..d1bfe5009a 100644 --- a/protocols/Twitter/src/stdafx.cxx +++ b/protocols/Twitter/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/theme.cpp b/protocols/Twitter/src/theme.cpp index 162073b20f..b7b8c01e97 100644 --- a/protocols/Twitter/src/theme.cpp +++ b/protocols/Twitter/src/theme.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/theme.h b/protocols/Twitter/src/theme.h index 181fa6e486..0f0875fab2 100644 --- a/protocols/Twitter/src/theme.h +++ b/protocols/Twitter/src/theme.h @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/twitter.cpp b/protocols/Twitter/src/twitter.cpp index b8308c1fb4..fa6d441363 100644 --- a/protocols/Twitter/src/twitter.cpp +++ b/protocols/Twitter/src/twitter.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/ui.cpp b/protocols/Twitter/src/ui.cpp index 404a59ed29..978f791f83 100644 --- a/protocols/Twitter/src/ui.cpp +++ b/protocols/Twitter/src/ui.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/ui.h b/protocols/Twitter/src/ui.h index 067baeeffd..8762886cc8 100644 --- a/protocols/Twitter/src/ui.h +++ b/protocols/Twitter/src/ui.h @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/utility.cpp b/protocols/Twitter/src/utility.cpp index 24f32a5315..06b8c10c65 100644 --- a/protocols/Twitter/src/utility.cpp +++ b/protocols/Twitter/src/utility.cpp @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/utility.h b/protocols/Twitter/src/utility.h index 6fa12846e6..58a6f9028f 100644 --- a/protocols/Twitter/src/utility.h +++ b/protocols/Twitter/src/utility.h @@ -1,5 +1,5 @@ /* -Copyright © 2012-22 Miranda NG team +Copyright © 2012-23 Miranda NG team Copyright © 2009 Jim Porter This program is free software: you can redistribute it and/or modify diff --git a/protocols/Twitter/src/version.h b/protocols/Twitter/src/version.h index de7fb6516b..a3d829c1de 100644 --- a/protocols/Twitter/src/version.h +++ b/protocols/Twitter/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Twitter protocol support for Miranda NG." #define __AUTHOR "dentist, omniwolf, Thief" #define __AUTHORWEB "https://miranda-ng.org/p/Twitter" -#define __COPYRIGHT "© 2009-2010 dentist, 2010-2012 omniwolf and Thief, 2012-22 Miranda NG team" +#define __COPYRIGHT "© 2009-2010 dentist, 2010-2012 omniwolf and Thief, 2012-23 Miranda NG team" diff --git a/protocols/VKontakte/src/main.cpp b/protocols/VKontakte/src/main.cpp index 07fa2a6a6b..ee2ccc09c2 100644 --- a/protocols/VKontakte/src/main.cpp +++ b/protocols/VKontakte/src/main.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/misc.cpp b/protocols/VKontakte/src/misc.cpp index a671733f58..c60dbb36ad 100644 --- a/protocols/VKontakte/src/misc.cpp +++ b/protocols/VKontakte/src/misc.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/stdafx.cxx b/protocols/VKontakte/src/stdafx.cxx index 07c147c9ca..7b26873eaa 100644 --- a/protocols/VKontakte/src/stdafx.cxx +++ b/protocols/VKontakte/src/stdafx.cxx @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/stdafx.h b/protocols/VKontakte/src/stdafx.h index f0030969d0..04c0aae0c4 100644 --- a/protocols/VKontakte/src/stdafx.h +++ b/protocols/VKontakte/src/stdafx.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/version.h b/protocols/VKontakte/src/version.h index d23dfb98db..38b974c8c1 100644 --- a/protocols/VKontakte/src/version.h +++ b/protocols/VKontakte/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "VKontakte protocol support for Miranda NG." #define __AUTHOR "Miranda NG team" #define __AUTHORWEB "https://miranda-ng.org/p/VKontakte" -#define __COPYRIGHT "© 2013-22 Miranda NG team" +#define __COPYRIGHT "© 2013-23 Miranda NG team" diff --git a/protocols/VKontakte/src/vk.h b/protocols/VKontakte/src/vk.h index fa463a8a0a..f1af226f8a 100644 --- a/protocols/VKontakte/src/vk.h +++ b/protocols/VKontakte/src/vk.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_avatars.cpp b/protocols/VKontakte/src/vk_avatars.cpp index 6553366518..b4ee6cc7cb 100644 --- a/protocols/VKontakte/src/vk_avatars.cpp +++ b/protocols/VKontakte/src/vk_avatars.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_captcha.cpp b/protocols/VKontakte/src/vk_captcha.cpp index 5337e20181..e82d1a3400 100644 --- a/protocols/VKontakte/src/vk_captcha.cpp +++ b/protocols/VKontakte/src/vk_captcha.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_chats.cpp b/protocols/VKontakte/src/vk_chats.cpp index 6b05135d02..1771b5d474 100644 --- a/protocols/VKontakte/src/vk_chats.cpp +++ b/protocols/VKontakte/src/vk_chats.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_dialogs.cpp b/protocols/VKontakte/src/vk_dialogs.cpp index 49adcbc308..79ca3854ef 100644 --- a/protocols/VKontakte/src/vk_dialogs.cpp +++ b/protocols/VKontakte/src/vk_dialogs.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_dialogs.h b/protocols/VKontakte/src/vk_dialogs.h index 279b876232..ee3f7893cd 100644 --- a/protocols/VKontakte/src/vk_dialogs.h +++ b/protocols/VKontakte/src/vk_dialogs.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_feed.cpp b/protocols/VKontakte/src/vk_feed.cpp index 478071e34e..787565d9f5 100644 --- a/protocols/VKontakte/src/vk_feed.cpp +++ b/protocols/VKontakte/src/vk_feed.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_files.cpp b/protocols/VKontakte/src/vk_files.cpp index 9fa8195ed5..a7306c1568 100644 --- a/protocols/VKontakte/src/vk_files.cpp +++ b/protocols/VKontakte/src/vk_files.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_history.cpp b/protocols/VKontakte/src/vk_history.cpp index 839b3c669c..00457abd62 100644 --- a/protocols/VKontakte/src/vk_history.cpp +++ b/protocols/VKontakte/src/vk_history.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_messages.cpp b/protocols/VKontakte/src/vk_messages.cpp index 846142a72b..e6f31c2256 100644 --- a/protocols/VKontakte/src/vk_messages.cpp +++ b/protocols/VKontakte/src/vk_messages.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_options.cpp b/protocols/VKontakte/src/vk_options.cpp index 3b24c08d24..8342842cd3 100644 --- a/protocols/VKontakte/src/vk_options.cpp +++ b/protocols/VKontakte/src/vk_options.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_options.h b/protocols/VKontakte/src/vk_options.h index 3e5228b476..cf7385dfbb 100644 --- a/protocols/VKontakte/src/vk_options.h +++ b/protocols/VKontakte/src/vk_options.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_pollserver.cpp b/protocols/VKontakte/src/vk_pollserver.cpp index 8ee4e31a6f..0b0218e38b 100644 --- a/protocols/VKontakte/src/vk_pollserver.cpp +++ b/protocols/VKontakte/src/vk_pollserver.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_proto.cpp b/protocols/VKontakte/src/vk_proto.cpp index d361fdee6f..b894b36a64 100644 --- a/protocols/VKontakte/src/vk_proto.cpp +++ b/protocols/VKontakte/src/vk_proto.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_proto.h b/protocols/VKontakte/src/vk_proto.h index f5bf492d6c..fd680580a6 100644 --- a/protocols/VKontakte/src/vk_proto.h +++ b/protocols/VKontakte/src/vk_proto.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_queue.cpp b/protocols/VKontakte/src/vk_queue.cpp index cd9912410c..ecf98468d0 100644 --- a/protocols/VKontakte/src/vk_queue.cpp +++ b/protocols/VKontakte/src/vk_queue.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_search.cpp b/protocols/VKontakte/src/vk_search.cpp index 7f95b792e7..c7e929c063 100644 --- a/protocols/VKontakte/src/vk_search.cpp +++ b/protocols/VKontakte/src/vk_search.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_status.cpp b/protocols/VKontakte/src/vk_status.cpp index 34d41bc55a..4fb2405aa2 100644 --- a/protocols/VKontakte/src/vk_status.cpp +++ b/protocols/VKontakte/src/vk_status.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_struct.cpp b/protocols/VKontakte/src/vk_struct.cpp index c021850374..dffe7a2341 100644 --- a/protocols/VKontakte/src/vk_struct.cpp +++ b/protocols/VKontakte/src/vk_struct.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_struct.h b/protocols/VKontakte/src/vk_struct.h index e3b6e5f66e..f6b1da600d 100644 --- a/protocols/VKontakte/src/vk_struct.h +++ b/protocols/VKontakte/src/vk_struct.h @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_thread.cpp b/protocols/VKontakte/src/vk_thread.cpp index ccf9a4a7eb..bafba46074 100644 --- a/protocols/VKontakte/src/vk_thread.cpp +++ b/protocols/VKontakte/src/vk_thread.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vk_wallpost.cpp b/protocols/VKontakte/src/vk_wallpost.cpp index 3d2f43bbb2..28fe3fcbd9 100644 --- a/protocols/VKontakte/src/vk_wallpost.cpp +++ b/protocols/VKontakte/src/vk_wallpost.cpp @@ -1,5 +1,5 @@ /* -Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +Copyright (c) 2013-23 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 diff --git a/protocols/VKontakte/src/vkjs.js b/protocols/VKontakte/src/vkjs.js index 2392c2eeb9..8f1ed1185f 100644 --- a/protocols/VKontakte/src/vkjs.js +++ b/protocols/VKontakte/src/vkjs.js @@ -1,4 +1,4 @@ -// Copyright (c) 2013-22 Miranda NG team (https://miranda-ng.org) +// Copyright (c) 2013-23 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 diff --git a/protocols/Weather/src/stdafx.cxx b/protocols/Weather/src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/Weather/src/stdafx.cxx +++ b/protocols/Weather/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/Weather/src/stdafx.h b/protocols/Weather/src/stdafx.h index 88376fee8f..8c5abe874f 100644 --- a/protocols/Weather/src/stdafx.h +++ b/protocols/Weather/src/stdafx.h @@ -1,547 +1,547 @@ -/* -Weather Protocol plugin for Miranda NG -Copyright (C) 2012-22 Miranda NG team -Copyright (c) 2005-2011 Boris Krasnovskiy All Rights Reserved -Copyright (c) 2002-2005 Calvin Che - -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 . -*/ - -/* This file contains the includes, weather constants/declarations, - the structs, and the primitives for some of the functions. -*/ - -#pragma once - -//============ THE INCLUDES =========== - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include "resource.h" -#include "version.h" - -//============ CONSTANTS ============ - -// name -#define MODULENAME "Weather" -#define WEATHERPROTOTEXT "Weather" -#define DEFCURRENTWEATHER "WeatherCondition" -#define WEATHERCONDITION "Current" - -// weather conditions -enum EWeatherCondition -{ - SUNNY, - NA, - PCLOUDY, - CLOUDY, - RAIN, - RSHOWER, - FOG, - SNOW, - SSHOWER, - LIGHT, - MAX_COND -}; - -// status -#define NOSTATUSDATA 1 - -// limits -#define MAX_TEXT_SIZE 4096 -#define MAX_DATA_LEN 1024 - -// db info mangement mode -#define WDBM_REMOVE 1 -#define WDBM_DETAILDISPLAY 2 - -// more info list column width -#define LIST_COLUMN 150 - -// others -#define NODATA TranslateT("N/A") -#define UM_SETCONTACT 40000 - -// weather update error codes -#define INVALID_ID_FORMAT 10 -#define INVALID_SVC 11 -#define INVALID_ID 12 -#define SVC_NOT_FOUND 20 -#define NETLIB_ERROR 30 -#define DATA_EMPTY 40 -#define DOC_NOT_FOUND 42 -#define DOC_TOO_SHORT 43 -#define UNKNOWN_ERROR 99 - -// weather update error text -#define E10 TranslateT("Invalid ID format, missing \"/\" (10)") -#define E11 TranslateT("Invalid service (11)") -#define E12 TranslateT("Invalid station (12)") -#define E20 TranslateT("Weather service ini for this station is not found (20)") -#define E30 TranslateT("Netlib error - check your internet connection (30)") -#define E40 TranslateT("Empty data is retrieved (40)") -#define E42 TranslateT("Document not found (42)") -#define E43 TranslateT("Document too short to contain any weather data (43)") -#define E99 TranslateT("Unknown error (99)") - -// HTTP error... not all translated -// 100 Continue -// 101 Switching Protocols -// 200 OK -// 201 Created -// 202 Accepted -// 203 Non-Authoritative Information -#define E204 TranslateT("HTTP Error: No content (204)") -// 205 Reset Content -// 206 Partial Content -// 300 Multiple Choices -#define E301 TranslateT("HTTP Error: Data moved (301)") -// 302 Found -// 303 See Other -// 304 Not Modified -#define E305 TranslateT("HTTP Error: Use proxy (305)") -// 306 (Unused) -#define E307 TranslateT("HTTP Error: Temporary redirect (307)") -#define E400 TranslateT("HTTP Error: Bad request (400)") -#define E401 TranslateT("HTTP Error: Unauthorized (401)") -#define E402 TranslateT("HTTP Error: Payment required (402)") -#define E403 TranslateT("HTTP Error: Forbidden (403)") -#define E404 TranslateT("HTTP Error: Not found (404)") -#define E405 TranslateT("HTTP Error: Method not allowed (405)") -// 406 Not Acceptable -#define E407 TranslateT("HTTP Error: Proxy authentication required (407)") -// 408 Request Timeout -// 409 Conflict -#define E410 TranslateT("HTTP Error: Gone (410)") -// 411 Length Required -// 412 Precondition Failed -// 413 Request Entity Too Large -// 414 Request-URI Too Long -// 415 Unsupported Media Type -// 416 Requested Range Not Satisfiable -// 417 Expectation Failed -#define E500 TranslateT("HTTP Error: Internal server error (500)") -// 501 Not Implemented -#define E502 TranslateT("HTTP Error: Bad gateway (502)") -#define E503 TranslateT("HTTP Error: Service unavailable (503)") -#define E504 TranslateT("HTTP Error: Gateway timeout (504)") -// 505 HTTP Version Not Supported - -// defaults constants -#define VAR_LIST_OPT TranslateT("%c\tcurrent condition\n%d\tcurrent date\n%e\tdewpoint\n%f\tfeel-like temp\n%h\ttoday's high\n%i\twind direction\n%l\ttoday's low\n%m\thumidity\n%n\tstation name\n%p\tpressure\n%r\tsunrise time\n%s\tstation ID\n%t\ttemperature\n%u\tupdate time\n%v\tvisibility\n%w\twind speed\n%y\tsun set\n----------\n\\n\tnew line") - -//============ OPTION STRUCT ============ - -// option struct -struct MYOPTIONS -{ - // main options - uint8_t AutoUpdate; - uint8_t CAutoUpdate; - uint8_t StartupUpdate; - uint8_t NoProtoCondition; - uint8_t UpdateOnlyConditionChanged; - uint8_t RemoveOldData; - uint8_t MakeItalic; - - uint16_t UpdateTime; - uint16_t AvatarSize; - - // units - uint16_t tUnit; - uint16_t wUnit; - uint16_t vUnit; - uint16_t pUnit; - uint16_t dUnit; - uint16_t eUnit; - wchar_t DegreeSign[4]; - uint8_t DoNotAppendUnit; - uint8_t NoFrac; - - // advanced - uint8_t DisCondIcon; - - // popup options - uint8_t UpdatePopup; - uint8_t AlertPopup; - uint8_t PopupOnChange; - uint8_t ShowWarnings; - - // popup colors - uint8_t UseWinColors; - COLORREF BGColour; - COLORREF TextColour; - - // popup actions - uint32_t LeftClickAction; - uint32_t RightClickAction; - - // popup delay - uint32_t pDelay; - - // other misc stuff - wchar_t Default[64]; - MCONTACT DefStn; -}; - -//============ STRUCT USED TO MAKE AN UPDATE LIST ============ -struct WCONTACTLIST { - MCONTACT hContact; - struct WCONTACTLIST *next; -}; - -typedef struct WCONTACTLIST UPDATELIST; - -extern UPDATELIST *UpdateListHead, *UpdateListTail; - -void DestroyUpdateList(void); - -//============ DATA FORMAT STRUCT ============ - -#define WID_NORMAL 0 -#define WID_SET 1 -#define WID_BREAK 2 - -struct WIDATAITEM -{ - wchar_t *Name; - wchar_t *Start; - wchar_t *End; - wchar_t *Unit; - char *Url; - wchar_t *Break; - int Type; -}; - -struct WITEMLIST -{ - WIDATAITEM Item; - struct WITEMLIST *Next; -}; - -typedef struct WITEMLIST WIDATAITEMLIST; - -struct WIIDSEARCH -{ - BOOL Available; - char *SearchURL; - wchar_t *NotFoundStr; - WIDATAITEM Name; -}; - -struct WINAMESEARCHTYPE -{ - BOOL Available; - wchar_t *First; - WIDATAITEM Name; - WIDATAITEM ID; -}; - -struct WINAMESEARCH -{ - char *SearchURL; - wchar_t *NotFoundStr; - wchar_t *SingleStr; - WINAMESEARCHTYPE Single; - WINAMESEARCHTYPE Multiple; -}; - -struct STRLIST -{ - wchar_t *Item; - struct STRLIST *Next; -}; - -typedef struct STRLIST WICONDITEM; - -struct WICONDLIST -{ - WICONDITEM *Head; - WICONDITEM *Tail; -}; - -struct WIDATA -{ - wchar_t *FileName; - wchar_t *ShortFileName; - BOOL Enabled; - - // header - wchar_t *DisplayName; - wchar_t *InternalName; - wchar_t *Description; - wchar_t *Author; - wchar_t *Version; - int InternalVer; - size_t MemUsed; - - // default - char *DefaultURL; - wchar_t *DefaultMap; - char *UpdateURL; - char *UpdateURL2; - char *UpdateURL3; - char *UpdateURL4; - char *Cookie; - char *UserAgent; - - // items - int UpdateDataCount; - WIDATAITEMLIST *UpdateData; - WIDATAITEMLIST *UpdateDataTail; - WIIDSEARCH IDSearch; - WINAMESEARCH NameSearch; - WICONDLIST CondList[MAX_COND]; -}; - -//============ DATA LIST (LINKED LIST) ============ - -struct DATALIST -{ - WIDATA Data; - struct DATALIST *next; -}; - -typedef struct DATALIST WIDATALIST; - -//============ GLOBAL VARIABLES ============ - -extern WIDATALIST *WIHead, *WITail; - -extern HWND hPopupWindow, hWndSetup; - -extern MYOPTIONS opt; - -extern unsigned status, old_status; - -extern MWindowList hDataWindowList, hWindowList; - -extern HNETLIBUSER hNetlibUser; -extern HANDLE hHookWeatherUpdated, hHookWeatherError, hTBButton, hUpdateMutex; -extern UINT_PTR timerId; - -extern HGENMENU hMwinMenu; - -// check if weather is currently updating -extern BOOL ThreadRunning; -extern bool g_bIsUtf; - -//============ FUNCTION PRIMITIVES ============ - -// functions in weather_addstn.c -INT_PTR WeatherAddToList(WPARAM wParam,LPARAM lParam); -BOOL CheckSearch(); - -int IDSearch(wchar_t *id, const int searchId); -int NameSearch(wchar_t *name, const int searchId); - -INT_PTR WeatherBasicSearch(WPARAM wParam,LPARAM lParam); -INT_PTR WeatherCreateAdvancedSearchUI(WPARAM wParam, LPARAM lParam); -INT_PTR WeatherAdvancedSearch(WPARAM wParam, LPARAM lParam); - -int WeatherAdd(WPARAM wParam, LPARAM lParam); - -// functions used in weather_contacts.c -INT_PTR ViewLog(WPARAM wParam,LPARAM lParam); -INT_PTR LoadForecast(WPARAM wParam,LPARAM lParam); -INT_PTR WeatherMap(WPARAM wParam,LPARAM lParam); -INT_PTR EditSettings(WPARAM wParam,LPARAM lParam); - -int ContactDeleted(WPARAM wParam,LPARAM lParam); - -BOOL IsMyContact(MCONTACT hContact); - -// functions in weather_conv.c -void GetTemp(wchar_t *tempchar, wchar_t *unit, wchar_t *str); -void GetSpeed(wchar_t *tempchar, wchar_t *unit, wchar_t *str); -void GetPressure(wchar_t *tempchar, wchar_t *unit, wchar_t *str); -void GetDist(wchar_t *tempchar, wchar_t *unit, wchar_t *str); -void GetElev(wchar_t *tempchar, wchar_t *unit, wchar_t *str); - -void ClearStatusIcons(); -int MapCondToStatus(MCONTACT hContact); -HICON GetStatusIcon(MCONTACT hContact); -HICON GetStatusIconBig(MCONTACT hContact); - -uint16_t GetIcon(const wchar_t* cond, WIDATA *Data); -void CaseConv(wchar_t *str); -void TrimString(char *str); -void TrimString(wchar_t *str); -void ConvertBackslashes(char *str); -char *GetSearchStr(char *dis); - -wchar_t *GetDisplay(WEATHERINFO *w, const wchar_t *dis, wchar_t* str); -INT_PTR GetDisplaySvcFunc(WPARAM wParam, LPARAM lParam); - -void GetSvc(wchar_t *pszID); -void GetID(wchar_t *pszID); - -wchar_t *GetError(int code); - -// functions in weather_data.c -void GetStationID(MCONTACT hContact, wchar_t* id, int idlen); -WEATHERINFO LoadWeatherInfo(MCONTACT Change); -int DBGetData(MCONTACT hContact, char *setting, DBVARIANT *dbv); - -void EraseAllInfo(void); - -void GetDataValue(WIDATAITEM *UpdateData, wchar_t *Data, wchar_t** szInfo); -void ConvertDataValue(WIDATAITEM *UpdateData, wchar_t *Data); -void wSetData(char *&Data, const char *Value); -void wSetData(wchar_t *&Data, const char *Value); -void wSetData(wchar_t *&Data, const wchar_t *Value); -void wfree(char *&Data); -void wfree(wchar_t *&Data); - -void DBDataManage(MCONTACT hContact, uint16_t Mode, WPARAM wParam, LPARAM lParam); - -// functions in weather_http.c -int InternetDownloadFile (char *szUrl, char *cookie, char *userAgent, wchar_t** szData); -void NetlibInit(); - -// functions in weather_ini.c -WIDATA* GetWIData(wchar_t *pszServ); - -bool IsContainedInCondList(const wchar_t *pszStr, WICONDLIST *List); - -void DestroyWIList(); -bool LoadWIData(bool dial); - -INT_PTR CALLBACK DlgPopupOpts(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam); - -// functions in weather_info.c -void GetINIInfo(wchar_t *pszSvc); -wchar_t* GetINIVersionNum(int iVersion); - -void MoreVarList(); - -// functions in weather_opt.c -void LoadOptions(); -void SaveOptions(); - -int OptInit(WPARAM wParam,LPARAM lParam); - -CMStringW GetTextValue(int c); -const wchar_t* GetDefaultText(int c); - -// functions in weather_popup.c -int WeatherPopup(WPARAM wParam, LPARAM lParam); -int WeatherError(WPARAM wParam, LPARAM lParam); -int WPShowMessage(const wchar_t* lpzText, uint16_t kind); - -LRESULT CALLBACK PopupWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); - -// functions in weather_svcs.c -void InitServices(void); - -INT_PTR WeatherSetStatus(WPARAM new_status, LPARAM lParam); -INT_PTR WeatherGetCaps(WPARAM wParam, LPARAM lParam); -INT_PTR WeatherGetName(WPARAM wParam, LPARAM lParam); -INT_PTR WeatherGetStatus(WPARAM wParam, LPARAM lParam); -INT_PTR WeatherLoadIcon(WPARAM wParam, LPARAM lParam); - -void UpdateMenu(BOOL State); -void UpdatePopupMenu(BOOL State); -void AddMenuItems(); -void AvatarDownloaded(MCONTACT hContact); - -// functions in weather_update.c -int UpdateWeather(MCONTACT hContact); - -void UpdateAll(BOOL AutoUpdate, BOOL RemoveOld); -INT_PTR UpdateSingleStation(WPARAM wParam,LPARAM lParam); -INT_PTR UpdateAllInfo(WPARAM wParam,LPARAM lParam); -INT_PTR UpdateSingleRemove(WPARAM wParam,LPARAM lParam); -INT_PTR UpdateAllRemove(WPARAM wParam,LPARAM lParam); - -int GetWeatherData(MCONTACT hContact); - -void CALLBACK timerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime); -void CALLBACK timerProc2(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime); - -// function from multiwin module -void InitMwin(void); -void DestroyMwin(void); -INT_PTR Mwin_MenuClicked(WPARAM wParam, LPARAM lParam); -int BuildContactMenu(WPARAM wparam, LPARAM lparam); -void UpdateMwinData(MCONTACT hContact); -void removeWindow(MCONTACT hContact); - -// functions in weather_userinfo.c -int UserInfoInit(WPARAM wParam, LPARAM lParam); - -#define WM_UPDATEDATA WM_USER + 2687 - -int BriefInfo(WPARAM wParam, LPARAM lParam); - -/////////////////////////////////////////////////////////////////////////////// -// UI Classes - -class WeatherMyDetailsDlg : public CUserInfoPageDlg -{ - CCtrlButton btnReload; - -public: - WeatherMyDetailsDlg(); - - bool OnInitDialog() override; - - void onClick_Reload(CCtrlButton *); -}; - -//============ Plugin Class ============ - -struct CMPlugin : public PLUGIN -{ - CMPlugin(); - - HINSTANCE hIconsDll = nullptr; - CMOption bPopups; - - int Load() override; - int Unload() override; -}; - +/* +Weather Protocol plugin for Miranda NG +Copyright (C) 2012-23 Miranda NG team +Copyright (c) 2005-2011 Boris Krasnovskiy All Rights Reserved +Copyright (c) 2002-2005 Calvin Che + +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 . +*/ + +/* This file contains the includes, weather constants/declarations, + the structs, and the primitives for some of the functions. +*/ + +#pragma once + +//============ THE INCLUDES =========== + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "resource.h" +#include "version.h" + +//============ CONSTANTS ============ + +// name +#define MODULENAME "Weather" +#define WEATHERPROTOTEXT "Weather" +#define DEFCURRENTWEATHER "WeatherCondition" +#define WEATHERCONDITION "Current" + +// weather conditions +enum EWeatherCondition +{ + SUNNY, + NA, + PCLOUDY, + CLOUDY, + RAIN, + RSHOWER, + FOG, + SNOW, + SSHOWER, + LIGHT, + MAX_COND +}; + +// status +#define NOSTATUSDATA 1 + +// limits +#define MAX_TEXT_SIZE 4096 +#define MAX_DATA_LEN 1024 + +// db info mangement mode +#define WDBM_REMOVE 1 +#define WDBM_DETAILDISPLAY 2 + +// more info list column width +#define LIST_COLUMN 150 + +// others +#define NODATA TranslateT("N/A") +#define UM_SETCONTACT 40000 + +// weather update error codes +#define INVALID_ID_FORMAT 10 +#define INVALID_SVC 11 +#define INVALID_ID 12 +#define SVC_NOT_FOUND 20 +#define NETLIB_ERROR 30 +#define DATA_EMPTY 40 +#define DOC_NOT_FOUND 42 +#define DOC_TOO_SHORT 43 +#define UNKNOWN_ERROR 99 + +// weather update error text +#define E10 TranslateT("Invalid ID format, missing \"/\" (10)") +#define E11 TranslateT("Invalid service (11)") +#define E12 TranslateT("Invalid station (12)") +#define E20 TranslateT("Weather service ini for this station is not found (20)") +#define E30 TranslateT("Netlib error - check your internet connection (30)") +#define E40 TranslateT("Empty data is retrieved (40)") +#define E42 TranslateT("Document not found (42)") +#define E43 TranslateT("Document too short to contain any weather data (43)") +#define E99 TranslateT("Unknown error (99)") + +// HTTP error... not all translated +// 100 Continue +// 101 Switching Protocols +// 200 OK +// 201 Created +// 202 Accepted +// 203 Non-Authoritative Information +#define E204 TranslateT("HTTP Error: No content (204)") +// 205 Reset Content +// 206 Partial Content +// 300 Multiple Choices +#define E301 TranslateT("HTTP Error: Data moved (301)") +// 302 Found +// 303 See Other +// 304 Not Modified +#define E305 TranslateT("HTTP Error: Use proxy (305)") +// 306 (Unused) +#define E307 TranslateT("HTTP Error: Temporary redirect (307)") +#define E400 TranslateT("HTTP Error: Bad request (400)") +#define E401 TranslateT("HTTP Error: Unauthorized (401)") +#define E402 TranslateT("HTTP Error: Payment required (402)") +#define E403 TranslateT("HTTP Error: Forbidden (403)") +#define E404 TranslateT("HTTP Error: Not found (404)") +#define E405 TranslateT("HTTP Error: Method not allowed (405)") +// 406 Not Acceptable +#define E407 TranslateT("HTTP Error: Proxy authentication required (407)") +// 408 Request Timeout +// 409 Conflict +#define E410 TranslateT("HTTP Error: Gone (410)") +// 411 Length Required +// 412 Precondition Failed +// 413 Request Entity Too Large +// 414 Request-URI Too Long +// 415 Unsupported Media Type +// 416 Requested Range Not Satisfiable +// 417 Expectation Failed +#define E500 TranslateT("HTTP Error: Internal server error (500)") +// 501 Not Implemented +#define E502 TranslateT("HTTP Error: Bad gateway (502)") +#define E503 TranslateT("HTTP Error: Service unavailable (503)") +#define E504 TranslateT("HTTP Error: Gateway timeout (504)") +// 505 HTTP Version Not Supported + +// defaults constants +#define VAR_LIST_OPT TranslateT("%c\tcurrent condition\n%d\tcurrent date\n%e\tdewpoint\n%f\tfeel-like temp\n%h\ttoday's high\n%i\twind direction\n%l\ttoday's low\n%m\thumidity\n%n\tstation name\n%p\tpressure\n%r\tsunrise time\n%s\tstation ID\n%t\ttemperature\n%u\tupdate time\n%v\tvisibility\n%w\twind speed\n%y\tsun set\n----------\n\\n\tnew line") + +//============ OPTION STRUCT ============ + +// option struct +struct MYOPTIONS +{ + // main options + uint8_t AutoUpdate; + uint8_t CAutoUpdate; + uint8_t StartupUpdate; + uint8_t NoProtoCondition; + uint8_t UpdateOnlyConditionChanged; + uint8_t RemoveOldData; + uint8_t MakeItalic; + + uint16_t UpdateTime; + uint16_t AvatarSize; + + // units + uint16_t tUnit; + uint16_t wUnit; + uint16_t vUnit; + uint16_t pUnit; + uint16_t dUnit; + uint16_t eUnit; + wchar_t DegreeSign[4]; + uint8_t DoNotAppendUnit; + uint8_t NoFrac; + + // advanced + uint8_t DisCondIcon; + + // popup options + uint8_t UpdatePopup; + uint8_t AlertPopup; + uint8_t PopupOnChange; + uint8_t ShowWarnings; + + // popup colors + uint8_t UseWinColors; + COLORREF BGColour; + COLORREF TextColour; + + // popup actions + uint32_t LeftClickAction; + uint32_t RightClickAction; + + // popup delay + uint32_t pDelay; + + // other misc stuff + wchar_t Default[64]; + MCONTACT DefStn; +}; + +//============ STRUCT USED TO MAKE AN UPDATE LIST ============ +struct WCONTACTLIST { + MCONTACT hContact; + struct WCONTACTLIST *next; +}; + +typedef struct WCONTACTLIST UPDATELIST; + +extern UPDATELIST *UpdateListHead, *UpdateListTail; + +void DestroyUpdateList(void); + +//============ DATA FORMAT STRUCT ============ + +#define WID_NORMAL 0 +#define WID_SET 1 +#define WID_BREAK 2 + +struct WIDATAITEM +{ + wchar_t *Name; + wchar_t *Start; + wchar_t *End; + wchar_t *Unit; + char *Url; + wchar_t *Break; + int Type; +}; + +struct WITEMLIST +{ + WIDATAITEM Item; + struct WITEMLIST *Next; +}; + +typedef struct WITEMLIST WIDATAITEMLIST; + +struct WIIDSEARCH +{ + BOOL Available; + char *SearchURL; + wchar_t *NotFoundStr; + WIDATAITEM Name; +}; + +struct WINAMESEARCHTYPE +{ + BOOL Available; + wchar_t *First; + WIDATAITEM Name; + WIDATAITEM ID; +}; + +struct WINAMESEARCH +{ + char *SearchURL; + wchar_t *NotFoundStr; + wchar_t *SingleStr; + WINAMESEARCHTYPE Single; + WINAMESEARCHTYPE Multiple; +}; + +struct STRLIST +{ + wchar_t *Item; + struct STRLIST *Next; +}; + +typedef struct STRLIST WICONDITEM; + +struct WICONDLIST +{ + WICONDITEM *Head; + WICONDITEM *Tail; +}; + +struct WIDATA +{ + wchar_t *FileName; + wchar_t *ShortFileName; + BOOL Enabled; + + // header + wchar_t *DisplayName; + wchar_t *InternalName; + wchar_t *Description; + wchar_t *Author; + wchar_t *Version; + int InternalVer; + size_t MemUsed; + + // default + char *DefaultURL; + wchar_t *DefaultMap; + char *UpdateURL; + char *UpdateURL2; + char *UpdateURL3; + char *UpdateURL4; + char *Cookie; + char *UserAgent; + + // items + int UpdateDataCount; + WIDATAITEMLIST *UpdateData; + WIDATAITEMLIST *UpdateDataTail; + WIIDSEARCH IDSearch; + WINAMESEARCH NameSearch; + WICONDLIST CondList[MAX_COND]; +}; + +//============ DATA LIST (LINKED LIST) ============ + +struct DATALIST +{ + WIDATA Data; + struct DATALIST *next; +}; + +typedef struct DATALIST WIDATALIST; + +//============ GLOBAL VARIABLES ============ + +extern WIDATALIST *WIHead, *WITail; + +extern HWND hPopupWindow, hWndSetup; + +extern MYOPTIONS opt; + +extern unsigned status, old_status; + +extern MWindowList hDataWindowList, hWindowList; + +extern HNETLIBUSER hNetlibUser; +extern HANDLE hHookWeatherUpdated, hHookWeatherError, hTBButton, hUpdateMutex; +extern UINT_PTR timerId; + +extern HGENMENU hMwinMenu; + +// check if weather is currently updating +extern BOOL ThreadRunning; +extern bool g_bIsUtf; + +//============ FUNCTION PRIMITIVES ============ + +// functions in weather_addstn.c +INT_PTR WeatherAddToList(WPARAM wParam,LPARAM lParam); +BOOL CheckSearch(); + +int IDSearch(wchar_t *id, const int searchId); +int NameSearch(wchar_t *name, const int searchId); + +INT_PTR WeatherBasicSearch(WPARAM wParam,LPARAM lParam); +INT_PTR WeatherCreateAdvancedSearchUI(WPARAM wParam, LPARAM lParam); +INT_PTR WeatherAdvancedSearch(WPARAM wParam, LPARAM lParam); + +int WeatherAdd(WPARAM wParam, LPARAM lParam); + +// functions used in weather_contacts.c +INT_PTR ViewLog(WPARAM wParam,LPARAM lParam); +INT_PTR LoadForecast(WPARAM wParam,LPARAM lParam); +INT_PTR WeatherMap(WPARAM wParam,LPARAM lParam); +INT_PTR EditSettings(WPARAM wParam,LPARAM lParam); + +int ContactDeleted(WPARAM wParam,LPARAM lParam); + +BOOL IsMyContact(MCONTACT hContact); + +// functions in weather_conv.c +void GetTemp(wchar_t *tempchar, wchar_t *unit, wchar_t *str); +void GetSpeed(wchar_t *tempchar, wchar_t *unit, wchar_t *str); +void GetPressure(wchar_t *tempchar, wchar_t *unit, wchar_t *str); +void GetDist(wchar_t *tempchar, wchar_t *unit, wchar_t *str); +void GetElev(wchar_t *tempchar, wchar_t *unit, wchar_t *str); + +void ClearStatusIcons(); +int MapCondToStatus(MCONTACT hContact); +HICON GetStatusIcon(MCONTACT hContact); +HICON GetStatusIconBig(MCONTACT hContact); + +uint16_t GetIcon(const wchar_t* cond, WIDATA *Data); +void CaseConv(wchar_t *str); +void TrimString(char *str); +void TrimString(wchar_t *str); +void ConvertBackslashes(char *str); +char *GetSearchStr(char *dis); + +wchar_t *GetDisplay(WEATHERINFO *w, const wchar_t *dis, wchar_t* str); +INT_PTR GetDisplaySvcFunc(WPARAM wParam, LPARAM lParam); + +void GetSvc(wchar_t *pszID); +void GetID(wchar_t *pszID); + +wchar_t *GetError(int code); + +// functions in weather_data.c +void GetStationID(MCONTACT hContact, wchar_t* id, int idlen); +WEATHERINFO LoadWeatherInfo(MCONTACT Change); +int DBGetData(MCONTACT hContact, char *setting, DBVARIANT *dbv); + +void EraseAllInfo(void); + +void GetDataValue(WIDATAITEM *UpdateData, wchar_t *Data, wchar_t** szInfo); +void ConvertDataValue(WIDATAITEM *UpdateData, wchar_t *Data); +void wSetData(char *&Data, const char *Value); +void wSetData(wchar_t *&Data, const char *Value); +void wSetData(wchar_t *&Data, const wchar_t *Value); +void wfree(char *&Data); +void wfree(wchar_t *&Data); + +void DBDataManage(MCONTACT hContact, uint16_t Mode, WPARAM wParam, LPARAM lParam); + +// functions in weather_http.c +int InternetDownloadFile (char *szUrl, char *cookie, char *userAgent, wchar_t** szData); +void NetlibInit(); + +// functions in weather_ini.c +WIDATA* GetWIData(wchar_t *pszServ); + +bool IsContainedInCondList(const wchar_t *pszStr, WICONDLIST *List); + +void DestroyWIList(); +bool LoadWIData(bool dial); + +INT_PTR CALLBACK DlgPopupOpts(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam); + +// functions in weather_info.c +void GetINIInfo(wchar_t *pszSvc); +wchar_t* GetINIVersionNum(int iVersion); + +void MoreVarList(); + +// functions in weather_opt.c +void LoadOptions(); +void SaveOptions(); + +int OptInit(WPARAM wParam,LPARAM lParam); + +CMStringW GetTextValue(int c); +const wchar_t* GetDefaultText(int c); + +// functions in weather_popup.c +int WeatherPopup(WPARAM wParam, LPARAM lParam); +int WeatherError(WPARAM wParam, LPARAM lParam); +int WPShowMessage(const wchar_t* lpzText, uint16_t kind); + +LRESULT CALLBACK PopupWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + +// functions in weather_svcs.c +void InitServices(void); + +INT_PTR WeatherSetStatus(WPARAM new_status, LPARAM lParam); +INT_PTR WeatherGetCaps(WPARAM wParam, LPARAM lParam); +INT_PTR WeatherGetName(WPARAM wParam, LPARAM lParam); +INT_PTR WeatherGetStatus(WPARAM wParam, LPARAM lParam); +INT_PTR WeatherLoadIcon(WPARAM wParam, LPARAM lParam); + +void UpdateMenu(BOOL State); +void UpdatePopupMenu(BOOL State); +void AddMenuItems(); +void AvatarDownloaded(MCONTACT hContact); + +// functions in weather_update.c +int UpdateWeather(MCONTACT hContact); + +void UpdateAll(BOOL AutoUpdate, BOOL RemoveOld); +INT_PTR UpdateSingleStation(WPARAM wParam,LPARAM lParam); +INT_PTR UpdateAllInfo(WPARAM wParam,LPARAM lParam); +INT_PTR UpdateSingleRemove(WPARAM wParam,LPARAM lParam); +INT_PTR UpdateAllRemove(WPARAM wParam,LPARAM lParam); + +int GetWeatherData(MCONTACT hContact); + +void CALLBACK timerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime); +void CALLBACK timerProc2(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime); + +// function from multiwin module +void InitMwin(void); +void DestroyMwin(void); +INT_PTR Mwin_MenuClicked(WPARAM wParam, LPARAM lParam); +int BuildContactMenu(WPARAM wparam, LPARAM lparam); +void UpdateMwinData(MCONTACT hContact); +void removeWindow(MCONTACT hContact); + +// functions in weather_userinfo.c +int UserInfoInit(WPARAM wParam, LPARAM lParam); + +#define WM_UPDATEDATA WM_USER + 2687 + +int BriefInfo(WPARAM wParam, LPARAM lParam); + +/////////////////////////////////////////////////////////////////////////////// +// UI Classes + +class WeatherMyDetailsDlg : public CUserInfoPageDlg +{ + CCtrlButton btnReload; + +public: + WeatherMyDetailsDlg(); + + bool OnInitDialog() override; + + void onClick_Reload(CCtrlButton *); +}; + +//============ Plugin Class ============ + +struct CMPlugin : public PLUGIN +{ + CMPlugin(); + + HINSTANCE hIconsDll = nullptr; + CMOption bPopups; + + int Load() override; + int Unload() override; +}; + diff --git a/protocols/Weather/src/version.h b/protocols/Weather/src/version.h index bb157edd18..446326f779 100644 --- a/protocols/Weather/src/version.h +++ b/protocols/Weather/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "Retrieves weather information and displays it in your contact list." #define __AUTHOR "Miranda NG team" #define __AUTHORWEB "https://miranda-ng.org/p/Weather" -#define __COPYRIGHT "© 2002-2005 NoName, 2005-2010 Boris Krasnovskiy, 2012-22 Miranda NG team" +#define __COPYRIGHT "© 2002-2005 NoName, 2005-2010 Boris Krasnovskiy, 2012-23 Miranda NG team" diff --git a/protocols/WebView/src/main.cpp b/protocols/WebView/src/main.cpp index 1d5fe29cb6..858422a4c8 100644 --- a/protocols/WebView/src/main.cpp +++ b/protocols/WebView/src/main.cpp @@ -1,274 +1,274 @@ -/* - * A plugin for Miranda IM which displays web page text in a window. - * Copyright (C) 2005 Vincent Joyce. - * - * Miranda IM: the free icq client for MS Windows - * Copyright (C) 2000-22 Richard Hughes, Roland Rabien & Tristan Van de Vreede - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ - -#include "stdafx.h" -#include "webview.h" - -MWindowList hWindowList; -HNETLIBUSER hNetlibUser; -HANDLE hHookDisplayDataAlert, hHookAlertPopup, hHookAlertWPopup, hHookErrorPopup, hHookAlertOSD; - -CMPlugin g_plugin; - -static HMODULE hRichEd = nullptr; - -///////////////////////////////////////////////////////////////////////////////////////// - -PLUGININFOEX pluginInfoEx = { - sizeof(PLUGININFOEX), - __PLUGIN_NAME, - PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), - __DESCRIPTION, - __AUTHOR, - __COPYRIGHT, - __AUTHORWEB, - UNICODE_AWARE, - // {CD5427FB-5320-4f65-B4BF-86B7CF7B5087} - {0xcd5427fb, 0x5320, 0x4f65, {0xb4, 0xbf, 0x86, 0xb7, 0xcf, 0x7b, 0x50, 0x87}} -}; - -CMPlugin::CMPlugin() : - PLUGIN(MODULENAME, pluginInfoEx) -{ - RegisterProtocol(PROTOTYPE_PROTOCOL); - SetUniqueId("PreserveName"); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void InitServices() -{ - CreateProtoServiceFunction(MODULENAME, PS_GETCAPS, GetCaps); - CreateProtoServiceFunction(MODULENAME, PS_GETNAME, GetName); - CreateProtoServiceFunction(MODULENAME, PS_LOADICON, BPLoadIcon); - CreateProtoServiceFunction(MODULENAME, PS_SETSTATUS, SetStatus); - CreateProtoServiceFunction(MODULENAME, PS_GETSTATUS, GetStatus); - CreateProtoServiceFunction(MODULENAME, PS_BASICSEARCH, BasicSearch); - CreateProtoServiceFunction(MODULENAME, PS_ADDTOLIST, AddToList); - CreateProtoServiceFunction(MODULENAME, PSS_GETINFO, GetInfo); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void ChangeContactStatus(int con_stat) -{ - uint16_t status_code = 0; - if (con_stat == 0) - status_code = ID_STATUS_OFFLINE; - if (con_stat == 1) - status_code = ID_STATUS_ONLINE; - if (con_stat == 2) - status_code = ID_STATUS_AWAY; - if (con_stat == 3) - status_code = ID_STATUS_NA; - - for (auto &hContact : Contacts(MODULENAME)) - g_plugin.setWord(hContact, "Status", status_code); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; - -///////////////////////////////////////////////////////////////////////////////////////// - -int CMPlugin::Load() -{ - HookEvent(ME_CLIST_DOUBLECLICKED, Doubleclick); - - hMenu = LoadMenu(g_plugin.getInst(), MAKEINTRESOURCE(IDR_CONTEXT)); - hRichEd = LoadLibrary(L"Msftedit.dll"); - - /*TIMERS*/ - if ((g_plugin.getDword(REFRESH_KEY, TIME) != 0)) { - timerId = SetTimer(nullptr, 0, ((g_plugin.getDword(REFRESH_KEY, TIME)) * MINUTE), timerfunc); - g_plugin.setDword(COUNTDOWN_KEY, 0); - Countdown = SetTimer(nullptr, 0, MINUTE, Countdownfunc); - } - - InitialiseGlobals(); - - // register netlib handle - NETLIBUSER nlu = {}; - nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS; - nlu.szSettingsModule = MODULENAME; - nlu.szDescriptiveName.a = MODULENAME; - hNetlibUser = Netlib_RegisterUser(&nlu); - - //protocol services - InitServices(); - - //add sound event to options - g_plugin.addSound("webviewalert", _A2W(MODULENAME), LPGENW("Alert event")); - - CMenuItem mi(&g_plugin); - mi.flags = CMIF_UNICODE; - mi.root = g_plugin.addRootMenu(MO_MAIN, _A2W(MODULENAME), 20200001); - Menu_ConfigureItem(mi.root, MCI_OPT_UID, "403BE07B-7954-4F3E-B318-4301571776B8"); - - /*DISABLE WEBVIEW*/ - SET_UID(mi, 0xdedeb697, 0xfc10, 0x4622, 0x8b, 0x97, 0x74, 0x39, 0x32, 0x68, 0xa7, 0x7b); - CreateServiceFunction("DisableWebview", AutoUpdateMCmd); - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_SITE)); - if (g_plugin.getByte(DISABLE_AUTOUPDATE_KEY, 0)) - mi.name.w = LPGENW("Auto update disabled"); - else - mi.name.w = LPGENW("Auto update enabled"); - mi.pszService = "DisableWebview"; - hMenuItem1 = Menu_AddMainMenuItem(&mi); - - // Update all webview contacts - SET_UID(mi, 0xf324ede, 0xfdf, 0x498a, 0x8f, 0x49, 0x6d, 0x2a, 0x9f, 0xda, 0x58, 0x6); - CreateServiceFunction("UpdateAll", UpdateAllMenuCommand); - mi.position = 500090002; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_UPDATEALL)); - mi.name.w = LPGENW("Update all Webview sites"); - mi.pszService = "UpdateAll"; - Menu_AddMainMenuItem(&mi); - - // Mark All Webview Sites As Read - SET_UID(mi, 0x1fa5fa21, 0x2ee1, 0x4372, 0xae, 0x3e, 0x3b, 0x96, 0xac, 0xd, 0xe8, 0x49); - CreateServiceFunction("MarkAllSitesRead", MarkAllReadMenuCommand); - mi.position = 500090099; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_MARKALLREAD)); - mi.name.w = LPGENW("Mark all Webview sites as read"); - mi.pszService = "MarkAllSitesRead"; - Menu_AddMainMenuItem(&mi); - - // open cache directory - SET_UID(mi, 0xfed046a8, 0xaae5, 0x4cbe, 0xa8, 0xc, 0x3c, 0x50, 0x3e, 0x3e, 0x9b, 0x15); - CreateServiceFunction("OpenCacheFolder", OpenCacheDir); - mi.position = 500090099; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_FOLDER)); - mi.name.w = LPGENW("Open cache folder"); - mi.pszService = "OpenCacheFolder"; - Menu_AddMainMenuItem(&mi); - - // Countdown test - SET_UID(mi, 0xbb1a94a9, 0xca63, 0x4966, 0x98, 0x48, 0x8b, 0x3f, 0x9d, 0xac, 0x6a, 0x10); - CreateServiceFunction("Countdown", CountdownMenuCommand); - mi.flags |= CMIF_KEEPUNTRANSLATED; - wchar_t countername[100]; - mir_snwprintf(countername, TranslateT("%d minutes to update"), g_plugin.getDword(COUNTDOWN_KEY, 0)); - mi.position = 600090099; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_UPDATEALL)); - mi.name.w = countername; - mi.pszService = "Countdown"; - hMenuItemCountdown = Menu_AddMainMenuItem(&mi); - - // contact menu - mi.flags = CMIF_UNICODE; - - SET_UID(mi, 0xadc6a9a4, 0xdf7, 0x4f63, 0x89, 0x11, 0x8e, 0x42, 0x1d, 0xd6, 0x29, 0x31); - CreateServiceFunction("Open web page", WebsiteMenuCommand); - mi.position = 100; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_URL)); - mi.pszService = "Open web page"; - mi.name.w = LPGENW("Open web page"); - Menu_AddContactMenuItem(&mi, MODULENAME); - - SET_UID(mi, 0x9d803e61, 0xc929, 0x4c6e, 0x9e, 0x7, 0x93, 0x0, 0xab, 0x14, 0x13, 0x50); - CreateServiceFunction("OpenClose Window", DataWndMenuCommand); - mi.pszService = "OpenClose Window"; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_SHOW_HIDE)); - mi.name.w = LPGENW("Open/Close window"); - Menu_AddContactMenuItem(&mi, MODULENAME); - - SET_UID(mi, 0x3840cc71, 0xcc85, 0x448d, 0xb5, 0xc8, 0x1a, 0x7d, 0xfe, 0xf0, 0x8, 0x85); - mi.position = 2222220; - mi.pszService = "UpdateData"; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_UPDATE)); - mi.name.w = LPGENW("Update data"); - Menu_AddContactMenuItem(&mi, MODULENAME); - - SET_UID(mi, 0xd1ab586c, 0x2c71, 0x429c, 0xb1, 0x79, 0x7b, 0x3a, 0x1d, 0x4a, 0xc1, 0x7d); - CreateServiceFunction("ContactOptions", CntOptionsMenuCommand); - mi.pszService = "ContactOptions"; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_OPTIONS)); - mi.name.w = LPGENW("Contact options"); - Menu_AddContactMenuItem(&mi, MODULENAME); - - SET_UID(mi, 0xe4cda597, 0x9def, 0x4f54, 0x8a, 0xc6, 0x69, 0x3b, 0x5a, 0x7d, 0x77, 0xb6); - CreateServiceFunction("ContactAlertOpts", CntAlertMenuCommand); - mi.pszService = "ContactAlertOpts"; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_ALERT)); - mi.name.w = LPGENW("Contact alert options"); - Menu_AddContactMenuItem(&mi, MODULENAME); - - SET_UID(mi, 0x63fdeed8, 0xf880, 0x423f, 0x95, 0xae, 0x20, 0x8c, 0x86, 0x3c, 0x5, 0xd8); - CreateServiceFunction("PingWebsite", PingWebsiteMenuCommand); - mi.pszService = "PingWebsite"; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_PING)); - mi.name.w = LPGENW("Ping web site"); - Menu_AddContactMenuItem(&mi, MODULENAME); - - SET_UID(mi, 0x28fd36de, 0x6ce1, 0x43d0, 0xa1, 0x6e, 0x98, 0x71, 0x53, 0xe8, 0xc9, 0xf4); - CreateServiceFunction("StopDataProcessing", StpPrcssMenuCommand); - mi.pszService = "StopDataProcessing"; - mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_STOP)); - mi.name.w = LPGENW("Stop data processing"); - Menu_AddContactMenuItem(&mi, MODULENAME); - - hWindowList = WindowList_Create(); - - HookEvent(ME_SYSTEM_MODULESLOADED, ModulesLoaded); - HookEvent(ME_DB_CONTACT_SETTINGCHANGED, DBSettingChanged); - HookEvent(ME_DB_CONTACT_DELETED, SiteDeleted); - HookEvent(ME_OPT_INITIALISE, OptInitialise); - - g_plugin.setByte(HAS_CRASHED_KEY, 1); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int CMPlugin::Unload() -{ - ChangeContactStatus(0); - - KillTimer(nullptr, timerId); - KillTimer(nullptr, Countdown); - - g_plugin.setByte(HAS_CRASHED_KEY, 0); - SavewinSettings(); - if (hRichEd) - FreeLibrary(hRichEd); - - if (hNetlibUser) { - Netlib_CloseHandle(hNetlibUser); - hNetlibUser = nullptr; - } - - if (hHookDisplayDataAlert) - DestroyHookableEvent(hHookDisplayDataAlert); - if (hHookAlertPopup) - DestroyHookableEvent(hHookAlertPopup); - if (hHookAlertWPopup) - DestroyHookableEvent(hHookAlertWPopup); - - if (h_font != nullptr) - DeleteObject(h_font); - if (hMenu) - DestroyMenu(hMenu); - WindowList_Destroy(hWindowList); - return 0; -} +/* + * A plugin for Miranda IM which displays web page text in a window. + * Copyright (C) 2005 Vincent Joyce. + * + * Miranda IM: the free icq client for MS Windows + * Copyright (C) 2000-23 Richard Hughes, Roland Rabien & Tristan Van de Vreede + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#include "stdafx.h" +#include "webview.h" + +MWindowList hWindowList; +HNETLIBUSER hNetlibUser; +HANDLE hHookDisplayDataAlert, hHookAlertPopup, hHookAlertWPopup, hHookErrorPopup, hHookAlertOSD; + +CMPlugin g_plugin; + +static HMODULE hRichEd = nullptr; + +///////////////////////////////////////////////////////////////////////////////////////// + +PLUGININFOEX pluginInfoEx = { + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, + // {CD5427FB-5320-4f65-B4BF-86B7CF7B5087} + {0xcd5427fb, 0x5320, 0x4f65, {0xb4, 0xbf, 0x86, 0xb7, 0xcf, 0x7b, 0x50, 0x87}} +}; + +CMPlugin::CMPlugin() : + PLUGIN(MODULENAME, pluginInfoEx) +{ + RegisterProtocol(PROTOTYPE_PROTOCOL); + SetUniqueId("PreserveName"); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void InitServices() +{ + CreateProtoServiceFunction(MODULENAME, PS_GETCAPS, GetCaps); + CreateProtoServiceFunction(MODULENAME, PS_GETNAME, GetName); + CreateProtoServiceFunction(MODULENAME, PS_LOADICON, BPLoadIcon); + CreateProtoServiceFunction(MODULENAME, PS_SETSTATUS, SetStatus); + CreateProtoServiceFunction(MODULENAME, PS_GETSTATUS, GetStatus); + CreateProtoServiceFunction(MODULENAME, PS_BASICSEARCH, BasicSearch); + CreateProtoServiceFunction(MODULENAME, PS_ADDTOLIST, AddToList); + CreateProtoServiceFunction(MODULENAME, PSS_GETINFO, GetInfo); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void ChangeContactStatus(int con_stat) +{ + uint16_t status_code = 0; + if (con_stat == 0) + status_code = ID_STATUS_OFFLINE; + if (con_stat == 1) + status_code = ID_STATUS_ONLINE; + if (con_stat == 2) + status_code = ID_STATUS_AWAY; + if (con_stat == 3) + status_code = ID_STATUS_NA; + + for (auto &hContact : Contacts(MODULENAME)) + g_plugin.setWord(hContact, "Status", status_code); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// + +int CMPlugin::Load() +{ + HookEvent(ME_CLIST_DOUBLECLICKED, Doubleclick); + + hMenu = LoadMenu(g_plugin.getInst(), MAKEINTRESOURCE(IDR_CONTEXT)); + hRichEd = LoadLibrary(L"Msftedit.dll"); + + /*TIMERS*/ + if ((g_plugin.getDword(REFRESH_KEY, TIME) != 0)) { + timerId = SetTimer(nullptr, 0, ((g_plugin.getDword(REFRESH_KEY, TIME)) * MINUTE), timerfunc); + g_plugin.setDword(COUNTDOWN_KEY, 0); + Countdown = SetTimer(nullptr, 0, MINUTE, Countdownfunc); + } + + InitialiseGlobals(); + + // register netlib handle + NETLIBUSER nlu = {}; + nlu.flags = NUF_OUTGOING | NUF_HTTPCONNS; + nlu.szSettingsModule = MODULENAME; + nlu.szDescriptiveName.a = MODULENAME; + hNetlibUser = Netlib_RegisterUser(&nlu); + + //protocol services + InitServices(); + + //add sound event to options + g_plugin.addSound("webviewalert", _A2W(MODULENAME), LPGENW("Alert event")); + + CMenuItem mi(&g_plugin); + mi.flags = CMIF_UNICODE; + mi.root = g_plugin.addRootMenu(MO_MAIN, _A2W(MODULENAME), 20200001); + Menu_ConfigureItem(mi.root, MCI_OPT_UID, "403BE07B-7954-4F3E-B318-4301571776B8"); + + /*DISABLE WEBVIEW*/ + SET_UID(mi, 0xdedeb697, 0xfc10, 0x4622, 0x8b, 0x97, 0x74, 0x39, 0x32, 0x68, 0xa7, 0x7b); + CreateServiceFunction("DisableWebview", AutoUpdateMCmd); + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_SITE)); + if (g_plugin.getByte(DISABLE_AUTOUPDATE_KEY, 0)) + mi.name.w = LPGENW("Auto update disabled"); + else + mi.name.w = LPGENW("Auto update enabled"); + mi.pszService = "DisableWebview"; + hMenuItem1 = Menu_AddMainMenuItem(&mi); + + // Update all webview contacts + SET_UID(mi, 0xf324ede, 0xfdf, 0x498a, 0x8f, 0x49, 0x6d, 0x2a, 0x9f, 0xda, 0x58, 0x6); + CreateServiceFunction("UpdateAll", UpdateAllMenuCommand); + mi.position = 500090002; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_UPDATEALL)); + mi.name.w = LPGENW("Update all Webview sites"); + mi.pszService = "UpdateAll"; + Menu_AddMainMenuItem(&mi); + + // Mark All Webview Sites As Read + SET_UID(mi, 0x1fa5fa21, 0x2ee1, 0x4372, 0xae, 0x3e, 0x3b, 0x96, 0xac, 0xd, 0xe8, 0x49); + CreateServiceFunction("MarkAllSitesRead", MarkAllReadMenuCommand); + mi.position = 500090099; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_MARKALLREAD)); + mi.name.w = LPGENW("Mark all Webview sites as read"); + mi.pszService = "MarkAllSitesRead"; + Menu_AddMainMenuItem(&mi); + + // open cache directory + SET_UID(mi, 0xfed046a8, 0xaae5, 0x4cbe, 0xa8, 0xc, 0x3c, 0x50, 0x3e, 0x3e, 0x9b, 0x15); + CreateServiceFunction("OpenCacheFolder", OpenCacheDir); + mi.position = 500090099; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_FOLDER)); + mi.name.w = LPGENW("Open cache folder"); + mi.pszService = "OpenCacheFolder"; + Menu_AddMainMenuItem(&mi); + + // Countdown test + SET_UID(mi, 0xbb1a94a9, 0xca63, 0x4966, 0x98, 0x48, 0x8b, 0x3f, 0x9d, 0xac, 0x6a, 0x10); + CreateServiceFunction("Countdown", CountdownMenuCommand); + mi.flags |= CMIF_KEEPUNTRANSLATED; + wchar_t countername[100]; + mir_snwprintf(countername, TranslateT("%d minutes to update"), g_plugin.getDword(COUNTDOWN_KEY, 0)); + mi.position = 600090099; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_UPDATEALL)); + mi.name.w = countername; + mi.pszService = "Countdown"; + hMenuItemCountdown = Menu_AddMainMenuItem(&mi); + + // contact menu + mi.flags = CMIF_UNICODE; + + SET_UID(mi, 0xadc6a9a4, 0xdf7, 0x4f63, 0x89, 0x11, 0x8e, 0x42, 0x1d, 0xd6, 0x29, 0x31); + CreateServiceFunction("Open web page", WebsiteMenuCommand); + mi.position = 100; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_URL)); + mi.pszService = "Open web page"; + mi.name.w = LPGENW("Open web page"); + Menu_AddContactMenuItem(&mi, MODULENAME); + + SET_UID(mi, 0x9d803e61, 0xc929, 0x4c6e, 0x9e, 0x7, 0x93, 0x0, 0xab, 0x14, 0x13, 0x50); + CreateServiceFunction("OpenClose Window", DataWndMenuCommand); + mi.pszService = "OpenClose Window"; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_SHOW_HIDE)); + mi.name.w = LPGENW("Open/Close window"); + Menu_AddContactMenuItem(&mi, MODULENAME); + + SET_UID(mi, 0x3840cc71, 0xcc85, 0x448d, 0xb5, 0xc8, 0x1a, 0x7d, 0xfe, 0xf0, 0x8, 0x85); + mi.position = 2222220; + mi.pszService = "UpdateData"; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_UPDATE)); + mi.name.w = LPGENW("Update data"); + Menu_AddContactMenuItem(&mi, MODULENAME); + + SET_UID(mi, 0xd1ab586c, 0x2c71, 0x429c, 0xb1, 0x79, 0x7b, 0x3a, 0x1d, 0x4a, 0xc1, 0x7d); + CreateServiceFunction("ContactOptions", CntOptionsMenuCommand); + mi.pszService = "ContactOptions"; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_OPTIONS)); + mi.name.w = LPGENW("Contact options"); + Menu_AddContactMenuItem(&mi, MODULENAME); + + SET_UID(mi, 0xe4cda597, 0x9def, 0x4f54, 0x8a, 0xc6, 0x69, 0x3b, 0x5a, 0x7d, 0x77, 0xb6); + CreateServiceFunction("ContactAlertOpts", CntAlertMenuCommand); + mi.pszService = "ContactAlertOpts"; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_ALERT)); + mi.name.w = LPGENW("Contact alert options"); + Menu_AddContactMenuItem(&mi, MODULENAME); + + SET_UID(mi, 0x63fdeed8, 0xf880, 0x423f, 0x95, 0xae, 0x20, 0x8c, 0x86, 0x3c, 0x5, 0xd8); + CreateServiceFunction("PingWebsite", PingWebsiteMenuCommand); + mi.pszService = "PingWebsite"; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_PING)); + mi.name.w = LPGENW("Ping web site"); + Menu_AddContactMenuItem(&mi, MODULENAME); + + SET_UID(mi, 0x28fd36de, 0x6ce1, 0x43d0, 0xa1, 0x6e, 0x98, 0x71, 0x53, 0xe8, 0xc9, 0xf4); + CreateServiceFunction("StopDataProcessing", StpPrcssMenuCommand); + mi.pszService = "StopDataProcessing"; + mi.hIcolibItem = LoadIcon(g_plugin.getInst(), MAKEINTRESOURCE(IDI_STOP)); + mi.name.w = LPGENW("Stop data processing"); + Menu_AddContactMenuItem(&mi, MODULENAME); + + hWindowList = WindowList_Create(); + + HookEvent(ME_SYSTEM_MODULESLOADED, ModulesLoaded); + HookEvent(ME_DB_CONTACT_SETTINGCHANGED, DBSettingChanged); + HookEvent(ME_DB_CONTACT_DELETED, SiteDeleted); + HookEvent(ME_OPT_INITIALISE, OptInitialise); + + g_plugin.setByte(HAS_CRASHED_KEY, 1); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int CMPlugin::Unload() +{ + ChangeContactStatus(0); + + KillTimer(nullptr, timerId); + KillTimer(nullptr, Countdown); + + g_plugin.setByte(HAS_CRASHED_KEY, 0); + SavewinSettings(); + if (hRichEd) + FreeLibrary(hRichEd); + + if (hNetlibUser) { + Netlib_CloseHandle(hNetlibUser); + hNetlibUser = nullptr; + } + + if (hHookDisplayDataAlert) + DestroyHookableEvent(hHookDisplayDataAlert); + if (hHookAlertPopup) + DestroyHookableEvent(hHookAlertPopup); + if (hHookAlertWPopup) + DestroyHookableEvent(hHookAlertWPopup); + + if (h_font != nullptr) + DeleteObject(h_font); + if (hMenu) + DestroyMenu(hMenu); + WindowList_Destroy(hWindowList); + return 0; +} diff --git a/protocols/WebView/src/stdafx.cxx b/protocols/WebView/src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/WebView/src/stdafx.cxx +++ b/protocols/WebView/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file diff --git a/protocols/WhatsApp/src/appsync.cpp b/protocols/WhatsApp/src/appsync.cpp index 42e911a3b7..d298f880d2 100644 --- a/protocols/WhatsApp/src/appsync.cpp +++ b/protocols/WhatsApp/src/appsync.cpp @@ -1,7 +1,7 @@ /* WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan +Copyright © 2019-23 George Hazan */ diff --git a/protocols/WhatsApp/src/avatars.cpp b/protocols/WhatsApp/src/avatars.cpp index d2eda4014a..d648bb3801 100644 --- a/protocols/WhatsApp/src/avatars.cpp +++ b/protocols/WhatsApp/src/avatars.cpp @@ -1,169 +1,169 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan - -*/ - -#include "stdafx.h" - -void WhatsAppProto::OnIqGetAvatar(const WANode &node) -{ - auto *pUser = FindUser(node.getAttr("from")); - if (pUser == nullptr) - return; - - PROTO_AVATAR_INFORMATION ai = {}; - ai.hContact = pUser->hContact; - ai.format = PA_FORMAT_JPEG; - wcsncpy_s(ai.filename, GetAvatarFileName(pUser->hContact), _TRUNCATE); - - auto *pNode = node.getChild("picture"); - - DWORD dwLastChangeTime = pNode->getAttrInt("id"); - - CMStringA szUrl(pNode->getAttr("url")); - if (szUrl.IsEmpty()) { - setDword(pUser->hContact, DBKEY_AVATAR_TAG, 0); // avatar doesn't exist, don't check it later - -LBL_Error: - ProtoBroadcastAck(pUser->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, HANDLE(&ai)); - return; - } - - // if avatar was changed or not present at all, download it - if (dwLastChangeTime > getDword(pUser->hContact, DBKEY_AVATAR_TAG)) { - if (!g_plugin.SaveFile(szUrl, ai)) - goto LBL_Error; - - // set timestamp of avatar being saved - setDword(pUser->hContact, DBKEY_AVATAR_TAG, dwLastChangeTime); - } - ProtoBroadcastAck(pUser->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, HANDLE(&ai)); -} - -INT_PTR WhatsAppProto::GetAvatarInfo(WPARAM wParam, LPARAM lParam) -{ - PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION*)lParam; - - ptrA jid(getStringA(pai->hContact, isChatRoom(pai->hContact) ? "ChatRoomID" : DBKEY_ID)); - if (jid == NULL) - return GAIR_NOAVATAR; - - CMStringW tszFileName(GetAvatarFileName(pai->hContact)); - wcsncpy_s(pai->filename, tszFileName.c_str(), _TRUNCATE); - pai->format = PA_FORMAT_JPEG; - - DWORD dwTag = getDword(pai->hContact, DBKEY_AVATAR_TAG, -1); - if (dwTag == -1 || (wParam & GAIF_FORCE) != 0) - if (pai->hContact != NULL && isOnline()) { - ServerFetchAvatar(jid); - return GAIR_WAITFOR; - } - - debugLogA("No avatar"); - return GAIR_NOAVATAR; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR WhatsAppProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) -{ - switch (wParam) { - case AF_PROPORTION: - return PIP_SQUARE; - - case AF_FORMATSUPPORTED: // Jabber supports avatars of virtually all formats - return PA_FORMAT_JPEG; - - case AF_ENABLED: - return TRUE; - - case AF_MAXSIZE: - POINT *size = (POINT*)lParam; - if (size) - size->x = size->y = 640; - return 0; - } - return -1; -} - -CMStringW WhatsAppProto::GetAvatarFileName(MCONTACT hContact) -{ - CMStringW result = m_tszAvatarFolder + L"\\"; - - CMStringA jid; - if (hContact != NULL) { - ptrA szId(getStringA(hContact, isChatRoom(hContact) ? "ChatRoomID" : DBKEY_ID)); - if (szId == NULL) - return L""; - - jid = szId; - } - else jid = m_szJid; - - return result + _A2T(jid.c_str()) + L".jpg"; -} - -INT_PTR WhatsAppProto::GetMyAvatar(WPARAM wParam, LPARAM lParam) -{ - std::wstring tszOwnAvatar(m_tszAvatarFolder + L"\\myavatar.jpg"); - wcsncpy_s((wchar_t*)wParam, lParam, tszOwnAvatar.c_str(), _TRUNCATE); - return 0; -} - -INT_PTR WhatsAppProto::SetMyAvatar(WPARAM, LPARAM) -{ - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::ServerFetchAvatar(const char *jid) -{ - WANodeIq iq(IQ::GET, "w:profile:picture", jid); - *iq.addChild("picture") << CHAR_PARAM("type", "preview") << CHAR_PARAM("query", "url"); - WSSendNode(iq, &WhatsAppProto::OnIqGetAvatar); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -bool CMPlugin::SaveFile(const char *pszUrl, PROTO_AVATAR_INFORMATION &ai) -{ - NETLIBHTTPREQUEST req = {}; - req.cbSize = sizeof(req); - req.flags = NLHRF_NODUMP | NLHRF_PERSISTENT | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; - req.requestType = REQUEST_GET; - req.szUrl = (char*)pszUrl; - req.nlc = hAvatarConn; - - NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(hAvatarUser, &req); - if (pReply == nullptr) { - hAvatarConn = nullptr; - debugLogA("Failed to retrieve avatar from url: %s", pszUrl); - return false; - } - - hAvatarConn = pReply->nlc; - - bool bSuccess = false; - if (pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { - if (auto *pszHdr = Netlib_GetHeader(pReply, "Content-Type")) - ai.format = ProtoGetAvatarFormatByMimeType(pszHdr); - - if (ai.format != PA_FORMAT_UNKNOWN) { - FILE *fout = _wfopen(ai.filename, L"wb"); - if (fout) { - fwrite(pReply->pData, 1, pReply->dataLength, fout); - fclose(fout); - bSuccess = true; - } - else debugLogA("Error saving avatar to file %S", ai.filename); - } - else debugLogA("unknown avatar mime type"); - } - else debugLogA("Error %d reading avatar from url: %s", pReply->resultCode, pszUrl); - - Netlib_FreeHttpRequest(pReply); - return bSuccess; -} +/* + +WhatsApp plugin for Miranda NG +Copyright 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +void WhatsAppProto::OnIqGetAvatar(const WANode &node) +{ + auto *pUser = FindUser(node.getAttr("from")); + if (pUser == nullptr) + return; + + PROTO_AVATAR_INFORMATION ai = {}; + ai.hContact = pUser->hContact; + ai.format = PA_FORMAT_JPEG; + wcsncpy_s(ai.filename, GetAvatarFileName(pUser->hContact), _TRUNCATE); + + auto *pNode = node.getChild("picture"); + + DWORD dwLastChangeTime = pNode->getAttrInt("id"); + + CMStringA szUrl(pNode->getAttr("url")); + if (szUrl.IsEmpty()) { + setDword(pUser->hContact, DBKEY_AVATAR_TAG, 0); // avatar doesn't exist, don't check it later + +LBL_Error: + ProtoBroadcastAck(pUser->hContact, ACKTYPE_AVATAR, ACKRESULT_FAILED, HANDLE(&ai)); + return; + } + + // if avatar was changed or not present at all, download it + if (dwLastChangeTime > getDword(pUser->hContact, DBKEY_AVATAR_TAG)) { + if (!g_plugin.SaveFile(szUrl, ai)) + goto LBL_Error; + + // set timestamp of avatar being saved + setDword(pUser->hContact, DBKEY_AVATAR_TAG, dwLastChangeTime); + } + ProtoBroadcastAck(pUser->hContact, ACKTYPE_AVATAR, ACKRESULT_SUCCESS, HANDLE(&ai)); +} + +INT_PTR WhatsAppProto::GetAvatarInfo(WPARAM wParam, LPARAM lParam) +{ + PROTO_AVATAR_INFORMATION *pai = (PROTO_AVATAR_INFORMATION*)lParam; + + ptrA jid(getStringA(pai->hContact, isChatRoom(pai->hContact) ? "ChatRoomID" : DBKEY_ID)); + if (jid == NULL) + return GAIR_NOAVATAR; + + CMStringW tszFileName(GetAvatarFileName(pai->hContact)); + wcsncpy_s(pai->filename, tszFileName.c_str(), _TRUNCATE); + pai->format = PA_FORMAT_JPEG; + + DWORD dwTag = getDword(pai->hContact, DBKEY_AVATAR_TAG, -1); + if (dwTag == -1 || (wParam & GAIF_FORCE) != 0) + if (pai->hContact != NULL && isOnline()) { + ServerFetchAvatar(jid); + return GAIR_WAITFOR; + } + + debugLogA("No avatar"); + return GAIR_NOAVATAR; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR WhatsAppProto::GetAvatarCaps(WPARAM wParam, LPARAM lParam) +{ + switch (wParam) { + case AF_PROPORTION: + return PIP_SQUARE; + + case AF_FORMATSUPPORTED: // Jabber supports avatars of virtually all formats + return PA_FORMAT_JPEG; + + case AF_ENABLED: + return TRUE; + + case AF_MAXSIZE: + POINT *size = (POINT*)lParam; + if (size) + size->x = size->y = 640; + return 0; + } + return -1; +} + +CMStringW WhatsAppProto::GetAvatarFileName(MCONTACT hContact) +{ + CMStringW result = m_tszAvatarFolder + L"\\"; + + CMStringA jid; + if (hContact != NULL) { + ptrA szId(getStringA(hContact, isChatRoom(hContact) ? "ChatRoomID" : DBKEY_ID)); + if (szId == NULL) + return L""; + + jid = szId; + } + else jid = m_szJid; + + return result + _A2T(jid.c_str()) + L".jpg"; +} + +INT_PTR WhatsAppProto::GetMyAvatar(WPARAM wParam, LPARAM lParam) +{ + std::wstring tszOwnAvatar(m_tszAvatarFolder + L"\\myavatar.jpg"); + wcsncpy_s((wchar_t*)wParam, lParam, tszOwnAvatar.c_str(), _TRUNCATE); + return 0; +} + +INT_PTR WhatsAppProto::SetMyAvatar(WPARAM, LPARAM) +{ + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::ServerFetchAvatar(const char *jid) +{ + WANodeIq iq(IQ::GET, "w:profile:picture", jid); + *iq.addChild("picture") << CHAR_PARAM("type", "preview") << CHAR_PARAM("query", "url"); + WSSendNode(iq, &WhatsAppProto::OnIqGetAvatar); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +bool CMPlugin::SaveFile(const char *pszUrl, PROTO_AVATAR_INFORMATION &ai) +{ + NETLIBHTTPREQUEST req = {}; + req.cbSize = sizeof(req); + req.flags = NLHRF_NODUMP | NLHRF_PERSISTENT | NLHRF_SSL | NLHRF_HTTP11 | NLHRF_REDIRECT; + req.requestType = REQUEST_GET; + req.szUrl = (char*)pszUrl; + req.nlc = hAvatarConn; + + NETLIBHTTPREQUEST *pReply = Netlib_HttpTransaction(hAvatarUser, &req); + if (pReply == nullptr) { + hAvatarConn = nullptr; + debugLogA("Failed to retrieve avatar from url: %s", pszUrl); + return false; + } + + hAvatarConn = pReply->nlc; + + bool bSuccess = false; + if (pReply->resultCode == 200 && pReply->pData && pReply->dataLength) { + if (auto *pszHdr = Netlib_GetHeader(pReply, "Content-Type")) + ai.format = ProtoGetAvatarFormatByMimeType(pszHdr); + + if (ai.format != PA_FORMAT_UNKNOWN) { + FILE *fout = _wfopen(ai.filename, L"wb"); + if (fout) { + fwrite(pReply->pData, 1, pReply->dataLength, fout); + fclose(fout); + bSuccess = true; + } + else debugLogA("Error saving avatar to file %S", ai.filename); + } + else debugLogA("unknown avatar mime type"); + } + else debugLogA("Error %d reading avatar from url: %s", pReply->resultCode, pszUrl); + + Netlib_FreeHttpRequest(pReply); + return bSuccess; +} diff --git a/protocols/WhatsApp/src/chats.cpp b/protocols/WhatsApp/src/chats.cpp index 5fbe0cf1ef..59f98d4548 100644 --- a/protocols/WhatsApp/src/chats.cpp +++ b/protocols/WhatsApp/src/chats.cpp @@ -1,7 +1,7 @@ /* WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan +Copyright © 2019-23 George Hazan */ diff --git a/protocols/WhatsApp/src/crypt.cpp b/protocols/WhatsApp/src/crypt.cpp index 20e8ddd1bd..049a75844c 100644 --- a/protocols/WhatsApp/src/crypt.cpp +++ b/protocols/WhatsApp/src/crypt.cpp @@ -1,177 +1,177 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#include "stdafx.h" - -MBinBuffer aesDecrypt( - const EVP_CIPHER *cipher, - const uint8_t *key, - const uint8_t *iv, - const void *data, size_t dataLen, - const void *additionalData, size_t additionalLen) -{ - int tag_len = 0, dec_len = 0, final_len = 0; - EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); - EVP_DecryptInit_ex(ctx, cipher, NULL, key, iv); - - if (additionalLen) - EVP_DecryptUpdate(ctx, nullptr, &tag_len, (uint8_t *)additionalData, (int)additionalLen); - - if (cipher == EVP_aes_256_gcm()) { - dataLen -= 16; - EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (uint8_t *)data + dataLen); - } - - MBinBuffer ret; - uint8_t outbuf[2000]; - for (size_t len = 0; len < dataLen; len += 1024) { - size_t portionSize = dataLen - len; - EVP_DecryptUpdate(ctx, outbuf, &dec_len, (uint8_t *)data + len, (int)min(portionSize, 1024)); - ret.append(outbuf, dec_len); - } - - EVP_DecryptFinal_ex(ctx, outbuf, &final_len); - if (final_len) - ret.append(outbuf, final_len); - - EVP_CIPHER_CTX_free(ctx); - return ret; -} - -MBinBuffer aesEncrypt( - const EVP_CIPHER *cipher, - const uint8_t *key, - const uint8_t *iv, - const void *data, size_t dataLen, - const void *additionalData, size_t additionalLen) -{ - int tag_len = 0, dec_len = 0, final_len = 0; - EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); - EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv); - - if (additionalLen) - EVP_EncryptUpdate(ctx, nullptr, &tag_len, (uint8_t *)additionalData, (int)additionalLen); - - if (cipher == EVP_aes_256_gcm()) { - dataLen -= 16; - EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (uint8_t *)data + dataLen); - } - - MBinBuffer ret; - uint8_t outbuf[2000]; - for (size_t len = 0; len < dataLen; len += 1024) { - size_t portionSize = dataLen - len; - EVP_EncryptUpdate(ctx, outbuf, &dec_len, (uint8_t *)data + len, (int)min(portionSize, 1024)); - ret.append(outbuf, dec_len); - } - - EVP_EncryptFinal_ex(ctx, outbuf, &final_len); - if (final_len) - ret.append(outbuf, final_len); - - EVP_CIPHER_CTX_free(ctx); - return ret; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static unsigned char *HKDF_Extract(const EVP_MD *evp_md, - const unsigned char *salt, size_t salt_len, - const unsigned char *key, size_t key_len, - unsigned char *prk, size_t *prk_len) -{ - unsigned int tmp_len; - - if (!HMAC(evp_md, salt, (int)salt_len, key, (int)key_len, prk, &tmp_len)) - return NULL; - - *prk_len = tmp_len; - return prk; -} - -static unsigned char *HKDF_Expand(const EVP_MD *evp_md, - const unsigned char *prk, size_t prk_len, - const unsigned char *info, size_t info_len, - unsigned char *okm, size_t okm_len) -{ - HMAC_CTX *hmac; - unsigned char *ret = NULL; - - unsigned int i; - - unsigned char prev[EVP_MAX_MD_SIZE]; - - size_t done_len = 0, dig_len = EVP_MD_size(evp_md); - - size_t n = okm_len / dig_len; - if (okm_len % dig_len) - n++; - - if (n > 255 || okm == NULL) - return NULL; - - if ((hmac = HMAC_CTX_new()) == NULL) - return NULL; - - if (!HMAC_Init_ex(hmac, prk, (int)prk_len, evp_md, NULL)) - goto err; - - for (i = 1; i <= n; i++) { - size_t copy_len; - const unsigned char ctr = i; - - if (i > 1) { - if (!HMAC_Init_ex(hmac, NULL, 0, NULL, NULL)) - goto err; - - if (!HMAC_Update(hmac, prev, dig_len)) - goto err; - } - - if (!HMAC_Update(hmac, info, info_len)) - goto err; - - if (!HMAC_Update(hmac, &ctr, 1)) - goto err; - - if (!HMAC_Final(hmac, prev, NULL)) - goto err; - - copy_len = (done_len + dig_len > okm_len) ? - okm_len - done_len : - dig_len; - - memcpy(okm + done_len, prev, copy_len); - - done_len += copy_len; - } - ret = okm; - -err: - OPENSSL_cleanse(prev, sizeof(prev)); - HMAC_CTX_free(hmac); - return ret; -} - -unsigned char *HKDF(const EVP_MD *evp_md, - const unsigned char *salt, size_t salt_len, - const unsigned char *key, size_t key_len, - const unsigned char *info, size_t info_len, - unsigned char *okm, size_t okm_len) -{ - unsigned char prk[EVP_MAX_MD_SIZE]; - unsigned char *ret; - size_t prk_len; - - if (!HKDF_Extract(evp_md, salt, salt_len, key, key_len, prk, &prk_len)) - return NULL; - - ret = HKDF_Expand(evp_md, prk, prk_len, info, info_len, okm, okm_len); - OPENSSL_cleanse(prk, sizeof(prk)); - - return ret; -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +MBinBuffer aesDecrypt( + const EVP_CIPHER *cipher, + const uint8_t *key, + const uint8_t *iv, + const void *data, size_t dataLen, + const void *additionalData, size_t additionalLen) +{ + int tag_len = 0, dec_len = 0, final_len = 0; + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + EVP_DecryptInit_ex(ctx, cipher, NULL, key, iv); + + if (additionalLen) + EVP_DecryptUpdate(ctx, nullptr, &tag_len, (uint8_t *)additionalData, (int)additionalLen); + + if (cipher == EVP_aes_256_gcm()) { + dataLen -= 16; + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (uint8_t *)data + dataLen); + } + + MBinBuffer ret; + uint8_t outbuf[2000]; + for (size_t len = 0; len < dataLen; len += 1024) { + size_t portionSize = dataLen - len; + EVP_DecryptUpdate(ctx, outbuf, &dec_len, (uint8_t *)data + len, (int)min(portionSize, 1024)); + ret.append(outbuf, dec_len); + } + + EVP_DecryptFinal_ex(ctx, outbuf, &final_len); + if (final_len) + ret.append(outbuf, final_len); + + EVP_CIPHER_CTX_free(ctx); + return ret; +} + +MBinBuffer aesEncrypt( + const EVP_CIPHER *cipher, + const uint8_t *key, + const uint8_t *iv, + const void *data, size_t dataLen, + const void *additionalData, size_t additionalLen) +{ + int tag_len = 0, dec_len = 0, final_len = 0; + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, cipher, NULL, key, iv); + + if (additionalLen) + EVP_EncryptUpdate(ctx, nullptr, &tag_len, (uint8_t *)additionalData, (int)additionalLen); + + if (cipher == EVP_aes_256_gcm()) { + dataLen -= 16; + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, (uint8_t *)data + dataLen); + } + + MBinBuffer ret; + uint8_t outbuf[2000]; + for (size_t len = 0; len < dataLen; len += 1024) { + size_t portionSize = dataLen - len; + EVP_EncryptUpdate(ctx, outbuf, &dec_len, (uint8_t *)data + len, (int)min(portionSize, 1024)); + ret.append(outbuf, dec_len); + } + + EVP_EncryptFinal_ex(ctx, outbuf, &final_len); + if (final_len) + ret.append(outbuf, final_len); + + EVP_CIPHER_CTX_free(ctx); + return ret; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static unsigned char *HKDF_Extract(const EVP_MD *evp_md, + const unsigned char *salt, size_t salt_len, + const unsigned char *key, size_t key_len, + unsigned char *prk, size_t *prk_len) +{ + unsigned int tmp_len; + + if (!HMAC(evp_md, salt, (int)salt_len, key, (int)key_len, prk, &tmp_len)) + return NULL; + + *prk_len = tmp_len; + return prk; +} + +static unsigned char *HKDF_Expand(const EVP_MD *evp_md, + const unsigned char *prk, size_t prk_len, + const unsigned char *info, size_t info_len, + unsigned char *okm, size_t okm_len) +{ + HMAC_CTX *hmac; + unsigned char *ret = NULL; + + unsigned int i; + + unsigned char prev[EVP_MAX_MD_SIZE]; + + size_t done_len = 0, dig_len = EVP_MD_size(evp_md); + + size_t n = okm_len / dig_len; + if (okm_len % dig_len) + n++; + + if (n > 255 || okm == NULL) + return NULL; + + if ((hmac = HMAC_CTX_new()) == NULL) + return NULL; + + if (!HMAC_Init_ex(hmac, prk, (int)prk_len, evp_md, NULL)) + goto err; + + for (i = 1; i <= n; i++) { + size_t copy_len; + const unsigned char ctr = i; + + if (i > 1) { + if (!HMAC_Init_ex(hmac, NULL, 0, NULL, NULL)) + goto err; + + if (!HMAC_Update(hmac, prev, dig_len)) + goto err; + } + + if (!HMAC_Update(hmac, info, info_len)) + goto err; + + if (!HMAC_Update(hmac, &ctr, 1)) + goto err; + + if (!HMAC_Final(hmac, prev, NULL)) + goto err; + + copy_len = (done_len + dig_len > okm_len) ? + okm_len - done_len : + dig_len; + + memcpy(okm + done_len, prev, copy_len); + + done_len += copy_len; + } + ret = okm; + +err: + OPENSSL_cleanse(prev, sizeof(prev)); + HMAC_CTX_free(hmac); + return ret; +} + +unsigned char *HKDF(const EVP_MD *evp_md, + const unsigned char *salt, size_t salt_len, + const unsigned char *key, size_t key_len, + const unsigned char *info, size_t info_len, + unsigned char *okm, size_t okm_len) +{ + unsigned char prk[EVP_MAX_MD_SIZE]; + unsigned char *ret; + size_t prk_len; + + if (!HKDF_Extract(evp_md, salt, salt_len, key, key_len, prk, &prk_len)) + return NULL; + + ret = HKDF_Expand(evp_md, prk, prk_len, info, info_len, okm, okm_len); + OPENSSL_cleanse(prk, sizeof(prk)); + + return ret; +} diff --git a/protocols/WhatsApp/src/db.h b/protocols/WhatsApp/src/db.h index cb562715f4..1e4bebc6a8 100644 --- a/protocols/WhatsApp/src/db.h +++ b/protocols/WhatsApp/src/db.h @@ -1,43 +1,43 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan - -*/ - -#define MODULENAME "WhatsApp" - -// DB keys -#define DBKEY_ID "ID" -#define DBKEY_DEVICE_ID "DeviceId" -#define DBKEY_EPHEMERAL_TS "EphemeralTS" -#define DBKEY_EPHEMERAL_EXPIRE "EphemeralExpire" - -#define DBKEY_NOISE_PUB "NoisePublicKey" -#define DBKEY_NOISE_PRIV "NoisePrivateKey" - -#define DBKEY_SIGNED_IDENTITY_PUB "SignedIdentityPublicKey" -#define DBKEY_SIGNED_IDENTITY_PRIV "SignedIdentityPrivateKey" - -#define DBKEY_PREKEY "SignedPreKey1" -#define DBKEY_PREKEY_NEXT_ID "PrekeyNextId" -#define DBKEY_PREKEY_UPLOAD_ID "PrekeyUploadId" - -#define DBKEY_REG_ID "RegistrationId" -#define DBKEY_SECRET_KEY "AdvSecretKey" - -#define DBKEY_NICK "Nick" -#define DBKEY_DEF_GROUP "DefaultGroup" -#define DBKEY_AUTORUNCHATS "AutoRunChats" -#define DBKEY_AVATAR_TAG "AvatarTag" - -#define DBKEY_EVENT_CLIENT_COLBACK "PopupClientColorBack" -#define DBKEY_EVENT_CLIENT_COLTEXT "PopupClientColorText" -#define DBKEY_EVENT_CLIENT_TIMEOUT "PopupClientTimeout" -#define DBKEY_EVENT_CLIENT_DEFAULT "PopupClientColorDefault" - -#define DBKEY_EVENT_OTHER_COLBACK "PopupOtherColorBack" -#define DBKEY_EVENT_OTHER_COLTEXT "PopupOtherColorText" -#define DBKEY_EVENT_OTHER_TIMEOUT "PopupOtherTimeout" -#define DBKEY_EVENT_OTHER_DEFAULT "PopupOtherColorDefault" - +/* + +WhatsApp plugin for Miranda NG +Copyright 2019-23 George Hazan + +*/ + +#define MODULENAME "WhatsApp" + +// DB keys +#define DBKEY_ID "ID" +#define DBKEY_DEVICE_ID "DeviceId" +#define DBKEY_EPHEMERAL_TS "EphemeralTS" +#define DBKEY_EPHEMERAL_EXPIRE "EphemeralExpire" + +#define DBKEY_NOISE_PUB "NoisePublicKey" +#define DBKEY_NOISE_PRIV "NoisePrivateKey" + +#define DBKEY_SIGNED_IDENTITY_PUB "SignedIdentityPublicKey" +#define DBKEY_SIGNED_IDENTITY_PRIV "SignedIdentityPrivateKey" + +#define DBKEY_PREKEY "SignedPreKey1" +#define DBKEY_PREKEY_NEXT_ID "PrekeyNextId" +#define DBKEY_PREKEY_UPLOAD_ID "PrekeyUploadId" + +#define DBKEY_REG_ID "RegistrationId" +#define DBKEY_SECRET_KEY "AdvSecretKey" + +#define DBKEY_NICK "Nick" +#define DBKEY_DEF_GROUP "DefaultGroup" +#define DBKEY_AUTORUNCHATS "AutoRunChats" +#define DBKEY_AVATAR_TAG "AvatarTag" + +#define DBKEY_EVENT_CLIENT_COLBACK "PopupClientColorBack" +#define DBKEY_EVENT_CLIENT_COLTEXT "PopupClientColorText" +#define DBKEY_EVENT_CLIENT_TIMEOUT "PopupClientTimeout" +#define DBKEY_EVENT_CLIENT_DEFAULT "PopupClientColorDefault" + +#define DBKEY_EVENT_OTHER_COLBACK "PopupOtherColorBack" +#define DBKEY_EVENT_OTHER_COLTEXT "PopupOtherColorText" +#define DBKEY_EVENT_OTHER_TIMEOUT "PopupOtherTimeout" +#define DBKEY_EVENT_OTHER_DEFAULT "PopupOtherColorDefault" + diff --git a/protocols/WhatsApp/src/dicts.h b/protocols/WhatsApp/src/dicts.h index 7b84487d14..ab37f10abb 100644 --- a/protocols/WhatsApp/src/dicts.h +++ b/protocols/WhatsApp/src/dicts.h @@ -1,194 +1,194 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan - -*/ - -static char *SingleByteTokens[] = { - "", "xmlstreamstart", "xmlstreamend", "s.whatsapp.net", "type", "participant", "from", "receipt", "id", "broadcast", "status", "message", "notification", "notify", "to", "jid", "user", - "class", "offline", "g.us", "result", "mediatype", "enc", "skmsg", "off_cnt", "xmlns", "presence", "participants", "ack", "t", "iq", "device_hash", "read", "value", "media", "picture", - "chatstate", "unavailable", "text", "urn:xmpp:whatsapp:push", "devices", "verified_name", "contact", "composing", "edge_routing", "routing_info", "item", "image", "verified_level", - "get", "fallback_hostname", "2", "media_conn", "1", "v", "handshake", "fallback_class", "count", "config", "offline_preview", "download_buckets", "w:profile:picture", "set", - "creation", "location", "fallback_ip4", "msg", "urn:xmpp:ping", "fallback_ip6", "call-creator", "relaylatency", "success", "subscribe", "video", "business_hours_config", - "platform", "hostname", "version", "unknown", "0", "ping", "hash", "edit", "subject", "max_buckets", "download", "delivery", "props", "sticker", "name", "last", "contacts", - "business", "primary", "preview", "w:p", "pkmsg", "call-id", "retry", "prop", "call", "auth_ttl", "available", "relay_id", "last_id", "day_of_week", "w", "host", "seen", - "bits", "list", "atn", "upload", "is_new", "w:stats", "key", "paused", "specific_hours", "multicast", "stream:error", "mmg.whatsapp.net", "code", "deny", "played", "profile", - "fna", "device-list", "close_time", "latency", "gcm", "pop", "audio", "26", "w:web", "open_time", "error", "auth", "ip4", "update", "profile_options", "config_value", "category", - "catalog_not_created", "00", "config_code", "mode", "catalog_status", "ip6", "blocklist", "registration", "7", "web", "fail", "w:m", "cart_enabled", "ttl", "gif", "300", - "device_orientation", "identity", "query", "401", "media-gig2-1.cdn.whatsapp.net", "in", "3", "te2", "add", "fallback", "categories", "ptt", "encrypt", "notice", - "thumbnail-document", "item-not-found", "12", "thumbnail-image", "stage", "thumbnail-link", "usync", "out", "thumbnail-video", "8", "01", "context", "sidelist", - "thumbnail-gif", "terminate", "not-authorized", "orientation", "dhash", "capability", "side_list", "md-app-state", "description", "serial", "readreceipts", "te", - "business_hours", "md-msg-hist", "tag", "attribute_padding", "document", "open_24h", "delete", "expiration", "active", "prev_v_id", "true", "passive", "index", "4", - "conflict", "remove", "w:gp2", "config_expo_key", "screen_height", "replaced", "02", "screen_width", "uploadfieldstat", "2:47DEQpj8", "media-bog1-1.cdn.whatsapp.net", - "encopt", "url", "catalog_exists", "keygen", "rate", "offer", "opus", "media-mia3-1.cdn.whatsapp.net", "privacy", "media-mia3-2.cdn.whatsapp.net", "signature", - "preaccept", "token_id", "media-eze1-1.cdn.whatsapp.net" -}; - -static char *dict0[] = { - "media-for1-1.cdn.whatsapp.net", "relay", "media-gru2-2.cdn.whatsapp.net", "uncompressed", "medium", "voip_settings", "device", "reason", - "media-lim1-1.cdn.whatsapp.net", "media-qro1-2.cdn.whatsapp.net", "media-gru1-2.cdn.whatsapp.net", "action", "features", "media-gru2-1.cdn.whatsapp.net", - "media-gru1-1.cdn.whatsapp.net", "media-otp1-1.cdn.whatsapp.net", "kyc-id", "priority", "phash", "mute", "token", "100", "media-qro1-1.cdn.whatsapp.net", - "none", "media-mrs2-2.cdn.whatsapp.net", "sign_credential", "03", "media-mrs2-1.cdn.whatsapp.net", "protocol", "timezone", "transport", "eph_setting", "1080", - "original_dimensions", "media-frx5-1.cdn.whatsapp.net", "background", "disable", "original_image_url", "5", "transaction-id", "direct_path", "103", "appointment_only", - "request_image_url", "peer_pid", "address", "105", "104", "102", "media-cdt1-1.cdn.whatsapp.net", "101", "109", "110", "106", "background_location", "v_id", "sync", - "status-old", "111", "107", "ppic", "media-scl2-1.cdn.whatsapp.net", "business_profile", "108", "invite", "04", "audio_duration", "media-mct1-1.cdn.whatsapp.net", - "media-cdg2-1.cdn.whatsapp.net", "media-los2-1.cdn.whatsapp.net", "invis", "net", "voip_payload_type", "status-revoke-delay", "404", "state", "use_correct_order_for_hmac_sha1", - "ver", "media-mad1-1.cdn.whatsapp.net", "order", "540", "skey", "blinded_credential", "android", "contact_remove", "enable_downlink_relay_latency_only", "duration", - "enable_vid_one_way_codec_nego", "6", "media-sof1-1.cdn.whatsapp.net", "accept", "all", "signed_credential", "media-atl3-1.cdn.whatsapp.net", "media-lhr8-1.cdn.whatsapp.net", - "website", "05", "latitude", "media-dfw5-1.cdn.whatsapp.net", "forbidden", "enable_audio_piggyback_network_mtu_fix", "media-dfw5-2.cdn.whatsapp.net", "note.m4r", - "media-atl3-2.cdn.whatsapp.net", "jb_nack_discard_count_fix", "longitude", "Opening.m4r", "media-arn2-1.cdn.whatsapp.net", "email", "timestamp", "admin", - "media-pmo1-1.cdn.whatsapp.net", "America/Sao_Paulo", "contact_add", "media-sin6-1.cdn.whatsapp.net", "interactive", "8000", "acs_public_key", - "sigquit_anr_detector_release_rollover_percent", "media.fmed1-2.fna.whatsapp.net", "groupadd", "enabled_for_video_upgrade", "latency_update_threshold", - "media-frt3-2.cdn.whatsapp.net", "calls_row_constraint_layout", "media.fgbb2-1.fna.whatsapp.net", "mms4_media_retry_notification_encryption_enabled", "timeout", - "media-sin6-3.cdn.whatsapp.net", "audio_nack_jitter_multiplier", "jb_discard_count_adjust_pct_rc", "audio_reserve_bps", "delta", "account_sync", "default", - "media.fjed4-6.fna.whatsapp.net", "06", "lock_video_orientation", "media-frt3-1.cdn.whatsapp.net", "w:g2", "media-sin6-2.cdn.whatsapp.net", "audio_nack_algo_mask", - "media.fgbb2-2.fna.whatsapp.net", "media.fmed1-1.fna.whatsapp.net", "cond_range_target_bitrate", "mms4_server_error_receipt_encryption_enabled", "vid_rc_dyn", "fri", - "cart_v1_1_order_message_changes_enabled", "reg_push", "jb_hist_deposit_value", "privatestats", "media.fist7-2.fna.whatsapp.net", "thu", "jb_discard_count_adjust_pct", - "mon", "group_call_video_maximization_enabled", "mms_cat_v1_forward_hot_override_enabled", "audio_nack_new_rtt", "media.fsub2-3.fna.whatsapp.net", - "media_upload_aggressive_retry_exponential_backoff_enabled", "tue", "wed", "media.fruh4-2.fna.whatsapp.net", "audio_nack_max_seq_req", "max_rtp_audio_packet_resends", - "jb_hist_max_cdf_value", "07", "audio_nack_max_jb_delay", "mms_forward_partially_downloaded_video", "media-lcy1-1.cdn.whatsapp.net", "resume", "jb_inband_fec_aware", - "new_commerce_entry_point_enabled", "480", "payments_upi_generate_qr_amount_limit", "sigquit_anr_detector_rollover_percent", "media.fsdu2-1.fna.whatsapp.net", "fbns", - "aud_pkt_reorder_pct", "dec", "stop_probing_before_accept_send", "media_upload_max_aggressive_retries", "edit_business_profile_new_mode_enabled", - "media.fhex4-1.fna.whatsapp.net", "media.fjed4-3.fna.whatsapp.net", "sigquit_anr_detector_64bit_rollover_percent", "cond_range_ema_jb_last_delay", - "watls_enable_early_data_http_get", "media.fsdu2-2.fna.whatsapp.net", "message_qr_disambiguation_enabled", "media-mxp1-1.cdn.whatsapp.net", "sat", "vertical", - "media.fruh4-5.fna.whatsapp.net", "200", "media-sof1-2.cdn.whatsapp.net", "-1", "height", "product_catalog_hide_show_items_enabled", "deep_copy_frm_last", - "tsoffline", "vp8/h.264", "media.fgye5-3.fna.whatsapp.net", "media.ftuc1-2.fna.whatsapp.net", "smb_upsell_chat_banner_enabled", "canonical", "08", "9", ".", - "media.fgyd4-4.fna.whatsapp.net", "media.fsti4-1.fna.whatsapp.net", "mms_vcache_aggregation_enabled", "mms_hot_content_timespan_in_seconds", "nse_ver", "rte", - "third_party_sticker_web_sync", "cond_range_target_total_bitrate", "media_upload_aggressive_retry_enabled", "instrument_spam_report_enabled", "disable_reconnect_tone", - "move_media_folder_from_sister_app", "one_tap_calling_in_group_chat_size", "10", "storage_mgmt_banner_threshold_mb", "enable_backup_passive_mode", "sharechat_inline_player_enabled", - "media.fcnq2-1.fna.whatsapp.net", "media.fhex4-2.fna.whatsapp.net", "media.fist6-3.fna.whatsapp.net", "ephemeral_drop_column_stage", "reconnecting_after_network_change_threshold_ms", - "media-lhr8-2.cdn.whatsapp.net", "cond_jb_last_delay_ema_alpha", "entry_point_block_logging_enabled", "critical_event_upload_log_config", "respect_initial_bitrate_estimate", - "smaller_image_thumbs_status_enabled", "media.fbtz1-4.fna.whatsapp.net", "media.fjed4-1.fna.whatsapp.net", "width", "720", "enable_frame_dropper", "enable_one_side_mode", - "urn:xmpp:whatsapp:dirty", "new_sticker_animation_behavior_v2", "media.flim3-2.fna.whatsapp.net", "media.fuio6-2.fna.whatsapp.net", "skip_forced_signaling", "dleq_proof", - "status_video_max_bitrate", "lazy_send_probing_req", "enhanced_storage_management", "android_privatestats_endpoint_dit_enabled", "media.fscl13-2.fna.whatsapp.net", "video_duration" -}; - -static char *dict1[] = { - "group_call_discoverability_enabled", "media.faep9-2.fna.whatsapp.net", "msgr", "bloks_loggedin_access_app_id", "db_status_migration_step", "watls_prefer_ip6", - "jabber:iq:privacy", "68", "media.fsaw1-11.fna.whatsapp.net", "mms4_media_conn_persist_enabled", "animated_stickers_thread_clean_up", "media.fcgk3-2.fna.whatsapp.net", - "media.fcgk4-6.fna.whatsapp.net", "media.fgye5-2.fna.whatsapp.net", "media.flpb1-1.fna.whatsapp.net", "media.fsub2-1.fna.whatsapp.net", "media.fuio6-3.fna.whatsapp.net", - "not-allowed", "partial_pjpeg_bw_threshold", "cap_estimated_bitrate", "mms_chatd_resume_check_over_thrift", "smb_upsell_business_profile_enabled", "product_catalog_webclient", - "groups", "sigquit_anr_detector_release_updated_rollout", "syncd_key_rotation_enabled", "media.fdmm2-1.fna.whatsapp.net", "media-hou1-1.cdn.whatsapp.net", - "remove_old_chat_notifications", "smb_biztools_deeplink_enabled", "use_downloadable_filters_int", "group_qr_codes_enabled", "max_receipt_processing_time", - "optimistic_image_processing_enabled", "smaller_video_thumbs_status_enabled", "watls_early_data", "reconnecting_before_relay_failover_threshold_ms", "cond_range_packet_loss_pct", - "groups_privacy_blacklist", "status-revoke-drop", "stickers_animated_thumbnail_download", "dedupe_transcode_shared_images", "dedupe_transcode_shared_videos", - "media.fcnq2-2.fna.whatsapp.net", "media.fgyd4-1.fna.whatsapp.net", "media.fist7-1.fna.whatsapp.net", "media.flim3-3.fna.whatsapp.net", "add_contact_by_qr_enabled", - "https://faq.whatsapp.com/payments", "multicast_limit_global", "sticker_notification_preview", "smb_better_catalog_list_adapters_enabled", "bloks_use_minscript_android", - "pen_smoothing_enabled", "media.fcgk4-5.fna.whatsapp.net", "media.fevn1-3.fna.whatsapp.net", "media.fpoj7-1.fna.whatsapp.net", "media-arn2-2.cdn.whatsapp.net", - "reconnecting_before_network_change_threshold_ms", "android_media_use_fresco_for_gifs", "cond_in_congestion", "status_image_max_edge", "sticker_search_enabled", - "starred_stickers_web_sync", "db_blank_me_jid_migration_step", "media.fist6-2.fna.whatsapp.net", "media.ftuc1-1.fna.whatsapp.net", "09", "anr_fast_logs_upload_rollout", - "camera_core_integration_enabled", "11", "third_party_sticker_caching", "thread_dump_contact_support", "wam_privatestats_enabled", "vcard_as_document_size_kb", "maxfpp", - "fbip", "ephemeral_allow_group_members", "media-bom1-2.cdn.whatsapp.net", "media-xsp1-1.cdn.whatsapp.net", "disable_prewarm", "frequently_forwarded_max", - "media.fbtz1-5.fna.whatsapp.net", "media.fevn7-1.fna.whatsapp.net", "media.fgyd4-2.fna.whatsapp.net", "sticker_tray_animation_fully_visible_items", - "green_alert_banner_duration", "reconnecting_after_p2p_failover_threshold_ms", "connected", "share_biz_vcard_enabled", "stickers_animation", "0a", "1200", "WhatsApp", - "group_description_length", "p_v_id", "payments_upi_intent_transaction_limit", "frequently_forwarded_messages", "media-xsp1-2.cdn.whatsapp.net", - "media.faep8-1.fna.whatsapp.net", "media.faep8-2.fna.whatsapp.net", "media.faep9-1.fna.whatsapp.net", "media.fdmm2-2.fna.whatsapp.net", "media.fgzt3-1.fna.whatsapp.net", - "media.flim4-2.fna.whatsapp.net", "media.frao1-1.fna.whatsapp.net", "media.fscl9-2.fna.whatsapp.net", "media.fsub2-2.fna.whatsapp.net", "superadmin", - "media.fbog10-1.fna.whatsapp.net", "media.fcgh28-1.fna.whatsapp.net", "media.fjdo10-1.fna.whatsapp.net", "third_party_animated_sticker_import", "delay_fec", - "attachment_picker_refresh", "android_linked_devices_re_auth_enabled", "rc_dyn", "green_alert_block_jitter", "add_contact_logging_enabled", "biz_message_logging_enabled", - "conversation_media_preview_v2", "media-jnb1-1.cdn.whatsapp.net", "ab_key", "media.fcgk4-2.fna.whatsapp.net", "media.fevn1-1.fna.whatsapp.net", "media.fist6-1.fna.whatsapp.net", - "media.fruh4-4.fna.whatsapp.net", "media.fsti4-2.fna.whatsapp.net", "mms_vcard_autodownload_size_kb", "watls_enabled", "notif_ch_override_off", "media.fsaw1-14.fna.whatsapp.net", - "media.fscl13-1.fna.whatsapp.net", "db_group_participant_migration_step", "1020", "cond_range_sterm_rtt", "invites_logging_enabled", "triggered_block_enabled", - "group_call_max_participants", "media-iad3-1.cdn.whatsapp.net", "product_catalog_open_deeplink", "shops_required_tos_version", "image_max_kbytes", - "cond_low_quality_vid_mode", "db_receipt_migration_step", "jb_early_prob_hist_shrink", "media.fdmm2-3.fna.whatsapp.net", "media.fdmm2-4.fna.whatsapp.net", - "media.fruh4-1.fna.whatsapp.net", "media.fsaw2-2.fna.whatsapp.net", "remove_geolocation_videos", "new_animation_behavior", "fieldstats_beacon_chance", "403", - "authkey_reset_on_ban", "continuous_ptt_playback", "reconnecting_after_relay_failover_threshold_ms", "false", "group", "sun", "conversation_swipe_to_reply", - "ephemeral_messages_setting", "smaller_video_thumbs_enabled", "md_device_sync_enabled", "bloks_shops_pdp_url_regex", "lasso_integration_enabled", - "media-bom1-1.cdn.whatsapp.net", "new_backup_format_enabled", "256", "media.faep6-1.fna.whatsapp.net", "media.fasr1-1.fna.whatsapp.net", "media.fbtz1-7.fna.whatsapp.net", - "media.fesb4-1.fna.whatsapp.net", "media.fjdo1-2.fna.whatsapp.net", "media.frba2-1.fna.whatsapp.net", "watls_no_dns", "600", "db_broadcast_me_jid_migration_step", - "new_wam_runtime_enabled", "group_update", "enhanced_block_enabled", "sync_wifi_threshold_kb", "mms_download_nc_cat", "bloks_minification_enabled", - "ephemeral_messages_enabled", "reject", "voip_outgoing_xml_signaling", "creator", "dl_bw", "payments_request_messages", "target_bitrate", "bloks_rendercore_enabled", - "media-hbe1-1.cdn.whatsapp.net", "media-hel3-1.cdn.whatsapp.net", "media-kut2-2.cdn.whatsapp.net", "media-lax3-1.cdn.whatsapp.net", "media-lax3-2.cdn.whatsapp.net", - "sticker_pack_deeplink_enabled", "hq_image_bw_threshold", "status_info", "voip", "dedupe_transcode_videos", "grp_uii_cleanup", "linked_device_max_count", - "media.flim1-1.fna.whatsapp.net", "media.fsaw2-1.fna.whatsapp.net", "reconnecting_after_call_active_threshold_ms", "1140", "catalog_pdp_new_design", - "media.fbtz1-10.fna.whatsapp.net", "media.fsaw1-15.fna.whatsapp.net", "0b", "consumer_rc_provider", "mms_async_fast_forward_ttl", "jb_eff_size_fix", - "voip_incoming_xml_signaling", "media_provider_share_by_uuid", "suspicious_links", "dedupe_transcode_images", "green_alert_modal_start", "media-cgk1-1.cdn.whatsapp.net", - "media-lga3-1.cdn.whatsapp.net", "template_doc_mime_types", "important_messages", "user_add", "vcard_max_size_kb", "media.fada2-1.fna.whatsapp.net", - "media.fbog2-5.fna.whatsapp.net", "media.fbtz1-3.fna.whatsapp.net", "media.fcgk3-1.fna.whatsapp.net", "media.fcgk7-1.fna.whatsapp.net", "media.flim1-3.fna.whatsapp.net", - "media.fscl9-1.fna.whatsapp.net", "ctwa_context_enterprise_enabled", "media.fsaw1-13.fna.whatsapp.net", "media.fuio11-2.fna.whatsapp.net", "status_collapse_muted", - "db_migration_level_force", "recent_stickers_web_sync", "bloks_session_state", "bloks_shops_enabled", "green_alert_setting_deep_links_enabled", "restrict_groups", - "battery", "green_alert_block_start", "refresh", "ctwa_context_enabled", "md_messaging_enabled", "status_image_quality", "md_blocklist_v2_server", - "media-del1-1.cdn.whatsapp.net", "13", "userrate", "a_v_id", "cond_rtt_ema_alpha", "invalid" -}; - -static char *dict2[] = { - "media.fada1-1.fna.whatsapp.net", "media.fadb3-2.fna.whatsapp.net", "media.fbhz2-1.fna.whatsapp.net", "media.fcor2-1.fna.whatsapp.net", "media.fjed4-2.fna.whatsapp.net", - "media.flhe4-1.fna.whatsapp.net", "media.frak1-2.fna.whatsapp.net", "media.fsub6-3.fna.whatsapp.net", "media.fsub6-7.fna.whatsapp.net", "media.fvvi1-1.fna.whatsapp.net", - "search_v5_eligible", "wam_real_time_enabled", "report_disk_event", "max_tx_rott_based_bitrate", "product", "media.fjdo10-2.fna.whatsapp.net", "video_frame_crc_sample_interval", - "media_max_autodownload", "15", "h.264", "wam_privatestats_buffer_count", "md_phash_v2_enabled", "account_transfer_enabled", "business_product_catalog", - "enable_non_dyn_codec_param_fix", "is_user_under_epd_jurisdiction", "media.fbog2-4.fna.whatsapp.net", "media.fbtz1-2.fna.whatsapp.net", "media.fcfc1-1.fna.whatsapp.net", - "media.fjed4-5.fna.whatsapp.net", "media.flhe4-2.fna.whatsapp.net", "media.flim1-2.fna.whatsapp.net", "media.flos5-1.fna.whatsapp.net", "android_key_store_auth_ver", - "010", "anr_process_monitor", "delete_old_auth_key", "media.fcor10-3.fna.whatsapp.net", "storage_usage_enabled", "android_camera2_support_level", "dirty", - "consumer_content_provider", "status_video_max_duration", "0c", "bloks_cache_enabled", "media.fadb2-2.fna.whatsapp.net", "media.fbko1-1.fna.whatsapp.net", - "media.fbtz1-9.fna.whatsapp.net", "media.fcgk4-4.fna.whatsapp.net", "media.fesb4-2.fna.whatsapp.net", "media.fevn1-2.fna.whatsapp.net", "media.fist2-4.fna.whatsapp.net", - "media.fjdo1-1.fna.whatsapp.net", "media.fruh4-6.fna.whatsapp.net", "media.fsrg5-1.fna.whatsapp.net", "media.fsub6-6.fna.whatsapp.net", "minfpp", "5000", "locales", - "video_max_bitrate", "use_new_auth_key", "bloks_http_enabled", "heartbeat_interval", "media.fbog11-1.fna.whatsapp.net", "ephemeral_group_query_ts", "fec_nack", - "search_in_storage_usage", "c", "media-amt2-1.cdn.whatsapp.net", "linked_devices_ui_enabled", "14", "async_data_load_on_startup", "voip_incoming_xml_ack", "16", - "db_migration_step", "init_bwe", "max_participants", "wam_buffer_count", "media.fada2-2.fna.whatsapp.net", "media.fadb3-1.fna.whatsapp.net", "media.fcor2-2.fna.whatsapp.net", - "media.fdiy1-2.fna.whatsapp.net", "media.frba3-2.fna.whatsapp.net", "media.fsaw2-3.fna.whatsapp.net", "1280", "status_grid_enabled", "w:biz", "product_catalog_deeplink", - "media.fgye10-2.fna.whatsapp.net", "media.fuio11-1.fna.whatsapp.net", "optimistic_upload", "work_manager_init", "lc", "catalog_message", "cond_net_medium", - "enable_periodical_aud_rr_processing", "cond_range_ema_rtt", "media-tir2-1.cdn.whatsapp.net", "frame_ms", "group_invite_sending", "payments_web_enabled", - "wallpapers_v2", "0d", "browser", "hq_image_max_edge", "image_edit_zoom", "linked_devices_re_auth_enabled", "media.faly3-2.fna.whatsapp.net", - "media.fdoh5-3.fna.whatsapp.net", "media.fesb3-1.fna.whatsapp.net", "media.fknu1-1.fna.whatsapp.net", "media.fmex3-1.fna.whatsapp.net", "media.fruh4-3.fna.whatsapp.net", - "255", "web_upgrade_to_md_modal", "audio_piggyback_timeout_msec", "enable_audio_oob_fec_feature", "from_ip", "image_max_edge", "message_qr_enabled", "powersave", - "receipt_pre_acking", "video_max_edge", "full", "011", "012", "enable_audio_oob_fec_for_sender", "md_voip_enabled", "enable_privatestats", "max_fec_ratio", - "payments_cs_faq_url", "media-xsp1-3.cdn.whatsapp.net", "hq_image_quality", "media.fasr1-2.fna.whatsapp.net", "media.fbog3-1.fna.whatsapp.net", - "media.ffjr1-6.fna.whatsapp.net", "media.fist2-3.fna.whatsapp.net", "media.flim4-3.fna.whatsapp.net", "media.fpbc2-4.fna.whatsapp.net", "media.fpku1-1.fna.whatsapp.net", - "media.frba1-1.fna.whatsapp.net", "media.fudi1-1.fna.whatsapp.net", "media.fvvi1-2.fna.whatsapp.net", "gcm_fg_service", "enable_dec_ltr_size_check", "clear", "lg", - "media.fgru11-1.fna.whatsapp.net", "18", "media-lga3-2.cdn.whatsapp.net", "pkey", "0e", "max_subject", "cond_range_lterm_rtt", "announcement_groups", "biz_profile_options", - "s_t", "media.fabv2-1.fna.whatsapp.net", "media.fcai3-1.fna.whatsapp.net", "media.fcgh1-1.fna.whatsapp.net", "media.fctg1-4.fna.whatsapp.net", "media.fdiy1-1.fna.whatsapp.net", - "media.fisb4-1.fna.whatsapp.net", "media.fpku1-2.fna.whatsapp.net", "media.fros9-1.fna.whatsapp.net", "status_v3_text", "usync_sidelist", "17", "announcement", "...", - "md_group_notification", "0f", "animated_pack_in_store", "013", "America/Mexico_City", "1260", "media-ams4-1.cdn.whatsapp.net", "media-cgk1-2.cdn.whatsapp.net", - "media-cpt1-1.cdn.whatsapp.net", "media-maa2-1.cdn.whatsapp.net", "media.fgye10-1.fna.whatsapp.net", "e", "catalog_cart", "hfm_string_changes", "init_bitrate", - "packless_hsm", "group_info", "America/Belem", "50", "960", "cond_range_bwe", "decode", "encode", "media.fada1-8.fna.whatsapp.net", "media.fadb1-2.fna.whatsapp.net", - "media.fasu6-1.fna.whatsapp.net", "media.fbog4-1.fna.whatsapp.net", "media.fcgk9-2.fna.whatsapp.net", "media.fdoh5-2.fna.whatsapp.net", "media.ffjr1-2.fna.whatsapp.net", - "media.fgua1-1.fna.whatsapp.net", "media.fgye1-1.fna.whatsapp.net", "media.fist1-4.fna.whatsapp.net", "media.fpbc2-2.fna.whatsapp.net", "media.fres2-1.fna.whatsapp.net", - "media.fsdq1-2.fna.whatsapp.net", "media.fsub6-5.fna.whatsapp.net", "profilo_enabled", "template_hsm", "use_disorder_prefetching_timer", "video_codec_priority", - "vpx_max_qp", "ptt_reduce_recording_delay", "25", "iphone", "Windows", "s_o", "Africa/Lagos", "abt", "media-kut2-1.cdn.whatsapp.net", "media-mba1-1.cdn.whatsapp.net", - "media-mxp1-2.cdn.whatsapp.net", "md_blocklist_v2", "url_text", "enable_short_offset", "group_join_permissions", "enable_audio_piggyback_feature", "image_quality", - "media.fcgk7-2.fna.whatsapp.net", "media.fcgk8-2.fna.whatsapp.net", "media.fclo7-1.fna.whatsapp.net", "media.fcmn1-1.fna.whatsapp.net", "media.feoh1-1.fna.whatsapp.net", - "media.fgyd4-3.fna.whatsapp.net", "media.fjed4-4.fna.whatsapp.net", "media.flim1-4.fna.whatsapp.net", "media.flim2-4.fna.whatsapp.net", "media.fplu6-1.fna.whatsapp.net", - "media.frak1-1.fna.whatsapp.net", "media.fsdq1-1.fna.whatsapp.net", "to_ip", "015", "vp8", "19", "21", "1320", "auth_key_ver", "message_processing_dedup", "server-error", - "wap4_enabled", "420", "014", "cond_range_rtt", "ptt_fast_lock_enabled", "media-ort2-1.cdn.whatsapp.net", "fwd_ui_start_ts" -}; - -static char *dict3[] = { - "contact_blacklist", "Asia/Jakarta", "media.fepa10-1.fna.whatsapp.net", "media.fmex10-3.fna.whatsapp.net", "disorder_prefetching_start_when_empty", "America/Bogota", - "use_local_probing_rx_bitrate", "America/Argentina/Buenos_Aires", "cross_post", "media.fabb1-1.fna.whatsapp.net", "media.fbog4-2.fna.whatsapp.net", "media.fcgk9-1.fna.whatsapp.net", - "media.fcmn2-1.fna.whatsapp.net", "media.fdel3-1.fna.whatsapp.net", "media.ffjr1-1.fna.whatsapp.net", "media.fgdl5-1.fna.whatsapp.net", "media.flpb1-2.fna.whatsapp.net", - "media.fmex2-1.fna.whatsapp.net", "media.frba2-2.fna.whatsapp.net", "media.fros2-2.fna.whatsapp.net", "media.fruh2-1.fna.whatsapp.net", "media.fybz2-2.fna.whatsapp.net", - "options", "20", "a", "017", "018", "mute_always", "user_notice", "Asia/Kolkata", "gif_provider", "locked", "media-gua1-1.cdn.whatsapp.net", "piggyback_exclude_force_flush", - "24", "media.frec39-1.fna.whatsapp.net", "user_remove", "file_max_size", "cond_packet_loss_pct_ema_alpha", "media.facc1-1.fna.whatsapp.net", "media.fadb2-1.fna.whatsapp.net", - "media.faly3-1.fna.whatsapp.net", "media.fbdo6-2.fna.whatsapp.net", "media.fcmn2-2.fna.whatsapp.net", "media.fctg1-3.fna.whatsapp.net", "media.ffez1-2.fna.whatsapp.net", - "media.fist1-3.fna.whatsapp.net", "media.fist2-2.fna.whatsapp.net", "media.flim2-2.fna.whatsapp.net", "media.fmct2-3.fna.whatsapp.net", "media.fpei3-1.fna.whatsapp.net", - "media.frba3-1.fna.whatsapp.net", "media.fsdu8-2.fna.whatsapp.net", "media.fstu2-1.fna.whatsapp.net", "media_type", "receipt_agg", "016", "enable_pli_for_crc_mismatch", - "live", "enc_rekey", "frskmsg", "d", "media.fdel11-2.fna.whatsapp.net", "proto", "2250", "audio_piggyback_enable_cache", "skip_nack_if_ltrp_sent", "mark_dtx_jb_frames", - "web_service_delay", "7282", "catalog_send_all", "outgoing", "360", "30", "LIMITED", "019", "audio_picker", "bpv2_phase", "media.fada1-7.fna.whatsapp.net", - "media.faep7-1.fna.whatsapp.net", "media.fbko1-2.fna.whatsapp.net", "media.fbni1-2.fna.whatsapp.net", "media.fbtz1-1.fna.whatsapp.net", "media.fbtz1-8.fna.whatsapp.net", - "media.fcjs3-1.fna.whatsapp.net", "media.fesb3-2.fna.whatsapp.net", "media.fgdl5-4.fna.whatsapp.net", "media.fist2-1.fna.whatsapp.net", "media.flhe2-2.fna.whatsapp.net", - "media.flim2-1.fna.whatsapp.net", "media.fmex1-1.fna.whatsapp.net", "media.fpat3-2.fna.whatsapp.net", "media.fpat3-3.fna.whatsapp.net", "media.fros2-1.fna.whatsapp.net", - "media.fsdu8-1.fna.whatsapp.net", "media.fsub3-2.fna.whatsapp.net", "payments_chat_plugin", "cond_congestion_no_rtcp_thr", "green_alert", "not-a-biz", "..", - "shops_pdp_urls_config", "source", "media-dus1-1.cdn.whatsapp.net", "mute_video", "01b", "currency", "max_keys", "resume_check", "contact_array", "qr_scanning", "23", - "b", "media.fbfh15-1.fna.whatsapp.net", "media.flim22-1.fna.whatsapp.net", "media.fsdu11-1.fna.whatsapp.net", "media.fsdu15-1.fna.whatsapp.net", "Chrome", "fts_version", - "60", "media.fada1-6.fna.whatsapp.net", "media.faep4-2.fna.whatsapp.net", "media.fbaq5-1.fna.whatsapp.net", "media.fbni1-1.fna.whatsapp.net", "media.fcai3-2.fna.whatsapp.net", - "media.fdel3-2.fna.whatsapp.net", "media.fdmm3-2.fna.whatsapp.net", "media.fhex3-1.fna.whatsapp.net", "media.fisb4-2.fna.whatsapp.net", "media.fkhi5-2.fna.whatsapp.net", - "media.flos2-1.fna.whatsapp.net", "media.fmct2-1.fna.whatsapp.net", "media.fntr7-1.fna.whatsapp.net", "media.frak3-1.fna.whatsapp.net", "media.fruh5-2.fna.whatsapp.net", - "media.fsub6-1.fna.whatsapp.net", "media.fuab1-2.fna.whatsapp.net", "media.fuio1-1.fna.whatsapp.net", "media.fver1-1.fna.whatsapp.net", "media.fymy1-1.fna.whatsapp.net", - "product_catalog", "1380", "audio_oob_fec_max_pkts", "22", "254", "media-ort2-2.cdn.whatsapp.net", "media-sjc3-1.cdn.whatsapp.net", "1600", "01a", "01c", "405", - "key_frame_interval", "body", "media.fcgh20-1.fna.whatsapp.net", "media.fesb10-2.fna.whatsapp.net", "125", "2000", "media.fbsb1-1.fna.whatsapp.net", - "media.fcmn3-2.fna.whatsapp.net", "media.fcpq1-1.fna.whatsapp.net", "media.fdel1-2.fna.whatsapp.net", "media.ffor2-1.fna.whatsapp.net", "media.fgdl1-4.fna.whatsapp.net", - "media.fhex2-1.fna.whatsapp.net", "media.fist1-2.fna.whatsapp.net", "media.fjed5-2.fna.whatsapp.net", "media.flim6-4.fna.whatsapp.net", "media.flos2-2.fna.whatsapp.net", - "media.fntr6-2.fna.whatsapp.net", "media.fpku3-2.fna.whatsapp.net", "media.fros8-1.fna.whatsapp.net", "media.fymy1-2.fna.whatsapp.net", "ul_bw", "ltrp_qp_offset", - "request", "nack", "dtx_delay_state_reset", "timeoffline", "28", "01f", "32", "enable_ltr_pool", "wa_msys_crypto", "01d", "58", "dtx_freeze_hg_update", - "nack_if_rpsi_throttled", "253", "840", "media.famd15-1.fna.whatsapp.net", "media.fbog17-2.fna.whatsapp.net", "media.fcai19-2.fna.whatsapp.net", "media.fcai21-4.fna.whatsapp.net", - "media.fesb10-4.fna.whatsapp.net", "media.fesb10-5.fna.whatsapp.net", "media.fmaa12-1.fna.whatsapp.net", "media.fmex11-3.fna.whatsapp.net", "media.fpoa33-1.fna.whatsapp.net", - "1050", "021", "clean", "cond_range_ema_packet_loss_pct", "media.fadb6-5.fna.whatsapp.net", "media.faqp4-1.fna.whatsapp.net", "media.fbaq3-1.fna.whatsapp.net", - "media.fbel2-1.fna.whatsapp.net", "media.fblr4-2.fna.whatsapp.net", "media.fclo8-1.fna.whatsapp.net", "media.fcoo1-2.fna.whatsapp.net", "media.ffjr1-4.fna.whatsapp.net", - "media.ffor9-1.fna.whatsapp.net", "media.fisb3-1.fna.whatsapp.net", "media.fkhi2-2.fna.whatsapp.net", "media.fkhi4-1.fna.whatsapp.net", "media.fpbc1-2.fna.whatsapp.net", - "media.fruh2-2.fna.whatsapp.net", "media.fruh5-1.fna.whatsapp.net", "media.fsub3-1.fna.whatsapp.net", "payments_transaction_limit", "252", "27", "29", "tintagel", "01e", - "237", "780", "callee_updated_payload", "020", "257", "price", "025", "239", "payments_cs_phone_number", "mediaretry", "w:auth:backup:token", "Glass.caf", "max_bitrate", - "240", "251", "660", "media.fbog16-1.fna.whatsapp.net", "media.fcgh21-1.fna.whatsapp.net", "media.fkul19-2.fna.whatsapp.net", "media.flim21-2.fna.whatsapp.net", - "media.fmex10-4.fna.whatsapp.net", "64", "33", "34", "35", "interruption", "media.fabv3-1.fna.whatsapp.net", "media.fadb6-1.fna.whatsapp.net", "media.fagr1-1.fna.whatsapp.net", - "media.famd1-1.fna.whatsapp.net", "media.famm6-1.fna.whatsapp.net", "media.faqp2-3.fna.whatsapp.net" -}; +/* + +WhatsApp plugin for Miranda NG +Copyright 2019-23 George Hazan + +*/ + +static char *SingleByteTokens[] = { + "", "xmlstreamstart", "xmlstreamend", "s.whatsapp.net", "type", "participant", "from", "receipt", "id", "broadcast", "status", "message", "notification", "notify", "to", "jid", "user", + "class", "offline", "g.us", "result", "mediatype", "enc", "skmsg", "off_cnt", "xmlns", "presence", "participants", "ack", "t", "iq", "device_hash", "read", "value", "media", "picture", + "chatstate", "unavailable", "text", "urn:xmpp:whatsapp:push", "devices", "verified_name", "contact", "composing", "edge_routing", "routing_info", "item", "image", "verified_level", + "get", "fallback_hostname", "2", "media_conn", "1", "v", "handshake", "fallback_class", "count", "config", "offline_preview", "download_buckets", "w:profile:picture", "set", + "creation", "location", "fallback_ip4", "msg", "urn:xmpp:ping", "fallback_ip6", "call-creator", "relaylatency", "success", "subscribe", "video", "business_hours_config", + "platform", "hostname", "version", "unknown", "0", "ping", "hash", "edit", "subject", "max_buckets", "download", "delivery", "props", "sticker", "name", "last", "contacts", + "business", "primary", "preview", "w:p", "pkmsg", "call-id", "retry", "prop", "call", "auth_ttl", "available", "relay_id", "last_id", "day_of_week", "w", "host", "seen", + "bits", "list", "atn", "upload", "is_new", "w:stats", "key", "paused", "specific_hours", "multicast", "stream:error", "mmg.whatsapp.net", "code", "deny", "played", "profile", + "fna", "device-list", "close_time", "latency", "gcm", "pop", "audio", "26", "w:web", "open_time", "error", "auth", "ip4", "update", "profile_options", "config_value", "category", + "catalog_not_created", "00", "config_code", "mode", "catalog_status", "ip6", "blocklist", "registration", "7", "web", "fail", "w:m", "cart_enabled", "ttl", "gif", "300", + "device_orientation", "identity", "query", "401", "media-gig2-1.cdn.whatsapp.net", "in", "3", "te2", "add", "fallback", "categories", "ptt", "encrypt", "notice", + "thumbnail-document", "item-not-found", "12", "thumbnail-image", "stage", "thumbnail-link", "usync", "out", "thumbnail-video", "8", "01", "context", "sidelist", + "thumbnail-gif", "terminate", "not-authorized", "orientation", "dhash", "capability", "side_list", "md-app-state", "description", "serial", "readreceipts", "te", + "business_hours", "md-msg-hist", "tag", "attribute_padding", "document", "open_24h", "delete", "expiration", "active", "prev_v_id", "true", "passive", "index", "4", + "conflict", "remove", "w:gp2", "config_expo_key", "screen_height", "replaced", "02", "screen_width", "uploadfieldstat", "2:47DEQpj8", "media-bog1-1.cdn.whatsapp.net", + "encopt", "url", "catalog_exists", "keygen", "rate", "offer", "opus", "media-mia3-1.cdn.whatsapp.net", "privacy", "media-mia3-2.cdn.whatsapp.net", "signature", + "preaccept", "token_id", "media-eze1-1.cdn.whatsapp.net" +}; + +static char *dict0[] = { + "media-for1-1.cdn.whatsapp.net", "relay", "media-gru2-2.cdn.whatsapp.net", "uncompressed", "medium", "voip_settings", "device", "reason", + "media-lim1-1.cdn.whatsapp.net", "media-qro1-2.cdn.whatsapp.net", "media-gru1-2.cdn.whatsapp.net", "action", "features", "media-gru2-1.cdn.whatsapp.net", + "media-gru1-1.cdn.whatsapp.net", "media-otp1-1.cdn.whatsapp.net", "kyc-id", "priority", "phash", "mute", "token", "100", "media-qro1-1.cdn.whatsapp.net", + "none", "media-mrs2-2.cdn.whatsapp.net", "sign_credential", "03", "media-mrs2-1.cdn.whatsapp.net", "protocol", "timezone", "transport", "eph_setting", "1080", + "original_dimensions", "media-frx5-1.cdn.whatsapp.net", "background", "disable", "original_image_url", "5", "transaction-id", "direct_path", "103", "appointment_only", + "request_image_url", "peer_pid", "address", "105", "104", "102", "media-cdt1-1.cdn.whatsapp.net", "101", "109", "110", "106", "background_location", "v_id", "sync", + "status-old", "111", "107", "ppic", "media-scl2-1.cdn.whatsapp.net", "business_profile", "108", "invite", "04", "audio_duration", "media-mct1-1.cdn.whatsapp.net", + "media-cdg2-1.cdn.whatsapp.net", "media-los2-1.cdn.whatsapp.net", "invis", "net", "voip_payload_type", "status-revoke-delay", "404", "state", "use_correct_order_for_hmac_sha1", + "ver", "media-mad1-1.cdn.whatsapp.net", "order", "540", "skey", "blinded_credential", "android", "contact_remove", "enable_downlink_relay_latency_only", "duration", + "enable_vid_one_way_codec_nego", "6", "media-sof1-1.cdn.whatsapp.net", "accept", "all", "signed_credential", "media-atl3-1.cdn.whatsapp.net", "media-lhr8-1.cdn.whatsapp.net", + "website", "05", "latitude", "media-dfw5-1.cdn.whatsapp.net", "forbidden", "enable_audio_piggyback_network_mtu_fix", "media-dfw5-2.cdn.whatsapp.net", "note.m4r", + "media-atl3-2.cdn.whatsapp.net", "jb_nack_discard_count_fix", "longitude", "Opening.m4r", "media-arn2-1.cdn.whatsapp.net", "email", "timestamp", "admin", + "media-pmo1-1.cdn.whatsapp.net", "America/Sao_Paulo", "contact_add", "media-sin6-1.cdn.whatsapp.net", "interactive", "8000", "acs_public_key", + "sigquit_anr_detector_release_rollover_percent", "media.fmed1-2.fna.whatsapp.net", "groupadd", "enabled_for_video_upgrade", "latency_update_threshold", + "media-frt3-2.cdn.whatsapp.net", "calls_row_constraint_layout", "media.fgbb2-1.fna.whatsapp.net", "mms4_media_retry_notification_encryption_enabled", "timeout", + "media-sin6-3.cdn.whatsapp.net", "audio_nack_jitter_multiplier", "jb_discard_count_adjust_pct_rc", "audio_reserve_bps", "delta", "account_sync", "default", + "media.fjed4-6.fna.whatsapp.net", "06", "lock_video_orientation", "media-frt3-1.cdn.whatsapp.net", "w:g2", "media-sin6-2.cdn.whatsapp.net", "audio_nack_algo_mask", + "media.fgbb2-2.fna.whatsapp.net", "media.fmed1-1.fna.whatsapp.net", "cond_range_target_bitrate", "mms4_server_error_receipt_encryption_enabled", "vid_rc_dyn", "fri", + "cart_v1_1_order_message_changes_enabled", "reg_push", "jb_hist_deposit_value", "privatestats", "media.fist7-2.fna.whatsapp.net", "thu", "jb_discard_count_adjust_pct", + "mon", "group_call_video_maximization_enabled", "mms_cat_v1_forward_hot_override_enabled", "audio_nack_new_rtt", "media.fsub2-3.fna.whatsapp.net", + "media_upload_aggressive_retry_exponential_backoff_enabled", "tue", "wed", "media.fruh4-2.fna.whatsapp.net", "audio_nack_max_seq_req", "max_rtp_audio_packet_resends", + "jb_hist_max_cdf_value", "07", "audio_nack_max_jb_delay", "mms_forward_partially_downloaded_video", "media-lcy1-1.cdn.whatsapp.net", "resume", "jb_inband_fec_aware", + "new_commerce_entry_point_enabled", "480", "payments_upi_generate_qr_amount_limit", "sigquit_anr_detector_rollover_percent", "media.fsdu2-1.fna.whatsapp.net", "fbns", + "aud_pkt_reorder_pct", "dec", "stop_probing_before_accept_send", "media_upload_max_aggressive_retries", "edit_business_profile_new_mode_enabled", + "media.fhex4-1.fna.whatsapp.net", "media.fjed4-3.fna.whatsapp.net", "sigquit_anr_detector_64bit_rollover_percent", "cond_range_ema_jb_last_delay", + "watls_enable_early_data_http_get", "media.fsdu2-2.fna.whatsapp.net", "message_qr_disambiguation_enabled", "media-mxp1-1.cdn.whatsapp.net", "sat", "vertical", + "media.fruh4-5.fna.whatsapp.net", "200", "media-sof1-2.cdn.whatsapp.net", "-1", "height", "product_catalog_hide_show_items_enabled", "deep_copy_frm_last", + "tsoffline", "vp8/h.264", "media.fgye5-3.fna.whatsapp.net", "media.ftuc1-2.fna.whatsapp.net", "smb_upsell_chat_banner_enabled", "canonical", "08", "9", ".", + "media.fgyd4-4.fna.whatsapp.net", "media.fsti4-1.fna.whatsapp.net", "mms_vcache_aggregation_enabled", "mms_hot_content_timespan_in_seconds", "nse_ver", "rte", + "third_party_sticker_web_sync", "cond_range_target_total_bitrate", "media_upload_aggressive_retry_enabled", "instrument_spam_report_enabled", "disable_reconnect_tone", + "move_media_folder_from_sister_app", "one_tap_calling_in_group_chat_size", "10", "storage_mgmt_banner_threshold_mb", "enable_backup_passive_mode", "sharechat_inline_player_enabled", + "media.fcnq2-1.fna.whatsapp.net", "media.fhex4-2.fna.whatsapp.net", "media.fist6-3.fna.whatsapp.net", "ephemeral_drop_column_stage", "reconnecting_after_network_change_threshold_ms", + "media-lhr8-2.cdn.whatsapp.net", "cond_jb_last_delay_ema_alpha", "entry_point_block_logging_enabled", "critical_event_upload_log_config", "respect_initial_bitrate_estimate", + "smaller_image_thumbs_status_enabled", "media.fbtz1-4.fna.whatsapp.net", "media.fjed4-1.fna.whatsapp.net", "width", "720", "enable_frame_dropper", "enable_one_side_mode", + "urn:xmpp:whatsapp:dirty", "new_sticker_animation_behavior_v2", "media.flim3-2.fna.whatsapp.net", "media.fuio6-2.fna.whatsapp.net", "skip_forced_signaling", "dleq_proof", + "status_video_max_bitrate", "lazy_send_probing_req", "enhanced_storage_management", "android_privatestats_endpoint_dit_enabled", "media.fscl13-2.fna.whatsapp.net", "video_duration" +}; + +static char *dict1[] = { + "group_call_discoverability_enabled", "media.faep9-2.fna.whatsapp.net", "msgr", "bloks_loggedin_access_app_id", "db_status_migration_step", "watls_prefer_ip6", + "jabber:iq:privacy", "68", "media.fsaw1-11.fna.whatsapp.net", "mms4_media_conn_persist_enabled", "animated_stickers_thread_clean_up", "media.fcgk3-2.fna.whatsapp.net", + "media.fcgk4-6.fna.whatsapp.net", "media.fgye5-2.fna.whatsapp.net", "media.flpb1-1.fna.whatsapp.net", "media.fsub2-1.fna.whatsapp.net", "media.fuio6-3.fna.whatsapp.net", + "not-allowed", "partial_pjpeg_bw_threshold", "cap_estimated_bitrate", "mms_chatd_resume_check_over_thrift", "smb_upsell_business_profile_enabled", "product_catalog_webclient", + "groups", "sigquit_anr_detector_release_updated_rollout", "syncd_key_rotation_enabled", "media.fdmm2-1.fna.whatsapp.net", "media-hou1-1.cdn.whatsapp.net", + "remove_old_chat_notifications", "smb_biztools_deeplink_enabled", "use_downloadable_filters_int", "group_qr_codes_enabled", "max_receipt_processing_time", + "optimistic_image_processing_enabled", "smaller_video_thumbs_status_enabled", "watls_early_data", "reconnecting_before_relay_failover_threshold_ms", "cond_range_packet_loss_pct", + "groups_privacy_blacklist", "status-revoke-drop", "stickers_animated_thumbnail_download", "dedupe_transcode_shared_images", "dedupe_transcode_shared_videos", + "media.fcnq2-2.fna.whatsapp.net", "media.fgyd4-1.fna.whatsapp.net", "media.fist7-1.fna.whatsapp.net", "media.flim3-3.fna.whatsapp.net", "add_contact_by_qr_enabled", + "https://faq.whatsapp.com/payments", "multicast_limit_global", "sticker_notification_preview", "smb_better_catalog_list_adapters_enabled", "bloks_use_minscript_android", + "pen_smoothing_enabled", "media.fcgk4-5.fna.whatsapp.net", "media.fevn1-3.fna.whatsapp.net", "media.fpoj7-1.fna.whatsapp.net", "media-arn2-2.cdn.whatsapp.net", + "reconnecting_before_network_change_threshold_ms", "android_media_use_fresco_for_gifs", "cond_in_congestion", "status_image_max_edge", "sticker_search_enabled", + "starred_stickers_web_sync", "db_blank_me_jid_migration_step", "media.fist6-2.fna.whatsapp.net", "media.ftuc1-1.fna.whatsapp.net", "09", "anr_fast_logs_upload_rollout", + "camera_core_integration_enabled", "11", "third_party_sticker_caching", "thread_dump_contact_support", "wam_privatestats_enabled", "vcard_as_document_size_kb", "maxfpp", + "fbip", "ephemeral_allow_group_members", "media-bom1-2.cdn.whatsapp.net", "media-xsp1-1.cdn.whatsapp.net", "disable_prewarm", "frequently_forwarded_max", + "media.fbtz1-5.fna.whatsapp.net", "media.fevn7-1.fna.whatsapp.net", "media.fgyd4-2.fna.whatsapp.net", "sticker_tray_animation_fully_visible_items", + "green_alert_banner_duration", "reconnecting_after_p2p_failover_threshold_ms", "connected", "share_biz_vcard_enabled", "stickers_animation", "0a", "1200", "WhatsApp", + "group_description_length", "p_v_id", "payments_upi_intent_transaction_limit", "frequently_forwarded_messages", "media-xsp1-2.cdn.whatsapp.net", + "media.faep8-1.fna.whatsapp.net", "media.faep8-2.fna.whatsapp.net", "media.faep9-1.fna.whatsapp.net", "media.fdmm2-2.fna.whatsapp.net", "media.fgzt3-1.fna.whatsapp.net", + "media.flim4-2.fna.whatsapp.net", "media.frao1-1.fna.whatsapp.net", "media.fscl9-2.fna.whatsapp.net", "media.fsub2-2.fna.whatsapp.net", "superadmin", + "media.fbog10-1.fna.whatsapp.net", "media.fcgh28-1.fna.whatsapp.net", "media.fjdo10-1.fna.whatsapp.net", "third_party_animated_sticker_import", "delay_fec", + "attachment_picker_refresh", "android_linked_devices_re_auth_enabled", "rc_dyn", "green_alert_block_jitter", "add_contact_logging_enabled", "biz_message_logging_enabled", + "conversation_media_preview_v2", "media-jnb1-1.cdn.whatsapp.net", "ab_key", "media.fcgk4-2.fna.whatsapp.net", "media.fevn1-1.fna.whatsapp.net", "media.fist6-1.fna.whatsapp.net", + "media.fruh4-4.fna.whatsapp.net", "media.fsti4-2.fna.whatsapp.net", "mms_vcard_autodownload_size_kb", "watls_enabled", "notif_ch_override_off", "media.fsaw1-14.fna.whatsapp.net", + "media.fscl13-1.fna.whatsapp.net", "db_group_participant_migration_step", "1020", "cond_range_sterm_rtt", "invites_logging_enabled", "triggered_block_enabled", + "group_call_max_participants", "media-iad3-1.cdn.whatsapp.net", "product_catalog_open_deeplink", "shops_required_tos_version", "image_max_kbytes", + "cond_low_quality_vid_mode", "db_receipt_migration_step", "jb_early_prob_hist_shrink", "media.fdmm2-3.fna.whatsapp.net", "media.fdmm2-4.fna.whatsapp.net", + "media.fruh4-1.fna.whatsapp.net", "media.fsaw2-2.fna.whatsapp.net", "remove_geolocation_videos", "new_animation_behavior", "fieldstats_beacon_chance", "403", + "authkey_reset_on_ban", "continuous_ptt_playback", "reconnecting_after_relay_failover_threshold_ms", "false", "group", "sun", "conversation_swipe_to_reply", + "ephemeral_messages_setting", "smaller_video_thumbs_enabled", "md_device_sync_enabled", "bloks_shops_pdp_url_regex", "lasso_integration_enabled", + "media-bom1-1.cdn.whatsapp.net", "new_backup_format_enabled", "256", "media.faep6-1.fna.whatsapp.net", "media.fasr1-1.fna.whatsapp.net", "media.fbtz1-7.fna.whatsapp.net", + "media.fesb4-1.fna.whatsapp.net", "media.fjdo1-2.fna.whatsapp.net", "media.frba2-1.fna.whatsapp.net", "watls_no_dns", "600", "db_broadcast_me_jid_migration_step", + "new_wam_runtime_enabled", "group_update", "enhanced_block_enabled", "sync_wifi_threshold_kb", "mms_download_nc_cat", "bloks_minification_enabled", + "ephemeral_messages_enabled", "reject", "voip_outgoing_xml_signaling", "creator", "dl_bw", "payments_request_messages", "target_bitrate", "bloks_rendercore_enabled", + "media-hbe1-1.cdn.whatsapp.net", "media-hel3-1.cdn.whatsapp.net", "media-kut2-2.cdn.whatsapp.net", "media-lax3-1.cdn.whatsapp.net", "media-lax3-2.cdn.whatsapp.net", + "sticker_pack_deeplink_enabled", "hq_image_bw_threshold", "status_info", "voip", "dedupe_transcode_videos", "grp_uii_cleanup", "linked_device_max_count", + "media.flim1-1.fna.whatsapp.net", "media.fsaw2-1.fna.whatsapp.net", "reconnecting_after_call_active_threshold_ms", "1140", "catalog_pdp_new_design", + "media.fbtz1-10.fna.whatsapp.net", "media.fsaw1-15.fna.whatsapp.net", "0b", "consumer_rc_provider", "mms_async_fast_forward_ttl", "jb_eff_size_fix", + "voip_incoming_xml_signaling", "media_provider_share_by_uuid", "suspicious_links", "dedupe_transcode_images", "green_alert_modal_start", "media-cgk1-1.cdn.whatsapp.net", + "media-lga3-1.cdn.whatsapp.net", "template_doc_mime_types", "important_messages", "user_add", "vcard_max_size_kb", "media.fada2-1.fna.whatsapp.net", + "media.fbog2-5.fna.whatsapp.net", "media.fbtz1-3.fna.whatsapp.net", "media.fcgk3-1.fna.whatsapp.net", "media.fcgk7-1.fna.whatsapp.net", "media.flim1-3.fna.whatsapp.net", + "media.fscl9-1.fna.whatsapp.net", "ctwa_context_enterprise_enabled", "media.fsaw1-13.fna.whatsapp.net", "media.fuio11-2.fna.whatsapp.net", "status_collapse_muted", + "db_migration_level_force", "recent_stickers_web_sync", "bloks_session_state", "bloks_shops_enabled", "green_alert_setting_deep_links_enabled", "restrict_groups", + "battery", "green_alert_block_start", "refresh", "ctwa_context_enabled", "md_messaging_enabled", "status_image_quality", "md_blocklist_v2_server", + "media-del1-1.cdn.whatsapp.net", "13", "userrate", "a_v_id", "cond_rtt_ema_alpha", "invalid" +}; + +static char *dict2[] = { + "media.fada1-1.fna.whatsapp.net", "media.fadb3-2.fna.whatsapp.net", "media.fbhz2-1.fna.whatsapp.net", "media.fcor2-1.fna.whatsapp.net", "media.fjed4-2.fna.whatsapp.net", + "media.flhe4-1.fna.whatsapp.net", "media.frak1-2.fna.whatsapp.net", "media.fsub6-3.fna.whatsapp.net", "media.fsub6-7.fna.whatsapp.net", "media.fvvi1-1.fna.whatsapp.net", + "search_v5_eligible", "wam_real_time_enabled", "report_disk_event", "max_tx_rott_based_bitrate", "product", "media.fjdo10-2.fna.whatsapp.net", "video_frame_crc_sample_interval", + "media_max_autodownload", "15", "h.264", "wam_privatestats_buffer_count", "md_phash_v2_enabled", "account_transfer_enabled", "business_product_catalog", + "enable_non_dyn_codec_param_fix", "is_user_under_epd_jurisdiction", "media.fbog2-4.fna.whatsapp.net", "media.fbtz1-2.fna.whatsapp.net", "media.fcfc1-1.fna.whatsapp.net", + "media.fjed4-5.fna.whatsapp.net", "media.flhe4-2.fna.whatsapp.net", "media.flim1-2.fna.whatsapp.net", "media.flos5-1.fna.whatsapp.net", "android_key_store_auth_ver", + "010", "anr_process_monitor", "delete_old_auth_key", "media.fcor10-3.fna.whatsapp.net", "storage_usage_enabled", "android_camera2_support_level", "dirty", + "consumer_content_provider", "status_video_max_duration", "0c", "bloks_cache_enabled", "media.fadb2-2.fna.whatsapp.net", "media.fbko1-1.fna.whatsapp.net", + "media.fbtz1-9.fna.whatsapp.net", "media.fcgk4-4.fna.whatsapp.net", "media.fesb4-2.fna.whatsapp.net", "media.fevn1-2.fna.whatsapp.net", "media.fist2-4.fna.whatsapp.net", + "media.fjdo1-1.fna.whatsapp.net", "media.fruh4-6.fna.whatsapp.net", "media.fsrg5-1.fna.whatsapp.net", "media.fsub6-6.fna.whatsapp.net", "minfpp", "5000", "locales", + "video_max_bitrate", "use_new_auth_key", "bloks_http_enabled", "heartbeat_interval", "media.fbog11-1.fna.whatsapp.net", "ephemeral_group_query_ts", "fec_nack", + "search_in_storage_usage", "c", "media-amt2-1.cdn.whatsapp.net", "linked_devices_ui_enabled", "14", "async_data_load_on_startup", "voip_incoming_xml_ack", "16", + "db_migration_step", "init_bwe", "max_participants", "wam_buffer_count", "media.fada2-2.fna.whatsapp.net", "media.fadb3-1.fna.whatsapp.net", "media.fcor2-2.fna.whatsapp.net", + "media.fdiy1-2.fna.whatsapp.net", "media.frba3-2.fna.whatsapp.net", "media.fsaw2-3.fna.whatsapp.net", "1280", "status_grid_enabled", "w:biz", "product_catalog_deeplink", + "media.fgye10-2.fna.whatsapp.net", "media.fuio11-1.fna.whatsapp.net", "optimistic_upload", "work_manager_init", "lc", "catalog_message", "cond_net_medium", + "enable_periodical_aud_rr_processing", "cond_range_ema_rtt", "media-tir2-1.cdn.whatsapp.net", "frame_ms", "group_invite_sending", "payments_web_enabled", + "wallpapers_v2", "0d", "browser", "hq_image_max_edge", "image_edit_zoom", "linked_devices_re_auth_enabled", "media.faly3-2.fna.whatsapp.net", + "media.fdoh5-3.fna.whatsapp.net", "media.fesb3-1.fna.whatsapp.net", "media.fknu1-1.fna.whatsapp.net", "media.fmex3-1.fna.whatsapp.net", "media.fruh4-3.fna.whatsapp.net", + "255", "web_upgrade_to_md_modal", "audio_piggyback_timeout_msec", "enable_audio_oob_fec_feature", "from_ip", "image_max_edge", "message_qr_enabled", "powersave", + "receipt_pre_acking", "video_max_edge", "full", "011", "012", "enable_audio_oob_fec_for_sender", "md_voip_enabled", "enable_privatestats", "max_fec_ratio", + "payments_cs_faq_url", "media-xsp1-3.cdn.whatsapp.net", "hq_image_quality", "media.fasr1-2.fna.whatsapp.net", "media.fbog3-1.fna.whatsapp.net", + "media.ffjr1-6.fna.whatsapp.net", "media.fist2-3.fna.whatsapp.net", "media.flim4-3.fna.whatsapp.net", "media.fpbc2-4.fna.whatsapp.net", "media.fpku1-1.fna.whatsapp.net", + "media.frba1-1.fna.whatsapp.net", "media.fudi1-1.fna.whatsapp.net", "media.fvvi1-2.fna.whatsapp.net", "gcm_fg_service", "enable_dec_ltr_size_check", "clear", "lg", + "media.fgru11-1.fna.whatsapp.net", "18", "media-lga3-2.cdn.whatsapp.net", "pkey", "0e", "max_subject", "cond_range_lterm_rtt", "announcement_groups", "biz_profile_options", + "s_t", "media.fabv2-1.fna.whatsapp.net", "media.fcai3-1.fna.whatsapp.net", "media.fcgh1-1.fna.whatsapp.net", "media.fctg1-4.fna.whatsapp.net", "media.fdiy1-1.fna.whatsapp.net", + "media.fisb4-1.fna.whatsapp.net", "media.fpku1-2.fna.whatsapp.net", "media.fros9-1.fna.whatsapp.net", "status_v3_text", "usync_sidelist", "17", "announcement", "...", + "md_group_notification", "0f", "animated_pack_in_store", "013", "America/Mexico_City", "1260", "media-ams4-1.cdn.whatsapp.net", "media-cgk1-2.cdn.whatsapp.net", + "media-cpt1-1.cdn.whatsapp.net", "media-maa2-1.cdn.whatsapp.net", "media.fgye10-1.fna.whatsapp.net", "e", "catalog_cart", "hfm_string_changes", "init_bitrate", + "packless_hsm", "group_info", "America/Belem", "50", "960", "cond_range_bwe", "decode", "encode", "media.fada1-8.fna.whatsapp.net", "media.fadb1-2.fna.whatsapp.net", + "media.fasu6-1.fna.whatsapp.net", "media.fbog4-1.fna.whatsapp.net", "media.fcgk9-2.fna.whatsapp.net", "media.fdoh5-2.fna.whatsapp.net", "media.ffjr1-2.fna.whatsapp.net", + "media.fgua1-1.fna.whatsapp.net", "media.fgye1-1.fna.whatsapp.net", "media.fist1-4.fna.whatsapp.net", "media.fpbc2-2.fna.whatsapp.net", "media.fres2-1.fna.whatsapp.net", + "media.fsdq1-2.fna.whatsapp.net", "media.fsub6-5.fna.whatsapp.net", "profilo_enabled", "template_hsm", "use_disorder_prefetching_timer", "video_codec_priority", + "vpx_max_qp", "ptt_reduce_recording_delay", "25", "iphone", "Windows", "s_o", "Africa/Lagos", "abt", "media-kut2-1.cdn.whatsapp.net", "media-mba1-1.cdn.whatsapp.net", + "media-mxp1-2.cdn.whatsapp.net", "md_blocklist_v2", "url_text", "enable_short_offset", "group_join_permissions", "enable_audio_piggyback_feature", "image_quality", + "media.fcgk7-2.fna.whatsapp.net", "media.fcgk8-2.fna.whatsapp.net", "media.fclo7-1.fna.whatsapp.net", "media.fcmn1-1.fna.whatsapp.net", "media.feoh1-1.fna.whatsapp.net", + "media.fgyd4-3.fna.whatsapp.net", "media.fjed4-4.fna.whatsapp.net", "media.flim1-4.fna.whatsapp.net", "media.flim2-4.fna.whatsapp.net", "media.fplu6-1.fna.whatsapp.net", + "media.frak1-1.fna.whatsapp.net", "media.fsdq1-1.fna.whatsapp.net", "to_ip", "015", "vp8", "19", "21", "1320", "auth_key_ver", "message_processing_dedup", "server-error", + "wap4_enabled", "420", "014", "cond_range_rtt", "ptt_fast_lock_enabled", "media-ort2-1.cdn.whatsapp.net", "fwd_ui_start_ts" +}; + +static char *dict3[] = { + "contact_blacklist", "Asia/Jakarta", "media.fepa10-1.fna.whatsapp.net", "media.fmex10-3.fna.whatsapp.net", "disorder_prefetching_start_when_empty", "America/Bogota", + "use_local_probing_rx_bitrate", "America/Argentina/Buenos_Aires", "cross_post", "media.fabb1-1.fna.whatsapp.net", "media.fbog4-2.fna.whatsapp.net", "media.fcgk9-1.fna.whatsapp.net", + "media.fcmn2-1.fna.whatsapp.net", "media.fdel3-1.fna.whatsapp.net", "media.ffjr1-1.fna.whatsapp.net", "media.fgdl5-1.fna.whatsapp.net", "media.flpb1-2.fna.whatsapp.net", + "media.fmex2-1.fna.whatsapp.net", "media.frba2-2.fna.whatsapp.net", "media.fros2-2.fna.whatsapp.net", "media.fruh2-1.fna.whatsapp.net", "media.fybz2-2.fna.whatsapp.net", + "options", "20", "a", "017", "018", "mute_always", "user_notice", "Asia/Kolkata", "gif_provider", "locked", "media-gua1-1.cdn.whatsapp.net", "piggyback_exclude_force_flush", + "24", "media.frec39-1.fna.whatsapp.net", "user_remove", "file_max_size", "cond_packet_loss_pct_ema_alpha", "media.facc1-1.fna.whatsapp.net", "media.fadb2-1.fna.whatsapp.net", + "media.faly3-1.fna.whatsapp.net", "media.fbdo6-2.fna.whatsapp.net", "media.fcmn2-2.fna.whatsapp.net", "media.fctg1-3.fna.whatsapp.net", "media.ffez1-2.fna.whatsapp.net", + "media.fist1-3.fna.whatsapp.net", "media.fist2-2.fna.whatsapp.net", "media.flim2-2.fna.whatsapp.net", "media.fmct2-3.fna.whatsapp.net", "media.fpei3-1.fna.whatsapp.net", + "media.frba3-1.fna.whatsapp.net", "media.fsdu8-2.fna.whatsapp.net", "media.fstu2-1.fna.whatsapp.net", "media_type", "receipt_agg", "016", "enable_pli_for_crc_mismatch", + "live", "enc_rekey", "frskmsg", "d", "media.fdel11-2.fna.whatsapp.net", "proto", "2250", "audio_piggyback_enable_cache", "skip_nack_if_ltrp_sent", "mark_dtx_jb_frames", + "web_service_delay", "7282", "catalog_send_all", "outgoing", "360", "30", "LIMITED", "019", "audio_picker", "bpv2_phase", "media.fada1-7.fna.whatsapp.net", + "media.faep7-1.fna.whatsapp.net", "media.fbko1-2.fna.whatsapp.net", "media.fbni1-2.fna.whatsapp.net", "media.fbtz1-1.fna.whatsapp.net", "media.fbtz1-8.fna.whatsapp.net", + "media.fcjs3-1.fna.whatsapp.net", "media.fesb3-2.fna.whatsapp.net", "media.fgdl5-4.fna.whatsapp.net", "media.fist2-1.fna.whatsapp.net", "media.flhe2-2.fna.whatsapp.net", + "media.flim2-1.fna.whatsapp.net", "media.fmex1-1.fna.whatsapp.net", "media.fpat3-2.fna.whatsapp.net", "media.fpat3-3.fna.whatsapp.net", "media.fros2-1.fna.whatsapp.net", + "media.fsdu8-1.fna.whatsapp.net", "media.fsub3-2.fna.whatsapp.net", "payments_chat_plugin", "cond_congestion_no_rtcp_thr", "green_alert", "not-a-biz", "..", + "shops_pdp_urls_config", "source", "media-dus1-1.cdn.whatsapp.net", "mute_video", "01b", "currency", "max_keys", "resume_check", "contact_array", "qr_scanning", "23", + "b", "media.fbfh15-1.fna.whatsapp.net", "media.flim22-1.fna.whatsapp.net", "media.fsdu11-1.fna.whatsapp.net", "media.fsdu15-1.fna.whatsapp.net", "Chrome", "fts_version", + "60", "media.fada1-6.fna.whatsapp.net", "media.faep4-2.fna.whatsapp.net", "media.fbaq5-1.fna.whatsapp.net", "media.fbni1-1.fna.whatsapp.net", "media.fcai3-2.fna.whatsapp.net", + "media.fdel3-2.fna.whatsapp.net", "media.fdmm3-2.fna.whatsapp.net", "media.fhex3-1.fna.whatsapp.net", "media.fisb4-2.fna.whatsapp.net", "media.fkhi5-2.fna.whatsapp.net", + "media.flos2-1.fna.whatsapp.net", "media.fmct2-1.fna.whatsapp.net", "media.fntr7-1.fna.whatsapp.net", "media.frak3-1.fna.whatsapp.net", "media.fruh5-2.fna.whatsapp.net", + "media.fsub6-1.fna.whatsapp.net", "media.fuab1-2.fna.whatsapp.net", "media.fuio1-1.fna.whatsapp.net", "media.fver1-1.fna.whatsapp.net", "media.fymy1-1.fna.whatsapp.net", + "product_catalog", "1380", "audio_oob_fec_max_pkts", "22", "254", "media-ort2-2.cdn.whatsapp.net", "media-sjc3-1.cdn.whatsapp.net", "1600", "01a", "01c", "405", + "key_frame_interval", "body", "media.fcgh20-1.fna.whatsapp.net", "media.fesb10-2.fna.whatsapp.net", "125", "2000", "media.fbsb1-1.fna.whatsapp.net", + "media.fcmn3-2.fna.whatsapp.net", "media.fcpq1-1.fna.whatsapp.net", "media.fdel1-2.fna.whatsapp.net", "media.ffor2-1.fna.whatsapp.net", "media.fgdl1-4.fna.whatsapp.net", + "media.fhex2-1.fna.whatsapp.net", "media.fist1-2.fna.whatsapp.net", "media.fjed5-2.fna.whatsapp.net", "media.flim6-4.fna.whatsapp.net", "media.flos2-2.fna.whatsapp.net", + "media.fntr6-2.fna.whatsapp.net", "media.fpku3-2.fna.whatsapp.net", "media.fros8-1.fna.whatsapp.net", "media.fymy1-2.fna.whatsapp.net", "ul_bw", "ltrp_qp_offset", + "request", "nack", "dtx_delay_state_reset", "timeoffline", "28", "01f", "32", "enable_ltr_pool", "wa_msys_crypto", "01d", "58", "dtx_freeze_hg_update", + "nack_if_rpsi_throttled", "253", "840", "media.famd15-1.fna.whatsapp.net", "media.fbog17-2.fna.whatsapp.net", "media.fcai19-2.fna.whatsapp.net", "media.fcai21-4.fna.whatsapp.net", + "media.fesb10-4.fna.whatsapp.net", "media.fesb10-5.fna.whatsapp.net", "media.fmaa12-1.fna.whatsapp.net", "media.fmex11-3.fna.whatsapp.net", "media.fpoa33-1.fna.whatsapp.net", + "1050", "021", "clean", "cond_range_ema_packet_loss_pct", "media.fadb6-5.fna.whatsapp.net", "media.faqp4-1.fna.whatsapp.net", "media.fbaq3-1.fna.whatsapp.net", + "media.fbel2-1.fna.whatsapp.net", "media.fblr4-2.fna.whatsapp.net", "media.fclo8-1.fna.whatsapp.net", "media.fcoo1-2.fna.whatsapp.net", "media.ffjr1-4.fna.whatsapp.net", + "media.ffor9-1.fna.whatsapp.net", "media.fisb3-1.fna.whatsapp.net", "media.fkhi2-2.fna.whatsapp.net", "media.fkhi4-1.fna.whatsapp.net", "media.fpbc1-2.fna.whatsapp.net", + "media.fruh2-2.fna.whatsapp.net", "media.fruh5-1.fna.whatsapp.net", "media.fsub3-1.fna.whatsapp.net", "payments_transaction_limit", "252", "27", "29", "tintagel", "01e", + "237", "780", "callee_updated_payload", "020", "257", "price", "025", "239", "payments_cs_phone_number", "mediaretry", "w:auth:backup:token", "Glass.caf", "max_bitrate", + "240", "251", "660", "media.fbog16-1.fna.whatsapp.net", "media.fcgh21-1.fna.whatsapp.net", "media.fkul19-2.fna.whatsapp.net", "media.flim21-2.fna.whatsapp.net", + "media.fmex10-4.fna.whatsapp.net", "64", "33", "34", "35", "interruption", "media.fabv3-1.fna.whatsapp.net", "media.fadb6-1.fna.whatsapp.net", "media.fagr1-1.fna.whatsapp.net", + "media.famd1-1.fna.whatsapp.net", "media.famm6-1.fna.whatsapp.net", "media.faqp2-3.fna.whatsapp.net" +}; diff --git a/protocols/WhatsApp/src/iq.cpp b/protocols/WhatsApp/src/iq.cpp index b9cf5bd7d2..24513dd06b 100644 --- a/protocols/WhatsApp/src/iq.cpp +++ b/protocols/WhatsApp/src/iq.cpp @@ -1,570 +1,570 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#include "stdafx.h" - -void WhatsAppProto::OnAccountSync(const WANode &node) -{ - if (auto *pList = node.getChild("devices")) { - auto *pUser = FindUser(m_szJid); - pUser->arDevices.destroy(); - - for (auto &it : pList->getChildren()) - if (it->title == "device") - pUser->arDevices.insert(new WAJid(it->getAttr("jid"), it->getAttrInt("key-index"))); - } - - if (auto *pList = node.getChild("blocklist")) - for (auto &it : pList->getChildren()) - if (it->title == "item") { - auto *pUser = AddUser(it->getAttr("jid"), false); - Contact::Hide(pUser->hContact, 0 == mir_strcmp(it->getAttr("action"), "block")); - } - - SendAck(node); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnIqBlockList(const WANode &node) -{ - for (auto &it : node.getChild("list")->getChildren()) { - auto *pUser = AddUser(it->getAttr("jid"), false); - Contact::Hide(pUser->hContact); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnIqCountPrekeys(const WANode &node) -{ - m_bUpdatedPrekeys = true; - - int iCount = node.getChild("count")->getAttrInt("value"); - if (iCount < 5) - UploadMorePrekeys(); -} - -void WhatsAppProto::UploadMorePrekeys() -{ - WANodeIq iq(IQ::SET, "encrypt"); - - auto regId = encodeBigEndian(getDword(DBKEY_REG_ID)); - iq.addChild("registration")->content.append(regId.c_str(), regId.size()); - - iq.addChild("type")->content.append(KEY_BUNDLE_TYPE, 1); - iq.addChild("identity")->content.append(m_signalStore.signedIdentity.pub); - - const int PORTION = 10; - m_signalStore.generatePrekeys(PORTION); - - int iStart = getDword(DBKEY_PREKEY_UPLOAD_ID, 1); - auto *n = iq.addChild("list"); - for (int i = 0; i < PORTION; i++) { - auto *nKey = n->addChild("key"); - - int keyId = iStart + i; - auto encId = encodeBigEndian(keyId, 3); - nKey->addChild("id")->content.append(encId.c_str(), encId.size()); - nKey->addChild("value")->content.append(getBlob(CMStringA(FORMAT, "PreKey%dPublic", keyId))); - } - setDword(DBKEY_PREKEY_UPLOAD_ID, iStart + PORTION); - - auto *skey = iq.addChild("skey"); - - auto encId = encodeBigEndian(m_signalStore.preKey.keyid, 3); - skey->addChild("id")->content.append(encId.c_str(), encId.size()); - skey->addChild("value")->content.append(m_signalStore.preKey.pub); - skey->addChild("signature")->content.append(m_signalStore.preKey.signature); - - WSSendNode(iq, &WhatsAppProto::OnIqDoNothing); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnIqDoNothing(const WANode&) -{ -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnIqGetKeys(const WANode &node, void *pUserInfo) -{ - for (auto &it : node.getChild("list")->getChildren()) - if (it->title == "user") - m_signalStore.injectSession(it->getAttr("jid"), it, it); - - // don't forget to send delayed message when all keys are retrieved - if (pUserInfo) - FinishTask((WASendTask *)pUserInfo); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnIqGetUsync(const WANode &node, void *pUserInfo) -{ - WANodeIq iq(IQ::GET, "encrypt"); - auto *pKey = iq.addChild("key"); - - for (auto *nUser : node.getChild("usync")->getChild("list")->getChildren()) { - auto *pszJid = nUser->getAttr("jid"); - - auto *pUser = AddUser(pszJid, false); - pUser->bDeviceInit = true; - pUser->arDevices.destroy(); - - if (auto *pList = nUser->getChild("devices")->getChild("device-list")) - for (auto &it : pList->getChildren()) - if (it->title == "device") - pUser->arDevices.insert(new WAJid(pszJid, it->getAttrInt("id"))); - - for (auto &it : pUser->arDevices) { - auto blob = getBlob(MSignalSession(it->user, it->device).getSetting()); - if (blob.isEmpty()) - pKey->addChild("user")->addAttr("jid", it->toString()); - } - } - - if (pKey->getChildren().getCount() > 0) - WSSendNode(iq, &WhatsAppProto::OnIqGetKeys, pUserInfo); - else if (pUserInfo) - FinishTask((WASendTask *)pUserInfo); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnIqPairDevice(const WANode &node) -{ - WSSendNode(WANodeIq(IQ::RESULT) << CHAR_PARAM("id", node.getAttr("id"))); - - if (auto *pRef = node.getChild("pair-device")->getChild("ref")) { - ShowQrCode(pRef->getBody()); - } - else { - debugLogA("OnIqPairDevice: got reply without ref, exiting"); - ShutdownSession(); - } -} - -void WhatsAppProto::OnIqPairSuccess(const WANode &node) -{ - CloseQrDialog(); - - auto *pRoot = node.getChild("pair-success"); - - try { - if (auto *pPlatform = pRoot->getChild("platform")) - debugLogA("Got response from platform: %s", pPlatform->getBody().c_str()); - - if (auto *pBiz = pRoot->getChild("biz")) - if (auto *pszName = pBiz->getAttr("name")) - setUString("Nick", pszName); - - if (auto *pDevice = pRoot->getChild("device")) { - if (auto *pszJid = pDevice->getAttr("jid")) { - WAJid jid(pszJid); - m_szJid = jid.user + "@" + jid.server; - m_arUsers.insert(new WAUser(0, m_szJid, false)); - - setUString(DBKEY_ID, m_szJid); - setDword(DBKEY_DEVICE_ID, jid.device); - } - } - else throw "OnIqPairSuccess: got reply without device info, exiting"; - - if (auto *pIdentity = pRoot->getChild("device-identity")) { - proto::ADVSignedDeviceIdentityHMAC payload(pIdentity->content); - - auto &hmac = payload->hmac; - auto &details = payload->details; - { - // check details signature using HMAC - uint8_t signature[32]; - unsigned int out_len = sizeof(signature); - MBinBuffer secret(getBlob(DBKEY_SECRET_KEY)); - HMAC(EVP_sha256(), secret.data(), (int)secret.length(), details.data, (int)details.len, signature, &out_len); - if (memcmp(hmac.data, signature, sizeof(signature))) - throw "OnIqPairSuccess: got reply with invalid details signature, exiting"; - } - - proto::ADVSignedDeviceIdentity account(details); - - auto &deviceDetails = account->details; - auto &accountSignature = account->accountsignature; - auto &accountSignatureKey = account->accountsignaturekey; - { - MBinBuffer buf; - buf.append("\x06\x00", 2); - buf.append(deviceDetails.data, deviceDetails.len); - buf.append(m_signalStore.signedIdentity.pub); - - ec_public_key key = {}; - memcpy(key.data, accountSignatureKey.data, sizeof(key.data)); - if (1 != curve_verify_signature(&key, buf.data(), buf.length(), accountSignature.data, accountSignature.len)) - throw "OnIqPairSuccess: got reply with invalid account signature, exiting"; - } - debugLogA("Received valid account signature"); - { - MBinBuffer buf; - buf.append("\x06\x01", 2); - buf.append(deviceDetails.data, deviceDetails.len); - buf.append(m_signalStore.signedIdentity.pub); - buf.append(accountSignatureKey.data, accountSignatureKey.len); - - signal_buffer *result; - ec_private_key key = {}; - memcpy(key.data, m_signalStore.signedIdentity.priv.data(), m_signalStore.signedIdentity.priv.length()); - if (curve_calculate_signature(m_signalStore.CTX(), &result, &key, buf.data(), buf.length()) != 0) - throw "OnIqPairSuccess: cannot calculate account signature, exiting"; - - account->devicesignature = proto::SetBinary(result->data, result->len); - account->has_devicesignature = true; - signal_buffer_free(result); - } - - { - MBinBuffer key; - if (accountSignatureKey.len == 32) - key.append(KEY_BUNDLE_TYPE, 1); - key.append(accountSignatureKey.data, accountSignatureKey.len); - db_set_blob(0, m_szModuleName, "SignalIdentifierKey", key.data(), (int)key.length()); - } - - proto::CleanBinary(account->accountsignaturekey); account->has_accountsignaturekey = false; - MBinBuffer accountEnc(proto::Serialize(account)); - db_set_blob(0, m_szModuleName, "WAAccount", accountEnc.data(), (int)accountEnc.length()); - - proto::ADVDeviceIdentity deviceIdentity(deviceDetails); - - WANodeIq reply(IQ::RESULT); reply << CHAR_PARAM("id", node.getAttr("id")); - - WANode *nodePair = reply.addChild("pair-device-sign"); - - WANode *nodeDeviceIdentity = nodePair->addChild("device-identity"); - nodeDeviceIdentity->addAttr("key-index", deviceIdentity->keyindex); - nodeDeviceIdentity->content.append(accountEnc.data(), accountEnc.length()); - WSSendNode(reply); - } - else throw "OnIqPairSuccess: got reply without identity, exiting"; - } - catch (const char *pErrMsg) { - debugLogA(pErrMsg); - ShutdownSession(); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnIqResult(const WANode &node) -{ - if (auto *pszId = node.getAttr("id")) { - for (auto &it : m_arPacketQueue) { - if (it->szPacketId == pszId) { - it->Execute(this, node); - m_arPacketQueue.remove(it); - break; - } - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnNotifyAny(const WANode &node) -{ - SendAck(node); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnReceiveChatState(const WANode &node) -{ - if (auto *pUser = FindUser(node.getAttr("from"))) { - if (node.getChild("composing")) { - pUser->m_timer1 = time(0); - pUser->m_timer2 = 0; - setWord(pUser->hContact, "Status", ID_STATUS_ONLINE); - - CallService(MS_PROTO_CONTACTISTYPING, pUser->hContact, 60); - } - else if (node.getChild("paused")) - CallService(MS_PROTO_CONTACTISTYPING, pUser->hContact, PROTOTYPE_CONTACTTYPING_OFF); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnNotifyDevices(const WANode &node) -{ - if (!mir_strcmp(node.getAttr("jid"), m_szJid)) - debugLogA("received list of my own devices"); - SendAck(node); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnNotifyEncrypt(const WANode &node) -{ - if (!mir_strcmp(node.getAttr("from"), S_WHATSAPP_NET)) - OnIqCountPrekeys(node); - SendAck(node); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnNotifyPicture(const WANode &node) -{ - if (auto *pszFrom = node.getAttr("from")) - if (m_szJid != pszFrom) - if (auto *pszUser = FindUser(pszFrom)) - ServerFetchAvatar(pszFrom); - - SendAck(node); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnProcessHandshake(const uint8_t *pData, int cbLen) -{ - proto::HandshakeMessage msg(pData, cbLen); - if (!msg) { - debugLogA("Error parsing data, exiting"); - -LBL_Error: - ShutdownSession(); - return; - } - - auto &static_ = msg->serverhello->static_; - auto &payload_ = msg->serverhello->payload; - auto &ephemeral_ = msg->serverhello->ephemeral; - - m_noise->updateHash(ephemeral_.data, ephemeral_.len); - m_noise->mixIntoKey(m_noise->ephemeral.priv.data(), ephemeral_.data); - - MBinBuffer decryptedStatic = m_noise->decrypt(static_.data, static_.len); - m_noise->mixIntoKey(m_noise->ephemeral.priv.data(), decryptedStatic.data()); - - proto::CertChain cert(m_noise->decrypt(payload_.data, payload_.len)); - proto::CertChain__NoiseCertificate__Details details(cert->intermediate->details); - if (details->issuerserial != 0) { - debugLogA("Invalid certificate serial number, exiting"); - goto LBL_Error; - } - - MBinBuffer encryptedPub = m_noise->encrypt(m_noise->noiseKeys.pub.data(), m_noise->noiseKeys.pub.length()); - m_noise->mixIntoKey(m_noise->noiseKeys.priv.data(), ephemeral_.data); - - // create reply - Wa__ClientPayload node; - Wa__ClientPayload__DevicePairingRegistrationData pairingData; - Wa__DeviceProps companion; - Wa__DeviceProps__AppVersion appVersion; - T2Utf devName(m_wszDeviceName); - - MFileVersion v; - Miranda_GetFileVersion(&v); - - // not received our jid from server? generate registration packet then - if (m_szJid.IsEmpty()) { - uint8_t buildHash[16]; - mir_md5_hash((BYTE *)APP_VERSION, sizeof(APP_VERSION) - 1, buildHash); - - appVersion.primary = v[0]; appVersion.has_primary = true; - appVersion.secondary = v[1]; appVersion.has_secondary = true; - appVersion.tertiary = v[2]; appVersion.has_tertiary = true; - appVersion.quaternary = v[3]; appVersion.has_quaternary = true; - - companion.os = devName.get(); - companion.version = &appVersion; - companion.platformtype = WA__DEVICE_PROPS__PLATFORM_TYPE__DESKTOP; companion.has_platformtype = true; - companion.requirefullsync = false; companion.has_requirefullsync = true; - - MBinBuffer buf(proto::Serialize(&companion)); - auto szRegId(encodeBigEndian(getDword(DBKEY_REG_ID))); - auto szKeyId(encodeBigEndian(m_signalStore.preKey.keyid)); - - pairingData.deviceprops = proto::SetBinary(buf.data(), buf.length()); pairingData.has_deviceprops = true; - pairingData.buildhash = proto::SetBinary(buildHash, sizeof(buildHash)); pairingData.has_buildhash = true; - pairingData.eregid = proto::SetBinary(szRegId.c_str(), szRegId.size()); pairingData.has_eregid = true; - pairingData.ekeytype = proto::SetBinary(KEY_BUNDLE_TYPE, 1); pairingData.has_ekeytype = true; - pairingData.eident = proto::SetBinary(m_signalStore.signedIdentity.pub.data(), m_signalStore.signedIdentity.pub.length()); pairingData.has_eident = true; - pairingData.eskeyid = proto::SetBinary(szKeyId.c_str(), szKeyId.size()); pairingData.has_eskeyid = true; - pairingData.eskeyval = proto::SetBinary(m_signalStore.preKey.pub.data(), m_signalStore.preKey.pub.length()); pairingData.has_eskeyval = true; - pairingData.eskeysig = proto::SetBinary(m_signalStore.preKey.signature.data(), m_signalStore.preKey.signature.length()); pairingData.has_eskeysig = true; - node.devicepairingdata = &pairingData; - - node.passive = false; node.has_passive = true; - } - // generate login packet - else { - WAJid jid(m_szJid); - node.username = _atoi64(jid.user); node.has_username = true; - node.device = getDword(DBKEY_DEVICE_ID); node.has_device = true; - node.passive = true; node.has_passive = true; - } - - Wa__ClientPayload__UserAgent__AppVersion userVersion; - userVersion.primary = 2; userVersion.has_primary = true; - userVersion.secondary = 2230; userVersion.has_secondary = true; - userVersion.tertiary = 15; userVersion.has_tertiary = true; - - Wa__ClientPayload__UserAgent userAgent; - userAgent.appversion = &userVersion; - userAgent.platform = WA__CLIENT_PAYLOAD__USER_AGENT__PLATFORM__WEB; userAgent.has_platform = true; - userAgent.releasechannel = WA__CLIENT_PAYLOAD__USER_AGENT__RELEASE_CHANNEL__RELEASE; userAgent.has_releasechannel = true; - userAgent.mcc = "000"; - userAgent.mnc = "000"; - userAgent.osversion = "0.1"; - userAgent.osbuildnumber = "0.1"; - userAgent.manufacturer = ""; - userAgent.device = "Desktop"; - userAgent.localelanguageiso6391 = "en"; - userAgent.localecountryiso31661alpha2 = "US"; - - Wa__ClientPayload__WebInfo webInfo; - webInfo.websubplatform = WA__CLIENT_PAYLOAD__WEB_INFO__WEB_SUB_PLATFORM__WEB_BROWSER; webInfo.has_websubplatform = true; - - node.connecttype = WA__CLIENT_PAYLOAD__CONNECT_TYPE__WIFI_UNKNOWN; node.has_connecttype = true; - node.connectreason = WA__CLIENT_PAYLOAD__CONNECT_REASON__USER_ACTIVATED; node.has_connectreason = true; - node.useragent = &userAgent; - node.webinfo = &webInfo; - - MBinBuffer payload(proto::Serialize(&node)); - MBinBuffer payloadEnc = m_noise->encrypt(payload.data(), payload.length()); - - Wa__HandshakeMessage__ClientFinish finish; - finish.payload = {payloadEnc.length(), payloadEnc.data()}; finish.has_payload = true; - finish.static_ = {encryptedPub.length(), encryptedPub.data()}; finish.has_static_ = true; - - Wa__HandshakeMessage handshake; - handshake.clientfinish = &finish; - WSSend(handshake); - - m_noise->finish(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnReceiveFailure(const WANode &node) -{ - m_bTerminated = true; - - ProcessFailure(node.getAttrInt("reason")); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnReceiveInfo(const WANode &node) -{ - if (auto *pChild = node.getFirstChild()) { - if (pChild->title == "offline") { - debugLogA("Processed %d offline events", pChild->getAttrInt("count")); - - // retrieve loaded prekeys count - if (!m_bUpdatedPrekeys) - WSSendNode(WANodeIq(IQ::GET, "encrypt") << XCHILD("count"), &WhatsAppProto::OnIqCountPrekeys); - - auto *pUser = FindUser(m_szJid); - if (pUser->arDevices.getCount() == 0) { - LIST jids(1); - jids.insert(m_szJid.GetBuffer()); - SendUsync(jids, nullptr); - } - - for (auto &it : m_arCollections) { - if (it->version == 0) { - m_impl.m_resyncApp.Stop(); - m_impl.m_resyncApp.Start(1000); - break; - } - } - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::ProcessReceipt(MCONTACT hContact, const char *msgId, bool bRead) -{ - MEVENT hEvent = db_event_getById(m_szModuleName, msgId); - if (hEvent == 0) - return; - - if (g_plugin.bHasMessageState) - CallService(MS_MESSAGESTATE_UPDATE, hContact, bRead ? MRD_TYPE_READ : MRD_TYPE_DELIVERED); - - if (bRead) - db_event_markRead(hContact, hEvent); -} - -void WhatsAppProto::OnReceiveReceipt(const WANode &node) -{ - if (!mir_strcmp(node.getAttr("type"), "retry")) { - for (auto &it : node.getChildren()) - if (it->title == "keys") - m_signalStore.injectSession(node.getAttr("from"), &node, it); - return; - } - - if (auto *pUser = FindUser(node.getAttr("from"))) { - bool bRead = mir_strcmp(node.getAttr("type"), "read") == 0; - ProcessReceipt(pUser->hContact, node.getAttr("id"), bRead); - - if (auto *pList = node.getChild("list")) - for (auto &it : pList->getChildren()) - if (it->title == "item") - ProcessReceipt(pUser->hContact, it->getAttr("id"), bRead); - } - - SendAck(node); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnStreamError(const WANode &node) -{ - m_bTerminated = true; - - if (auto *pszCode = node.getAttr("code")) - ProcessFailure(atoi(pszCode)); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnSuccess(const WANode &) -{ - OnLoggedIn(); - - WSSendNode(WANodeIq(IQ::SET, "passive") << XCHILD("active"), &WhatsAppProto::OnIqDoNothing); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::InitPersistentHandlers() -{ - m_arPersistent.insert(new WAPersistentHandler("iq", "set", "md", "pair-device", &WhatsAppProto::OnIqPairDevice)); - m_arPersistent.insert(new WAPersistentHandler("iq", "set", "md", "pair-success", &WhatsAppProto::OnIqPairSuccess)); - - m_arPersistent.insert(new WAPersistentHandler("notification", "devices", 0, 0, &WhatsAppProto::OnNotifyDevices)); - m_arPersistent.insert(new WAPersistentHandler("notification", "encrypt", 0, 0, &WhatsAppProto::OnNotifyEncrypt)); - m_arPersistent.insert(new WAPersistentHandler("notification", "picture", 0, 0, &WhatsAppProto::OnNotifyPicture)); - m_arPersistent.insert(new WAPersistentHandler("notification", "account_sync", 0, 0, &WhatsAppProto::OnAccountSync)); - m_arPersistent.insert(new WAPersistentHandler("notification", "server_sync", 0, 0, &WhatsAppProto::OnServerSync)); - m_arPersistent.insert(new WAPersistentHandler("notification", 0, 0, 0, &WhatsAppProto::OnNotifyAny)); - - m_arPersistent.insert(new WAPersistentHandler("ack", 0, 0, 0, &WhatsAppProto::OnReceiveAck)); - m_arPersistent.insert(new WAPersistentHandler("ib", 0, 0, 0, &WhatsAppProto::OnReceiveInfo)); - m_arPersistent.insert(new WAPersistentHandler("failure", 0, 0, 0, &WhatsAppProto::OnReceiveFailure)); - m_arPersistent.insert(new WAPersistentHandler("message", 0, 0, 0, &WhatsAppProto::OnReceiveMessage)); - m_arPersistent.insert(new WAPersistentHandler("receipt", 0, 0, 0, &WhatsAppProto::OnReceiveReceipt)); - m_arPersistent.insert(new WAPersistentHandler("chatstates", 0, 0, 0, &WhatsAppProto::OnReceiveChatState)); - m_arPersistent.insert(new WAPersistentHandler("stream:error", 0, 0, 0, &WhatsAppProto::OnStreamError)); - m_arPersistent.insert(new WAPersistentHandler("success", 0, 0, 0, &WhatsAppProto::OnSuccess)); - - m_arPersistent.insert(new WAPersistentHandler(0, "result", 0, 0, &WhatsAppProto::OnIqResult)); -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +void WhatsAppProto::OnAccountSync(const WANode &node) +{ + if (auto *pList = node.getChild("devices")) { + auto *pUser = FindUser(m_szJid); + pUser->arDevices.destroy(); + + for (auto &it : pList->getChildren()) + if (it->title == "device") + pUser->arDevices.insert(new WAJid(it->getAttr("jid"), it->getAttrInt("key-index"))); + } + + if (auto *pList = node.getChild("blocklist")) + for (auto &it : pList->getChildren()) + if (it->title == "item") { + auto *pUser = AddUser(it->getAttr("jid"), false); + Contact::Hide(pUser->hContact, 0 == mir_strcmp(it->getAttr("action"), "block")); + } + + SendAck(node); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnIqBlockList(const WANode &node) +{ + for (auto &it : node.getChild("list")->getChildren()) { + auto *pUser = AddUser(it->getAttr("jid"), false); + Contact::Hide(pUser->hContact); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnIqCountPrekeys(const WANode &node) +{ + m_bUpdatedPrekeys = true; + + int iCount = node.getChild("count")->getAttrInt("value"); + if (iCount < 5) + UploadMorePrekeys(); +} + +void WhatsAppProto::UploadMorePrekeys() +{ + WANodeIq iq(IQ::SET, "encrypt"); + + auto regId = encodeBigEndian(getDword(DBKEY_REG_ID)); + iq.addChild("registration")->content.append(regId.c_str(), regId.size()); + + iq.addChild("type")->content.append(KEY_BUNDLE_TYPE, 1); + iq.addChild("identity")->content.append(m_signalStore.signedIdentity.pub); + + const int PORTION = 10; + m_signalStore.generatePrekeys(PORTION); + + int iStart = getDword(DBKEY_PREKEY_UPLOAD_ID, 1); + auto *n = iq.addChild("list"); + for (int i = 0; i < PORTION; i++) { + auto *nKey = n->addChild("key"); + + int keyId = iStart + i; + auto encId = encodeBigEndian(keyId, 3); + nKey->addChild("id")->content.append(encId.c_str(), encId.size()); + nKey->addChild("value")->content.append(getBlob(CMStringA(FORMAT, "PreKey%dPublic", keyId))); + } + setDword(DBKEY_PREKEY_UPLOAD_ID, iStart + PORTION); + + auto *skey = iq.addChild("skey"); + + auto encId = encodeBigEndian(m_signalStore.preKey.keyid, 3); + skey->addChild("id")->content.append(encId.c_str(), encId.size()); + skey->addChild("value")->content.append(m_signalStore.preKey.pub); + skey->addChild("signature")->content.append(m_signalStore.preKey.signature); + + WSSendNode(iq, &WhatsAppProto::OnIqDoNothing); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnIqDoNothing(const WANode&) +{ +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnIqGetKeys(const WANode &node, void *pUserInfo) +{ + for (auto &it : node.getChild("list")->getChildren()) + if (it->title == "user") + m_signalStore.injectSession(it->getAttr("jid"), it, it); + + // don't forget to send delayed message when all keys are retrieved + if (pUserInfo) + FinishTask((WASendTask *)pUserInfo); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnIqGetUsync(const WANode &node, void *pUserInfo) +{ + WANodeIq iq(IQ::GET, "encrypt"); + auto *pKey = iq.addChild("key"); + + for (auto *nUser : node.getChild("usync")->getChild("list")->getChildren()) { + auto *pszJid = nUser->getAttr("jid"); + + auto *pUser = AddUser(pszJid, false); + pUser->bDeviceInit = true; + pUser->arDevices.destroy(); + + if (auto *pList = nUser->getChild("devices")->getChild("device-list")) + for (auto &it : pList->getChildren()) + if (it->title == "device") + pUser->arDevices.insert(new WAJid(pszJid, it->getAttrInt("id"))); + + for (auto &it : pUser->arDevices) { + auto blob = getBlob(MSignalSession(it->user, it->device).getSetting()); + if (blob.isEmpty()) + pKey->addChild("user")->addAttr("jid", it->toString()); + } + } + + if (pKey->getChildren().getCount() > 0) + WSSendNode(iq, &WhatsAppProto::OnIqGetKeys, pUserInfo); + else if (pUserInfo) + FinishTask((WASendTask *)pUserInfo); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnIqPairDevice(const WANode &node) +{ + WSSendNode(WANodeIq(IQ::RESULT) << CHAR_PARAM("id", node.getAttr("id"))); + + if (auto *pRef = node.getChild("pair-device")->getChild("ref")) { + ShowQrCode(pRef->getBody()); + } + else { + debugLogA("OnIqPairDevice: got reply without ref, exiting"); + ShutdownSession(); + } +} + +void WhatsAppProto::OnIqPairSuccess(const WANode &node) +{ + CloseQrDialog(); + + auto *pRoot = node.getChild("pair-success"); + + try { + if (auto *pPlatform = pRoot->getChild("platform")) + debugLogA("Got response from platform: %s", pPlatform->getBody().c_str()); + + if (auto *pBiz = pRoot->getChild("biz")) + if (auto *pszName = pBiz->getAttr("name")) + setUString("Nick", pszName); + + if (auto *pDevice = pRoot->getChild("device")) { + if (auto *pszJid = pDevice->getAttr("jid")) { + WAJid jid(pszJid); + m_szJid = jid.user + "@" + jid.server; + m_arUsers.insert(new WAUser(0, m_szJid, false)); + + setUString(DBKEY_ID, m_szJid); + setDword(DBKEY_DEVICE_ID, jid.device); + } + } + else throw "OnIqPairSuccess: got reply without device info, exiting"; + + if (auto *pIdentity = pRoot->getChild("device-identity")) { + proto::ADVSignedDeviceIdentityHMAC payload(pIdentity->content); + + auto &hmac = payload->hmac; + auto &details = payload->details; + { + // check details signature using HMAC + uint8_t signature[32]; + unsigned int out_len = sizeof(signature); + MBinBuffer secret(getBlob(DBKEY_SECRET_KEY)); + HMAC(EVP_sha256(), secret.data(), (int)secret.length(), details.data, (int)details.len, signature, &out_len); + if (memcmp(hmac.data, signature, sizeof(signature))) + throw "OnIqPairSuccess: got reply with invalid details signature, exiting"; + } + + proto::ADVSignedDeviceIdentity account(details); + + auto &deviceDetails = account->details; + auto &accountSignature = account->accountsignature; + auto &accountSignatureKey = account->accountsignaturekey; + { + MBinBuffer buf; + buf.append("\x06\x00", 2); + buf.append(deviceDetails.data, deviceDetails.len); + buf.append(m_signalStore.signedIdentity.pub); + + ec_public_key key = {}; + memcpy(key.data, accountSignatureKey.data, sizeof(key.data)); + if (1 != curve_verify_signature(&key, buf.data(), buf.length(), accountSignature.data, accountSignature.len)) + throw "OnIqPairSuccess: got reply with invalid account signature, exiting"; + } + debugLogA("Received valid account signature"); + { + MBinBuffer buf; + buf.append("\x06\x01", 2); + buf.append(deviceDetails.data, deviceDetails.len); + buf.append(m_signalStore.signedIdentity.pub); + buf.append(accountSignatureKey.data, accountSignatureKey.len); + + signal_buffer *result; + ec_private_key key = {}; + memcpy(key.data, m_signalStore.signedIdentity.priv.data(), m_signalStore.signedIdentity.priv.length()); + if (curve_calculate_signature(m_signalStore.CTX(), &result, &key, buf.data(), buf.length()) != 0) + throw "OnIqPairSuccess: cannot calculate account signature, exiting"; + + account->devicesignature = proto::SetBinary(result->data, result->len); + account->has_devicesignature = true; + signal_buffer_free(result); + } + + { + MBinBuffer key; + if (accountSignatureKey.len == 32) + key.append(KEY_BUNDLE_TYPE, 1); + key.append(accountSignatureKey.data, accountSignatureKey.len); + db_set_blob(0, m_szModuleName, "SignalIdentifierKey", key.data(), (int)key.length()); + } + + proto::CleanBinary(account->accountsignaturekey); account->has_accountsignaturekey = false; + MBinBuffer accountEnc(proto::Serialize(account)); + db_set_blob(0, m_szModuleName, "WAAccount", accountEnc.data(), (int)accountEnc.length()); + + proto::ADVDeviceIdentity deviceIdentity(deviceDetails); + + WANodeIq reply(IQ::RESULT); reply << CHAR_PARAM("id", node.getAttr("id")); + + WANode *nodePair = reply.addChild("pair-device-sign"); + + WANode *nodeDeviceIdentity = nodePair->addChild("device-identity"); + nodeDeviceIdentity->addAttr("key-index", deviceIdentity->keyindex); + nodeDeviceIdentity->content.append(accountEnc.data(), accountEnc.length()); + WSSendNode(reply); + } + else throw "OnIqPairSuccess: got reply without identity, exiting"; + } + catch (const char *pErrMsg) { + debugLogA(pErrMsg); + ShutdownSession(); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnIqResult(const WANode &node) +{ + if (auto *pszId = node.getAttr("id")) { + for (auto &it : m_arPacketQueue) { + if (it->szPacketId == pszId) { + it->Execute(this, node); + m_arPacketQueue.remove(it); + break; + } + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnNotifyAny(const WANode &node) +{ + SendAck(node); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnReceiveChatState(const WANode &node) +{ + if (auto *pUser = FindUser(node.getAttr("from"))) { + if (node.getChild("composing")) { + pUser->m_timer1 = time(0); + pUser->m_timer2 = 0; + setWord(pUser->hContact, "Status", ID_STATUS_ONLINE); + + CallService(MS_PROTO_CONTACTISTYPING, pUser->hContact, 60); + } + else if (node.getChild("paused")) + CallService(MS_PROTO_CONTACTISTYPING, pUser->hContact, PROTOTYPE_CONTACTTYPING_OFF); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnNotifyDevices(const WANode &node) +{ + if (!mir_strcmp(node.getAttr("jid"), m_szJid)) + debugLogA("received list of my own devices"); + SendAck(node); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnNotifyEncrypt(const WANode &node) +{ + if (!mir_strcmp(node.getAttr("from"), S_WHATSAPP_NET)) + OnIqCountPrekeys(node); + SendAck(node); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnNotifyPicture(const WANode &node) +{ + if (auto *pszFrom = node.getAttr("from")) + if (m_szJid != pszFrom) + if (auto *pszUser = FindUser(pszFrom)) + ServerFetchAvatar(pszFrom); + + SendAck(node); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnProcessHandshake(const uint8_t *pData, int cbLen) +{ + proto::HandshakeMessage msg(pData, cbLen); + if (!msg) { + debugLogA("Error parsing data, exiting"); + +LBL_Error: + ShutdownSession(); + return; + } + + auto &static_ = msg->serverhello->static_; + auto &payload_ = msg->serverhello->payload; + auto &ephemeral_ = msg->serverhello->ephemeral; + + m_noise->updateHash(ephemeral_.data, ephemeral_.len); + m_noise->mixIntoKey(m_noise->ephemeral.priv.data(), ephemeral_.data); + + MBinBuffer decryptedStatic = m_noise->decrypt(static_.data, static_.len); + m_noise->mixIntoKey(m_noise->ephemeral.priv.data(), decryptedStatic.data()); + + proto::CertChain cert(m_noise->decrypt(payload_.data, payload_.len)); + proto::CertChain__NoiseCertificate__Details details(cert->intermediate->details); + if (details->issuerserial != 0) { + debugLogA("Invalid certificate serial number, exiting"); + goto LBL_Error; + } + + MBinBuffer encryptedPub = m_noise->encrypt(m_noise->noiseKeys.pub.data(), m_noise->noiseKeys.pub.length()); + m_noise->mixIntoKey(m_noise->noiseKeys.priv.data(), ephemeral_.data); + + // create reply + Wa__ClientPayload node; + Wa__ClientPayload__DevicePairingRegistrationData pairingData; + Wa__DeviceProps companion; + Wa__DeviceProps__AppVersion appVersion; + T2Utf devName(m_wszDeviceName); + + MFileVersion v; + Miranda_GetFileVersion(&v); + + // not received our jid from server? generate registration packet then + if (m_szJid.IsEmpty()) { + uint8_t buildHash[16]; + mir_md5_hash((BYTE *)APP_VERSION, sizeof(APP_VERSION) - 1, buildHash); + + appVersion.primary = v[0]; appVersion.has_primary = true; + appVersion.secondary = v[1]; appVersion.has_secondary = true; + appVersion.tertiary = v[2]; appVersion.has_tertiary = true; + appVersion.quaternary = v[3]; appVersion.has_quaternary = true; + + companion.os = devName.get(); + companion.version = &appVersion; + companion.platformtype = WA__DEVICE_PROPS__PLATFORM_TYPE__DESKTOP; companion.has_platformtype = true; + companion.requirefullsync = false; companion.has_requirefullsync = true; + + MBinBuffer buf(proto::Serialize(&companion)); + auto szRegId(encodeBigEndian(getDword(DBKEY_REG_ID))); + auto szKeyId(encodeBigEndian(m_signalStore.preKey.keyid)); + + pairingData.deviceprops = proto::SetBinary(buf.data(), buf.length()); pairingData.has_deviceprops = true; + pairingData.buildhash = proto::SetBinary(buildHash, sizeof(buildHash)); pairingData.has_buildhash = true; + pairingData.eregid = proto::SetBinary(szRegId.c_str(), szRegId.size()); pairingData.has_eregid = true; + pairingData.ekeytype = proto::SetBinary(KEY_BUNDLE_TYPE, 1); pairingData.has_ekeytype = true; + pairingData.eident = proto::SetBinary(m_signalStore.signedIdentity.pub.data(), m_signalStore.signedIdentity.pub.length()); pairingData.has_eident = true; + pairingData.eskeyid = proto::SetBinary(szKeyId.c_str(), szKeyId.size()); pairingData.has_eskeyid = true; + pairingData.eskeyval = proto::SetBinary(m_signalStore.preKey.pub.data(), m_signalStore.preKey.pub.length()); pairingData.has_eskeyval = true; + pairingData.eskeysig = proto::SetBinary(m_signalStore.preKey.signature.data(), m_signalStore.preKey.signature.length()); pairingData.has_eskeysig = true; + node.devicepairingdata = &pairingData; + + node.passive = false; node.has_passive = true; + } + // generate login packet + else { + WAJid jid(m_szJid); + node.username = _atoi64(jid.user); node.has_username = true; + node.device = getDword(DBKEY_DEVICE_ID); node.has_device = true; + node.passive = true; node.has_passive = true; + } + + Wa__ClientPayload__UserAgent__AppVersion userVersion; + userVersion.primary = 2; userVersion.has_primary = true; + userVersion.secondary = 2230; userVersion.has_secondary = true; + userVersion.tertiary = 15; userVersion.has_tertiary = true; + + Wa__ClientPayload__UserAgent userAgent; + userAgent.appversion = &userVersion; + userAgent.platform = WA__CLIENT_PAYLOAD__USER_AGENT__PLATFORM__WEB; userAgent.has_platform = true; + userAgent.releasechannel = WA__CLIENT_PAYLOAD__USER_AGENT__RELEASE_CHANNEL__RELEASE; userAgent.has_releasechannel = true; + userAgent.mcc = "000"; + userAgent.mnc = "000"; + userAgent.osversion = "0.1"; + userAgent.osbuildnumber = "0.1"; + userAgent.manufacturer = ""; + userAgent.device = "Desktop"; + userAgent.localelanguageiso6391 = "en"; + userAgent.localecountryiso31661alpha2 = "US"; + + Wa__ClientPayload__WebInfo webInfo; + webInfo.websubplatform = WA__CLIENT_PAYLOAD__WEB_INFO__WEB_SUB_PLATFORM__WEB_BROWSER; webInfo.has_websubplatform = true; + + node.connecttype = WA__CLIENT_PAYLOAD__CONNECT_TYPE__WIFI_UNKNOWN; node.has_connecttype = true; + node.connectreason = WA__CLIENT_PAYLOAD__CONNECT_REASON__USER_ACTIVATED; node.has_connectreason = true; + node.useragent = &userAgent; + node.webinfo = &webInfo; + + MBinBuffer payload(proto::Serialize(&node)); + MBinBuffer payloadEnc = m_noise->encrypt(payload.data(), payload.length()); + + Wa__HandshakeMessage__ClientFinish finish; + finish.payload = {payloadEnc.length(), payloadEnc.data()}; finish.has_payload = true; + finish.static_ = {encryptedPub.length(), encryptedPub.data()}; finish.has_static_ = true; + + Wa__HandshakeMessage handshake; + handshake.clientfinish = &finish; + WSSend(handshake); + + m_noise->finish(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnReceiveFailure(const WANode &node) +{ + m_bTerminated = true; + + ProcessFailure(node.getAttrInt("reason")); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnReceiveInfo(const WANode &node) +{ + if (auto *pChild = node.getFirstChild()) { + if (pChild->title == "offline") { + debugLogA("Processed %d offline events", pChild->getAttrInt("count")); + + // retrieve loaded prekeys count + if (!m_bUpdatedPrekeys) + WSSendNode(WANodeIq(IQ::GET, "encrypt") << XCHILD("count"), &WhatsAppProto::OnIqCountPrekeys); + + auto *pUser = FindUser(m_szJid); + if (pUser->arDevices.getCount() == 0) { + LIST jids(1); + jids.insert(m_szJid.GetBuffer()); + SendUsync(jids, nullptr); + } + + for (auto &it : m_arCollections) { + if (it->version == 0) { + m_impl.m_resyncApp.Stop(); + m_impl.m_resyncApp.Start(1000); + break; + } + } + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::ProcessReceipt(MCONTACT hContact, const char *msgId, bool bRead) +{ + MEVENT hEvent = db_event_getById(m_szModuleName, msgId); + if (hEvent == 0) + return; + + if (g_plugin.bHasMessageState) + CallService(MS_MESSAGESTATE_UPDATE, hContact, bRead ? MRD_TYPE_READ : MRD_TYPE_DELIVERED); + + if (bRead) + db_event_markRead(hContact, hEvent); +} + +void WhatsAppProto::OnReceiveReceipt(const WANode &node) +{ + if (!mir_strcmp(node.getAttr("type"), "retry")) { + for (auto &it : node.getChildren()) + if (it->title == "keys") + m_signalStore.injectSession(node.getAttr("from"), &node, it); + return; + } + + if (auto *pUser = FindUser(node.getAttr("from"))) { + bool bRead = mir_strcmp(node.getAttr("type"), "read") == 0; + ProcessReceipt(pUser->hContact, node.getAttr("id"), bRead); + + if (auto *pList = node.getChild("list")) + for (auto &it : pList->getChildren()) + if (it->title == "item") + ProcessReceipt(pUser->hContact, it->getAttr("id"), bRead); + } + + SendAck(node); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnStreamError(const WANode &node) +{ + m_bTerminated = true; + + if (auto *pszCode = node.getAttr("code")) + ProcessFailure(atoi(pszCode)); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnSuccess(const WANode &) +{ + OnLoggedIn(); + + WSSendNode(WANodeIq(IQ::SET, "passive") << XCHILD("active"), &WhatsAppProto::OnIqDoNothing); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::InitPersistentHandlers() +{ + m_arPersistent.insert(new WAPersistentHandler("iq", "set", "md", "pair-device", &WhatsAppProto::OnIqPairDevice)); + m_arPersistent.insert(new WAPersistentHandler("iq", "set", "md", "pair-success", &WhatsAppProto::OnIqPairSuccess)); + + m_arPersistent.insert(new WAPersistentHandler("notification", "devices", 0, 0, &WhatsAppProto::OnNotifyDevices)); + m_arPersistent.insert(new WAPersistentHandler("notification", "encrypt", 0, 0, &WhatsAppProto::OnNotifyEncrypt)); + m_arPersistent.insert(new WAPersistentHandler("notification", "picture", 0, 0, &WhatsAppProto::OnNotifyPicture)); + m_arPersistent.insert(new WAPersistentHandler("notification", "account_sync", 0, 0, &WhatsAppProto::OnAccountSync)); + m_arPersistent.insert(new WAPersistentHandler("notification", "server_sync", 0, 0, &WhatsAppProto::OnServerSync)); + m_arPersistent.insert(new WAPersistentHandler("notification", 0, 0, 0, &WhatsAppProto::OnNotifyAny)); + + m_arPersistent.insert(new WAPersistentHandler("ack", 0, 0, 0, &WhatsAppProto::OnReceiveAck)); + m_arPersistent.insert(new WAPersistentHandler("ib", 0, 0, 0, &WhatsAppProto::OnReceiveInfo)); + m_arPersistent.insert(new WAPersistentHandler("failure", 0, 0, 0, &WhatsAppProto::OnReceiveFailure)); + m_arPersistent.insert(new WAPersistentHandler("message", 0, 0, 0, &WhatsAppProto::OnReceiveMessage)); + m_arPersistent.insert(new WAPersistentHandler("receipt", 0, 0, 0, &WhatsAppProto::OnReceiveReceipt)); + m_arPersistent.insert(new WAPersistentHandler("chatstates", 0, 0, 0, &WhatsAppProto::OnReceiveChatState)); + m_arPersistent.insert(new WAPersistentHandler("stream:error", 0, 0, 0, &WhatsAppProto::OnStreamError)); + m_arPersistent.insert(new WAPersistentHandler("success", 0, 0, 0, &WhatsAppProto::OnSuccess)); + + m_arPersistent.insert(new WAPersistentHandler(0, "result", 0, 0, &WhatsAppProto::OnIqResult)); +} diff --git a/protocols/WhatsApp/src/main.cpp b/protocols/WhatsApp/src/main.cpp index dc4c252927..318a027b76 100644 --- a/protocols/WhatsApp/src/main.cpp +++ b/protocols/WhatsApp/src/main.cpp @@ -1,71 +1,71 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan - -*/ - -#include "stdafx.h" -#include "version.h" - -CMPlugin g_plugin; - -PLUGININFOEX pluginInfo = { - sizeof(PLUGININFOEX), - __PLUGIN_NAME, - PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), - __DESCRIPTION, - __AUTHOR, - __COPYRIGHT, - __AUTHORWEB, - UNICODE_AWARE, //not transient - // {008B9CE1-154B-44E4-9823-97C1AAB00C3C} - { 0x8b9ce1, 0x154b, 0x44e4, { 0x98, 0x23, 0x97, 0xc1, 0xaa, 0xb0, 0xc, 0x3c }} -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// Interface information - -extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; - -///////////////////////////////////////////////////////////////////////////////////////// - -CMPlugin::CMPlugin() : - ACCPROTOPLUGIN(MODULENAME, pluginInfo) -{ - SetUniqueId(DBKEY_ID); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Load - -static int OnPluginLoaded(WPARAM, LPARAM) -{ - g_plugin.bHasMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE); - return 0; -} - -int CMPlugin::Load() -{ - HookEvent(ME_SYSTEM_MODULELOAD, OnPluginLoaded); - HookEvent(ME_SYSTEM_MODULEUNLOAD, OnPluginLoaded); - OnPluginLoaded(0, 0); - - // special netlib user for reading avatars, blobs etc via HTTP protocol - NETLIBUSER nlu = {}; - nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; - nlu.szSettingsModule = "WhatsApp"; - nlu.szDescriptiveName.w = TranslateT("WhatsApp (HTTP)"); - hAvatarUser = Netlib_RegisterUser(&nlu); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Unload - -int CMPlugin::Unload() -{ - Netlib_CloseHandle(hAvatarConn); - Netlib_CloseHandle(hAvatarUser); - return 0; -} +/* + +WhatsApp plugin for Miranda NG +Copyright 2019-23 George Hazan + +*/ + +#include "stdafx.h" +#include "version.h" + +CMPlugin g_plugin; + +PLUGININFOEX pluginInfo = { + sizeof(PLUGININFOEX), + __PLUGIN_NAME, + PLUGIN_MAKE_VERSION(__MAJOR_VERSION, __MINOR_VERSION, __RELEASE_NUM, __BUILD_NUM), + __DESCRIPTION, + __AUTHOR, + __COPYRIGHT, + __AUTHORWEB, + UNICODE_AWARE, //not transient + // {008B9CE1-154B-44E4-9823-97C1AAB00C3C} + { 0x8b9ce1, 0x154b, 0x44e4, { 0x98, 0x23, 0x97, 0xc1, 0xaa, 0xb0, 0xc, 0x3c }} +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// Interface information + +extern "C" __declspec(dllexport) const MUUID MirandaInterfaces[] = { MIID_PROTOCOL, MIID_LAST }; + +///////////////////////////////////////////////////////////////////////////////////////// + +CMPlugin::CMPlugin() : + ACCPROTOPLUGIN(MODULENAME, pluginInfo) +{ + SetUniqueId(DBKEY_ID); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Load + +static int OnPluginLoaded(WPARAM, LPARAM) +{ + g_plugin.bHasMessageState = ServiceExists(MS_MESSAGESTATE_UPDATE); + return 0; +} + +int CMPlugin::Load() +{ + HookEvent(ME_SYSTEM_MODULELOAD, OnPluginLoaded); + HookEvent(ME_SYSTEM_MODULEUNLOAD, OnPluginLoaded); + OnPluginLoaded(0, 0); + + // special netlib user for reading avatars, blobs etc via HTTP protocol + NETLIBUSER nlu = {}; + nlu.flags = NUF_INCOMING | NUF_OUTGOING | NUF_HTTPCONNS | NUF_UNICODE; + nlu.szSettingsModule = "WhatsApp"; + nlu.szDescriptiveName.w = TranslateT("WhatsApp (HTTP)"); + hAvatarUser = Netlib_RegisterUser(&nlu); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Unload + +int CMPlugin::Unload() +{ + Netlib_CloseHandle(hAvatarConn); + Netlib_CloseHandle(hAvatarUser); + return 0; +} diff --git a/protocols/WhatsApp/src/message.cpp b/protocols/WhatsApp/src/message.cpp index e8d2b775ec..250a63e4d1 100644 --- a/protocols/WhatsApp/src/message.cpp +++ b/protocols/WhatsApp/src/message.cpp @@ -1,502 +1,502 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#include "stdafx.h" - -void WhatsAppProto::OnReceiveMessage(const WANode &node) -{ - auto *msgId = node.getAttr("id"); - auto *msgType = node.getAttr("type"); - auto *msgFrom = node.getAttr("from"); - auto *category = node.getAttr("category"); - auto *recipient = node.getAttr("recipient"); - auto *participant = node.getAttr("participant"); - - if (msgType == nullptr || msgFrom == nullptr || msgId == nullptr) { - debugLogA("bad message received: <%s> <%s> <%s>", msgType, msgFrom, msgId); - return; - } - - SendAck(node); - - MEVENT hEvent = db_event_getById(m_szModuleName, msgId); - if (hEvent) { - debugLogA("this message is already processed: %s", msgId); - return; - } - - WAMSG type; - WAJid jid(msgFrom); - CMStringA szAuthor, szChatId; - - if (node.getAttr("offline")) - type.bOffline = true; - - // message from one user to another - if (jid.isUser()) { - if (recipient) { - if (m_szJid != msgFrom) { - debugLogA("strange message: with recipient, but not from me"); - return; - } - szChatId = recipient; - } - else szChatId = msgFrom; - - type.bPrivateChat = true; - szAuthor = msgFrom; - } - else if (jid.isGroup()) { - if (!participant) { - debugLogA("strange message: from group, but without participant"); - return; - } - - type.bGroupChat = true; - szAuthor = participant; - szChatId = msgFrom; - } - else if (jid.isBroadcast()) { - if (!participant) { - debugLogA("strange message: from group, but without participant"); - return; - } - - bool bIsMe = m_szJid == participant; - if (jid.isStatusBroadcast()) { - if (bIsMe) - type.bDirectStatus = true; - else - type.bOtherStatus = true; - } - else { - if (bIsMe) - type.bPeerBroadcast = true; - else - type.bOtherBroadcast = true; - } - szChatId = msgFrom; - szAuthor = participant; - } - else { - debugLogA("invalid message type"); - return; - } - - CMStringA szSender = (type.bPrivateChat) ? szAuthor : szChatId; - bool bFromMe = (m_szJid == msgFrom); - if (!bFromMe && participant) - bFromMe = m_szJid == participant; - - Wa__MessageKey key; - key.remotejid = szChatId.GetBuffer(); - key.id = (char*)msgId; - key.fromme = bFromMe; key.has_fromme = true; - if (participant) - key.participant = (char*)participant; - - Wa__WebMessageInfo msg; - msg.key = &key; - msg.messagetimestamp = _atoi64(node.getAttr("t")); msg.has_messagetimestamp = true; - msg.pushname = (char*)node.getAttr("notify"); - if (bFromMe) - msg.status = WA__WEB_MESSAGE_INFO__STATUS__SERVER_ACK, msg.has_status = true; - - int iDecryptable = 0; - - for (auto &it : node.getChildren()) { - if (it->title != "enc" || it->content.length() == 0) - continue; - - MBinBuffer msgBody; - auto *pszType = it->getAttr("type"); - try { - if (!mir_strcmp(pszType, "pkmsg") || !mir_strcmp(pszType, "msg")) { - CMStringA szUser = (WAJid(szSender).isUser()) ? szSender : szAuthor; - msgBody = m_signalStore.decryptSignalProto(szUser, pszType, it->content); - } - else if (!mir_strcmp(pszType, "skmsg")) { - msgBody = m_signalStore.decryptGroupSignalProto(szSender, szAuthor, it->content); - } - else throw "Invalid e2e type"; - - if (msgBody.isEmpty()) - throw "Invalid e2e message"; - - iDecryptable++; - - proto::Message encMsg(unpadBuffer16(msgBody)); - if (!encMsg) - throw "Invalid decoded message"; - - if (encMsg->devicesentmessage) - msg.message = encMsg->devicesentmessage->message; - else - msg.message = encMsg; - - if (encMsg->senderkeydistributionmessage) - m_signalStore.processSenderKeyMessage(szAuthor, encMsg->senderkeydistributionmessage); - - ProcessMessage(type, msg); - - // send receipt - const char *pszReceiptType = nullptr, *pszReceiptTo = participant; - if (!mir_strcmp(category, "peer")) - pszReceiptType = "peer_msg"; - else if (bFromMe) { - // message was sent by me from a different device - pszReceiptType = "sender"; - if (WAJid(szChatId).isUser()) - pszReceiptTo = szAuthor; - } - else if (!m_hServerConn) - pszReceiptType = "inactive"; - - SendReceipt(szChatId, pszReceiptTo, msgId, pszReceiptType); - } - catch (const char *pszError) { - debugLogA("Message decryption failed with error: %s", pszError); - } - - if (!iDecryptable) { - debugLogA("Nothing to decrypt"); - return; - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static const Wa__Message* getBody(const Wa__Message *message) -{ - if (message->ephemeralmessage) { - auto *pMsg = message->ephemeralmessage->message; - return (pMsg->viewoncemessage) ? pMsg->viewoncemessage->message : pMsg; - } - - if (message->viewoncemessage) - return message->viewoncemessage->message; - - return message; -} - -void WhatsAppProto::ProcessMessage(WAMSG type, const Wa__WebMessageInfo &msg) -{ - auto *key = msg.key; - auto *body = getBody(msg.message); - - debugLogA("Got a message: %s", protobuf_c_text_to_string(&msg).c_str()); - - uint32_t timestamp = msg.messagetimestamp; - char *participant = key->participant, *chatId; - auto *msgId = key->id; - - if (type.bPrivateChat || type.bGroupChat) - chatId = key->remotejid; - else - chatId = (participant) ? participant : key->remotejid; - - WAJid jidFrom(chatId); jidFrom.device = 0; - WAUser *pUser = AddUser(jidFrom.toString(), false); - - if (!key->fromme && msg.pushname && pUser && !pUser->bIsGroupChat) - setUString(pUser->hContact, "Nick", msg.pushname); - - // try to extract some text - if (pUser) { - CMStringA szMessageText(GetMessageText(body)); - if (!szMessageText.IsEmpty()) { - // for chats & group chats store message in profile - if (type.bPrivateChat || type.bGroupChat) { - PROTORECVEVENT pre = {}; - pre.timestamp = timestamp; - pre.szMessage = szMessageText.GetBuffer(); - pre.szMsgId = msgId; - if (type.bOffline) - pre.flags |= PREF_CREATEREAD; - if (key->fromme) - pre.flags |= PREF_SENT; - ProtoChainRecvMsg(pUser->hContact, &pre); - - if (pUser->bIsGroupChat) { - GCEVENT gce = {m_szModuleName, 0, GC_EVENT_MESSAGE}; - gce.dwFlags = GCEF_UTF8; - gce.pszID.a = pUser->szId; - gce.pszUID.a = participant; - gce.bIsMe = key->fromme; - gce.pszText.a = szMessageText.GetBuffer(); - gce.time = timestamp; - Chat_Event(&gce); - } - } - // translate statuses into status messages - else if (type.bOtherStatus || type.bDirectStatus || type.bPeerBroadcast || type.bOtherBroadcast) { - setUString(pUser->hContact, "StatusMsg", szMessageText); - } - } - } - - if (body->protocolmessage) { - auto *protoMsg = body->protocolmessage; - switch (protoMsg->type) { - case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__APP_STATE_SYNC_KEY_SHARE: - for (int i = 0; i < protoMsg->appstatesynckeyshare->n_keys; i++) { - auto *it = protoMsg->appstatesynckeyshare->keys[i]; - auto &keyid = it->keyid->keyid; - auto &keydata = it->keydata->keydata; - - CMStringA szSetting(FORMAT, "AppSyncKey%d", decodeBigEndian(keyid)); - db_set_blob(0, m_szModuleName, szSetting, keydata.data, (unsigned)keydata.len); - } - break; - - case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__APP_STATE_FATAL_EXCEPTION_NOTIFICATION: - m_impl.m_resyncApp.Stop(); - m_impl.m_resyncApp.Start(10000); - break; - - case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__HISTORY_SYNC_NOTIFICATION: - debugLogA("History sync notification"); - if (auto *pHist = protoMsg->historysyncnotification) { - MBinBuffer buf(DownloadEncryptedFile(directPath2url(pHist->directpath), pHist->mediakey, "History")); - if (!buf.isEmpty()) { - MBinBuffer inflate(unzip(unpadBuffer16(buf))); - - proto::HistorySync sync(inflate); - if (sync) - ProcessHistorySync(sync); - } - } - break; - - case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__REVOKE: - break; - - case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__EPHEMERAL_SETTING: - if (pUser) { - setDword(pUser->hContact, DBKEY_EPHEMERAL_TS, timestamp); - setDword(pUser->hContact, DBKEY_EPHEMERAL_EXPIRE, protoMsg->ephemeralexpiration); - } - break; - } - } - else if (body->reactionmessage) { - debugLogA("Got a reaction to a message"); - } - else if (msg.has_messagestubtype) { - switch (msg.messagestubtype) { - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_LEAVE: - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_REMOVE: - debugLogA("Participant %s removed from chat", participant); - break; - - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_ADD: - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_INVITE: - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_ADD_REQUEST_JOIN: - debugLogA("Participant %s added to chat", participant); - break; - - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_DEMOTE: - debugLogA("Participant %s demoted", participant); - break; - - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_PROMOTE: - debugLogA("Participant %s promoted", participant); - break; - - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_CHANGE_ANNOUNCE: - debugLogA("Groupchat announce", participant); - break; - - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_CHANGE_RESTRICT: - debugLogA("Groupchat restriction", participant); - break; - - case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_CHANGE_SUBJECT: - debugLogA("Groupchat subject was changed", participant); - break; - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::OnReceiveAck(const WANode &node) -{ - auto *pUser = FindUser(node.getAttr("from")); - if (pUser == nullptr) - return; - - if (!mir_strcmp(node.getAttr("class"), "message")) { - WAOwnMessage tmp(0, 0, node.getAttr("id")); - { - mir_cslock lck(m_csOwnMessages); - if (auto *pOwn = m_arOwnMsgs.find(&tmp)) { - tmp.pktId = pOwn->pktId; - m_arOwnMsgs.remove(pOwn); - } - else return; - } - ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)tmp.pktId, (LPARAM)tmp.szMessageId.c_str()); - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -bool WhatsAppProto::CreateMsgParticipant(WANode *pParticipants, const WAJid &jid, const MBinBuffer &orig) -{ - int type = 0; - - try { - MBinBuffer pBuffer(m_signalStore.encryptSignalProto(jid, orig, type)); - - auto *pNode = pParticipants->addChild("to"); - pNode->addAttr("jid", jid.toString()); - - auto *pEnc = pNode->addChild("enc"); - *pEnc << CHAR_PARAM("v", "2") << CHAR_PARAM("type", (type == 3) ? "pkmsg" : "msg"); - pEnc->content.assign(pBuffer.data(), pBuffer.length()); - } - catch (const char *) { - } - - return type == 3; -} - -int WhatsAppProto::SendTextMessage(const char *jid, const char *pszMsg) -{ - WAJid toJid(jid); - - // send task creation - auto *pTask = new WASendTask(jid); - - // basic message - Wa__Message__ExtendedTextMessage extMessage; - extMessage.text = (char *)pszMsg; - - Wa__Message body; - body.extendedtextmessage = &extMessage; - - LIST arCheckList(1); - if (toJid.isGroup()) { - MBinBuffer encodedMsg(proto::Serialize(&body)); - padBuffer16(encodedMsg); - - MBinBuffer skmsgKey; - MBinBuffer cipherText(m_signalStore.encryptSenderKey(toJid, m_szJid, encodedMsg, skmsgKey)); - - auto *pEnc = pTask->payLoad.addChild("enc"); - *pEnc << CHAR_PARAM("v", "2") << CHAR_PARAM("type", "skmsg"); - pEnc->content.append(cipherText.data(), cipherText.length()); - - Wa__Message__SenderKeyDistributionMessage sentBody; - sentBody.axolotlsenderkeydistributionmessage.data = skmsgKey.data(); - sentBody.axolotlsenderkeydistributionmessage.len = skmsgKey.length(); - sentBody.has_axolotlsenderkeydistributionmessage = true; - sentBody.groupid = (char*)jid; - - Wa__Message msg; - msg.senderkeydistributionmessage = &sentBody; - - pTask->content.append(proto::Serialize(&msg)); - - if (auto *pChatUser = FindUser(jid)) { - if (pChatUser->si) { - for (auto &it : pChatUser->si->arUsers) { - T2Utf userJid(it->pszUID); - auto *pUser = FindUser(jid); - if (pUser == nullptr) - m_arUsers.insert(pUser = new WAUser(INVALID_CONTACT_ID, userJid, false)); - if (pUser->bDeviceInit) { - for (auto &jt : pUser->arDevices) - pTask->arDest.insert(new WAJid(*jt)); - } - else arCheckList.insert(mir_strdup(userJid)); - } - } - } - } - else { - Wa__Message__DeviceSentMessage sentBody; - sentBody.message = &body; - sentBody.destinationjid = (char*)jid; - - Wa__Message msg; - msg.devicesentmessage = &sentBody; - - pTask->content.append(proto::Serialize(&msg)); - - if (auto *pUser = FindUser(jid)) { - if (pUser->bDeviceInit) { - for (auto &it : pUser->arDevices) - pTask->arDest.insert(new WAJid(*it)); - } - else arCheckList.insert(mir_strdup(jid));; - } - } - - padBuffer16(pTask->content); - - auto *pOwnUser = FindUser(m_szJid); - for (auto &it : pOwnUser->arDevices) - if (it->device != (int)getDword(DBKEY_DEVICE_ID)) - pTask->arDest.insert(new WAJid(*it)); - - // generate & reserve packet id - int pktId; - { - mir_cslock lck(m_csOwnMessages); - pktId = m_iPacketId++; - m_arOwnMsgs.insert(new WAOwnMessage(pktId, jid, pTask->szMsgId)); - } - - // if some keys are missing, schedule task for execution & retrieve keys - if (arCheckList.getCount()) { - SendUsync(arCheckList, pTask); - for (auto &it : arCheckList) - mir_free(it); - } - else // otherwise simply execute the task - SendTask(pTask); - - return pktId; -} - -void WhatsAppProto::FinishTask(WASendTask *pTask) -{ - if (auto *pUser = FindUser(pTask->payLoad.getAttr("to"))) { - if (pUser->bIsGroupChat) { - for (auto &it : pUser->si->getUserList()) - if (auto *pChatUser = FindUser(T2Utf(it->pszUID))) - for (auto &cc: pChatUser->arDevices) - pTask->arDest.insert(new WAJid(*cc)); - } - else for (auto &it : pUser->arDevices) - pTask->arDest.insert(new WAJid(*it)); - } - - SendTask(pTask); -} - -void WhatsAppProto::SendTask(WASendTask *pTask) -{ - // pack all data and send the whole payload - bool shouldIncludeIdentity = false; - auto *pParticipants = pTask->payLoad.addChild("participants"); - - for (auto &it : pTask->arDest) - shouldIncludeIdentity |= CreateMsgParticipant(pParticipants, *it, pTask->content); - - if (shouldIncludeIdentity) { - MBinBuffer encIdentity(m_signalStore.encodeSignedIdentity(true)); - auto *pNode = pTask->payLoad.addChild("device-identity"); - pNode->content.assign(encIdentity.data(), encIdentity.length()); - } - - WSSendNode(pTask->payLoad); - delete pTask; -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +void WhatsAppProto::OnReceiveMessage(const WANode &node) +{ + auto *msgId = node.getAttr("id"); + auto *msgType = node.getAttr("type"); + auto *msgFrom = node.getAttr("from"); + auto *category = node.getAttr("category"); + auto *recipient = node.getAttr("recipient"); + auto *participant = node.getAttr("participant"); + + if (msgType == nullptr || msgFrom == nullptr || msgId == nullptr) { + debugLogA("bad message received: <%s> <%s> <%s>", msgType, msgFrom, msgId); + return; + } + + SendAck(node); + + MEVENT hEvent = db_event_getById(m_szModuleName, msgId); + if (hEvent) { + debugLogA("this message is already processed: %s", msgId); + return; + } + + WAMSG type; + WAJid jid(msgFrom); + CMStringA szAuthor, szChatId; + + if (node.getAttr("offline")) + type.bOffline = true; + + // message from one user to another + if (jid.isUser()) { + if (recipient) { + if (m_szJid != msgFrom) { + debugLogA("strange message: with recipient, but not from me"); + return; + } + szChatId = recipient; + } + else szChatId = msgFrom; + + type.bPrivateChat = true; + szAuthor = msgFrom; + } + else if (jid.isGroup()) { + if (!participant) { + debugLogA("strange message: from group, but without participant"); + return; + } + + type.bGroupChat = true; + szAuthor = participant; + szChatId = msgFrom; + } + else if (jid.isBroadcast()) { + if (!participant) { + debugLogA("strange message: from group, but without participant"); + return; + } + + bool bIsMe = m_szJid == participant; + if (jid.isStatusBroadcast()) { + if (bIsMe) + type.bDirectStatus = true; + else + type.bOtherStatus = true; + } + else { + if (bIsMe) + type.bPeerBroadcast = true; + else + type.bOtherBroadcast = true; + } + szChatId = msgFrom; + szAuthor = participant; + } + else { + debugLogA("invalid message type"); + return; + } + + CMStringA szSender = (type.bPrivateChat) ? szAuthor : szChatId; + bool bFromMe = (m_szJid == msgFrom); + if (!bFromMe && participant) + bFromMe = m_szJid == participant; + + Wa__MessageKey key; + key.remotejid = szChatId.GetBuffer(); + key.id = (char*)msgId; + key.fromme = bFromMe; key.has_fromme = true; + if (participant) + key.participant = (char*)participant; + + Wa__WebMessageInfo msg; + msg.key = &key; + msg.messagetimestamp = _atoi64(node.getAttr("t")); msg.has_messagetimestamp = true; + msg.pushname = (char*)node.getAttr("notify"); + if (bFromMe) + msg.status = WA__WEB_MESSAGE_INFO__STATUS__SERVER_ACK, msg.has_status = true; + + int iDecryptable = 0; + + for (auto &it : node.getChildren()) { + if (it->title != "enc" || it->content.length() == 0) + continue; + + MBinBuffer msgBody; + auto *pszType = it->getAttr("type"); + try { + if (!mir_strcmp(pszType, "pkmsg") || !mir_strcmp(pszType, "msg")) { + CMStringA szUser = (WAJid(szSender).isUser()) ? szSender : szAuthor; + msgBody = m_signalStore.decryptSignalProto(szUser, pszType, it->content); + } + else if (!mir_strcmp(pszType, "skmsg")) { + msgBody = m_signalStore.decryptGroupSignalProto(szSender, szAuthor, it->content); + } + else throw "Invalid e2e type"; + + if (msgBody.isEmpty()) + throw "Invalid e2e message"; + + iDecryptable++; + + proto::Message encMsg(unpadBuffer16(msgBody)); + if (!encMsg) + throw "Invalid decoded message"; + + if (encMsg->devicesentmessage) + msg.message = encMsg->devicesentmessage->message; + else + msg.message = encMsg; + + if (encMsg->senderkeydistributionmessage) + m_signalStore.processSenderKeyMessage(szAuthor, encMsg->senderkeydistributionmessage); + + ProcessMessage(type, msg); + + // send receipt + const char *pszReceiptType = nullptr, *pszReceiptTo = participant; + if (!mir_strcmp(category, "peer")) + pszReceiptType = "peer_msg"; + else if (bFromMe) { + // message was sent by me from a different device + pszReceiptType = "sender"; + if (WAJid(szChatId).isUser()) + pszReceiptTo = szAuthor; + } + else if (!m_hServerConn) + pszReceiptType = "inactive"; + + SendReceipt(szChatId, pszReceiptTo, msgId, pszReceiptType); + } + catch (const char *pszError) { + debugLogA("Message decryption failed with error: %s", pszError); + } + + if (!iDecryptable) { + debugLogA("Nothing to decrypt"); + return; + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static const Wa__Message* getBody(const Wa__Message *message) +{ + if (message->ephemeralmessage) { + auto *pMsg = message->ephemeralmessage->message; + return (pMsg->viewoncemessage) ? pMsg->viewoncemessage->message : pMsg; + } + + if (message->viewoncemessage) + return message->viewoncemessage->message; + + return message; +} + +void WhatsAppProto::ProcessMessage(WAMSG type, const Wa__WebMessageInfo &msg) +{ + auto *key = msg.key; + auto *body = getBody(msg.message); + + debugLogA("Got a message: %s", protobuf_c_text_to_string(&msg).c_str()); + + uint32_t timestamp = msg.messagetimestamp; + char *participant = key->participant, *chatId; + auto *msgId = key->id; + + if (type.bPrivateChat || type.bGroupChat) + chatId = key->remotejid; + else + chatId = (participant) ? participant : key->remotejid; + + WAJid jidFrom(chatId); jidFrom.device = 0; + WAUser *pUser = AddUser(jidFrom.toString(), false); + + if (!key->fromme && msg.pushname && pUser && !pUser->bIsGroupChat) + setUString(pUser->hContact, "Nick", msg.pushname); + + // try to extract some text + if (pUser) { + CMStringA szMessageText(GetMessageText(body)); + if (!szMessageText.IsEmpty()) { + // for chats & group chats store message in profile + if (type.bPrivateChat || type.bGroupChat) { + PROTORECVEVENT pre = {}; + pre.timestamp = timestamp; + pre.szMessage = szMessageText.GetBuffer(); + pre.szMsgId = msgId; + if (type.bOffline) + pre.flags |= PREF_CREATEREAD; + if (key->fromme) + pre.flags |= PREF_SENT; + ProtoChainRecvMsg(pUser->hContact, &pre); + + if (pUser->bIsGroupChat) { + GCEVENT gce = {m_szModuleName, 0, GC_EVENT_MESSAGE}; + gce.dwFlags = GCEF_UTF8; + gce.pszID.a = pUser->szId; + gce.pszUID.a = participant; + gce.bIsMe = key->fromme; + gce.pszText.a = szMessageText.GetBuffer(); + gce.time = timestamp; + Chat_Event(&gce); + } + } + // translate statuses into status messages + else if (type.bOtherStatus || type.bDirectStatus || type.bPeerBroadcast || type.bOtherBroadcast) { + setUString(pUser->hContact, "StatusMsg", szMessageText); + } + } + } + + if (body->protocolmessage) { + auto *protoMsg = body->protocolmessage; + switch (protoMsg->type) { + case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__APP_STATE_SYNC_KEY_SHARE: + for (int i = 0; i < protoMsg->appstatesynckeyshare->n_keys; i++) { + auto *it = protoMsg->appstatesynckeyshare->keys[i]; + auto &keyid = it->keyid->keyid; + auto &keydata = it->keydata->keydata; + + CMStringA szSetting(FORMAT, "AppSyncKey%d", decodeBigEndian(keyid)); + db_set_blob(0, m_szModuleName, szSetting, keydata.data, (unsigned)keydata.len); + } + break; + + case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__APP_STATE_FATAL_EXCEPTION_NOTIFICATION: + m_impl.m_resyncApp.Stop(); + m_impl.m_resyncApp.Start(10000); + break; + + case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__HISTORY_SYNC_NOTIFICATION: + debugLogA("History sync notification"); + if (auto *pHist = protoMsg->historysyncnotification) { + MBinBuffer buf(DownloadEncryptedFile(directPath2url(pHist->directpath), pHist->mediakey, "History")); + if (!buf.isEmpty()) { + MBinBuffer inflate(unzip(unpadBuffer16(buf))); + + proto::HistorySync sync(inflate); + if (sync) + ProcessHistorySync(sync); + } + } + break; + + case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__REVOKE: + break; + + case WA__MESSAGE__PROTOCOL_MESSAGE__TYPE__EPHEMERAL_SETTING: + if (pUser) { + setDword(pUser->hContact, DBKEY_EPHEMERAL_TS, timestamp); + setDword(pUser->hContact, DBKEY_EPHEMERAL_EXPIRE, protoMsg->ephemeralexpiration); + } + break; + } + } + else if (body->reactionmessage) { + debugLogA("Got a reaction to a message"); + } + else if (msg.has_messagestubtype) { + switch (msg.messagestubtype) { + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_LEAVE: + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_REMOVE: + debugLogA("Participant %s removed from chat", participant); + break; + + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_ADD: + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_INVITE: + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_ADD_REQUEST_JOIN: + debugLogA("Participant %s added to chat", participant); + break; + + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_DEMOTE: + debugLogA("Participant %s demoted", participant); + break; + + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_PARTICIPANT_PROMOTE: + debugLogA("Participant %s promoted", participant); + break; + + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_CHANGE_ANNOUNCE: + debugLogA("Groupchat announce", participant); + break; + + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_CHANGE_RESTRICT: + debugLogA("Groupchat restriction", participant); + break; + + case WA__WEB_MESSAGE_INFO__STUB_TYPE__GROUP_CHANGE_SUBJECT: + debugLogA("Groupchat subject was changed", participant); + break; + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::OnReceiveAck(const WANode &node) +{ + auto *pUser = FindUser(node.getAttr("from")); + if (pUser == nullptr) + return; + + if (!mir_strcmp(node.getAttr("class"), "message")) { + WAOwnMessage tmp(0, 0, node.getAttr("id")); + { + mir_cslock lck(m_csOwnMessages); + if (auto *pOwn = m_arOwnMsgs.find(&tmp)) { + tmp.pktId = pOwn->pktId; + m_arOwnMsgs.remove(pOwn); + } + else return; + } + ProtoBroadcastAck(pUser->hContact, ACKTYPE_MESSAGE, ACKRESULT_SUCCESS, (HANDLE)tmp.pktId, (LPARAM)tmp.szMessageId.c_str()); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +bool WhatsAppProto::CreateMsgParticipant(WANode *pParticipants, const WAJid &jid, const MBinBuffer &orig) +{ + int type = 0; + + try { + MBinBuffer pBuffer(m_signalStore.encryptSignalProto(jid, orig, type)); + + auto *pNode = pParticipants->addChild("to"); + pNode->addAttr("jid", jid.toString()); + + auto *pEnc = pNode->addChild("enc"); + *pEnc << CHAR_PARAM("v", "2") << CHAR_PARAM("type", (type == 3) ? "pkmsg" : "msg"); + pEnc->content.assign(pBuffer.data(), pBuffer.length()); + } + catch (const char *) { + } + + return type == 3; +} + +int WhatsAppProto::SendTextMessage(const char *jid, const char *pszMsg) +{ + WAJid toJid(jid); + + // send task creation + auto *pTask = new WASendTask(jid); + + // basic message + Wa__Message__ExtendedTextMessage extMessage; + extMessage.text = (char *)pszMsg; + + Wa__Message body; + body.extendedtextmessage = &extMessage; + + LIST arCheckList(1); + if (toJid.isGroup()) { + MBinBuffer encodedMsg(proto::Serialize(&body)); + padBuffer16(encodedMsg); + + MBinBuffer skmsgKey; + MBinBuffer cipherText(m_signalStore.encryptSenderKey(toJid, m_szJid, encodedMsg, skmsgKey)); + + auto *pEnc = pTask->payLoad.addChild("enc"); + *pEnc << CHAR_PARAM("v", "2") << CHAR_PARAM("type", "skmsg"); + pEnc->content.append(cipherText.data(), cipherText.length()); + + Wa__Message__SenderKeyDistributionMessage sentBody; + sentBody.axolotlsenderkeydistributionmessage.data = skmsgKey.data(); + sentBody.axolotlsenderkeydistributionmessage.len = skmsgKey.length(); + sentBody.has_axolotlsenderkeydistributionmessage = true; + sentBody.groupid = (char*)jid; + + Wa__Message msg; + msg.senderkeydistributionmessage = &sentBody; + + pTask->content.append(proto::Serialize(&msg)); + + if (auto *pChatUser = FindUser(jid)) { + if (pChatUser->si) { + for (auto &it : pChatUser->si->arUsers) { + T2Utf userJid(it->pszUID); + auto *pUser = FindUser(jid); + if (pUser == nullptr) + m_arUsers.insert(pUser = new WAUser(INVALID_CONTACT_ID, userJid, false)); + if (pUser->bDeviceInit) { + for (auto &jt : pUser->arDevices) + pTask->arDest.insert(new WAJid(*jt)); + } + else arCheckList.insert(mir_strdup(userJid)); + } + } + } + } + else { + Wa__Message__DeviceSentMessage sentBody; + sentBody.message = &body; + sentBody.destinationjid = (char*)jid; + + Wa__Message msg; + msg.devicesentmessage = &sentBody; + + pTask->content.append(proto::Serialize(&msg)); + + if (auto *pUser = FindUser(jid)) { + if (pUser->bDeviceInit) { + for (auto &it : pUser->arDevices) + pTask->arDest.insert(new WAJid(*it)); + } + else arCheckList.insert(mir_strdup(jid));; + } + } + + padBuffer16(pTask->content); + + auto *pOwnUser = FindUser(m_szJid); + for (auto &it : pOwnUser->arDevices) + if (it->device != (int)getDword(DBKEY_DEVICE_ID)) + pTask->arDest.insert(new WAJid(*it)); + + // generate & reserve packet id + int pktId; + { + mir_cslock lck(m_csOwnMessages); + pktId = m_iPacketId++; + m_arOwnMsgs.insert(new WAOwnMessage(pktId, jid, pTask->szMsgId)); + } + + // if some keys are missing, schedule task for execution & retrieve keys + if (arCheckList.getCount()) { + SendUsync(arCheckList, pTask); + for (auto &it : arCheckList) + mir_free(it); + } + else // otherwise simply execute the task + SendTask(pTask); + + return pktId; +} + +void WhatsAppProto::FinishTask(WASendTask *pTask) +{ + if (auto *pUser = FindUser(pTask->payLoad.getAttr("to"))) { + if (pUser->bIsGroupChat) { + for (auto &it : pUser->si->getUserList()) + if (auto *pChatUser = FindUser(T2Utf(it->pszUID))) + for (auto &cc: pChatUser->arDevices) + pTask->arDest.insert(new WAJid(*cc)); + } + else for (auto &it : pUser->arDevices) + pTask->arDest.insert(new WAJid(*it)); + } + + SendTask(pTask); +} + +void WhatsAppProto::SendTask(WASendTask *pTask) +{ + // pack all data and send the whole payload + bool shouldIncludeIdentity = false; + auto *pParticipants = pTask->payLoad.addChild("participants"); + + for (auto &it : pTask->arDest) + shouldIncludeIdentity |= CreateMsgParticipant(pParticipants, *it, pTask->content); + + if (shouldIncludeIdentity) { + MBinBuffer encIdentity(m_signalStore.encodeSignedIdentity(true)); + auto *pNode = pTask->payLoad.addChild("device-identity"); + pNode->content.assign(encIdentity.data(), encIdentity.length()); + } + + WSSendNode(pTask->payLoad); + delete pTask; +} diff --git a/protocols/WhatsApp/src/noise.cpp b/protocols/WhatsApp/src/noise.cpp index 004b71cdef..18ae206519 100644 --- a/protocols/WhatsApp/src/noise.cpp +++ b/protocols/WhatsApp/src/noise.cpp @@ -1,201 +1,201 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -WANoise class implementation - -*/ - -#include "stdafx.h" - -static uint8_t intro_header[] = {87, 65, 6, DICT_VERSION}; -static uint8_t noise_init[] = "Noise_XX_25519_AESGCM_SHA256\0\0\0\0"; - -WANoise::WANoise(WhatsAppProto *_ppro) : - ppro(_ppro) -{ - salt.assign(noise_init, 32); - encKey.assign(noise_init, 32); - decKey.assign(noise_init, 32); - - // generate ephemeral keys: public & private - ec_key_pair *pKeys; - curve_generate_key_pair(ppro->m_signalStore.CTX(), &pKeys); - - auto *pPubKey = ec_key_pair_get_public(pKeys); - ephemeral.pub.assign(pPubKey->data, sizeof(pPubKey->data)); - - auto *pPrivKey = ec_key_pair_get_private(pKeys); - ephemeral.priv.assign(pPrivKey->data, sizeof(pPrivKey->data)); - ec_key_pair_destroy((signal_type_base*)pKeys); - - // prepare hash - memcpy(hash, noise_init, 32); - updateHash(intro_header, 4); - updateHash(ephemeral.pub.data(), ephemeral.pub.length()); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// libsignal data initialization - -void WANoise::init() -{ - // no data? generate them - if (ppro->getDword(DBKEY_REG_ID, 0xFFFF) == 0xFFFF) { - // generate registration id - uint32_t regId; - Utils_GetRandom(®Id, sizeof(regId)); - ppro->setDword(DBKEY_REG_ID, regId & 0x3FFF); - - // generate secret key - uint8_t secretKey[32]; - Utils_GetRandom(secretKey, sizeof(secretKey)); - db_set_blob(0, ppro->m_szModuleName, DBKEY_SECRET_KEY, secretKey, sizeof(secretKey)); - - // generate noise keys (private & public) - ec_key_pair *pKeys; - curve_generate_key_pair(ppro->m_signalStore.CTX(), &pKeys); - - auto *pPubKey = ec_key_pair_get_public(pKeys); - db_set_blob(0, ppro->m_szModuleName, DBKEY_NOISE_PUB, pPubKey->data, sizeof(pPubKey->data)); - - auto *pPrivKey = ec_key_pair_get_private(pKeys); - db_set_blob(0, ppro->m_szModuleName, DBKEY_NOISE_PRIV, pPrivKey->data, sizeof(pPrivKey->data)); - ec_key_pair_destroy((signal_type_base *)pKeys); - } - - noiseKeys.pub = ppro->getBlob(DBKEY_NOISE_PUB); - noiseKeys.priv = ppro->getBlob(DBKEY_NOISE_PRIV); -} - -void WANoise::finish() -{ - deriveKey("", 0, encKey, decKey); - readCounter = writeCounter = 0; - memset(hash, 0, sizeof(hash)); - bInitFinished = true; -} - -void WANoise::deriveKey(const void *pData, size_t cbLen, MBinBuffer &write, MBinBuffer &read) -{ - size_t outlen = 64; - uint8_t out[64]; - HKDF(EVP_sha256(), salt.data(), (int)salt.length(), (BYTE *)pData, (int)cbLen, (BYTE *)"", 0, out, outlen); - - write.assign(out, 32); - read.assign(out + 32, 32); -} - -void WANoise::mixIntoKey(const void *n, const void *p) -{ - uint8_t tmp[32]; - curve25519_donna((unsigned char *)tmp, (const unsigned char *)n, (const unsigned char *)p); - - deriveKey(tmp, sizeof(tmp), salt, encKey); - decKey.assign(encKey.data(), encKey.length()); - readCounter = writeCounter = 0; -} - -MBinBuffer WANoise::decrypt(const void *pData, size_t cbLen) -{ - uint8_t iv[12]; - generateIV(iv, (bInitFinished) ? readCounter : writeCounter); - - MBinBuffer res; - if (!bInitFinished) - res = aesDecrypt(EVP_aes_256_gcm(), decKey.data(), iv, pData, cbLen, hash, sizeof(hash)); - else - res = aesDecrypt(EVP_aes_256_gcm(), decKey.data(), iv, pData, cbLen); - - updateHash(pData, cbLen); - return res; -} - -size_t WANoise::decodeFrame(const uint8_t *&p, size_t &cbLen) -{ - if (cbLen < 3) - return 0; - - size_t payloadLen = 0; - for (int i = 0; i < 3; i++) { - payloadLen <<= 8; - payloadLen += p[i]; - } - - // ppro->debugLogA("got payload of size %d", payloadLen); - - cbLen -= 3; - if (payloadLen > cbLen) { - ppro->debugLogA("payload length %d exceeds capacity %d", payloadLen, cbLen); - return 0; - } - - p += 3; - return payloadLen; -} - -MBinBuffer WANoise::encodeFrame(const void *pData, size_t cbLen) -{ - MBinBuffer res; - if (!bSendIntro) { - bSendIntro = true; - res.append(intro_header, 4); - } - - uint8_t buf[3]; - size_t foo = cbLen; - for (int i = 0; i < 3; i++) { - buf[2 - i] = foo & 0xFF; - foo >>= 8; - } - res.append(buf, 3); - res.append(pData, cbLen); - return res; -} - -MBinBuffer WANoise::encrypt(const void *pData, size_t cbLen) -{ - uint8_t iv[12]; - generateIV(iv, writeCounter); - - MBinBuffer res; - uint8_t outbuf[1024 + 64]; - - int enc_len = 0, final_len = 0; - EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); - EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, encKey.data(), iv); - - if (!bInitFinished) - EVP_EncryptUpdate(ctx, NULL, &enc_len, hash, sizeof(hash)); - - for (size_t len = 0; len < cbLen; len += 1024) { - size_t portionSize = cbLen - len; - EVP_EncryptUpdate(ctx, outbuf, &enc_len, (BYTE *)pData + len, (int)min(portionSize, 1024)); - res.append(outbuf, enc_len); - } - EVP_EncryptFinal_ex(ctx, outbuf, &final_len); - if (final_len) - res.append(outbuf, final_len); - - uint8_t tag[16]; - EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, sizeof(tag), tag); - res.append(tag, sizeof(tag)); - - EVP_CIPHER_CTX_free(ctx); - - updateHash(res.data(), res.length()); - return res; -} - -void WANoise::updateHash(const void *pData, size_t cbLen) -{ - if (bInitFinished) - return; - - SHA256_CTX ctx; - SHA256_Init(&ctx); - SHA256_Update(&ctx, hash, sizeof(hash)); - SHA256_Update(&ctx, pData, cbLen); - SHA256_Final(hash, &ctx); -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +WANoise class implementation + +*/ + +#include "stdafx.h" + +static uint8_t intro_header[] = {87, 65, 6, DICT_VERSION}; +static uint8_t noise_init[] = "Noise_XX_25519_AESGCM_SHA256\0\0\0\0"; + +WANoise::WANoise(WhatsAppProto *_ppro) : + ppro(_ppro) +{ + salt.assign(noise_init, 32); + encKey.assign(noise_init, 32); + decKey.assign(noise_init, 32); + + // generate ephemeral keys: public & private + ec_key_pair *pKeys; + curve_generate_key_pair(ppro->m_signalStore.CTX(), &pKeys); + + auto *pPubKey = ec_key_pair_get_public(pKeys); + ephemeral.pub.assign(pPubKey->data, sizeof(pPubKey->data)); + + auto *pPrivKey = ec_key_pair_get_private(pKeys); + ephemeral.priv.assign(pPrivKey->data, sizeof(pPrivKey->data)); + ec_key_pair_destroy((signal_type_base*)pKeys); + + // prepare hash + memcpy(hash, noise_init, 32); + updateHash(intro_header, 4); + updateHash(ephemeral.pub.data(), ephemeral.pub.length()); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// libsignal data initialization + +void WANoise::init() +{ + // no data? generate them + if (ppro->getDword(DBKEY_REG_ID, 0xFFFF) == 0xFFFF) { + // generate registration id + uint32_t regId; + Utils_GetRandom(®Id, sizeof(regId)); + ppro->setDword(DBKEY_REG_ID, regId & 0x3FFF); + + // generate secret key + uint8_t secretKey[32]; + Utils_GetRandom(secretKey, sizeof(secretKey)); + db_set_blob(0, ppro->m_szModuleName, DBKEY_SECRET_KEY, secretKey, sizeof(secretKey)); + + // generate noise keys (private & public) + ec_key_pair *pKeys; + curve_generate_key_pair(ppro->m_signalStore.CTX(), &pKeys); + + auto *pPubKey = ec_key_pair_get_public(pKeys); + db_set_blob(0, ppro->m_szModuleName, DBKEY_NOISE_PUB, pPubKey->data, sizeof(pPubKey->data)); + + auto *pPrivKey = ec_key_pair_get_private(pKeys); + db_set_blob(0, ppro->m_szModuleName, DBKEY_NOISE_PRIV, pPrivKey->data, sizeof(pPrivKey->data)); + ec_key_pair_destroy((signal_type_base *)pKeys); + } + + noiseKeys.pub = ppro->getBlob(DBKEY_NOISE_PUB); + noiseKeys.priv = ppro->getBlob(DBKEY_NOISE_PRIV); +} + +void WANoise::finish() +{ + deriveKey("", 0, encKey, decKey); + readCounter = writeCounter = 0; + memset(hash, 0, sizeof(hash)); + bInitFinished = true; +} + +void WANoise::deriveKey(const void *pData, size_t cbLen, MBinBuffer &write, MBinBuffer &read) +{ + size_t outlen = 64; + uint8_t out[64]; + HKDF(EVP_sha256(), salt.data(), (int)salt.length(), (BYTE *)pData, (int)cbLen, (BYTE *)"", 0, out, outlen); + + write.assign(out, 32); + read.assign(out + 32, 32); +} + +void WANoise::mixIntoKey(const void *n, const void *p) +{ + uint8_t tmp[32]; + curve25519_donna((unsigned char *)tmp, (const unsigned char *)n, (const unsigned char *)p); + + deriveKey(tmp, sizeof(tmp), salt, encKey); + decKey.assign(encKey.data(), encKey.length()); + readCounter = writeCounter = 0; +} + +MBinBuffer WANoise::decrypt(const void *pData, size_t cbLen) +{ + uint8_t iv[12]; + generateIV(iv, (bInitFinished) ? readCounter : writeCounter); + + MBinBuffer res; + if (!bInitFinished) + res = aesDecrypt(EVP_aes_256_gcm(), decKey.data(), iv, pData, cbLen, hash, sizeof(hash)); + else + res = aesDecrypt(EVP_aes_256_gcm(), decKey.data(), iv, pData, cbLen); + + updateHash(pData, cbLen); + return res; +} + +size_t WANoise::decodeFrame(const uint8_t *&p, size_t &cbLen) +{ + if (cbLen < 3) + return 0; + + size_t payloadLen = 0; + for (int i = 0; i < 3; i++) { + payloadLen <<= 8; + payloadLen += p[i]; + } + + // ppro->debugLogA("got payload of size %d", payloadLen); + + cbLen -= 3; + if (payloadLen > cbLen) { + ppro->debugLogA("payload length %d exceeds capacity %d", payloadLen, cbLen); + return 0; + } + + p += 3; + return payloadLen; +} + +MBinBuffer WANoise::encodeFrame(const void *pData, size_t cbLen) +{ + MBinBuffer res; + if (!bSendIntro) { + bSendIntro = true; + res.append(intro_header, 4); + } + + uint8_t buf[3]; + size_t foo = cbLen; + for (int i = 0; i < 3; i++) { + buf[2 - i] = foo & 0xFF; + foo >>= 8; + } + res.append(buf, 3); + res.append(pData, cbLen); + return res; +} + +MBinBuffer WANoise::encrypt(const void *pData, size_t cbLen) +{ + uint8_t iv[12]; + generateIV(iv, writeCounter); + + MBinBuffer res; + uint8_t outbuf[1024 + 64]; + + int enc_len = 0, final_len = 0; + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, encKey.data(), iv); + + if (!bInitFinished) + EVP_EncryptUpdate(ctx, NULL, &enc_len, hash, sizeof(hash)); + + for (size_t len = 0; len < cbLen; len += 1024) { + size_t portionSize = cbLen - len; + EVP_EncryptUpdate(ctx, outbuf, &enc_len, (BYTE *)pData + len, (int)min(portionSize, 1024)); + res.append(outbuf, enc_len); + } + EVP_EncryptFinal_ex(ctx, outbuf, &final_len); + if (final_len) + res.append(outbuf, final_len); + + uint8_t tag[16]; + EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, sizeof(tag), tag); + res.append(tag, sizeof(tag)); + + EVP_CIPHER_CTX_free(ctx); + + updateHash(res.data(), res.length()); + return res; +} + +void WANoise::updateHash(const void *pData, size_t cbLen) +{ + if (bInitFinished) + return; + + SHA256_CTX ctx; + SHA256_Init(&ctx); + SHA256_Update(&ctx, hash, sizeof(hash)); + SHA256_Update(&ctx, pData, cbLen); + SHA256_Final(hash, &ctx); +} diff --git a/protocols/WhatsApp/src/options.cpp b/protocols/WhatsApp/src/options.cpp index 3df8e69343..9945da04e5 100644 --- a/protocols/WhatsApp/src/options.cpp +++ b/protocols/WhatsApp/src/options.cpp @@ -1,95 +1,95 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -class COptionsDlg : public CProtoDlgBase -{ - CCtrlCheck chkHideChats, chkBbcodes; - CCtrlEdit edtGroup, edtNick, edtDevName; - CCtrlButton btnUnregister; - ptrW m_wszOldGroup; - -public: - COptionsDlg(WhatsAppProto *ppro, int iDlgID, bool bFullDlg) : - CProtoDlgBase(ppro, iDlgID), - chkBbcodes(this, IDC_USEBBCODES), - chkHideChats(this, IDC_HIDECHATS), - edtNick(this, IDC_NICK), - edtGroup(this, IDC_DEFGROUP), - edtDevName(this, IDC_DEVICE_NAME), - btnUnregister(this, IDC_UNREGISTER), - m_wszOldGroup(mir_wstrdup(ppro->m_wszDefaultGroup)) - { - CreateLink(edtNick, ppro->m_wszNick); - CreateLink(edtGroup, ppro->m_wszDefaultGroup); - CreateLink(edtDevName, ppro->m_wszDeviceName); - - if (bFullDlg) { - CreateLink(chkHideChats, ppro->m_bHideGroupchats); - CreateLink(chkBbcodes, ppro->m_bUseBbcodes); - } - - btnUnregister.OnClick = Callback(this, &COptionsDlg::onClick_Unregister); - } - - bool OnInitDialog() override - { - if (!m_proto->getMStringA(DBKEY_ID).IsEmpty()) - edtDevName.Disable(); - return true; - } - - bool OnApply() override - { - if (mir_wstrlen(m_proto->m_wszNick)) { - SetFocus(edtNick.GetHwnd()); - return false; - } - - if (mir_wstrcmp(m_proto->m_wszDefaultGroup, m_wszOldGroup)) - Clist_GroupCreate(0, m_proto->m_wszDefaultGroup); - return true; - } - - void onClick_Unregister(CCtrlButton *) - { - if (IDYES != MessageBoxW(0, TranslateT("Do you really want to unregister Miranda?"), m_proto->m_tszUserName, MB_ICONQUESTION | MB_YESNO)) - return; - - if (m_proto->isOnline()) - m_proto->SendUnregister(); - else - m_proto->OnErase(); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -INT_PTR WhatsAppProto::SvcCreateAccMgrUI(WPARAM, LPARAM hwndParent) -{ - auto *pDlg = new COptionsDlg(this, IDD_ACCMGRUI, false); - pDlg->SetParent((HWND)hwndParent); - pDlg->Create(); - return (INT_PTR)pDlg->GetHwnd(); -} - -int WhatsAppProto::OnOptionsInit(WPARAM wParam, LPARAM) -{ - OPTIONSDIALOGPAGE odp = {}; - odp.szTitle.w = m_tszUserName; - odp.flags = ODPF_UNICODE; - odp.szGroup.w = LPGENW("Network"); - - odp.position = 1; - odp.szTab.w = LPGENW("Account"); - odp.pDialog = new COptionsDlg(this, IDD_OPTIONS, true); - g_plugin.addOptions(wParam, &odp); - return 0; -} +/* + +WhatsApp plugin for Miranda NG +Copyright 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +class COptionsDlg : public CProtoDlgBase +{ + CCtrlCheck chkHideChats, chkBbcodes; + CCtrlEdit edtGroup, edtNick, edtDevName; + CCtrlButton btnUnregister; + ptrW m_wszOldGroup; + +public: + COptionsDlg(WhatsAppProto *ppro, int iDlgID, bool bFullDlg) : + CProtoDlgBase(ppro, iDlgID), + chkBbcodes(this, IDC_USEBBCODES), + chkHideChats(this, IDC_HIDECHATS), + edtNick(this, IDC_NICK), + edtGroup(this, IDC_DEFGROUP), + edtDevName(this, IDC_DEVICE_NAME), + btnUnregister(this, IDC_UNREGISTER), + m_wszOldGroup(mir_wstrdup(ppro->m_wszDefaultGroup)) + { + CreateLink(edtNick, ppro->m_wszNick); + CreateLink(edtGroup, ppro->m_wszDefaultGroup); + CreateLink(edtDevName, ppro->m_wszDeviceName); + + if (bFullDlg) { + CreateLink(chkHideChats, ppro->m_bHideGroupchats); + CreateLink(chkBbcodes, ppro->m_bUseBbcodes); + } + + btnUnregister.OnClick = Callback(this, &COptionsDlg::onClick_Unregister); + } + + bool OnInitDialog() override + { + if (!m_proto->getMStringA(DBKEY_ID).IsEmpty()) + edtDevName.Disable(); + return true; + } + + bool OnApply() override + { + if (mir_wstrlen(m_proto->m_wszNick)) { + SetFocus(edtNick.GetHwnd()); + return false; + } + + if (mir_wstrcmp(m_proto->m_wszDefaultGroup, m_wszOldGroup)) + Clist_GroupCreate(0, m_proto->m_wszDefaultGroup); + return true; + } + + void onClick_Unregister(CCtrlButton *) + { + if (IDYES != MessageBoxW(0, TranslateT("Do you really want to unregister Miranda?"), m_proto->m_tszUserName, MB_ICONQUESTION | MB_YESNO)) + return; + + if (m_proto->isOnline()) + m_proto->SendUnregister(); + else + m_proto->OnErase(); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +INT_PTR WhatsAppProto::SvcCreateAccMgrUI(WPARAM, LPARAM hwndParent) +{ + auto *pDlg = new COptionsDlg(this, IDD_ACCMGRUI, false); + pDlg->SetParent((HWND)hwndParent); + pDlg->Create(); + return (INT_PTR)pDlg->GetHwnd(); +} + +int WhatsAppProto::OnOptionsInit(WPARAM wParam, LPARAM) +{ + OPTIONSDIALOGPAGE odp = {}; + odp.szTitle.w = m_tszUserName; + odp.flags = ODPF_UNICODE; + odp.szGroup.w = LPGENW("Network"); + + odp.position = 1; + odp.szTab.w = LPGENW("Account"); + odp.pDialog = new COptionsDlg(this, IDD_OPTIONS, true); + g_plugin.addOptions(wParam, &odp); + return 0; +} diff --git a/protocols/WhatsApp/src/proto.cpp b/protocols/WhatsApp/src/proto.cpp index f97aa989e8..006f78dfc0 100644 --- a/protocols/WhatsApp/src/proto.cpp +++ b/protocols/WhatsApp/src/proto.cpp @@ -1,7 +1,7 @@ /* WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan +Copyright 2019-23 George Hazan */ diff --git a/protocols/WhatsApp/src/proto.h b/protocols/WhatsApp/src/proto.h index e6088cd4b8..dbcf57b597 100644 --- a/protocols/WhatsApp/src/proto.h +++ b/protocols/WhatsApp/src/proto.h @@ -1,7 +1,7 @@ /* WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan +Copyright 2019-23 George Hazan */ diff --git a/protocols/WhatsApp/src/qrcode.cpp b/protocols/WhatsApp/src/qrcode.cpp index 18edfc3e91..22c9e1da95 100644 --- a/protocols/WhatsApp/src/qrcode.cpp +++ b/protocols/WhatsApp/src/qrcode.cpp @@ -1,137 +1,137 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan - -*/ - -#include "stdafx.h" - -class CWhatsAppQRDlg : public CProtoDlgBase -{ -public: - CWhatsAppQRDlg(WhatsAppProto *ppro) : - CProtoDlgBase(ppro, IDD_SHOWQR) - {} - - void OnDestroy() override - { - m_proto->m_pQRDlg = nullptr; - - if (!m_bSucceeded) - m_proto->ShutdownSession(); - } - - void SetSuccess() - { - m_bSucceeded = true; - } - - void SetData(const CMStringA &str) - { - auto *pQR = QRcode_encodeString(str, 0, QR_ECLEVEL_L, QR_MODE_8, 1); - - HWND hwndRc = GetDlgItem(m_hwnd, IDC_QRPIC); - RECT rc; - GetClientRect(hwndRc, &rc); - - ::SetForegroundWindow(m_hwnd); - - int scale = 8; // (rc.bottom - rc.top) / pQR->width; - int rowLen = pQR->width * scale * 3; - if (rowLen % 4) - rowLen = (rowLen / 4 + 1) * 4; - int dataLen = rowLen * pQR->width * scale; - - mir_ptr pData((BYTE *)mir_alloc(dataLen)); - if (pData == nullptr) { - QRcode_free(pQR); - return; - } - - memset(pData, 0xFF, dataLen); // white background by default - - const BYTE *s = pQR->data; - for (int y = 0; y < pQR->width; y++) { - BYTE *d = pData.get() + rowLen * y * scale; - for (int x = 0; x < pQR->width; x++) { - if (*s & 1) - for (int i = 0; i < scale; i++) - for (int j = 0; j < scale; j++) { - d[j * 3 + i * rowLen] = 0; - d[1 + j * 3 + i * rowLen] = 0; - d[2 + j * 3 + i * rowLen] = 0; - } - - d += scale * 3; - s++; - } - } - - BITMAPFILEHEADER fih = {}; - fih.bfType = 0x4d42; // "BM" - fih.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dataLen; - fih.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); - - BITMAPINFOHEADER bih = {}; - bih.biSize = sizeof(BITMAPINFOHEADER); - bih.biWidth = pQR->width * scale; - bih.biHeight = -bih.biWidth; - bih.biPlanes = 1; - bih.biBitCount = 24; - bih.biCompression = BI_RGB; - - wchar_t wszTempPath[MAX_PATH], wszTempFile[MAX_PATH]; - GetTempPathW(_countof(wszTempPath), wszTempPath); - GetTempFileNameW(wszTempPath, L"wa_", TRUE, wszTempFile); - FILE *f = _wfopen(wszTempFile, L"wb"); - fwrite(&fih, sizeof(BITMAPFILEHEADER), 1, f); - fwrite(&bih, sizeof(BITMAPINFOHEADER), 1, f); - fwrite(pData, sizeof(unsigned char), dataLen, f); - fclose(f); - - SendMessage(hwndRc, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)Image_Load(wszTempFile)); - - DeleteFileW(wszTempFile); - QRcode_free(pQR); - } -}; - -static INT_PTR __stdcall sttShowDialog(void *param) -{ - WhatsAppProto *ppro = (WhatsAppProto *)param; - - if (ppro->m_pQRDlg == nullptr) { - ppro->m_pQRDlg = new CWhatsAppQRDlg(ppro); - ppro->m_pQRDlg->Show(); - } - else { - SetForegroundWindow(ppro->m_pQRDlg->GetHwnd()); - SetActiveWindow(ppro->m_pQRDlg->GetHwnd()); - } - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void WhatsAppProto::CloseQrDialog() -{ - if (m_pQRDlg) { - m_pQRDlg->SetSuccess(); - m_pQRDlg->Close(); - } -} - -bool WhatsAppProto::ShowQrCode(const CMStringA &ref) -{ - CallFunctionSync(sttShowDialog, this); - - MBinBuffer secret(getBlob(DBKEY_SECRET_KEY)); - - ptrA s1(mir_base64_encode(m_noise->noiseKeys.pub)); - ptrA s2(mir_base64_encode(m_signalStore.signedIdentity.pub)); - ptrA s3(mir_base64_encode(secret)); - CMStringA szQrData(FORMAT, "%s,%s,%s,%s", ref.c_str(), s1.get(), s2.get(), s3.get()); - m_pQRDlg->SetData(szQrData); - return true; -} +/* + +WhatsApp plugin for Miranda NG +Copyright 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +class CWhatsAppQRDlg : public CProtoDlgBase +{ +public: + CWhatsAppQRDlg(WhatsAppProto *ppro) : + CProtoDlgBase(ppro, IDD_SHOWQR) + {} + + void OnDestroy() override + { + m_proto->m_pQRDlg = nullptr; + + if (!m_bSucceeded) + m_proto->ShutdownSession(); + } + + void SetSuccess() + { + m_bSucceeded = true; + } + + void SetData(const CMStringA &str) + { + auto *pQR = QRcode_encodeString(str, 0, QR_ECLEVEL_L, QR_MODE_8, 1); + + HWND hwndRc = GetDlgItem(m_hwnd, IDC_QRPIC); + RECT rc; + GetClientRect(hwndRc, &rc); + + ::SetForegroundWindow(m_hwnd); + + int scale = 8; // (rc.bottom - rc.top) / pQR->width; + int rowLen = pQR->width * scale * 3; + if (rowLen % 4) + rowLen = (rowLen / 4 + 1) * 4; + int dataLen = rowLen * pQR->width * scale; + + mir_ptr pData((BYTE *)mir_alloc(dataLen)); + if (pData == nullptr) { + QRcode_free(pQR); + return; + } + + memset(pData, 0xFF, dataLen); // white background by default + + const BYTE *s = pQR->data; + for (int y = 0; y < pQR->width; y++) { + BYTE *d = pData.get() + rowLen * y * scale; + for (int x = 0; x < pQR->width; x++) { + if (*s & 1) + for (int i = 0; i < scale; i++) + for (int j = 0; j < scale; j++) { + d[j * 3 + i * rowLen] = 0; + d[1 + j * 3 + i * rowLen] = 0; + d[2 + j * 3 + i * rowLen] = 0; + } + + d += scale * 3; + s++; + } + } + + BITMAPFILEHEADER fih = {}; + fih.bfType = 0x4d42; // "BM" + fih.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dataLen; + fih.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); + + BITMAPINFOHEADER bih = {}; + bih.biSize = sizeof(BITMAPINFOHEADER); + bih.biWidth = pQR->width * scale; + bih.biHeight = -bih.biWidth; + bih.biPlanes = 1; + bih.biBitCount = 24; + bih.biCompression = BI_RGB; + + wchar_t wszTempPath[MAX_PATH], wszTempFile[MAX_PATH]; + GetTempPathW(_countof(wszTempPath), wszTempPath); + GetTempFileNameW(wszTempPath, L"wa_", TRUE, wszTempFile); + FILE *f = _wfopen(wszTempFile, L"wb"); + fwrite(&fih, sizeof(BITMAPFILEHEADER), 1, f); + fwrite(&bih, sizeof(BITMAPINFOHEADER), 1, f); + fwrite(pData, sizeof(unsigned char), dataLen, f); + fclose(f); + + SendMessage(hwndRc, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)Image_Load(wszTempFile)); + + DeleteFileW(wszTempFile); + QRcode_free(pQR); + } +}; + +static INT_PTR __stdcall sttShowDialog(void *param) +{ + WhatsAppProto *ppro = (WhatsAppProto *)param; + + if (ppro->m_pQRDlg == nullptr) { + ppro->m_pQRDlg = new CWhatsAppQRDlg(ppro); + ppro->m_pQRDlg->Show(); + } + else { + SetForegroundWindow(ppro->m_pQRDlg->GetHwnd()); + SetActiveWindow(ppro->m_pQRDlg->GetHwnd()); + } + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void WhatsAppProto::CloseQrDialog() +{ + if (m_pQRDlg) { + m_pQRDlg->SetSuccess(); + m_pQRDlg->Close(); + } +} + +bool WhatsAppProto::ShowQrCode(const CMStringA &ref) +{ + CallFunctionSync(sttShowDialog, this); + + MBinBuffer secret(getBlob(DBKEY_SECRET_KEY)); + + ptrA s1(mir_base64_encode(m_noise->noiseKeys.pub)); + ptrA s2(mir_base64_encode(m_signalStore.signedIdentity.pub)); + ptrA s3(mir_base64_encode(secret)); + CMStringA szQrData(FORMAT, "%s,%s,%s,%s", ref.c_str(), s1.get(), s2.get(), s3.get()); + m_pQRDlg->SetData(szQrData); + return true; +} diff --git a/protocols/WhatsApp/src/server.cpp b/protocols/WhatsApp/src/server.cpp index 0afe96662d..7512b05caf 100644 --- a/protocols/WhatsApp/src/server.cpp +++ b/protocols/WhatsApp/src/server.cpp @@ -1,7 +1,7 @@ /* WhatsApp plugin for Miranda NG -Copyright 2019-22 George Hazan +Copyright 2019-23 George Hazan */ diff --git a/protocols/WhatsApp/src/signal.cpp b/protocols/WhatsApp/src/signal.cpp index b94a3452e4..d8a57b0222 100644 --- a/protocols/WhatsApp/src/signal.cpp +++ b/protocols/WhatsApp/src/signal.cpp @@ -1,788 +1,788 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#include "stdafx.h" - -///////////////////////////////////////////////////////////////////////////////////////// -// MSignalStore members - -static int CompareSessions(const MSignalSession *p1, const MSignalSession *p2) -{ - if (int ret = mir_strcmp(p1->szName, p2->szName)) - return ret; - - return p1->getDeviceId() - p2->getDeviceId(); -} - -MSignalStore::MSignalStore(PROTO_INTERFACE *_1, const char *_2) : - pProto(_1), - prefix(_2), - arSessions(1, &CompareSessions) -{ - init(); -} - -MSignalStore::~MSignalStore() -{ - signal_protocol_store_context_destroy(m_pStore); - signal_context_destroy(m_pContext); -} - -void MSignalStore::logError(int err, const char *pszMessage) -{ - if (err < 0) { - pProto->debugLogA("libsignal error %d", err); - throw pszMessage; - } -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static void log_func(int level, const char *pmsg, size_t /*msgLen*/, void *pUserData) -{ - auto *pStore = (MSignalStore *)pUserData; - pStore->pProto->debugLogA("libsignal {%d}: %s", level, pmsg); -} - -static int hmac_sha256_init(void **hmac_context, const uint8_t *key, size_t key_len, void *) -{ - HMAC_CTX *ctx = HMAC_CTX_new(); - *hmac_context = ctx; - HMAC_Init(ctx, key, (int)key_len, EVP_sha256()); - return 0; -} - -static int hmac_sha256_update(void *hmac_context, const uint8_t *data, size_t data_len, void *) -{ - return HMAC_Update((HMAC_CTX *)hmac_context, data, data_len); -} - -static int hmac_sha256_final(void *hmac_context, signal_buffer **output, void *) -{ - BYTE data[200]; - unsigned len = 0; - if (!HMAC_Final((HMAC_CTX *)hmac_context, data, &len)) - return 1; - - *output = signal_buffer_create(data, len); - return 0; -} - -static void hmac_sha256_cleanup(void *hmac_context, void *) -{ - HMAC_CTX_free((HMAC_CTX *)hmac_context); -} - -static int random_func(uint8_t *pData, size_t size, void *) -{ - Utils_GetRandom(pData, size); - return 0; -} - -static int decrypt_func(signal_buffer **output, - int /*cipher*/, - const uint8_t *key, size_t /*key_len*/, - const uint8_t *iv, size_t /*iv_len*/, - const uint8_t *ciphertext, size_t ciphertext_len, - void * /*user_data*/) -{ - MBinBuffer res = aesDecrypt(EVP_aes_256_cbc(), key, iv, ciphertext, ciphertext_len); - *output = signal_buffer_create(res.data(), res.length()); - return SG_SUCCESS; -} - -static int encrypt_func(signal_buffer **output, - int /*cipher*/, - const uint8_t *key, size_t /*key_len*/, - const uint8_t *iv, size_t /*iv_len*/, - const uint8_t *ciphertext, size_t ciphertext_len, - void * /*user_data*/) -{ - MBinBuffer res = aesEncrypt(EVP_aes_256_cbc(), key, iv, ciphertext, ciphertext_len); - *output = signal_buffer_create(res.data(), res.length()); - return SG_SUCCESS; -} - -static int contains_session_func(const signal_protocol_address *address, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - auto *pSession = pStore->getSession(address); - return pSession != nullptr; -} - -static int delete_all_sessions_func(const char *name, size_t name_len, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - auto &pList = pStore->arSessions; - - int count = 0; - for (auto &it : pList.rev_iter()) { - if (it->hasAddress(name, name_len)) { - pStore->pProto->delSetting(it->getSetting()); - pList.remove(pList.indexOf(&it)); - count++; - } - } - return count; -} - -int delete_session_func(const signal_protocol_address *address, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - auto &pList = pStore->arSessions; - - MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id); - int idx = pList.getIndex(&tmp); - if (idx != -1) { - pStore->pProto->delSetting(tmp.getSetting()); - pList.remove(idx); - } - return 0; -} - -static int get_sub_device_sessions_func(signal_int_list **sessions, const char *name, size_t name_len, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - CMStringA szName(name, (int)name_len); - - signal_int_list *l = signal_int_list_alloc(); - unsigned int array_size = 0; - - for (auto &it : pStore->arSessions) - if (it->szName == szName) { - array_size++; - signal_int_list_push_back(l, it->getDeviceId()); - } - - *sessions = l; - return array_size; -} - -static void destroy_func(void *) -{} - -int load_session_func(signal_buffer **record, signal_buffer **user_data_storage, const signal_protocol_address *address, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - auto *pSession = pStore->getSession(address); - if (pSession == nullptr) - return 0; - - *record = signal_buffer_create(pSession->sessionData.data(), pSession->sessionData.length()); - *user_data_storage = 0; - return 1; -} - -static int store_session_func(const signal_protocol_address *address, uint8_t *record, size_t record_len, uint8_t *, size_t, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id); - auto *pSession = pStore->arSessions.find(&tmp); - if (pSession == nullptr) { - pSession = new MSignalSession(tmp); - pStore->arSessions.insert(pSession); - } - - pSession->sessionData.assign(record, record_len); - db_set_blob(0, pStore->pProto->m_szModuleName, pSession->getSetting(), record, (unsigned)record_len); - return 0; -} - -static int contains_pre_key(uint32_t pre_key_id, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(FORMAT, "%s%d", "PreKey", pre_key_id); - MBinBuffer blob(pStore->pProto->getBlob(szSetting)); - return (blob.data() != 0); -} - -static int load_pre_key(signal_buffer **record, uint32_t pre_key_id, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(FORMAT, "%s%d", "PreKey", pre_key_id); - MBinBuffer blob(pStore->pProto->getBlob(szSetting)); - if (blob.isEmpty()) { - pStore->pProto->debugLogA("Prekey #%d not found", pre_key_id); - return SG_ERR_INVALID_KEY_ID; - } - - *record = signal_buffer_create(blob.data(), blob.length()); - return SG_SUCCESS; //key exists and succesfully loaded -} - -static int remove_pre_key(uint32_t pre_key_id, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - pStore->pProto->debugLogA("Request to remove prekey #%d", pre_key_id); - - /* - CMStringA szSetting(FORMAT, "%s%d", "PreKey", pre_key_id); - pStore->pProto->delSetting(szSetting); - - szSetting.Format("PreKey%uPublic", pre_key_id); - pStore->pProto->delSetting(szSetting); - - szSetting.Format("PreKey%uPrivate", pre_key_id); - pStore->pProto->delSetting(szSetting); - */ - return 0; -} - -static int store_pre_key(uint32_t pre_key_id, uint8_t *record, size_t record_len, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(FORMAT, "%s%d", "PreKey", pre_key_id); - db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, record, (unsigned int)record_len); - - session_pre_key *prekey = nullptr; - session_pre_key_deserialize(&prekey, record, record_len, pStore->CTX()); //TODO: handle error - if (prekey) { - ec_key_pair *pre_key_pair = session_pre_key_get_key_pair(prekey); - - SignalBuffer key_buf(ec_key_pair_get_public(pre_key_pair)); - szSetting.Format("PreKey%uPublic", pre_key_id); - db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, key_buf.data(), key_buf.len()); - } - - return 0; -} - -static int contains_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(FORMAT, "%s%d", "SignedPreKey", signed_pre_key_id); - MBinBuffer blob(pStore->pProto->getBlob(szSetting)); - return blob.data() != 0; -} - -static int load_signed_pre_key(signal_buffer **record, uint32_t signed_pre_key_id, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - if (signed_pre_key_id == 0) - signed_pre_key_id = 1; - - CMStringA szSetting(FORMAT, "%s%d", "SignedPreKey", signed_pre_key_id); - MBinBuffer blob(pStore->pProto->getBlob(szSetting)); - if (blob.isEmpty()) { - pStore->pProto->debugLogA("Signed prekey #%d not found", signed_pre_key_id); - return SG_ERR_INVALID_KEY_ID; - } - - *record = signal_buffer_create(blob.data(), blob.length()); - return SG_SUCCESS; //key exist and succesfully loaded -} - -static int store_signed_pre_key(uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(FORMAT, "%s%d", "SignedPreKey", signed_pre_key_id); - db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, record, (unsigned int)record_len); - return 0; -} - -static int remove_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(FORMAT, "%s%d", "SignedPreKey", signed_pre_key_id); - pStore->pProto->delSetting(szSetting); - return 0; -} - -static int get_identity_key_pair(signal_buffer **public_data, signal_buffer **private_data, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - MBinBuffer buf; - buf.append(KEY_BUNDLE_TYPE, 1); - buf.append(pStore->signedIdentity.pub); - *public_data = signal_buffer_create(buf.data(), (int)buf.length()); - - *private_data = signal_buffer_create(pStore->signedIdentity.priv.data(), (int)pStore->signedIdentity.priv.length()); - return 0; -} - -static int get_local_registration_id(void *user_data, uint32_t *registration_id) -{ - auto *pStore = (MSignalStore *)user_data; - *registration_id = pStore->pProto->getDword(DBKEY_REG_ID); - return 0; -} - -static int save_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(FORMAT, "%s_%s_%d", "SignalIdentity", CMStringA(address->name, (int)address->name_len).c_str(), address->device_id); - if (key_data != nullptr) - db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, key_data, (unsigned int)key_len); //TODO: check return value - else - pStore->pProto->delSetting(szSetting); - return 0; -} - -static int is_trusted_identity(const signal_protocol_address * /*address*/, uint8_t * /*key_data*/, size_t /*key_len*/, void * /*user_data*/) -{ - return 1; -} - -static CMStringA get_sender_setting(const signal_protocol_sender_key_name *skn) -{ - WAJid jid(CMStringA(skn->sender.name, (int)skn->sender.name_len)); - return CMStringA(FORMAT, "SenderKey_%*s_%s_%d", (unsigned)skn->group_id_len, skn->group_id, jid.user.c_str(), skn->sender.device_id); -} - -static int load_sender_key(signal_buffer **record, signal_buffer **, const signal_protocol_sender_key_name *skn, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(get_sender_setting(skn)); - MBinBuffer blob(pStore->pProto->getBlob(szSetting)); - if (blob.isEmpty()) - return 0; - - *record = signal_buffer_create(blob.data(), blob.length()); - return 1; -} - -static int store_sender_key(const signal_protocol_sender_key_name *skn, uint8_t *record, size_t record_len, uint8_t*, size_t, void *user_data) -{ - auto *pStore = (MSignalStore *)user_data; - - CMStringA szSetting(get_sender_setting(skn)); - db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, record, (unsigned)record_len); - return 0; -} - -void MSignalStore::init() -{ - signal_context_create(&m_pContext, this); - signal_context_set_log_function(m_pContext, log_func); - - signal_crypto_provider prov; - memset(&prov, 0xFF, sizeof(prov)); - prov.hmac_sha256_init_func = hmac_sha256_init; - prov.hmac_sha256_final_func = hmac_sha256_final; - prov.hmac_sha256_update_func = hmac_sha256_update; - prov.hmac_sha256_cleanup_func = hmac_sha256_cleanup; - prov.random_func = random_func; - prov.decrypt_func = decrypt_func; - prov.encrypt_func = encrypt_func; - signal_context_set_crypto_provider(m_pContext, &prov); - - // read resident data from database - MBinBuffer blob(pProto->getBlob(DBKEY_PREKEY)); - if (blob.isEmpty()) { - // nothing? generate signed identity keys (private & public) - ratchet_identity_key_pair *keyPair; - signal_protocol_key_helper_generate_identity_key_pair(&keyPair, m_pContext); - - auto *pPubKey = ratchet_identity_key_pair_get_public(keyPair); - db_set_blob(0, pProto->m_szModuleName, DBKEY_SIGNED_IDENTITY_PUB, pPubKey->data, sizeof(pPubKey->data)); - - auto *pPrivKey = ratchet_identity_key_pair_get_private(keyPair); - db_set_blob(0, pProto->m_szModuleName, DBKEY_SIGNED_IDENTITY_PRIV, pPrivKey->data, sizeof(pPrivKey->data)); - - session_signed_pre_key *signed_pre_key; - signal_protocol_key_helper_generate_signed_pre_key(&signed_pre_key, keyPair, 1, time(0), m_pContext); - - SignalBuffer prekeyBuf(signed_pre_key); - db_set_blob(0, pProto->m_szModuleName, DBKEY_PREKEY, prekeyBuf.data(), prekeyBuf.len()); - blob.assign(prekeyBuf.data(), prekeyBuf.len()); - - SIGNAL_UNREF(signed_pre_key); - SIGNAL_UNREF(keyPair); - } - - session_signed_pre_key *signed_pre_key; - session_signed_pre_key_deserialize(&signed_pre_key, blob.data(), blob.length(), m_pContext); - - ec_key_pair *pKeys = session_signed_pre_key_get_key_pair(signed_pre_key); - auto *pPubKey = ec_key_pair_get_public(pKeys); - preKey.pub.assign(pPubKey->data, sizeof(pPubKey->data)); - - auto *pPrivKey = ec_key_pair_get_private(pKeys); - preKey.priv.assign(pPrivKey->data, sizeof(pPrivKey->data)); - - preKey.signature.assign(session_signed_pre_key_get_signature(signed_pre_key), session_signed_pre_key_get_signature_len(signed_pre_key)); - preKey.keyid = session_signed_pre_key_get_id(signed_pre_key); - SIGNAL_UNREF(signed_pre_key); - - signedIdentity.pub = pProto->getBlob(DBKEY_SIGNED_IDENTITY_PUB); - signedIdentity.priv = pProto->getBlob(DBKEY_SIGNED_IDENTITY_PRIV); - - // create store with callbacks - signal_protocol_store_context_create(&m_pStore, m_pContext); - - signal_protocol_session_store ss; - ss.contains_session_func = &contains_session_func; - ss.delete_all_sessions_func = &delete_all_sessions_func; - ss.delete_session_func = &delete_session_func; - ss.destroy_func = &destroy_func; - ss.get_sub_device_sessions_func = &get_sub_device_sessions_func; - ss.load_session_func = &load_session_func; - ss.store_session_func = &store_session_func; - ss.user_data = this; - signal_protocol_store_context_set_session_store(m_pStore, &ss); - - signal_protocol_pre_key_store sp; - sp.contains_pre_key = &contains_pre_key; - sp.destroy_func = &destroy_func; - sp.load_pre_key = &load_pre_key; - sp.remove_pre_key = &remove_pre_key; - sp.store_pre_key = &store_pre_key; - sp.user_data = this; - signal_protocol_store_context_set_pre_key_store(m_pStore, &sp); - - signal_protocol_sender_key_store sk; - sk.destroy_func = destroy_func; - sk.load_sender_key = load_sender_key; - sk.store_sender_key = store_sender_key; - sk.user_data = this; - signal_protocol_store_context_set_sender_key_store(m_pStore, &sk); - - signal_protocol_signed_pre_key_store ssp; - ssp.contains_signed_pre_key = &contains_signed_pre_key; - ssp.destroy_func = &destroy_func; - ssp.load_signed_pre_key = &load_signed_pre_key; - ssp.remove_signed_pre_key = &remove_signed_pre_key; - ssp.store_signed_pre_key = &store_signed_pre_key; - ssp.user_data = this; - signal_protocol_store_context_set_signed_pre_key_store(m_pStore, &ssp); - - signal_protocol_identity_key_store sip; - sip.destroy_func = &destroy_func; - sip.get_identity_key_pair = &get_identity_key_pair; - sip.get_local_registration_id = &get_local_registration_id; - sip.is_trusted_identity = &is_trusted_identity; - sip.save_identity = &save_identity; - sip.user_data = this; - signal_protocol_store_context_set_identity_key_store(m_pStore, &sip); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// MSignalSession members - -MSignalSession::MSignalSession(const CMStringA &_1, int _2) : - szName(_1) -{ - address.name = szName.GetBuffer(); - address.name_len = szName.GetLength(); - address.device_id = _2; -} - -MSignalSession::~MSignalSession() -{ - session_cipher_free(cipher); -} - -bool MSignalSession::hasAddress(const char *name, size_t name_len) const -{ - if (address.name_len != name_len) - return false; - return memcmp(address.name, name, name_len) == 0; -} - -CMStringA MSignalSession::getSetting() const -{ - return CMStringA(FORMAT, "SignalSession_%s_%d", szName.c_str(), getDeviceId()); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MSignalSession* MSignalStore::createSession(const CMStringA &szName, int deviceId) -{ - signal_protocol_address tmp = {szName.c_str(), (unsigned)szName.GetLength(), deviceId}; - auto *pSession = getSession(&tmp); - if (pSession == nullptr) { - pSession = new MSignalSession(szName, deviceId); - arSessions.insert(pSession); - } - - if (pSession->cipher == nullptr) - logError( - session_cipher_create(&pSession->cipher, m_pStore, &pSession->address, m_pContext), - "session_cipher_create failure"); - - return pSession; -} - -MSignalSession* MSignalStore::getSession(const signal_protocol_address *address) -{ - MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id); - auto *pSession = arSessions.find(&tmp); - if (pSession == nullptr) { - MBinBuffer blob(pProto->getBlob(tmp.getSetting())); - if (blob.isEmpty()) - return nullptr; - - pSession = new MSignalSession(tmp); - pSession->sessionData.assign(blob.data(), blob.length()); - arSessions.insert(pSession); - } - - return pSession; -} - -void MSignalStore::importPublicKey(ec_public_key **result, MBinBuffer &buf) -{ - buf.appendBefore("\x05", 1); - curve_decode_point(result, buf.data(), buf.length(), m_pContext); -} - -void MSignalStore::injectSession(const char *szJid, const WANode *pNode, const WANode *pKey) -{ - WAJid jid(szJid); - auto *signedKey = pKey->getChild("skey"); - auto *key = pKey->getChild("key"); - auto *identity = pKey->getChild("identity"); - auto *registration = pNode->getChild("registration"); - if (!signedKey || !key || !identity || !registration) { - pProto->debugLogA("Bad key data for %s", jid.toString().c_str()); - return; - } - - signal_protocol_address address = {jid.user.c_str(), (unsigned)jid.user.GetLength(), jid.device}; - - session_builder *builder; - logError( - session_builder_create(&builder, m_pStore, &address, m_pContext), - "unable to create session cipher"); - - int regId = decodeBigEndian(registration->content); - int preKeyId = decodeBigEndian(key->getChild("id")->content); - int signedPreKeyId = decodeBigEndian(signedKey->getChild("id")->content); - - ec_public_key *preKeyPub, *signedPreKeyPub, *identityKey; - importPublicKey(&preKeyPub, key->getChild("value")->content); - importPublicKey(&identityKey, identity->content); - importPublicKey(&signedPreKeyPub, signedKey->getChild("value")->content); - - auto &sign = signedKey->getChild("signature")->content; - - session_pre_key_bundle *bundle; - logError( - session_pre_key_bundle_create(&bundle, regId, jid.device, preKeyId, preKeyPub, signedPreKeyId, signedPreKeyPub, sign.data(), sign.length(), identityKey), - "unable to create pre key bundle"); - - logError( - session_builder_process_pre_key_bundle(builder, bundle), - "unable to process pre key bundle"); - - session_pre_key_bundle_destroy((signal_type_base*)bundle); - session_builder_free(builder); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MBinBuffer MSignalStore::decryptSignalProto(const CMStringA &from, const char *pszType, const MBinBuffer &encrypted) -{ - WAJid jid(from); - auto *pSession = createSession(jid.user, jid.device); - - signal_buffer *result = nullptr; - if (!mir_strcmp(pszType, "pkmsg")) { - pre_key_signal_message *pMsg; - logError( - pre_key_signal_message_deserialize(&pMsg, encrypted.data(), encrypted.length(), m_pContext), - "unable to deserialize prekey message"); - - logError( - session_cipher_decrypt_pre_key_signal_message(pSession->getCipher(), pMsg, this, &result), - "unable to decrypt prekey message"); - - pre_key_signal_message_destroy((signal_type_base*)pMsg); - } - else { - signal_message *pMsg; - logError( - signal_message_deserialize(&pMsg, encrypted.data(), encrypted.length(), m_pContext), - "unable to deserialize signal message"); - - logError( - session_cipher_decrypt_signal_message(pSession->getCipher(), pMsg, this, &result), - "unable to decrypt signal message"); - - signal_message_destroy((signal_type_base *)pMsg); - } - - MBinBuffer res; - res.assign(result->data, result->len); - signal_buffer_free(result); - return res; -} - -MBinBuffer MSignalStore::decryptGroupSignalProto(const CMStringA &group, const CMStringA &sender, const MBinBuffer &encrypted) -{ - WAJid jid(sender); - signal_protocol_sender_key_name senderKeyName; - senderKeyName.group_id = group.c_str(); - senderKeyName.group_id_len = group.GetLength(); - senderKeyName.sender.device_id = 0; - senderKeyName.sender.name = jid.user.c_str(); - senderKeyName.sender.name_len = jid.user.GetLength(); - - group_cipher *cipher; - logError( - group_cipher_create(&cipher, m_pStore, &senderKeyName, m_pContext), - "unable to create group cipher"); - - sender_key_message *skmsg; - logError( - sender_key_message_deserialize(&skmsg, encrypted.data(), encrypted.length(), m_pContext), - "unable to deserialize skmsg"); - - signal_buffer *result = nullptr; - logError( - group_cipher_decrypt(cipher, skmsg, this, &result), - "unable to decrypt skmsg"); - - sender_key_message_destroy((signal_type_base *)skmsg); - group_cipher_free(cipher); - - MBinBuffer res; - res.assign(result->data, result->len); - signal_buffer_free(result); - return res; -} - -void MSignalStore::processSenderKeyMessage(const CMStringA &author, const Wa__Message__SenderKeyDistributionMessage *msg) -{ - WAJid jid(author); - signal_protocol_sender_key_name senderKeyName; - senderKeyName.group_id = msg->groupid; - senderKeyName.group_id_len = mir_strlen(msg->groupid); - senderKeyName.sender.device_id = 0; - senderKeyName.sender.name = jid.user.c_str(); - senderKeyName.sender.name_len = jid.user.GetLength(); - - group_session_builder *builder; - logError( - group_session_builder_create(&builder, m_pStore, m_pContext), - "unable to create session builder"); - - sender_key_distribution_message *skmsg; - logError( - sender_key_distribution_message_deserialize(&skmsg, msg->axolotlsenderkeydistributionmessage.data, msg->axolotlsenderkeydistributionmessage.len, m_pContext), - "unable to decode skd message"); - - logError( - group_session_builder_process_session(builder, &senderKeyName, skmsg), - "unable to process skd message"); - - sender_key_distribution_message_destroy((signal_type_base *)skmsg); - group_session_builder_free(builder); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// encryption - -MBinBuffer MSignalStore::encryptSenderKey(const WAJid &to, const CMStringA &from, const MBinBuffer &buf, MBinBuffer &skmsgKey) -{ - signal_protocol_sender_key_name senderKeyName; - senderKeyName.group_id = to.user.c_str(); - senderKeyName.group_id_len = to.user.GetLength(); - senderKeyName.sender.device_id = 0; - senderKeyName.sender.name = from.c_str(); - senderKeyName.sender.name_len = from.GetLength(); - - group_session_builder *builder; - logError( - group_session_builder_create(&builder, m_pStore, m_pContext), - "unable to create session builder"); - - sender_key_distribution_message *skmsg; - logError( - group_session_builder_create_session(builder, &skmsg, &senderKeyName), - "unable to create session"); - - group_cipher *cipher; - logError( - group_cipher_create(&cipher, m_pStore, &senderKeyName, m_pContext), - "unable to create group cipher"); - - ciphertext_message *encMessage; - logError( - group_cipher_encrypt(cipher, buf.data(), buf.length(), &encMessage), - "unable to encrypt group message"); - - MBinBuffer res; - auto *cipherText = ciphertext_message_get_serialized(encMessage); - res.assign(cipherText->data, cipherText->len); - - auto *pKey = sender_key_distribution_message_get_signature_key(skmsg); - skmsgKey.assign(pKey->data, sizeof(pKey->data)); - - sender_key_distribution_message_destroy((signal_type_base*)skmsg); - group_cipher_free(cipher); - group_session_builder_free(builder); - return res; -} - -MBinBuffer MSignalStore::encryptSignalProto(const WAJid &to, const MBinBuffer &buf, int &type) -{ - auto *pSession = createSession(to.user, to.device); - - ciphertext_message *pEncrypted; - logError( - session_cipher_encrypt(pSession->getCipher(), buf.data(), buf.length(), &pEncrypted), - "unable to encrypt signal message"); - - type = ciphertext_message_get_type(pEncrypted); - - MBinBuffer res; - auto *encBuf = ciphertext_message_get_serialized(pEncrypted); - res.assign(encBuf->data, encBuf->len); - SIGNAL_UNREF(pEncrypted); - return res; -} - -MBinBuffer MSignalStore::encodeSignedIdentity(bool bIncludeSignatureKey) -{ - proto::ADVSignedDeviceIdentity identity(pProto->getBlob("WAAccount")); - - if (!bIncludeSignatureKey) - proto::CleanBinary(identity->accountsignaturekey), identity->has_accountsignaturekey = false; - - return proto::Serialize(identity); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// generate and save pre keys set - -void MSignalStore::generatePrekeys(int count) -{ - int iNextKeyId = pProto->getDword(DBKEY_PREKEY_NEXT_ID, 1); - - CMStringA szSetting; - signal_protocol_key_helper_pre_key_list_node *keys_root; - signal_protocol_key_helper_generate_pre_keys(&keys_root, iNextKeyId, count, m_pContext); - for (auto *it = keys_root; it; it = signal_protocol_key_helper_key_list_next(it)) { - session_pre_key *pre_key = signal_protocol_key_helper_key_list_element(it); - uint32_t pre_key_id = session_pre_key_get_id(pre_key); - - SignalBuffer buf(pre_key); - szSetting.Format("PreKey%d", pre_key_id); - db_set_blob(0, pProto->m_szModuleName, szSetting, buf.data(), buf.len()); - - ec_key_pair *pre_key_pair = session_pre_key_get_key_pair(pre_key); - auto *pPubKey = ec_key_pair_get_public(pre_key_pair); - szSetting.Format("PreKey%dPublic", pre_key_id); - db_set_blob(0, pProto->m_szModuleName, szSetting, pPubKey->data, sizeof(pPubKey->data)); - } - signal_protocol_key_helper_key_list_free(keys_root); - - pProto->setDword(DBKEY_PREKEY_NEXT_ID, iNextKeyId + count); -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +///////////////////////////////////////////////////////////////////////////////////////// +// MSignalStore members + +static int CompareSessions(const MSignalSession *p1, const MSignalSession *p2) +{ + if (int ret = mir_strcmp(p1->szName, p2->szName)) + return ret; + + return p1->getDeviceId() - p2->getDeviceId(); +} + +MSignalStore::MSignalStore(PROTO_INTERFACE *_1, const char *_2) : + pProto(_1), + prefix(_2), + arSessions(1, &CompareSessions) +{ + init(); +} + +MSignalStore::~MSignalStore() +{ + signal_protocol_store_context_destroy(m_pStore); + signal_context_destroy(m_pContext); +} + +void MSignalStore::logError(int err, const char *pszMessage) +{ + if (err < 0) { + pProto->debugLogA("libsignal error %d", err); + throw pszMessage; + } +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static void log_func(int level, const char *pmsg, size_t /*msgLen*/, void *pUserData) +{ + auto *pStore = (MSignalStore *)pUserData; + pStore->pProto->debugLogA("libsignal {%d}: %s", level, pmsg); +} + +static int hmac_sha256_init(void **hmac_context, const uint8_t *key, size_t key_len, void *) +{ + HMAC_CTX *ctx = HMAC_CTX_new(); + *hmac_context = ctx; + HMAC_Init(ctx, key, (int)key_len, EVP_sha256()); + return 0; +} + +static int hmac_sha256_update(void *hmac_context, const uint8_t *data, size_t data_len, void *) +{ + return HMAC_Update((HMAC_CTX *)hmac_context, data, data_len); +} + +static int hmac_sha256_final(void *hmac_context, signal_buffer **output, void *) +{ + BYTE data[200]; + unsigned len = 0; + if (!HMAC_Final((HMAC_CTX *)hmac_context, data, &len)) + return 1; + + *output = signal_buffer_create(data, len); + return 0; +} + +static void hmac_sha256_cleanup(void *hmac_context, void *) +{ + HMAC_CTX_free((HMAC_CTX *)hmac_context); +} + +static int random_func(uint8_t *pData, size_t size, void *) +{ + Utils_GetRandom(pData, size); + return 0; +} + +static int decrypt_func(signal_buffer **output, + int /*cipher*/, + const uint8_t *key, size_t /*key_len*/, + const uint8_t *iv, size_t /*iv_len*/, + const uint8_t *ciphertext, size_t ciphertext_len, + void * /*user_data*/) +{ + MBinBuffer res = aesDecrypt(EVP_aes_256_cbc(), key, iv, ciphertext, ciphertext_len); + *output = signal_buffer_create(res.data(), res.length()); + return SG_SUCCESS; +} + +static int encrypt_func(signal_buffer **output, + int /*cipher*/, + const uint8_t *key, size_t /*key_len*/, + const uint8_t *iv, size_t /*iv_len*/, + const uint8_t *ciphertext, size_t ciphertext_len, + void * /*user_data*/) +{ + MBinBuffer res = aesEncrypt(EVP_aes_256_cbc(), key, iv, ciphertext, ciphertext_len); + *output = signal_buffer_create(res.data(), res.length()); + return SG_SUCCESS; +} + +static int contains_session_func(const signal_protocol_address *address, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + auto *pSession = pStore->getSession(address); + return pSession != nullptr; +} + +static int delete_all_sessions_func(const char *name, size_t name_len, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + auto &pList = pStore->arSessions; + + int count = 0; + for (auto &it : pList.rev_iter()) { + if (it->hasAddress(name, name_len)) { + pStore->pProto->delSetting(it->getSetting()); + pList.remove(pList.indexOf(&it)); + count++; + } + } + return count; +} + +int delete_session_func(const signal_protocol_address *address, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + auto &pList = pStore->arSessions; + + MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id); + int idx = pList.getIndex(&tmp); + if (idx != -1) { + pStore->pProto->delSetting(tmp.getSetting()); + pList.remove(idx); + } + return 0; +} + +static int get_sub_device_sessions_func(signal_int_list **sessions, const char *name, size_t name_len, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + CMStringA szName(name, (int)name_len); + + signal_int_list *l = signal_int_list_alloc(); + unsigned int array_size = 0; + + for (auto &it : pStore->arSessions) + if (it->szName == szName) { + array_size++; + signal_int_list_push_back(l, it->getDeviceId()); + } + + *sessions = l; + return array_size; +} + +static void destroy_func(void *) +{} + +int load_session_func(signal_buffer **record, signal_buffer **user_data_storage, const signal_protocol_address *address, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + auto *pSession = pStore->getSession(address); + if (pSession == nullptr) + return 0; + + *record = signal_buffer_create(pSession->sessionData.data(), pSession->sessionData.length()); + *user_data_storage = 0; + return 1; +} + +static int store_session_func(const signal_protocol_address *address, uint8_t *record, size_t record_len, uint8_t *, size_t, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id); + auto *pSession = pStore->arSessions.find(&tmp); + if (pSession == nullptr) { + pSession = new MSignalSession(tmp); + pStore->arSessions.insert(pSession); + } + + pSession->sessionData.assign(record, record_len); + db_set_blob(0, pStore->pProto->m_szModuleName, pSession->getSetting(), record, (unsigned)record_len); + return 0; +} + +static int contains_pre_key(uint32_t pre_key_id, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(FORMAT, "%s%d", "PreKey", pre_key_id); + MBinBuffer blob(pStore->pProto->getBlob(szSetting)); + return (blob.data() != 0); +} + +static int load_pre_key(signal_buffer **record, uint32_t pre_key_id, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(FORMAT, "%s%d", "PreKey", pre_key_id); + MBinBuffer blob(pStore->pProto->getBlob(szSetting)); + if (blob.isEmpty()) { + pStore->pProto->debugLogA("Prekey #%d not found", pre_key_id); + return SG_ERR_INVALID_KEY_ID; + } + + *record = signal_buffer_create(blob.data(), blob.length()); + return SG_SUCCESS; //key exists and succesfully loaded +} + +static int remove_pre_key(uint32_t pre_key_id, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + pStore->pProto->debugLogA("Request to remove prekey #%d", pre_key_id); + + /* + CMStringA szSetting(FORMAT, "%s%d", "PreKey", pre_key_id); + pStore->pProto->delSetting(szSetting); + + szSetting.Format("PreKey%uPublic", pre_key_id); + pStore->pProto->delSetting(szSetting); + + szSetting.Format("PreKey%uPrivate", pre_key_id); + pStore->pProto->delSetting(szSetting); + */ + return 0; +} + +static int store_pre_key(uint32_t pre_key_id, uint8_t *record, size_t record_len, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(FORMAT, "%s%d", "PreKey", pre_key_id); + db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, record, (unsigned int)record_len); + + session_pre_key *prekey = nullptr; + session_pre_key_deserialize(&prekey, record, record_len, pStore->CTX()); //TODO: handle error + if (prekey) { + ec_key_pair *pre_key_pair = session_pre_key_get_key_pair(prekey); + + SignalBuffer key_buf(ec_key_pair_get_public(pre_key_pair)); + szSetting.Format("PreKey%uPublic", pre_key_id); + db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, key_buf.data(), key_buf.len()); + } + + return 0; +} + +static int contains_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(FORMAT, "%s%d", "SignedPreKey", signed_pre_key_id); + MBinBuffer blob(pStore->pProto->getBlob(szSetting)); + return blob.data() != 0; +} + +static int load_signed_pre_key(signal_buffer **record, uint32_t signed_pre_key_id, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + if (signed_pre_key_id == 0) + signed_pre_key_id = 1; + + CMStringA szSetting(FORMAT, "%s%d", "SignedPreKey", signed_pre_key_id); + MBinBuffer blob(pStore->pProto->getBlob(szSetting)); + if (blob.isEmpty()) { + pStore->pProto->debugLogA("Signed prekey #%d not found", signed_pre_key_id); + return SG_ERR_INVALID_KEY_ID; + } + + *record = signal_buffer_create(blob.data(), blob.length()); + return SG_SUCCESS; //key exist and succesfully loaded +} + +static int store_signed_pre_key(uint32_t signed_pre_key_id, uint8_t *record, size_t record_len, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(FORMAT, "%s%d", "SignedPreKey", signed_pre_key_id); + db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, record, (unsigned int)record_len); + return 0; +} + +static int remove_signed_pre_key(uint32_t signed_pre_key_id, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(FORMAT, "%s%d", "SignedPreKey", signed_pre_key_id); + pStore->pProto->delSetting(szSetting); + return 0; +} + +static int get_identity_key_pair(signal_buffer **public_data, signal_buffer **private_data, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + MBinBuffer buf; + buf.append(KEY_BUNDLE_TYPE, 1); + buf.append(pStore->signedIdentity.pub); + *public_data = signal_buffer_create(buf.data(), (int)buf.length()); + + *private_data = signal_buffer_create(pStore->signedIdentity.priv.data(), (int)pStore->signedIdentity.priv.length()); + return 0; +} + +static int get_local_registration_id(void *user_data, uint32_t *registration_id) +{ + auto *pStore = (MSignalStore *)user_data; + *registration_id = pStore->pProto->getDword(DBKEY_REG_ID); + return 0; +} + +static int save_identity(const signal_protocol_address *address, uint8_t *key_data, size_t key_len, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(FORMAT, "%s_%s_%d", "SignalIdentity", CMStringA(address->name, (int)address->name_len).c_str(), address->device_id); + if (key_data != nullptr) + db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, key_data, (unsigned int)key_len); //TODO: check return value + else + pStore->pProto->delSetting(szSetting); + return 0; +} + +static int is_trusted_identity(const signal_protocol_address * /*address*/, uint8_t * /*key_data*/, size_t /*key_len*/, void * /*user_data*/) +{ + return 1; +} + +static CMStringA get_sender_setting(const signal_protocol_sender_key_name *skn) +{ + WAJid jid(CMStringA(skn->sender.name, (int)skn->sender.name_len)); + return CMStringA(FORMAT, "SenderKey_%*s_%s_%d", (unsigned)skn->group_id_len, skn->group_id, jid.user.c_str(), skn->sender.device_id); +} + +static int load_sender_key(signal_buffer **record, signal_buffer **, const signal_protocol_sender_key_name *skn, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(get_sender_setting(skn)); + MBinBuffer blob(pStore->pProto->getBlob(szSetting)); + if (blob.isEmpty()) + return 0; + + *record = signal_buffer_create(blob.data(), blob.length()); + return 1; +} + +static int store_sender_key(const signal_protocol_sender_key_name *skn, uint8_t *record, size_t record_len, uint8_t*, size_t, void *user_data) +{ + auto *pStore = (MSignalStore *)user_data; + + CMStringA szSetting(get_sender_setting(skn)); + db_set_blob(0, pStore->pProto->m_szModuleName, szSetting, record, (unsigned)record_len); + return 0; +} + +void MSignalStore::init() +{ + signal_context_create(&m_pContext, this); + signal_context_set_log_function(m_pContext, log_func); + + signal_crypto_provider prov; + memset(&prov, 0xFF, sizeof(prov)); + prov.hmac_sha256_init_func = hmac_sha256_init; + prov.hmac_sha256_final_func = hmac_sha256_final; + prov.hmac_sha256_update_func = hmac_sha256_update; + prov.hmac_sha256_cleanup_func = hmac_sha256_cleanup; + prov.random_func = random_func; + prov.decrypt_func = decrypt_func; + prov.encrypt_func = encrypt_func; + signal_context_set_crypto_provider(m_pContext, &prov); + + // read resident data from database + MBinBuffer blob(pProto->getBlob(DBKEY_PREKEY)); + if (blob.isEmpty()) { + // nothing? generate signed identity keys (private & public) + ratchet_identity_key_pair *keyPair; + signal_protocol_key_helper_generate_identity_key_pair(&keyPair, m_pContext); + + auto *pPubKey = ratchet_identity_key_pair_get_public(keyPair); + db_set_blob(0, pProto->m_szModuleName, DBKEY_SIGNED_IDENTITY_PUB, pPubKey->data, sizeof(pPubKey->data)); + + auto *pPrivKey = ratchet_identity_key_pair_get_private(keyPair); + db_set_blob(0, pProto->m_szModuleName, DBKEY_SIGNED_IDENTITY_PRIV, pPrivKey->data, sizeof(pPrivKey->data)); + + session_signed_pre_key *signed_pre_key; + signal_protocol_key_helper_generate_signed_pre_key(&signed_pre_key, keyPair, 1, time(0), m_pContext); + + SignalBuffer prekeyBuf(signed_pre_key); + db_set_blob(0, pProto->m_szModuleName, DBKEY_PREKEY, prekeyBuf.data(), prekeyBuf.len()); + blob.assign(prekeyBuf.data(), prekeyBuf.len()); + + SIGNAL_UNREF(signed_pre_key); + SIGNAL_UNREF(keyPair); + } + + session_signed_pre_key *signed_pre_key; + session_signed_pre_key_deserialize(&signed_pre_key, blob.data(), blob.length(), m_pContext); + + ec_key_pair *pKeys = session_signed_pre_key_get_key_pair(signed_pre_key); + auto *pPubKey = ec_key_pair_get_public(pKeys); + preKey.pub.assign(pPubKey->data, sizeof(pPubKey->data)); + + auto *pPrivKey = ec_key_pair_get_private(pKeys); + preKey.priv.assign(pPrivKey->data, sizeof(pPrivKey->data)); + + preKey.signature.assign(session_signed_pre_key_get_signature(signed_pre_key), session_signed_pre_key_get_signature_len(signed_pre_key)); + preKey.keyid = session_signed_pre_key_get_id(signed_pre_key); + SIGNAL_UNREF(signed_pre_key); + + signedIdentity.pub = pProto->getBlob(DBKEY_SIGNED_IDENTITY_PUB); + signedIdentity.priv = pProto->getBlob(DBKEY_SIGNED_IDENTITY_PRIV); + + // create store with callbacks + signal_protocol_store_context_create(&m_pStore, m_pContext); + + signal_protocol_session_store ss; + ss.contains_session_func = &contains_session_func; + ss.delete_all_sessions_func = &delete_all_sessions_func; + ss.delete_session_func = &delete_session_func; + ss.destroy_func = &destroy_func; + ss.get_sub_device_sessions_func = &get_sub_device_sessions_func; + ss.load_session_func = &load_session_func; + ss.store_session_func = &store_session_func; + ss.user_data = this; + signal_protocol_store_context_set_session_store(m_pStore, &ss); + + signal_protocol_pre_key_store sp; + sp.contains_pre_key = &contains_pre_key; + sp.destroy_func = &destroy_func; + sp.load_pre_key = &load_pre_key; + sp.remove_pre_key = &remove_pre_key; + sp.store_pre_key = &store_pre_key; + sp.user_data = this; + signal_protocol_store_context_set_pre_key_store(m_pStore, &sp); + + signal_protocol_sender_key_store sk; + sk.destroy_func = destroy_func; + sk.load_sender_key = load_sender_key; + sk.store_sender_key = store_sender_key; + sk.user_data = this; + signal_protocol_store_context_set_sender_key_store(m_pStore, &sk); + + signal_protocol_signed_pre_key_store ssp; + ssp.contains_signed_pre_key = &contains_signed_pre_key; + ssp.destroy_func = &destroy_func; + ssp.load_signed_pre_key = &load_signed_pre_key; + ssp.remove_signed_pre_key = &remove_signed_pre_key; + ssp.store_signed_pre_key = &store_signed_pre_key; + ssp.user_data = this; + signal_protocol_store_context_set_signed_pre_key_store(m_pStore, &ssp); + + signal_protocol_identity_key_store sip; + sip.destroy_func = &destroy_func; + sip.get_identity_key_pair = &get_identity_key_pair; + sip.get_local_registration_id = &get_local_registration_id; + sip.is_trusted_identity = &is_trusted_identity; + sip.save_identity = &save_identity; + sip.user_data = this; + signal_protocol_store_context_set_identity_key_store(m_pStore, &sip); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// MSignalSession members + +MSignalSession::MSignalSession(const CMStringA &_1, int _2) : + szName(_1) +{ + address.name = szName.GetBuffer(); + address.name_len = szName.GetLength(); + address.device_id = _2; +} + +MSignalSession::~MSignalSession() +{ + session_cipher_free(cipher); +} + +bool MSignalSession::hasAddress(const char *name, size_t name_len) const +{ + if (address.name_len != name_len) + return false; + return memcmp(address.name, name, name_len) == 0; +} + +CMStringA MSignalSession::getSetting() const +{ + return CMStringA(FORMAT, "SignalSession_%s_%d", szName.c_str(), getDeviceId()); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MSignalSession* MSignalStore::createSession(const CMStringA &szName, int deviceId) +{ + signal_protocol_address tmp = {szName.c_str(), (unsigned)szName.GetLength(), deviceId}; + auto *pSession = getSession(&tmp); + if (pSession == nullptr) { + pSession = new MSignalSession(szName, deviceId); + arSessions.insert(pSession); + } + + if (pSession->cipher == nullptr) + logError( + session_cipher_create(&pSession->cipher, m_pStore, &pSession->address, m_pContext), + "session_cipher_create failure"); + + return pSession; +} + +MSignalSession* MSignalStore::getSession(const signal_protocol_address *address) +{ + MSignalSession tmp(CMStringA(address->name, (int)address->name_len), address->device_id); + auto *pSession = arSessions.find(&tmp); + if (pSession == nullptr) { + MBinBuffer blob(pProto->getBlob(tmp.getSetting())); + if (blob.isEmpty()) + return nullptr; + + pSession = new MSignalSession(tmp); + pSession->sessionData.assign(blob.data(), blob.length()); + arSessions.insert(pSession); + } + + return pSession; +} + +void MSignalStore::importPublicKey(ec_public_key **result, MBinBuffer &buf) +{ + buf.appendBefore("\x05", 1); + curve_decode_point(result, buf.data(), buf.length(), m_pContext); +} + +void MSignalStore::injectSession(const char *szJid, const WANode *pNode, const WANode *pKey) +{ + WAJid jid(szJid); + auto *signedKey = pKey->getChild("skey"); + auto *key = pKey->getChild("key"); + auto *identity = pKey->getChild("identity"); + auto *registration = pNode->getChild("registration"); + if (!signedKey || !key || !identity || !registration) { + pProto->debugLogA("Bad key data for %s", jid.toString().c_str()); + return; + } + + signal_protocol_address address = {jid.user.c_str(), (unsigned)jid.user.GetLength(), jid.device}; + + session_builder *builder; + logError( + session_builder_create(&builder, m_pStore, &address, m_pContext), + "unable to create session cipher"); + + int regId = decodeBigEndian(registration->content); + int preKeyId = decodeBigEndian(key->getChild("id")->content); + int signedPreKeyId = decodeBigEndian(signedKey->getChild("id")->content); + + ec_public_key *preKeyPub, *signedPreKeyPub, *identityKey; + importPublicKey(&preKeyPub, key->getChild("value")->content); + importPublicKey(&identityKey, identity->content); + importPublicKey(&signedPreKeyPub, signedKey->getChild("value")->content); + + auto &sign = signedKey->getChild("signature")->content; + + session_pre_key_bundle *bundle; + logError( + session_pre_key_bundle_create(&bundle, regId, jid.device, preKeyId, preKeyPub, signedPreKeyId, signedPreKeyPub, sign.data(), sign.length(), identityKey), + "unable to create pre key bundle"); + + logError( + session_builder_process_pre_key_bundle(builder, bundle), + "unable to process pre key bundle"); + + session_pre_key_bundle_destroy((signal_type_base*)bundle); + session_builder_free(builder); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MBinBuffer MSignalStore::decryptSignalProto(const CMStringA &from, const char *pszType, const MBinBuffer &encrypted) +{ + WAJid jid(from); + auto *pSession = createSession(jid.user, jid.device); + + signal_buffer *result = nullptr; + if (!mir_strcmp(pszType, "pkmsg")) { + pre_key_signal_message *pMsg; + logError( + pre_key_signal_message_deserialize(&pMsg, encrypted.data(), encrypted.length(), m_pContext), + "unable to deserialize prekey message"); + + logError( + session_cipher_decrypt_pre_key_signal_message(pSession->getCipher(), pMsg, this, &result), + "unable to decrypt prekey message"); + + pre_key_signal_message_destroy((signal_type_base*)pMsg); + } + else { + signal_message *pMsg; + logError( + signal_message_deserialize(&pMsg, encrypted.data(), encrypted.length(), m_pContext), + "unable to deserialize signal message"); + + logError( + session_cipher_decrypt_signal_message(pSession->getCipher(), pMsg, this, &result), + "unable to decrypt signal message"); + + signal_message_destroy((signal_type_base *)pMsg); + } + + MBinBuffer res; + res.assign(result->data, result->len); + signal_buffer_free(result); + return res; +} + +MBinBuffer MSignalStore::decryptGroupSignalProto(const CMStringA &group, const CMStringA &sender, const MBinBuffer &encrypted) +{ + WAJid jid(sender); + signal_protocol_sender_key_name senderKeyName; + senderKeyName.group_id = group.c_str(); + senderKeyName.group_id_len = group.GetLength(); + senderKeyName.sender.device_id = 0; + senderKeyName.sender.name = jid.user.c_str(); + senderKeyName.sender.name_len = jid.user.GetLength(); + + group_cipher *cipher; + logError( + group_cipher_create(&cipher, m_pStore, &senderKeyName, m_pContext), + "unable to create group cipher"); + + sender_key_message *skmsg; + logError( + sender_key_message_deserialize(&skmsg, encrypted.data(), encrypted.length(), m_pContext), + "unable to deserialize skmsg"); + + signal_buffer *result = nullptr; + logError( + group_cipher_decrypt(cipher, skmsg, this, &result), + "unable to decrypt skmsg"); + + sender_key_message_destroy((signal_type_base *)skmsg); + group_cipher_free(cipher); + + MBinBuffer res; + res.assign(result->data, result->len); + signal_buffer_free(result); + return res; +} + +void MSignalStore::processSenderKeyMessage(const CMStringA &author, const Wa__Message__SenderKeyDistributionMessage *msg) +{ + WAJid jid(author); + signal_protocol_sender_key_name senderKeyName; + senderKeyName.group_id = msg->groupid; + senderKeyName.group_id_len = mir_strlen(msg->groupid); + senderKeyName.sender.device_id = 0; + senderKeyName.sender.name = jid.user.c_str(); + senderKeyName.sender.name_len = jid.user.GetLength(); + + group_session_builder *builder; + logError( + group_session_builder_create(&builder, m_pStore, m_pContext), + "unable to create session builder"); + + sender_key_distribution_message *skmsg; + logError( + sender_key_distribution_message_deserialize(&skmsg, msg->axolotlsenderkeydistributionmessage.data, msg->axolotlsenderkeydistributionmessage.len, m_pContext), + "unable to decode skd message"); + + logError( + group_session_builder_process_session(builder, &senderKeyName, skmsg), + "unable to process skd message"); + + sender_key_distribution_message_destroy((signal_type_base *)skmsg); + group_session_builder_free(builder); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// encryption + +MBinBuffer MSignalStore::encryptSenderKey(const WAJid &to, const CMStringA &from, const MBinBuffer &buf, MBinBuffer &skmsgKey) +{ + signal_protocol_sender_key_name senderKeyName; + senderKeyName.group_id = to.user.c_str(); + senderKeyName.group_id_len = to.user.GetLength(); + senderKeyName.sender.device_id = 0; + senderKeyName.sender.name = from.c_str(); + senderKeyName.sender.name_len = from.GetLength(); + + group_session_builder *builder; + logError( + group_session_builder_create(&builder, m_pStore, m_pContext), + "unable to create session builder"); + + sender_key_distribution_message *skmsg; + logError( + group_session_builder_create_session(builder, &skmsg, &senderKeyName), + "unable to create session"); + + group_cipher *cipher; + logError( + group_cipher_create(&cipher, m_pStore, &senderKeyName, m_pContext), + "unable to create group cipher"); + + ciphertext_message *encMessage; + logError( + group_cipher_encrypt(cipher, buf.data(), buf.length(), &encMessage), + "unable to encrypt group message"); + + MBinBuffer res; + auto *cipherText = ciphertext_message_get_serialized(encMessage); + res.assign(cipherText->data, cipherText->len); + + auto *pKey = sender_key_distribution_message_get_signature_key(skmsg); + skmsgKey.assign(pKey->data, sizeof(pKey->data)); + + sender_key_distribution_message_destroy((signal_type_base*)skmsg); + group_cipher_free(cipher); + group_session_builder_free(builder); + return res; +} + +MBinBuffer MSignalStore::encryptSignalProto(const WAJid &to, const MBinBuffer &buf, int &type) +{ + auto *pSession = createSession(to.user, to.device); + + ciphertext_message *pEncrypted; + logError( + session_cipher_encrypt(pSession->getCipher(), buf.data(), buf.length(), &pEncrypted), + "unable to encrypt signal message"); + + type = ciphertext_message_get_type(pEncrypted); + + MBinBuffer res; + auto *encBuf = ciphertext_message_get_serialized(pEncrypted); + res.assign(encBuf->data, encBuf->len); + SIGNAL_UNREF(pEncrypted); + return res; +} + +MBinBuffer MSignalStore::encodeSignedIdentity(bool bIncludeSignatureKey) +{ + proto::ADVSignedDeviceIdentity identity(pProto->getBlob("WAAccount")); + + if (!bIncludeSignatureKey) + proto::CleanBinary(identity->accountsignaturekey), identity->has_accountsignaturekey = false; + + return proto::Serialize(identity); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// generate and save pre keys set + +void MSignalStore::generatePrekeys(int count) +{ + int iNextKeyId = pProto->getDword(DBKEY_PREKEY_NEXT_ID, 1); + + CMStringA szSetting; + signal_protocol_key_helper_pre_key_list_node *keys_root; + signal_protocol_key_helper_generate_pre_keys(&keys_root, iNextKeyId, count, m_pContext); + for (auto *it = keys_root; it; it = signal_protocol_key_helper_key_list_next(it)) { + session_pre_key *pre_key = signal_protocol_key_helper_key_list_element(it); + uint32_t pre_key_id = session_pre_key_get_id(pre_key); + + SignalBuffer buf(pre_key); + szSetting.Format("PreKey%d", pre_key_id); + db_set_blob(0, pProto->m_szModuleName, szSetting, buf.data(), buf.len()); + + ec_key_pair *pre_key_pair = session_pre_key_get_key_pair(pre_key); + auto *pPubKey = ec_key_pair_get_public(pre_key_pair); + szSetting.Format("PreKey%dPublic", pre_key_id); + db_set_blob(0, pProto->m_szModuleName, szSetting, pPubKey->data, sizeof(pPubKey->data)); + } + signal_protocol_key_helper_key_list_free(keys_root); + + pProto->setDword(DBKEY_PREKEY_NEXT_ID, iNextKeyId + count); +} diff --git a/protocols/WhatsApp/src/stdafx.cxx b/protocols/WhatsApp/src/stdafx.cxx index 7942e82b45..5146fe9a0a 100644 --- a/protocols/WhatsApp/src/stdafx.cxx +++ b/protocols/WhatsApp/src/stdafx.cxx @@ -1,8 +1,8 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#include "stdafx.h" +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" diff --git a/protocols/WhatsApp/src/stdafx.h b/protocols/WhatsApp/src/stdafx.h index 945e385af2..3b1326e277 100644 --- a/protocols/WhatsApp/src/stdafx.h +++ b/protocols/WhatsApp/src/stdafx.h @@ -1,72 +1,72 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#pragma once -#pragma warning(disable:4996 4290 4200 4239 4324) - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "../../libs/libqrencode/src/qrencode.h" -#include "../../libs/zlib/src/zlib.h" - -#include "../../utils/mir_signal.h" - -#include "pmsg.proto.h" - -///////////////////////////////////////////////////////////////////////////////////////// - -#include "db.h" -#include "utils.h" -#include "proto.h" -#include "resource.h" - -#pragma comment(lib, "libcrypto.lib") +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#pragma once +#pragma warning(disable:4996 4290 4200 4239 4324) + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "../../libs/libqrencode/src/qrencode.h" +#include "../../libs/zlib/src/zlib.h" + +#include "../../utils/mir_signal.h" + +#include "pmsg.proto.h" + +///////////////////////////////////////////////////////////////////////////////////////// + +#include "db.h" +#include "utils.h" +#include "proto.h" +#include "resource.h" + +#pragma comment(lib, "libcrypto.lib") diff --git a/protocols/WhatsApp/src/utils.cpp b/protocols/WhatsApp/src/utils.cpp index 05e9d2c3bc..1e2d39f553 100644 --- a/protocols/WhatsApp/src/utils.cpp +++ b/protocols/WhatsApp/src/utils.cpp @@ -1,589 +1,589 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#include "stdafx.h" - -WAJid::WAJid(const char *pszUser, const char *pszServer, int iDevice, int iAgent) : - user(pszUser ? pszUser : ""), - server(pszServer ? pszServer : ""), - device(iDevice), - agent(iAgent) -{} - -WAJid::WAJid(const char *pszJid, int _id) -{ - if (pszJid == nullptr) - pszJid = ""; - - auto *tmp = NEWSTR_ALLOCA(pszJid); - auto *p = strrchr(tmp, '@'); - if (p) { - *p = 0; - server = p + 1; - } - - if (p = strrchr(tmp, ':')) { - *p = 0; - device = atoi(p + 1); - } - else device = _id; - - if (p = strrchr(tmp, '_')) { - *p = 0; - agent = atoi(p + 1); - } - else agent = 0; - - user = tmp; -} - -bool WAJid::isUser() const -{ return server == "s.whatsapp.net"; -} - -bool WAJid::isGroup() const -{ return server == "g.us"; -} - -bool WAJid::isBroadcast() const -{ - return server == "broadcast"; -} - -bool WAJid::isStatusBroadcast() const -{ - return isBroadcast() && user == "status"; -} - -CMStringA WAJid::toString() const -{ - CMStringA ret(user); - if (agent > 0) - ret.AppendFormat("_%d", agent); - if (device > 0) - ret.AppendFormat(":%d", device); - ret.AppendFormat("@%s", server.c_str()); - return ret; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static uint8_t sttLtHashInfo[] = "WhatsApp Patch Integrity"; - -void LT_HASH::add(const void *pData, size_t len) -{ - uint16_t tmp[_countof(hash)]; - HKDF(EVP_sha256(), (BYTE *)"", 0, (BYTE*)pData, len, sttLtHashInfo, sizeof(sttLtHashInfo) - 1, (BYTE *)tmp, sizeof(tmp)); - - for (int i = 0; i < _countof(hash); i++) - hash[i] += tmp[i]; -} - -void LT_HASH::sub(const void *pData, size_t len) -{ - uint16_t tmp[_countof(hash)]; - HKDF(EVP_sha256(), (BYTE *)"", 0, (BYTE *)pData, len, sttLtHashInfo, sizeof(sttLtHashInfo) - 1, (BYTE *)tmp, sizeof(tmp)); - - for (int i = 0; i < _countof(hash); i++) - hash[i] -= tmp[i]; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -WAUser* WhatsAppProto::FindUser(const char *szId) -{ - if (szId == nullptr) - return nullptr; - - mir_cslock lck(m_csUsers); - auto *tmp = (WAUser *)_alloca(sizeof(WAUser)); - tmp->szId = (char*)szId; - return m_arUsers.find(tmp); -} - -WAUser* WhatsAppProto::AddUser(const char *szId, bool bTemporary) -{ - auto *pUser = FindUser(szId); - if (pUser != nullptr) - return pUser; - - MCONTACT hContact = db_add_contact(); - Proto_AddToContact(hContact, m_szModuleName); - - pUser = new WAUser(hContact, mir_strdup(szId)); - pUser->bIsGroupChat = WAJid(szId).isGroup(); - - if (pUser->bIsGroupChat) { - setByte(hContact, "ChatRoom", 1); - setString(hContact, "ChatRoomID", szId); - } - else { - setString(hContact, DBKEY_ID, szId); - if (m_wszDefaultGroup) - Clist_SetGroup(hContact, m_wszDefaultGroup); - } - - if (bTemporary) - Contact::RemoveFromList(hContact); - - mir_cslock lck(m_csUsers); - m_arUsers.insert(pUser); - return pUser; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -WA_PKT_HANDLER WhatsAppProto::FindPersistentHandler(const WANode &pNode) -{ - auto *pChild = pNode.getFirstChild(); - CMStringA szChild = (pChild) ? pChild->title : ""; - CMStringA szTitle = pNode.title; - CMStringA szType = pNode.getAttr("type"); - CMStringA szXmlns = pNode.getAttr("xmlns"); - - for (auto &it : m_arPersistent) { - if (it->pszTitle && szTitle != it->pszTitle) - continue; - if (it->pszType && szType != it->pszType) - continue; - if (it->pszXmlns && szXmlns != it->pszXmlns) - continue; - if (it->pszChild && szChild != it->pszChild) - continue; - return it->pHandler; - } - - return nullptr; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -CMStringA WhatsAppProto::GenerateMessageId() -{ - return CMStringA(FORMAT, "%d.%d-%d", m_wMsgPrefix[0], m_wMsgPrefix[1], m_iPacketId++); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// sends a piece of JSON to a server via a websocket, masked - -int WhatsAppProto::WSSend(const ProtobufCMessage &msg) -{ - if (m_hServerConn == nullptr) - return -1; - - MBinBuffer buf(proto::Serialize(&msg)); - Netlib_Dump(m_hServerConn, buf.data(), buf.length(), true, 0); - - MBinBuffer payload = m_noise->encodeFrame(buf.data(), buf.length()); - WebSocket_SendBinary(m_hServerConn, payload.data(), payload.length()); - return 0; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -int WhatsAppProto::WSSendNode(WANode &node) -{ - if (m_hServerConn == nullptr) - return 0; - - CMStringA szText; - node.print(szText); - debugLogA("Sending binary node:\n%s", szText.c_str()); - - WAWriter writer; - writer.writeNode(&node); - - MBinBuffer encData = m_noise->encrypt(writer.body.data(), writer.body.length()); - MBinBuffer payload = m_noise->encodeFrame(encData.data(), encData.length()); - WebSocket_SendBinary(m_hServerConn, payload.data(), payload.length()); - return 1; -} - -int WhatsAppProto::WSSendNode(WANode &node, WA_PKT_HANDLER pHandler) -{ - if (m_hServerConn == nullptr) - return 0; - - CMStringA id(GenerateMessageId()); - node.addAttr("id", id); - { - mir_cslock lck(m_csPacketQueue); - m_arPacketQueue.insert(new WARequestSimple(id, pHandler)); - } - - return WSSendNode(node); -} - -int WhatsAppProto::WSSendNode(WANode &node, WA_PKT_HANDLER_FULL pHandler, void *pUserInfo) -{ - if (m_hServerConn == nullptr) - return 0; - - CMStringA id(GenerateMessageId()); - node.addAttr("id", id); - { - mir_cslock lck(m_csPacketQueue); - m_arPacketQueue.insert(new WARequestParam(id, pHandler, pUserInfo)); - } - - return WSSendNode(node); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -std::string decodeBinStr(const std::string &buf) -{ - size_t cbLen; - void *pData = mir_base64_decode(buf.c_str(), &cbLen); - if (pData == nullptr) - return ""; - - std::string res((char *)pData, cbLen); - mir_free(pData); - return res; -} - -MBinBuffer decodeBufStr(const std::string &buf) -{ - MBinBuffer res; - size_t cbLen; - void *pData = mir_base64_decode(buf.c_str(), &cbLen); - if (pData == nullptr) - return res; - - res.assign(pData, cbLen); - mir_free(pData); - return res; -} - -uint32_t decodeBigEndian(const uint8_t *buf, size_t len) -{ - uint32_t ret = 0; - for (int i = 0; i < len; i++) { - ret <<= 8; - ret += buf[i]; - } - - return ret; -} - -std::string encodeBigEndian(uint32_t num, size_t len) -{ - std::string res; - for (int i = 0; i < len; i++) { - char c = num & 0xFF; - res = c + res; - num >>= 8; - } - return res; -} - -void generateIV(uint8_t *iv, uint32_t &pVar) -{ - auto counter = encodeBigEndian(pVar); - memset(iv, 0, 8); - memcpy(iv + 8, counter.c_str(), sizeof(uint32_t)); - - pVar++; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Padding - -void padBuffer16(MBinBuffer &buf) -{ - uint8_t c = 16 - buf.length() % 16; - - for (uint8_t i = 0; i < c; i++) - buf.append(&c, 1); -} - -MBinBuffer unpadBuffer16(const MBinBuffer &buf) -{ - size_t len = buf.length(); - auto p = buf.data() + len - 1; - if (*p <= 0x10) { - MBinBuffer res; - res.assign(buf.data(), len - *p); - return res; - } - - return buf; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// Popups - -void WhatsAppProto::InitPopups(void) -{ - g_plugin.addPopupOption(CMStringW(FORMAT, TranslateT("%s error notifications"), m_tszUserName), m_bUsePopups); - - char name[256]; - mir_snprintf(name, "%s_%s", m_szModuleName, "Error"); - - wchar_t desc[256]; - mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Errors")); - - POPUPCLASS ppc = {}; - ppc.flags = PCF_UNICODE; - ppc.pszName = name; - ppc.pszDescription.w = desc; - ppc.hIcon = IcoLib_GetIconByHandle(m_hProtoIcon); - ppc.colorBack = RGB(191, 0, 0); //Red - ppc.colorText = RGB(255, 245, 225); //Yellow - ppc.iSeconds = 60; - m_hPopupClass = Popup_RegisterClass(&ppc); - - IcoLib_ReleaseIcon(ppc.hIcon); -} - -void WhatsAppProto::Popup(MCONTACT hContact, const wchar_t *szMsg, const wchar_t *szTitle) -{ - if (!m_bUsePopups) - return; - - char name[256]; - mir_snprintf(name, "%s_%s", m_szModuleName, "Error"); - - CMStringW wszTitle(szTitle); - if (hContact == 0) { - wszTitle.Insert(0, L": "); - wszTitle.Insert(0, m_tszUserName); - } - - POPUPDATACLASS ppd = {}; - ppd.szTitle.w = wszTitle; - ppd.szText.w = szMsg; - ppd.pszClassName = name; - ppd.hContact = hContact; - Popup_AddClass(&ppd); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -MBinBuffer WhatsAppProto::unzip(const MBinBuffer &src) -{ - z_stream strm = {}; - inflateInit(&strm); - - strm.avail_in = (uInt)src.length(); - strm.next_in = (Bytef *)src.data(); - - MBinBuffer res; - Bytef buf[2048]; - - while (strm.avail_in > 0) { - strm.avail_out = sizeof(buf); - strm.next_out = buf; - - int ret = inflate(&strm, Z_NO_FLUSH); - switch (ret) { - case Z_NEED_DICT: - ret = Z_DATA_ERROR; - __fallthrough; - - case Z_DATA_ERROR: - case Z_MEM_ERROR: - inflateEnd(&strm); - return res; - } - - res.append(buf, sizeof(buf) - strm.avail_out); - if (ret == Z_STREAM_END) - break; - } - - inflateEnd(&strm); - return res; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void bin2file(const MBinBuffer &buf, const wchar_t *pwszFileName) -{ - int fileId = _wopen(pwszFileName, _O_WRONLY | _O_TRUNC | _O_BINARY | _O_CREAT, _S_IREAD | _S_IWRITE); - if (fileId != -1) { - write(fileId, buf.data(), (unsigned)buf.length()); - close(fileId); - } -} - -void string2file(const std::string &str, const wchar_t *pwszFileName) -{ - int fileId = _wopen(pwszFileName, _O_WRONLY | _O_TRUNC | _O_BINARY | _O_CREAT, _S_IREAD | _S_IWRITE); - if (fileId != -1) { - write(fileId, str.c_str(), (unsigned)str.size()); - close(fileId); - } -} - -CMStringA file2string(const wchar_t *pwszFileName) -{ - CMStringA res; - - int fileId = _wopen(pwszFileName, _O_RDONLY | _O_BINARY, _S_IREAD | _S_IWRITE); - if (fileId != -1) { - res.Truncate(filelength(fileId)); - read(fileId, res.GetBuffer(), res.GetLength()); - close(fileId); - } - return res; -} - -void WhatsAppProto::GetMessageContent( - CMStringA &txt, - const char *szType, - const char *szMimeType, - const char *szUrl, - const char *szDirectPath, - const ProtobufCBinaryData &pMediaKey, - const char *szCaption) -{ - if (szCaption) { - if (m_bUseBbcodes) - txt.Append("[b]"); - txt.Append(szCaption); - if (m_bUseBbcodes) - txt.Append("[/b]"); - txt.Append("\n"); - } - - CMStringA url = szUrl; - int idx = url.ReverseFind('/'); - if (idx != -1) - url.Delete(0, idx+1); - idx = url.ReverseFind('.'); - if (idx != -1) - url.Truncate(idx); - if (szMimeType) - url.Append(_T2A(ProtoGetAvatarExtension(ProtoGetAvatarFormatByMimeType(szMimeType)))); - - char *szMediaType = NEWSTR_ALLOCA(szType); - szMediaType[0] = toupper(szMediaType[0]); - - MBinBuffer buf = DownloadEncryptedFile(directPath2url(szDirectPath), pMediaKey, szMediaType); - if (!buf.isEmpty()) { - CMStringW pwszFileName(GetTmpFileName(szType, url)); - bin2file(buf, pwszFileName); - - pwszFileName.Replace(L"\\", L"/"); - txt.AppendFormat("file://%s", T2Utf(pwszFileName).get()); - } -} - -CMStringA WhatsAppProto::GetMessageText(const Wa__Message *pMessage) -{ - CMStringA szMessageText; - - if (pMessage) { - if (auto *pExt = pMessage->extendedtextmessage) { - if (pExt->title) { - if (m_bUseBbcodes) - szMessageText.Append("[b]"); - szMessageText.Append(pExt->title); - if (m_bUseBbcodes) - szMessageText.Append("[/b]"); - szMessageText.Append("\n"); - } - - if (pExt->contextinfo && pExt->contextinfo->quotedmessage) - szMessageText.AppendFormat("> %s\n\n", pExt->contextinfo->quotedmessage->conversation); - - if (pExt->text) - szMessageText.Append(pExt->text); - } - else if (auto *pAudio = pMessage->audiomessage) { - GetMessageContent(szMessageText, "audio", pAudio->url, pAudio->directpath, pAudio->mimetype, pAudio->mediakey); - } - else if (auto *pVideo = pMessage->videomessage) { - GetMessageContent(szMessageText, "video", pVideo->url, pVideo->directpath, pVideo->mimetype, pVideo->mediakey, pVideo->caption); - } - else if (auto *pImage = pMessage->imagemessage) { - GetMessageContent(szMessageText, "image", pImage->url, pImage->directpath, pImage->mimetype, pImage->mediakey, pImage->caption); - } - else if (mir_strlen(pMessage->conversation)) - szMessageText = pMessage->conversation; - } - - return szMessageText; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -void proto::CleanBinary(ProtobufCBinaryData &field) -{ - if (field.data) { - free(field.data); - field.data = nullptr; - } - field.len = 0; -} - -ProtobufCBinaryData proto::SetBinary(const void *pData, size_t len) -{ - ProtobufCBinaryData res; - if (pData == nullptr) { - res.data = nullptr; - res.len = 0; - } - else { - res.data = (uint8_t *)malloc(res.len = len); - memcpy(res.data, pData, len); - } - return res; -} - -MBinBuffer proto::Serialize(const ProtobufCMessage *msg) -{ - MBinBuffer res(protobuf_c_message_get_packed_size(msg)); - protobuf_c_message_pack(msg, res.data()); - return res; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -CMStringA directPath2url(const char *pszDirectPath) -{ - return CMStringA("https://mmg.whatsapp.net") + pszDirectPath; -} - -WAMediaKeys::WAMediaKeys(const uint8_t *pKey, size_t keyLen, const char *pszMediaType) -{ - CMStringA pszHkdfString(FORMAT, "WhatsApp %s Keys", pszMediaType); - - HKDF(EVP_sha256(), (BYTE *)"", 0, pKey, (int)keyLen, (BYTE *)pszHkdfString.c_str(), pszHkdfString.GetLength(), (BYTE *)this, sizeof(*this)); -} - -MBinBuffer WhatsAppProto::DownloadEncryptedFile(const char *url, const ProtobufCBinaryData &mediaKeys, const char *pszMediaType) -{ - NETLIBHTTPHEADER headers[1] = {{"Origin", "https://web.whatsapp.com"}}; - - NETLIBHTTPREQUEST req = {}; - req.cbSize = sizeof(req); - req.requestType = REQUEST_GET; - req.szUrl = (char*)url; - req.headersCount = _countof(headers); - req.headers = headers; - - MBinBuffer ret; - auto *pResp = Netlib_HttpTransaction(m_hNetlibUser, &req); - if (pResp) { - if (pResp->resultCode == 200) { - WAMediaKeys out(mediaKeys.data, mediaKeys.len, pszMediaType); - ret = aesDecrypt(EVP_aes_256_cbc(), out.cipherKey, out.iv, pResp->pData, pResp->dataLength); - } - } - - return ret; -} - -CMStringW WhatsAppProto::GetTmpFileName(const char *pszClass, const char *pszAddition) -{ - CMStringW ret(VARSW(L"%miranda_userdata%")); - ret.AppendFormat(L"/%S/%S_%S", m_szModuleName, pszClass, pszAddition); - return ret; -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" + +WAJid::WAJid(const char *pszUser, const char *pszServer, int iDevice, int iAgent) : + user(pszUser ? pszUser : ""), + server(pszServer ? pszServer : ""), + device(iDevice), + agent(iAgent) +{} + +WAJid::WAJid(const char *pszJid, int _id) +{ + if (pszJid == nullptr) + pszJid = ""; + + auto *tmp = NEWSTR_ALLOCA(pszJid); + auto *p = strrchr(tmp, '@'); + if (p) { + *p = 0; + server = p + 1; + } + + if (p = strrchr(tmp, ':')) { + *p = 0; + device = atoi(p + 1); + } + else device = _id; + + if (p = strrchr(tmp, '_')) { + *p = 0; + agent = atoi(p + 1); + } + else agent = 0; + + user = tmp; +} + +bool WAJid::isUser() const +{ return server == "s.whatsapp.net"; +} + +bool WAJid::isGroup() const +{ return server == "g.us"; +} + +bool WAJid::isBroadcast() const +{ + return server == "broadcast"; +} + +bool WAJid::isStatusBroadcast() const +{ + return isBroadcast() && user == "status"; +} + +CMStringA WAJid::toString() const +{ + CMStringA ret(user); + if (agent > 0) + ret.AppendFormat("_%d", agent); + if (device > 0) + ret.AppendFormat(":%d", device); + ret.AppendFormat("@%s", server.c_str()); + return ret; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static uint8_t sttLtHashInfo[] = "WhatsApp Patch Integrity"; + +void LT_HASH::add(const void *pData, size_t len) +{ + uint16_t tmp[_countof(hash)]; + HKDF(EVP_sha256(), (BYTE *)"", 0, (BYTE*)pData, len, sttLtHashInfo, sizeof(sttLtHashInfo) - 1, (BYTE *)tmp, sizeof(tmp)); + + for (int i = 0; i < _countof(hash); i++) + hash[i] += tmp[i]; +} + +void LT_HASH::sub(const void *pData, size_t len) +{ + uint16_t tmp[_countof(hash)]; + HKDF(EVP_sha256(), (BYTE *)"", 0, (BYTE *)pData, len, sttLtHashInfo, sizeof(sttLtHashInfo) - 1, (BYTE *)tmp, sizeof(tmp)); + + for (int i = 0; i < _countof(hash); i++) + hash[i] -= tmp[i]; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +WAUser* WhatsAppProto::FindUser(const char *szId) +{ + if (szId == nullptr) + return nullptr; + + mir_cslock lck(m_csUsers); + auto *tmp = (WAUser *)_alloca(sizeof(WAUser)); + tmp->szId = (char*)szId; + return m_arUsers.find(tmp); +} + +WAUser* WhatsAppProto::AddUser(const char *szId, bool bTemporary) +{ + auto *pUser = FindUser(szId); + if (pUser != nullptr) + return pUser; + + MCONTACT hContact = db_add_contact(); + Proto_AddToContact(hContact, m_szModuleName); + + pUser = new WAUser(hContact, mir_strdup(szId)); + pUser->bIsGroupChat = WAJid(szId).isGroup(); + + if (pUser->bIsGroupChat) { + setByte(hContact, "ChatRoom", 1); + setString(hContact, "ChatRoomID", szId); + } + else { + setString(hContact, DBKEY_ID, szId); + if (m_wszDefaultGroup) + Clist_SetGroup(hContact, m_wszDefaultGroup); + } + + if (bTemporary) + Contact::RemoveFromList(hContact); + + mir_cslock lck(m_csUsers); + m_arUsers.insert(pUser); + return pUser; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +WA_PKT_HANDLER WhatsAppProto::FindPersistentHandler(const WANode &pNode) +{ + auto *pChild = pNode.getFirstChild(); + CMStringA szChild = (pChild) ? pChild->title : ""; + CMStringA szTitle = pNode.title; + CMStringA szType = pNode.getAttr("type"); + CMStringA szXmlns = pNode.getAttr("xmlns"); + + for (auto &it : m_arPersistent) { + if (it->pszTitle && szTitle != it->pszTitle) + continue; + if (it->pszType && szType != it->pszType) + continue; + if (it->pszXmlns && szXmlns != it->pszXmlns) + continue; + if (it->pszChild && szChild != it->pszChild) + continue; + return it->pHandler; + } + + return nullptr; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +CMStringA WhatsAppProto::GenerateMessageId() +{ + return CMStringA(FORMAT, "%d.%d-%d", m_wMsgPrefix[0], m_wMsgPrefix[1], m_iPacketId++); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// sends a piece of JSON to a server via a websocket, masked + +int WhatsAppProto::WSSend(const ProtobufCMessage &msg) +{ + if (m_hServerConn == nullptr) + return -1; + + MBinBuffer buf(proto::Serialize(&msg)); + Netlib_Dump(m_hServerConn, buf.data(), buf.length(), true, 0); + + MBinBuffer payload = m_noise->encodeFrame(buf.data(), buf.length()); + WebSocket_SendBinary(m_hServerConn, payload.data(), payload.length()); + return 0; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +int WhatsAppProto::WSSendNode(WANode &node) +{ + if (m_hServerConn == nullptr) + return 0; + + CMStringA szText; + node.print(szText); + debugLogA("Sending binary node:\n%s", szText.c_str()); + + WAWriter writer; + writer.writeNode(&node); + + MBinBuffer encData = m_noise->encrypt(writer.body.data(), writer.body.length()); + MBinBuffer payload = m_noise->encodeFrame(encData.data(), encData.length()); + WebSocket_SendBinary(m_hServerConn, payload.data(), payload.length()); + return 1; +} + +int WhatsAppProto::WSSendNode(WANode &node, WA_PKT_HANDLER pHandler) +{ + if (m_hServerConn == nullptr) + return 0; + + CMStringA id(GenerateMessageId()); + node.addAttr("id", id); + { + mir_cslock lck(m_csPacketQueue); + m_arPacketQueue.insert(new WARequestSimple(id, pHandler)); + } + + return WSSendNode(node); +} + +int WhatsAppProto::WSSendNode(WANode &node, WA_PKT_HANDLER_FULL pHandler, void *pUserInfo) +{ + if (m_hServerConn == nullptr) + return 0; + + CMStringA id(GenerateMessageId()); + node.addAttr("id", id); + { + mir_cslock lck(m_csPacketQueue); + m_arPacketQueue.insert(new WARequestParam(id, pHandler, pUserInfo)); + } + + return WSSendNode(node); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +std::string decodeBinStr(const std::string &buf) +{ + size_t cbLen; + void *pData = mir_base64_decode(buf.c_str(), &cbLen); + if (pData == nullptr) + return ""; + + std::string res((char *)pData, cbLen); + mir_free(pData); + return res; +} + +MBinBuffer decodeBufStr(const std::string &buf) +{ + MBinBuffer res; + size_t cbLen; + void *pData = mir_base64_decode(buf.c_str(), &cbLen); + if (pData == nullptr) + return res; + + res.assign(pData, cbLen); + mir_free(pData); + return res; +} + +uint32_t decodeBigEndian(const uint8_t *buf, size_t len) +{ + uint32_t ret = 0; + for (int i = 0; i < len; i++) { + ret <<= 8; + ret += buf[i]; + } + + return ret; +} + +std::string encodeBigEndian(uint32_t num, size_t len) +{ + std::string res; + for (int i = 0; i < len; i++) { + char c = num & 0xFF; + res = c + res; + num >>= 8; + } + return res; +} + +void generateIV(uint8_t *iv, uint32_t &pVar) +{ + auto counter = encodeBigEndian(pVar); + memset(iv, 0, 8); + memcpy(iv + 8, counter.c_str(), sizeof(uint32_t)); + + pVar++; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Padding + +void padBuffer16(MBinBuffer &buf) +{ + uint8_t c = 16 - buf.length() % 16; + + for (uint8_t i = 0; i < c; i++) + buf.append(&c, 1); +} + +MBinBuffer unpadBuffer16(const MBinBuffer &buf) +{ + size_t len = buf.length(); + auto p = buf.data() + len - 1; + if (*p <= 0x10) { + MBinBuffer res; + res.assign(buf.data(), len - *p); + return res; + } + + return buf; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// Popups + +void WhatsAppProto::InitPopups(void) +{ + g_plugin.addPopupOption(CMStringW(FORMAT, TranslateT("%s error notifications"), m_tszUserName), m_bUsePopups); + + char name[256]; + mir_snprintf(name, "%s_%s", m_szModuleName, "Error"); + + wchar_t desc[256]; + mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Errors")); + + POPUPCLASS ppc = {}; + ppc.flags = PCF_UNICODE; + ppc.pszName = name; + ppc.pszDescription.w = desc; + ppc.hIcon = IcoLib_GetIconByHandle(m_hProtoIcon); + ppc.colorBack = RGB(191, 0, 0); //Red + ppc.colorText = RGB(255, 245, 225); //Yellow + ppc.iSeconds = 60; + m_hPopupClass = Popup_RegisterClass(&ppc); + + IcoLib_ReleaseIcon(ppc.hIcon); +} + +void WhatsAppProto::Popup(MCONTACT hContact, const wchar_t *szMsg, const wchar_t *szTitle) +{ + if (!m_bUsePopups) + return; + + char name[256]; + mir_snprintf(name, "%s_%s", m_szModuleName, "Error"); + + CMStringW wszTitle(szTitle); + if (hContact == 0) { + wszTitle.Insert(0, L": "); + wszTitle.Insert(0, m_tszUserName); + } + + POPUPDATACLASS ppd = {}; + ppd.szTitle.w = wszTitle; + ppd.szText.w = szMsg; + ppd.pszClassName = name; + ppd.hContact = hContact; + Popup_AddClass(&ppd); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +MBinBuffer WhatsAppProto::unzip(const MBinBuffer &src) +{ + z_stream strm = {}; + inflateInit(&strm); + + strm.avail_in = (uInt)src.length(); + strm.next_in = (Bytef *)src.data(); + + MBinBuffer res; + Bytef buf[2048]; + + while (strm.avail_in > 0) { + strm.avail_out = sizeof(buf); + strm.next_out = buf; + + int ret = inflate(&strm, Z_NO_FLUSH); + switch (ret) { + case Z_NEED_DICT: + ret = Z_DATA_ERROR; + __fallthrough; + + case Z_DATA_ERROR: + case Z_MEM_ERROR: + inflateEnd(&strm); + return res; + } + + res.append(buf, sizeof(buf) - strm.avail_out); + if (ret == Z_STREAM_END) + break; + } + + inflateEnd(&strm); + return res; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void bin2file(const MBinBuffer &buf, const wchar_t *pwszFileName) +{ + int fileId = _wopen(pwszFileName, _O_WRONLY | _O_TRUNC | _O_BINARY | _O_CREAT, _S_IREAD | _S_IWRITE); + if (fileId != -1) { + write(fileId, buf.data(), (unsigned)buf.length()); + close(fileId); + } +} + +void string2file(const std::string &str, const wchar_t *pwszFileName) +{ + int fileId = _wopen(pwszFileName, _O_WRONLY | _O_TRUNC | _O_BINARY | _O_CREAT, _S_IREAD | _S_IWRITE); + if (fileId != -1) { + write(fileId, str.c_str(), (unsigned)str.size()); + close(fileId); + } +} + +CMStringA file2string(const wchar_t *pwszFileName) +{ + CMStringA res; + + int fileId = _wopen(pwszFileName, _O_RDONLY | _O_BINARY, _S_IREAD | _S_IWRITE); + if (fileId != -1) { + res.Truncate(filelength(fileId)); + read(fileId, res.GetBuffer(), res.GetLength()); + close(fileId); + } + return res; +} + +void WhatsAppProto::GetMessageContent( + CMStringA &txt, + const char *szType, + const char *szMimeType, + const char *szUrl, + const char *szDirectPath, + const ProtobufCBinaryData &pMediaKey, + const char *szCaption) +{ + if (szCaption) { + if (m_bUseBbcodes) + txt.Append("[b]"); + txt.Append(szCaption); + if (m_bUseBbcodes) + txt.Append("[/b]"); + txt.Append("\n"); + } + + CMStringA url = szUrl; + int idx = url.ReverseFind('/'); + if (idx != -1) + url.Delete(0, idx+1); + idx = url.ReverseFind('.'); + if (idx != -1) + url.Truncate(idx); + if (szMimeType) + url.Append(_T2A(ProtoGetAvatarExtension(ProtoGetAvatarFormatByMimeType(szMimeType)))); + + char *szMediaType = NEWSTR_ALLOCA(szType); + szMediaType[0] = toupper(szMediaType[0]); + + MBinBuffer buf = DownloadEncryptedFile(directPath2url(szDirectPath), pMediaKey, szMediaType); + if (!buf.isEmpty()) { + CMStringW pwszFileName(GetTmpFileName(szType, url)); + bin2file(buf, pwszFileName); + + pwszFileName.Replace(L"\\", L"/"); + txt.AppendFormat("file://%s", T2Utf(pwszFileName).get()); + } +} + +CMStringA WhatsAppProto::GetMessageText(const Wa__Message *pMessage) +{ + CMStringA szMessageText; + + if (pMessage) { + if (auto *pExt = pMessage->extendedtextmessage) { + if (pExt->title) { + if (m_bUseBbcodes) + szMessageText.Append("[b]"); + szMessageText.Append(pExt->title); + if (m_bUseBbcodes) + szMessageText.Append("[/b]"); + szMessageText.Append("\n"); + } + + if (pExt->contextinfo && pExt->contextinfo->quotedmessage) + szMessageText.AppendFormat("> %s\n\n", pExt->contextinfo->quotedmessage->conversation); + + if (pExt->text) + szMessageText.Append(pExt->text); + } + else if (auto *pAudio = pMessage->audiomessage) { + GetMessageContent(szMessageText, "audio", pAudio->url, pAudio->directpath, pAudio->mimetype, pAudio->mediakey); + } + else if (auto *pVideo = pMessage->videomessage) { + GetMessageContent(szMessageText, "video", pVideo->url, pVideo->directpath, pVideo->mimetype, pVideo->mediakey, pVideo->caption); + } + else if (auto *pImage = pMessage->imagemessage) { + GetMessageContent(szMessageText, "image", pImage->url, pImage->directpath, pImage->mimetype, pImage->mediakey, pImage->caption); + } + else if (mir_strlen(pMessage->conversation)) + szMessageText = pMessage->conversation; + } + + return szMessageText; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +void proto::CleanBinary(ProtobufCBinaryData &field) +{ + if (field.data) { + free(field.data); + field.data = nullptr; + } + field.len = 0; +} + +ProtobufCBinaryData proto::SetBinary(const void *pData, size_t len) +{ + ProtobufCBinaryData res; + if (pData == nullptr) { + res.data = nullptr; + res.len = 0; + } + else { + res.data = (uint8_t *)malloc(res.len = len); + memcpy(res.data, pData, len); + } + return res; +} + +MBinBuffer proto::Serialize(const ProtobufCMessage *msg) +{ + MBinBuffer res(protobuf_c_message_get_packed_size(msg)); + protobuf_c_message_pack(msg, res.data()); + return res; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +CMStringA directPath2url(const char *pszDirectPath) +{ + return CMStringA("https://mmg.whatsapp.net") + pszDirectPath; +} + +WAMediaKeys::WAMediaKeys(const uint8_t *pKey, size_t keyLen, const char *pszMediaType) +{ + CMStringA pszHkdfString(FORMAT, "WhatsApp %s Keys", pszMediaType); + + HKDF(EVP_sha256(), (BYTE *)"", 0, pKey, (int)keyLen, (BYTE *)pszHkdfString.c_str(), pszHkdfString.GetLength(), (BYTE *)this, sizeof(*this)); +} + +MBinBuffer WhatsAppProto::DownloadEncryptedFile(const char *url, const ProtobufCBinaryData &mediaKeys, const char *pszMediaType) +{ + NETLIBHTTPHEADER headers[1] = {{"Origin", "https://web.whatsapp.com"}}; + + NETLIBHTTPREQUEST req = {}; + req.cbSize = sizeof(req); + req.requestType = REQUEST_GET; + req.szUrl = (char*)url; + req.headersCount = _countof(headers); + req.headers = headers; + + MBinBuffer ret; + auto *pResp = Netlib_HttpTransaction(m_hNetlibUser, &req); + if (pResp) { + if (pResp->resultCode == 200) { + WAMediaKeys out(mediaKeys.data, mediaKeys.len, pszMediaType); + ret = aesDecrypt(EVP_aes_256_cbc(), out.cipherKey, out.iv, pResp->pData, pResp->dataLength); + } + } + + return ret; +} + +CMStringW WhatsAppProto::GetTmpFileName(const char *pszClass, const char *pszAddition) +{ + CMStringW ret(VARSW(L"%miranda_userdata%")); + ret.AppendFormat(L"/%S/%S_%S", m_szModuleName, pszClass, pszAddition); + return ret; +} diff --git a/protocols/WhatsApp/src/utils.h b/protocols/WhatsApp/src/utils.h index 02882f1c17..af02695818 100644 --- a/protocols/WhatsApp/src/utils.h +++ b/protocols/WhatsApp/src/utils.h @@ -1,267 +1,267 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#define DICT_VERSION 2 - -#define LIST_EMPTY 0 -#define STREAM_END 2 -#define DICTIONARY_0 236 -#define DICTIONARY_1 237 -#define DICTIONARY_2 238 -#define DICTIONARY_3 239 -#define AD_JID 247 -#define LIST_8 248 -#define LIST_16 249 -#define JID_PAIR 250 -#define HEX_8 251 -#define BINARY_8 252 -#define BINARY_20 253 -#define BINARY_32 254 -#define NIBBLE_8 255 - -class WANode // kinda XML -{ - friend class WAReader; - friend class WAWriter; - - WANode *pParent = nullptr; - OBJLIST attrs; - OBJLIST children; - -public: - WANode(); - WANode(const char *pszTitle); - ~WANode(); - - void addAttr(const char *pszName, const char *pszValue); - void addAttr(const char *pszName, int iValue); - int getAttrInt(const char *pszName) const; - const char *getAttr(const char *pszName) const; - - CMStringA getBody() const; - - WANode *addChild(const char *pszName); - WANode *getChild(const char *pszName) const; - WANode *getFirstChild(void) const; - const OBJLIST &getChildren(void) const - { return children; - } - - void print(CMStringA &dest, int level = 0) const; - - CMStringA title; - MBinBuffer content; -}; - -__forceinline WANode &operator<<(WANode &node, const CHAR_PARAM ¶m) -{ - node.addAttr(param.szName, param.szValue); - return node; -} - -__forceinline WANode &operator<<(WANode &node, const INT_PARAM ¶m) -{ - node.addAttr(param.szName, param.iValue); - return node; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -namespace IQ -{ - enum Type { GET, SET, RESULT }; -}; - -struct WANodeIq : public WANode -{ - WANodeIq(IQ::Type type, const char *pszXmlns = nullptr, const char *pszTo = nullptr); -}; - -///////////////////////////////////////////////////////////////////////////////////////// - -struct XCHILD -{ - const char *name, *value; - - __forceinline XCHILD(const char *_name) : - name(_name) - {} -}; - -__forceinline WANode& operator<<(WANode &node, const XCHILD &child) -{ - node.addChild(child.name); - return node; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// WAReader - -class WAReader -{ - const BYTE *m_buf, *m_limit; - - uint32_t readIntN(int i); - CMStringA readStringFromChars(int size); - - bool readAttributes(WANode *node, int count); - uint32_t readInt20(); - bool readList(WANode *pParent, int tag); - int readListSize(int tag); - CMStringA readPacked(int tag); - CMStringA readString(int tag); - -public: - WAReader(const void *buf, size_t cbLen) : - m_buf((BYTE*)buf), - m_limit((BYTE*)buf + cbLen) - {} - - WANode* readNode(); - - __forceinline uint32_t readInt8() { return readIntN(1); } - __forceinline uint32_t readInt16() { return readIntN(2); } - __forceinline uint32_t readInt32() { return readIntN(4); } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// WAWriter - -class WAWriter -{ - __forceinline void writeInt8(int value) { writeIntN(value, 1); } - __forceinline void writeInt16(int value) { writeIntN(value, 2); } - __forceinline void writeInt32(int value) { writeIntN(value, 4); } - - void writeByte(uint8_t b); - void writeIntN(int value, int i); - void writeInt20(int value); - void writeLength(int value); - void writeListSize(int tag); - void writePacked(const CMStringA &str, int tag); - void writeString(const char *str); - bool writeToken(const char *str); - -public: - void writeNode(const WANode *pNode); - - MBinBuffer body; -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// WAJid - -struct WAJid -{ - int device, agent; - CMStringA user, server; - - WAJid(const char *pszJid, int device = 0); - WAJid(const char *pszUser, const char *pszServer, int device = 0, int agent = 0); - - CMStringA toString() const; - - bool isUser() const; - bool isGroup() const; - bool isBroadcast() const; - bool isStatusBroadcast() const; -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// WASendTask - -struct WASendTask -{ - WASendTask(const char *jid) : - payLoad("message"), - arDest(1) - { - uint8_t msgId[8]; - Utils_GetRandom(&msgId, sizeof(msgId)); - bin2hex(msgId, sizeof(msgId), szMsgId); - strupr(szMsgId); - - payLoad << CHAR_PARAM("id", szMsgId) << CHAR_PARAM("type", "text") << CHAR_PARAM("to", jid); - } - - char szMsgId[40]; - WANode payLoad; - OBJLIST arDest; - MBinBuffer content; -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// LT_HASH - -struct LT_HASH -{ - LT_HASH() - { - init(); - }; - - uint16_t hash[64]; - - void add(const void *pData, size_t len); - void sub(const void *pData, size_t len); - - void init() - { - SecureZeroMemory(hash, sizeof(hash)); - } -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// functions - -void bin2file(const MBinBuffer &buf, const wchar_t *pwszFileName); - -void string2file(const std::string &str, const wchar_t *pwszFileName); -CMStringA file2string(const wchar_t *pwszFileName); - -CMStringA directPath2url(const char *pszDirectPath); - -std::string decodeBinStr(const std::string &buf); -MBinBuffer decodeBufStr(const std::string &buf); - -void padBuffer16(MBinBuffer &buf); -MBinBuffer unpadBuffer16(const MBinBuffer &buf); - -CMStringA protobuf_c_text_to_string(const ProtobufCMessage *m); - -MBinBuffer aesDecrypt( - const EVP_CIPHER *cipher, - const uint8_t *key, - const uint8_t *iv, - const void *data, size_t dataLen, - const void *additionalData = 0, size_t additionalLen = 0); - -MBinBuffer aesEncrypt( - const EVP_CIPHER *cipher, - const uint8_t *key, - const uint8_t *iv, - const void *data, size_t dataLen, - const void *additionalData = 0, size_t additionalLen = 0); - -uint32_t decodeBigEndian(const uint8_t *pData, size_t len); - -__forceinline uint32_t decodeBigEndian(const ProtobufCBinaryData &buf) { - return decodeBigEndian(buf.data, buf.len); -} -__forceinline uint32_t decodeBigEndian(const MBinBuffer &buf) { - return decodeBigEndian(buf.data(), buf.length()); -} - -std::string encodeBigEndian(uint32_t num, size_t len = sizeof(uint32_t)); - -void generateIV(uint8_t *iv, uint32_t &pVar); - -unsigned char* HKDF(const EVP_MD *evp_md, - const unsigned char *salt, size_t salt_len, - const unsigned char *key, size_t key_len, - const unsigned char *info, size_t info_len, - unsigned char *okm, size_t okm_len); +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#define DICT_VERSION 2 + +#define LIST_EMPTY 0 +#define STREAM_END 2 +#define DICTIONARY_0 236 +#define DICTIONARY_1 237 +#define DICTIONARY_2 238 +#define DICTIONARY_3 239 +#define AD_JID 247 +#define LIST_8 248 +#define LIST_16 249 +#define JID_PAIR 250 +#define HEX_8 251 +#define BINARY_8 252 +#define BINARY_20 253 +#define BINARY_32 254 +#define NIBBLE_8 255 + +class WANode // kinda XML +{ + friend class WAReader; + friend class WAWriter; + + WANode *pParent = nullptr; + OBJLIST attrs; + OBJLIST children; + +public: + WANode(); + WANode(const char *pszTitle); + ~WANode(); + + void addAttr(const char *pszName, const char *pszValue); + void addAttr(const char *pszName, int iValue); + int getAttrInt(const char *pszName) const; + const char *getAttr(const char *pszName) const; + + CMStringA getBody() const; + + WANode *addChild(const char *pszName); + WANode *getChild(const char *pszName) const; + WANode *getFirstChild(void) const; + const OBJLIST &getChildren(void) const + { return children; + } + + void print(CMStringA &dest, int level = 0) const; + + CMStringA title; + MBinBuffer content; +}; + +__forceinline WANode &operator<<(WANode &node, const CHAR_PARAM ¶m) +{ + node.addAttr(param.szName, param.szValue); + return node; +} + +__forceinline WANode &operator<<(WANode &node, const INT_PARAM ¶m) +{ + node.addAttr(param.szName, param.iValue); + return node; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +namespace IQ +{ + enum Type { GET, SET, RESULT }; +}; + +struct WANodeIq : public WANode +{ + WANodeIq(IQ::Type type, const char *pszXmlns = nullptr, const char *pszTo = nullptr); +}; + +///////////////////////////////////////////////////////////////////////////////////////// + +struct XCHILD +{ + const char *name, *value; + + __forceinline XCHILD(const char *_name) : + name(_name) + {} +}; + +__forceinline WANode& operator<<(WANode &node, const XCHILD &child) +{ + node.addChild(child.name); + return node; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// WAReader + +class WAReader +{ + const BYTE *m_buf, *m_limit; + + uint32_t readIntN(int i); + CMStringA readStringFromChars(int size); + + bool readAttributes(WANode *node, int count); + uint32_t readInt20(); + bool readList(WANode *pParent, int tag); + int readListSize(int tag); + CMStringA readPacked(int tag); + CMStringA readString(int tag); + +public: + WAReader(const void *buf, size_t cbLen) : + m_buf((BYTE*)buf), + m_limit((BYTE*)buf + cbLen) + {} + + WANode* readNode(); + + __forceinline uint32_t readInt8() { return readIntN(1); } + __forceinline uint32_t readInt16() { return readIntN(2); } + __forceinline uint32_t readInt32() { return readIntN(4); } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// WAWriter + +class WAWriter +{ + __forceinline void writeInt8(int value) { writeIntN(value, 1); } + __forceinline void writeInt16(int value) { writeIntN(value, 2); } + __forceinline void writeInt32(int value) { writeIntN(value, 4); } + + void writeByte(uint8_t b); + void writeIntN(int value, int i); + void writeInt20(int value); + void writeLength(int value); + void writeListSize(int tag); + void writePacked(const CMStringA &str, int tag); + void writeString(const char *str); + bool writeToken(const char *str); + +public: + void writeNode(const WANode *pNode); + + MBinBuffer body; +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// WAJid + +struct WAJid +{ + int device, agent; + CMStringA user, server; + + WAJid(const char *pszJid, int device = 0); + WAJid(const char *pszUser, const char *pszServer, int device = 0, int agent = 0); + + CMStringA toString() const; + + bool isUser() const; + bool isGroup() const; + bool isBroadcast() const; + bool isStatusBroadcast() const; +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// WASendTask + +struct WASendTask +{ + WASendTask(const char *jid) : + payLoad("message"), + arDest(1) + { + uint8_t msgId[8]; + Utils_GetRandom(&msgId, sizeof(msgId)); + bin2hex(msgId, sizeof(msgId), szMsgId); + strupr(szMsgId); + + payLoad << CHAR_PARAM("id", szMsgId) << CHAR_PARAM("type", "text") << CHAR_PARAM("to", jid); + } + + char szMsgId[40]; + WANode payLoad; + OBJLIST arDest; + MBinBuffer content; +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// LT_HASH + +struct LT_HASH +{ + LT_HASH() + { + init(); + }; + + uint16_t hash[64]; + + void add(const void *pData, size_t len); + void sub(const void *pData, size_t len); + + void init() + { + SecureZeroMemory(hash, sizeof(hash)); + } +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// functions + +void bin2file(const MBinBuffer &buf, const wchar_t *pwszFileName); + +void string2file(const std::string &str, const wchar_t *pwszFileName); +CMStringA file2string(const wchar_t *pwszFileName); + +CMStringA directPath2url(const char *pszDirectPath); + +std::string decodeBinStr(const std::string &buf); +MBinBuffer decodeBufStr(const std::string &buf); + +void padBuffer16(MBinBuffer &buf); +MBinBuffer unpadBuffer16(const MBinBuffer &buf); + +CMStringA protobuf_c_text_to_string(const ProtobufCMessage *m); + +MBinBuffer aesDecrypt( + const EVP_CIPHER *cipher, + const uint8_t *key, + const uint8_t *iv, + const void *data, size_t dataLen, + const void *additionalData = 0, size_t additionalLen = 0); + +MBinBuffer aesEncrypt( + const EVP_CIPHER *cipher, + const uint8_t *key, + const uint8_t *iv, + const void *data, size_t dataLen, + const void *additionalData = 0, size_t additionalLen = 0); + +uint32_t decodeBigEndian(const uint8_t *pData, size_t len); + +__forceinline uint32_t decodeBigEndian(const ProtobufCBinaryData &buf) { + return decodeBigEndian(buf.data, buf.len); +} +__forceinline uint32_t decodeBigEndian(const MBinBuffer &buf) { + return decodeBigEndian(buf.data(), buf.length()); +} + +std::string encodeBigEndian(uint32_t num, size_t len = sizeof(uint32_t)); + +void generateIV(uint8_t *iv, uint32_t &pVar); + +unsigned char* HKDF(const EVP_MD *evp_md, + const unsigned char *salt, size_t salt_len, + const unsigned char *key, size_t key_len, + const unsigned char *info, size_t info_len, + unsigned char *okm, size_t okm_len); diff --git a/protocols/WhatsApp/src/version.h b/protocols/WhatsApp/src/version.h index 3a160f44ec..caa1a7ea52 100644 --- a/protocols/WhatsApp/src/version.h +++ b/protocols/WhatsApp/src/version.h @@ -10,4 +10,4 @@ #define __DESCRIPTION "WhatsApp protocol support for Miranda NG." #define __AUTHOR "George Hazan" #define __AUTHORWEB "https://miranda-ng.org/p/WhatsApp" -#define __COPYRIGHT "© 2019-22 Miranda NG team" +#define __COPYRIGHT "© 2019-23 Miranda NG team" diff --git a/protocols/WhatsApp/src/wanode.cpp b/protocols/WhatsApp/src/wanode.cpp index 0c3fe15d03..9c40912f28 100644 --- a/protocols/WhatsApp/src/wanode.cpp +++ b/protocols/WhatsApp/src/wanode.cpp @@ -1,608 +1,608 @@ -/* - -WhatsApp plugin for Miranda NG -Copyright © 2019-22 George Hazan - -*/ - -#include "stdafx.h" -#include "dicts.h" - -struct Attr -{ - Attr(const char *pszName, const char *pszValue) : - name(pszName), - value(pszValue) - {} - - Attr(const char *pszName, int iValue) : - name(pszName), - value(FORMAT, "%d", iValue) - {} - - CMStringA name, value; -}; - -///////////////////////////////////////////////////////////////////////////////////////// -// WANodeIq members - -WANodeIq::WANodeIq(IQ::Type type, const char *pszXmlns, const char *pszTo) : - WANode("iq") -{ - switch (type) { - case IQ::GET: addAttr("type", "get"); break; - case IQ::SET: addAttr("type", "set"); break; - case IQ::RESULT: addAttr("type", "result"); break; - } - - if (pszXmlns) - addAttr("xmlns", pszXmlns); - - addAttr("to", pszTo ? pszTo : S_WHATSAPP_NET); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// WANode members - -WANode::WANode() : - attrs(1), - children(1) -{} - -WANode::WANode(const char *pszTitle) : - attrs(1), - children(1), - title(pszTitle) -{} - -WANode::~WANode() -{ -} - -const char *WANode::getAttr(const char *pszName) const -{ - if (this != nullptr) - for (auto &p : attrs) - if (p->name == pszName) - return p->value.c_str(); - - return nullptr; -} - -int WANode::getAttrInt(const char *pszName) const -{ - if (this != nullptr) - for (auto &p : attrs) - if (p->name == pszName) - return atoi(p->value.c_str()); - - return 0; -} - -void WANode::addAttr(const char *pszName, const char *pszValue) -{ - attrs.insert(new Attr(pszName, pszValue)); -} - -void WANode::addAttr(const char *pszName, int iValue) -{ - attrs.insert(new Attr(pszName, iValue)); -} - -CMStringA WANode::getBody() const -{ - return CMStringA((char *)content.data(), (int)content.length()); -} - -WANode *WANode::addChild(const char *pszName) -{ - auto *pNew = new WANode(pszName); - pNew->pParent = this; - children.insert(pNew); - return pNew; -} - -WANode* WANode::getChild(const char *pszName) const -{ - if (this == nullptr) - return nullptr; - - for (auto &it : children) - if (it->title == pszName) - return it; - - return nullptr; -} - -WANode* WANode::getFirstChild(void) const -{ - return (children.getCount()) ? &children[0] : nullptr; -} - -void WANode::print(CMStringA &dest, int level) const -{ - for (int i = 0; i < level; i++) - dest.Append(" "); - - dest.AppendFormat("<%s ", title.c_str()); - for (auto &p : attrs) - dest.AppendFormat("%s=\"%s\" ", p->name.c_str(), p->value.c_str()); - dest.Truncate(dest.GetLength() - 1); - - if (content.isEmpty() && !children.getCount()) { - dest.Append("/>\n"); - return; - } - - dest.Append(">"); - if (!content.isEmpty()) { - ptrA tmp((char *)mir_alloc(content.length() * 2 + 1)); - bin2hex(content.data(), content.length(), tmp); - dest.AppendFormat("%s", tmp.get()); - } - - if (children.getCount()) { - dest.Append("\n"); - - for (auto &p : children) - p->print(dest, level + 1); - - for (int i = 0; i < level; i++) - dest.Append(" "); - } - - dest.AppendFormat("\n", title.c_str()); -} - -///////////////////////////////////////////////////////////////////////////////////////// -// WAReader class members - -bool WAReader::readAttributes(WANode *pNode, int count) -{ - if (count == 0) - return true; - - for (int i = 0; i < count; i++) { - CMStringA name = readString(readInt8()); - if (name.IsEmpty()) - return false; - - CMStringA value = readString(readInt8()); - if (value.IsEmpty()) - return false; - - pNode->addAttr(name, value); - } - return true; -} - -uint32_t WAReader::readInt20() -{ - if (m_limit - m_buf < 3) - return 0; - - int ret = (int(m_buf[0] & 0x0F) << 16) + (int(m_buf[1]) << 8) + int(m_buf[2]); - m_buf += 3; - return ret; -} - -uint32_t WAReader::readIntN(int n) -{ - if (m_limit - m_buf < n) - return 0; - - uint32_t res = 0; - for (int i = 0; i < n; i++, m_buf++) - res = (res <<= 8) + *m_buf; - return res; -} - -bool WAReader::readList(WANode *pParent, int tag) -{ - int size = readListSize(tag); - if (size == -1) - return false; - - for (int i = 0; i < size; i++) { - WANode *pNew = readNode(); - if (pNew == nullptr) - return false; - pParent->children.insert(pNew); - } - - return true; -} - -int WAReader::readListSize(int tag) -{ - switch (tag) { - case LIST_EMPTY: - return 0; - case LIST_8: - return readInt8(); - case LIST_16: - return readInt16(); - } - return -1; -} - -WANode *WAReader::readNode() -{ - int listSize = readListSize(readInt8()); - if (listSize == -1) - return nullptr; - - int descrTag = readInt8(); - if (descrTag == STREAM_END) - return nullptr; - - CMStringA name = readString(descrTag); - if (name.IsEmpty()) - return nullptr; - - std::unique_ptr ret(new WANode()); - ret->title = name.c_str(); - - if (!readAttributes(ret.get(), (listSize - 1) >> 1)) - return nullptr; - - if ((listSize % 2) == 1) - return ret.release(); - - int size, tag = readInt8(); - switch (tag) { - case LIST_EMPTY: case LIST_8: case LIST_16: - readList(ret.get(), tag); - break; - - case BINARY_8: - size = readInt8(); - -LBL_Binary: - if (m_limit - m_buf < size) - return false; - - ret->content.assign((void *)m_buf, size); - m_buf += size; - break; - - case BINARY_20: - size = readInt20(); - goto LBL_Binary; - - case BINARY_32: - size = readInt32(); - goto LBL_Binary; - - default: - CMStringA str = readString(tag); - ret->content.assign(str.GetBuffer(), str.GetLength() + 1); - } - - return ret.release(); -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static int unpackHex(int val) -{ - if (val < 0 || val > 15) - return -1; - - return (val < 10) ? val + '0' : val - 10 + 'A'; -} - -static int unpackNibble(int val) -{ - if (val < 0 || val > 15) - return -1; - - switch (val) { - case 10: return '-'; - case 11: return '.'; - case 15: return 0; - default: return '0' + val; - } -} - -CMStringA WAReader::readPacked(int tag) -{ - int startByte = readInt8(); - bool bTrim = false; - if (startByte & 0x80) { - startByte &= ~0x80; - bTrim = true; - } - - CMStringA ret; - for (int i = 0; i < startByte; i++) { - BYTE b = readInt8(); - int lower = (tag == NIBBLE_8) ? unpackNibble(b >> 4) : unpackHex(b >> 4); - if (lower == -1) - return ""; - - int higher = (tag == NIBBLE_8) ? unpackNibble(b & 0x0F) : unpackHex(b & 0x0F); - if (higher == -1) - return ""; - - ret.AppendChar(lower); - ret.AppendChar(higher); - } - - if (bTrim && !ret.IsEmpty()) - ret.Truncate(ret.GetLength() - 1); - return ret; -} - -CMStringA WAReader::readString(int tag) -{ - if (tag >= 1 && tag < _countof(SingleByteTokens)) - return SingleByteTokens[tag]; - - int idx; - switch (tag) { - case DICTIONARY_0: - idx = readInt8(); - return (idx < _countof(dict0)) ? dict0[idx] : ""; - - case DICTIONARY_1: - idx = readInt8(); - return (idx < _countof(dict1)) ? dict1[idx] : ""; - - case DICTIONARY_2: - idx = readInt8(); - return (idx < _countof(dict2)) ? dict2[idx] : ""; - - case DICTIONARY_3: - idx = readInt8(); - return (idx < _countof(dict3)) ? dict3[idx] : ""; - - case LIST_EMPTY: - return ""; - - case BINARY_8: - return readStringFromChars(readInt8()); - - case BINARY_20: - return readStringFromChars(readInt20()); - - case BINARY_32: - return readStringFromChars(readInt32()); - - case NIBBLE_8: - case HEX_8: - return readPacked(tag); - - case AD_JID: - { - int agent = readInt8(); - int device = readInt8(); - WAJid jid(readString(readInt8()), "s.whatsapp.net", device, agent); - return jid.toString(); - } - - case JID_PAIR: - CMStringA s1 = readString(readInt8()); - CMStringA s2 = readString(readInt8()); - if (s1.IsEmpty() && s2.IsEmpty()) - break; - - return CMStringA(FORMAT, "%s@%s", s1.c_str(), s2.c_str()); - } - - // error - return ""; -} - -CMStringA WAReader::readStringFromChars(int size) -{ - if (m_limit - m_buf < size) - return ""; - - CMStringA ret((char *)m_buf, size); - m_buf += size; - return ret; -} - -///////////////////////////////////////////////////////////////////////////////////////// -// WAWriter class members - -void WAWriter::writeByte(uint8_t b) -{ - body.append(&b, 1); -} - -void WAWriter::writeIntN(int value, int n) -{ - for (int i = n - 1; i >= 0; i--) - writeByte((value >> i * 8) & 0xFF); -} - -void WAWriter::writeInt20(int value) -{ - writeByte((value >> 16) & 0xFF); - writeByte((value >> 8) & 0xFF); - writeByte(value & 0xFF); -} - -void WAWriter::writeLength(int value) -{ - if (value >= (1 << 20)) { - writeByte(BINARY_32); - writeInt32(value); - } - else if (value >= 256) { - writeByte(BINARY_20); - writeInt20(value); - } - else { - writeByte(BINARY_8); - writeInt8(value); - } -} - -void WAWriter::writeListSize(int length) -{ - if (length == 0) - writeByte(LIST_EMPTY); - else if (length < 256) { - writeByte(LIST_8); - writeInt8(length); - } - else { - writeByte(LIST_16); - writeInt16(length); - } -} - -void WAWriter::writeNode(const WANode *pNode) -{ - // we never send zipped content - if (pNode->pParent == nullptr) - writeByte(0); - - int numAttrs = (int)pNode->attrs.getCount(); - int hasContent = pNode->content.length() != 0 || pNode->children.getCount() != 0; - writeListSize(2 * numAttrs + 1 + hasContent); - - writeString(pNode->title.c_str()); - - // write attributes - for (auto &it : pNode->attrs) { - if (it->value.IsEmpty()) - continue; - - writeString(it->name.c_str()); - writeString(it->value.c_str()); - } - - // write contents - if (pNode->content.length()) { - writeLength((int)pNode->content.length()); - body.append(pNode->content.data(), pNode->content.length()); - } - // write children - else if (pNode->children.getCount()) { - writeListSize(pNode->children.getCount()); - for (auto &it : pNode->children) - writeNode(it); - } -} - -bool WAWriter::writeToken(const char *str) -{ - for (auto &it : SingleByteTokens) - if (!strcmp(str, it)) { - writeByte(int(&it - SingleByteTokens)); - return true; - } - - return false; -} - -///////////////////////////////////////////////////////////////////////////////////////// - -static BYTE packNibble(char c) -{ - if (c >= '0' && c <= '9') - return c - '0'; - - switch (c) { - case '-': return 10; - case '.': return 11; - case 0: return 15; - } - - return -1; -} - -static BYTE packHex(char c) -{ - if (c >= '0' && c <= '9') - return c - '0'; - - if (c >= 'A' && c <= 'F') - return 10 + c - 'A'; - - if (c >= 'a' && c <= 'f') - return 10 + c - 'a'; - - if (c == 0) - return 15; - - return -1; -} - -static BYTE packPair(int type, char c1, char c2) -{ - BYTE b1 = (type == NIBBLE_8) ? packNibble(c1) : packHex(c1); - BYTE b2 = (type == NIBBLE_8) ? packNibble(c2) : packHex(c2); - return (b1 << 4) + b2; -} - -static bool isNibble(const CMStringA &str) -{ - return strspn(str, "0123456789-.") == str.GetLength(); -} - -static bool isHex(const CMStringA &str) -{ - return strspn(str, "0123456789abcdefABCDEF") == str.GetLength(); -} - -void WAWriter::writePacked(const CMStringA &str, int tag) -{ - if (str.GetLength() > 254) - return; - - writeByte(tag); - - int len = str.GetLength() / 2; - BYTE firstByte = (str.GetLength() % 2) == 0 ? 0 : 0x81; - writeByte(firstByte + len); - - const char *p = str; - for (int i = 0; i < len; i++, p += 2) - writeByte(packPair(tag, p[0], p[1])); - - if (firstByte != 0) - writeByte(packPair(tag, p[0], 0)); -} - -void WAWriter::writeString(const char *str) -{ - if (writeToken(str)) - return; - - auto *pszDelimiter = strchr(str, '@'); - if (pszDelimiter) { - WAJid jid(str); - if (jid.device || jid.agent) { - writeByte(AD_JID); - writeByte(jid.agent); - writeByte(jid.device); - writeString(jid.user); - } - else { - writeByte(JID_PAIR); - - if (jid.user.IsEmpty()) // empty user - writeByte(LIST_EMPTY); - else - writeString(jid.user); - - writeString(jid.server); - } - } - else { - CMStringA buf(str); - if (isNibble(buf)) - writePacked(buf, NIBBLE_8); - else if (isHex(buf)) - writePacked(buf, HEX_8); - else { - writeLength(buf.GetLength()); - body.append(buf, buf.GetLength()); - } - } -} +/* + +WhatsApp plugin for Miranda NG +Copyright © 2019-23 George Hazan + +*/ + +#include "stdafx.h" +#include "dicts.h" + +struct Attr +{ + Attr(const char *pszName, const char *pszValue) : + name(pszName), + value(pszValue) + {} + + Attr(const char *pszName, int iValue) : + name(pszName), + value(FORMAT, "%d", iValue) + {} + + CMStringA name, value; +}; + +///////////////////////////////////////////////////////////////////////////////////////// +// WANodeIq members + +WANodeIq::WANodeIq(IQ::Type type, const char *pszXmlns, const char *pszTo) : + WANode("iq") +{ + switch (type) { + case IQ::GET: addAttr("type", "get"); break; + case IQ::SET: addAttr("type", "set"); break; + case IQ::RESULT: addAttr("type", "result"); break; + } + + if (pszXmlns) + addAttr("xmlns", pszXmlns); + + addAttr("to", pszTo ? pszTo : S_WHATSAPP_NET); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// WANode members + +WANode::WANode() : + attrs(1), + children(1) +{} + +WANode::WANode(const char *pszTitle) : + attrs(1), + children(1), + title(pszTitle) +{} + +WANode::~WANode() +{ +} + +const char *WANode::getAttr(const char *pszName) const +{ + if (this != nullptr) + for (auto &p : attrs) + if (p->name == pszName) + return p->value.c_str(); + + return nullptr; +} + +int WANode::getAttrInt(const char *pszName) const +{ + if (this != nullptr) + for (auto &p : attrs) + if (p->name == pszName) + return atoi(p->value.c_str()); + + return 0; +} + +void WANode::addAttr(const char *pszName, const char *pszValue) +{ + attrs.insert(new Attr(pszName, pszValue)); +} + +void WANode::addAttr(const char *pszName, int iValue) +{ + attrs.insert(new Attr(pszName, iValue)); +} + +CMStringA WANode::getBody() const +{ + return CMStringA((char *)content.data(), (int)content.length()); +} + +WANode *WANode::addChild(const char *pszName) +{ + auto *pNew = new WANode(pszName); + pNew->pParent = this; + children.insert(pNew); + return pNew; +} + +WANode* WANode::getChild(const char *pszName) const +{ + if (this == nullptr) + return nullptr; + + for (auto &it : children) + if (it->title == pszName) + return it; + + return nullptr; +} + +WANode* WANode::getFirstChild(void) const +{ + return (children.getCount()) ? &children[0] : nullptr; +} + +void WANode::print(CMStringA &dest, int level) const +{ + for (int i = 0; i < level; i++) + dest.Append(" "); + + dest.AppendFormat("<%s ", title.c_str()); + for (auto &p : attrs) + dest.AppendFormat("%s=\"%s\" ", p->name.c_str(), p->value.c_str()); + dest.Truncate(dest.GetLength() - 1); + + if (content.isEmpty() && !children.getCount()) { + dest.Append("/>\n"); + return; + } + + dest.Append(">"); + if (!content.isEmpty()) { + ptrA tmp((char *)mir_alloc(content.length() * 2 + 1)); + bin2hex(content.data(), content.length(), tmp); + dest.AppendFormat("%s", tmp.get()); + } + + if (children.getCount()) { + dest.Append("\n"); + + for (auto &p : children) + p->print(dest, level + 1); + + for (int i = 0; i < level; i++) + dest.Append(" "); + } + + dest.AppendFormat("\n", title.c_str()); +} + +///////////////////////////////////////////////////////////////////////////////////////// +// WAReader class members + +bool WAReader::readAttributes(WANode *pNode, int count) +{ + if (count == 0) + return true; + + for (int i = 0; i < count; i++) { + CMStringA name = readString(readInt8()); + if (name.IsEmpty()) + return false; + + CMStringA value = readString(readInt8()); + if (value.IsEmpty()) + return false; + + pNode->addAttr(name, value); + } + return true; +} + +uint32_t WAReader::readInt20() +{ + if (m_limit - m_buf < 3) + return 0; + + int ret = (int(m_buf[0] & 0x0F) << 16) + (int(m_buf[1]) << 8) + int(m_buf[2]); + m_buf += 3; + return ret; +} + +uint32_t WAReader::readIntN(int n) +{ + if (m_limit - m_buf < n) + return 0; + + uint32_t res = 0; + for (int i = 0; i < n; i++, m_buf++) + res = (res <<= 8) + *m_buf; + return res; +} + +bool WAReader::readList(WANode *pParent, int tag) +{ + int size = readListSize(tag); + if (size == -1) + return false; + + for (int i = 0; i < size; i++) { + WANode *pNew = readNode(); + if (pNew == nullptr) + return false; + pParent->children.insert(pNew); + } + + return true; +} + +int WAReader::readListSize(int tag) +{ + switch (tag) { + case LIST_EMPTY: + return 0; + case LIST_8: + return readInt8(); + case LIST_16: + return readInt16(); + } + return -1; +} + +WANode *WAReader::readNode() +{ + int listSize = readListSize(readInt8()); + if (listSize == -1) + return nullptr; + + int descrTag = readInt8(); + if (descrTag == STREAM_END) + return nullptr; + + CMStringA name = readString(descrTag); + if (name.IsEmpty()) + return nullptr; + + std::unique_ptr ret(new WANode()); + ret->title = name.c_str(); + + if (!readAttributes(ret.get(), (listSize - 1) >> 1)) + return nullptr; + + if ((listSize % 2) == 1) + return ret.release(); + + int size, tag = readInt8(); + switch (tag) { + case LIST_EMPTY: case LIST_8: case LIST_16: + readList(ret.get(), tag); + break; + + case BINARY_8: + size = readInt8(); + +LBL_Binary: + if (m_limit - m_buf < size) + return false; + + ret->content.assign((void *)m_buf, size); + m_buf += size; + break; + + case BINARY_20: + size = readInt20(); + goto LBL_Binary; + + case BINARY_32: + size = readInt32(); + goto LBL_Binary; + + default: + CMStringA str = readString(tag); + ret->content.assign(str.GetBuffer(), str.GetLength() + 1); + } + + return ret.release(); +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static int unpackHex(int val) +{ + if (val < 0 || val > 15) + return -1; + + return (val < 10) ? val + '0' : val - 10 + 'A'; +} + +static int unpackNibble(int val) +{ + if (val < 0 || val > 15) + return -1; + + switch (val) { + case 10: return '-'; + case 11: return '.'; + case 15: return 0; + default: return '0' + val; + } +} + +CMStringA WAReader::readPacked(int tag) +{ + int startByte = readInt8(); + bool bTrim = false; + if (startByte & 0x80) { + startByte &= ~0x80; + bTrim = true; + } + + CMStringA ret; + for (int i = 0; i < startByte; i++) { + BYTE b = readInt8(); + int lower = (tag == NIBBLE_8) ? unpackNibble(b >> 4) : unpackHex(b >> 4); + if (lower == -1) + return ""; + + int higher = (tag == NIBBLE_8) ? unpackNibble(b & 0x0F) : unpackHex(b & 0x0F); + if (higher == -1) + return ""; + + ret.AppendChar(lower); + ret.AppendChar(higher); + } + + if (bTrim && !ret.IsEmpty()) + ret.Truncate(ret.GetLength() - 1); + return ret; +} + +CMStringA WAReader::readString(int tag) +{ + if (tag >= 1 && tag < _countof(SingleByteTokens)) + return SingleByteTokens[tag]; + + int idx; + switch (tag) { + case DICTIONARY_0: + idx = readInt8(); + return (idx < _countof(dict0)) ? dict0[idx] : ""; + + case DICTIONARY_1: + idx = readInt8(); + return (idx < _countof(dict1)) ? dict1[idx] : ""; + + case DICTIONARY_2: + idx = readInt8(); + return (idx < _countof(dict2)) ? dict2[idx] : ""; + + case DICTIONARY_3: + idx = readInt8(); + return (idx < _countof(dict3)) ? dict3[idx] : ""; + + case LIST_EMPTY: + return ""; + + case BINARY_8: + return readStringFromChars(readInt8()); + + case BINARY_20: + return readStringFromChars(readInt20()); + + case BINARY_32: + return readStringFromChars(readInt32()); + + case NIBBLE_8: + case HEX_8: + return readPacked(tag); + + case AD_JID: + { + int agent = readInt8(); + int device = readInt8(); + WAJid jid(readString(readInt8()), "s.whatsapp.net", device, agent); + return jid.toString(); + } + + case JID_PAIR: + CMStringA s1 = readString(readInt8()); + CMStringA s2 = readString(readInt8()); + if (s1.IsEmpty() && s2.IsEmpty()) + break; + + return CMStringA(FORMAT, "%s@%s", s1.c_str(), s2.c_str()); + } + + // error + return ""; +} + +CMStringA WAReader::readStringFromChars(int size) +{ + if (m_limit - m_buf < size) + return ""; + + CMStringA ret((char *)m_buf, size); + m_buf += size; + return ret; +} + +///////////////////////////////////////////////////////////////////////////////////////// +// WAWriter class members + +void WAWriter::writeByte(uint8_t b) +{ + body.append(&b, 1); +} + +void WAWriter::writeIntN(int value, int n) +{ + for (int i = n - 1; i >= 0; i--) + writeByte((value >> i * 8) & 0xFF); +} + +void WAWriter::writeInt20(int value) +{ + writeByte((value >> 16) & 0xFF); + writeByte((value >> 8) & 0xFF); + writeByte(value & 0xFF); +} + +void WAWriter::writeLength(int value) +{ + if (value >= (1 << 20)) { + writeByte(BINARY_32); + writeInt32(value); + } + else if (value >= 256) { + writeByte(BINARY_20); + writeInt20(value); + } + else { + writeByte(BINARY_8); + writeInt8(value); + } +} + +void WAWriter::writeListSize(int length) +{ + if (length == 0) + writeByte(LIST_EMPTY); + else if (length < 256) { + writeByte(LIST_8); + writeInt8(length); + } + else { + writeByte(LIST_16); + writeInt16(length); + } +} + +void WAWriter::writeNode(const WANode *pNode) +{ + // we never send zipped content + if (pNode->pParent == nullptr) + writeByte(0); + + int numAttrs = (int)pNode->attrs.getCount(); + int hasContent = pNode->content.length() != 0 || pNode->children.getCount() != 0; + writeListSize(2 * numAttrs + 1 + hasContent); + + writeString(pNode->title.c_str()); + + // write attributes + for (auto &it : pNode->attrs) { + if (it->value.IsEmpty()) + continue; + + writeString(it->name.c_str()); + writeString(it->value.c_str()); + } + + // write contents + if (pNode->content.length()) { + writeLength((int)pNode->content.length()); + body.append(pNode->content.data(), pNode->content.length()); + } + // write children + else if (pNode->children.getCount()) { + writeListSize(pNode->children.getCount()); + for (auto &it : pNode->children) + writeNode(it); + } +} + +bool WAWriter::writeToken(const char *str) +{ + for (auto &it : SingleByteTokens) + if (!strcmp(str, it)) { + writeByte(int(&it - SingleByteTokens)); + return true; + } + + return false; +} + +///////////////////////////////////////////////////////////////////////////////////////// + +static BYTE packNibble(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + + switch (c) { + case '-': return 10; + case '.': return 11; + case 0: return 15; + } + + return -1; +} + +static BYTE packHex(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + + if (c >= 'A' && c <= 'F') + return 10 + c - 'A'; + + if (c >= 'a' && c <= 'f') + return 10 + c - 'a'; + + if (c == 0) + return 15; + + return -1; +} + +static BYTE packPair(int type, char c1, char c2) +{ + BYTE b1 = (type == NIBBLE_8) ? packNibble(c1) : packHex(c1); + BYTE b2 = (type == NIBBLE_8) ? packNibble(c2) : packHex(c2); + return (b1 << 4) + b2; +} + +static bool isNibble(const CMStringA &str) +{ + return strspn(str, "0123456789-.") == str.GetLength(); +} + +static bool isHex(const CMStringA &str) +{ + return strspn(str, "0123456789abcdefABCDEF") == str.GetLength(); +} + +void WAWriter::writePacked(const CMStringA &str, int tag) +{ + if (str.GetLength() > 254) + return; + + writeByte(tag); + + int len = str.GetLength() / 2; + BYTE firstByte = (str.GetLength() % 2) == 0 ? 0 : 0x81; + writeByte(firstByte + len); + + const char *p = str; + for (int i = 0; i < len; i++, p += 2) + writeByte(packPair(tag, p[0], p[1])); + + if (firstByte != 0) + writeByte(packPair(tag, p[0], 0)); +} + +void WAWriter::writeString(const char *str) +{ + if (writeToken(str)) + return; + + auto *pszDelimiter = strchr(str, '@'); + if (pszDelimiter) { + WAJid jid(str); + if (jid.device || jid.agent) { + writeByte(AD_JID); + writeByte(jid.agent); + writeByte(jid.device); + writeString(jid.user); + } + else { + writeByte(JID_PAIR); + + if (jid.user.IsEmpty()) // empty user + writeByte(LIST_EMPTY); + else + writeString(jid.user); + + writeString(jid.server); + } + } + else { + CMStringA buf(str); + if (isNibble(buf)) + writePacked(buf, NIBBLE_8); + else if (isHex(buf)) + writePacked(buf, HEX_8); + else { + writeLength(buf.GetLength()); + body.append(buf, buf.GetLength()); + } + } +} diff --git a/protocols/YAMN/src/stdafx.cxx b/protocols/YAMN/src/stdafx.cxx index 1ab0efee94..ebbde0ade1 100644 --- a/protocols/YAMN/src/stdafx.cxx +++ b/protocols/YAMN/src/stdafx.cxx @@ -1,18 +1,18 @@ -/* -Copyright (C) 2012-22 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 . -*/ - +/* +Copyright (C) 2012-23 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 . +*/ + #include "stdafx.h" \ No newline at end of file -- cgit v1.2.3