summaryrefslogtreecommitdiff
path: root/protocols
diff options
context:
space:
mode:
authordartraiden <wowemuh@gmail.com>2023-01-02 21:10:29 +0300
committerdartraiden <wowemuh@gmail.com>2023-01-02 21:10:29 +0300
commit1979fd80424d16b2e489f9b57d01d9c7811d25a2 (patch)
tree960d42c5fe4a51f0fe2850bea91256e226bce221 /protocols
parentadfbbb217d4f4a05acf198755f219a5223d31c27 (diff)
Update copyrights
Diffstat (limited to 'protocols')
-rw-r--r--protocols/CloudFile/src/stdafx.cxx38
-rw-r--r--protocols/CloudFile/src/version.h2
-rw-r--r--protocols/ConnectionNotify/src/stdafx.cxx34
-rw-r--r--protocols/CurrencyRates/src/stdafx.cxx36
-rw-r--r--protocols/Dummy/src/dummy.h2
-rw-r--r--protocols/Dummy/src/dummy_options.cpp2
-rw-r--r--protocols/Dummy/src/dummy_proto.cpp376
-rw-r--r--protocols/Dummy/src/dummy_proto.h106
-rw-r--r--protocols/Dummy/src/main.cpp2
-rw-r--r--protocols/Dummy/src/stdafx.cxx36
-rw-r--r--protocols/Dummy/src/stdafx.h120
-rw-r--r--protocols/Dummy/src/version.h2
-rw-r--r--protocols/Facebook/src/avatars.cpp262
-rw-r--r--protocols/Facebook/src/db.h88
-rw-r--r--protocols/Facebook/src/dialogs.cpp156
-rw-r--r--protocols/Facebook/src/dialogs.h2
-rw-r--r--protocols/Facebook/src/groupchats.cpp510
-rw-r--r--protocols/Facebook/src/http.cpp370
-rw-r--r--protocols/Facebook/src/main.cpp144
-rw-r--r--protocols/Facebook/src/mqtt.cpp688
-rw-r--r--protocols/Facebook/src/mqtt.h278
-rw-r--r--protocols/Facebook/src/options.cpp168
-rw-r--r--protocols/Facebook/src/proto.cpp604
-rw-r--r--protocols/Facebook/src/proto.h1114
-rw-r--r--protocols/Facebook/src/server.cpp2102
-rw-r--r--protocols/Facebook/src/stdafx.cxx36
-rw-r--r--protocols/Facebook/src/stdafx.h134
-rw-r--r--protocols/Facebook/src/thrift.cpp616
-rw-r--r--protocols/Facebook/src/version.h2
-rw-r--r--protocols/Gadu-Gadu/src/stdafx.cxx2
-rw-r--r--protocols/Gadu-Gadu/src/userinfo.cpp656
-rw-r--r--protocols/GmailNotifier/src/stdafx.cxx34
-rw-r--r--protocols/ICQ-WIM/src/groupchats.cpp584
-rw-r--r--protocols/ICQ-WIM/src/http.cpp770
-rw-r--r--protocols/ICQ-WIM/src/ignore.cpp164
-rw-r--r--protocols/ICQ-WIM/src/main.cpp204
-rw-r--r--protocols/ICQ-WIM/src/mra.cpp282
-rw-r--r--protocols/ICQ-WIM/src/options.cpp788
-rw-r--r--protocols/ICQ-WIM/src/poll.cpp818
-rw-r--r--protocols/ICQ-WIM/src/proto.cpp1356
-rw-r--r--protocols/ICQ-WIM/src/proto.h988
-rw-r--r--protocols/ICQ-WIM/src/server.cpp2440
-rw-r--r--protocols/ICQ-WIM/src/stdafx.cxx36
-rw-r--r--protocols/ICQ-WIM/src/stdafx.h218
-rw-r--r--protocols/ICQ-WIM/src/userinfo.cpp128
-rw-r--r--protocols/ICQ-WIM/src/utils.cpp792
-rw-r--r--protocols/ICQ-WIM/src/version.h2
-rw-r--r--protocols/ICQCorp/src/stdafx.cxx36
-rw-r--r--protocols/ICQCorp/src/version.h2
-rw-r--r--protocols/IRCG/src/stdafx.cxx2
-rw-r--r--protocols/IRCG/src/version.h2
-rw-r--r--protocols/JabberG/src/jabber.cpp2
-rw-r--r--protocols/JabberG/src/jabber_adhoc.cpp2
-rw-r--r--protocols/JabberG/src/jabber_agent.cpp2
-rw-r--r--protocols/JabberG/src/jabber_api.cpp2
-rw-r--r--protocols/JabberG/src/jabber_archive.cpp2
-rw-r--r--protocols/JabberG/src/jabber_bookmarks.cpp2
-rw-r--r--protocols/JabberG/src/jabber_byte.cpp2
-rw-r--r--protocols/JabberG/src/jabber_byte.h2
-rw-r--r--protocols/JabberG/src/jabber_caps.cpp2
-rw-r--r--protocols/JabberG/src/jabber_caps.h2
-rw-r--r--protocols/JabberG/src/jabber_captcha.cpp2
-rw-r--r--protocols/JabberG/src/jabber_chat.cpp2
-rw-r--r--protocols/JabberG/src/jabber_console.cpp2
-rw-r--r--protocols/JabberG/src/jabber_disco.cpp2
-rw-r--r--protocols/JabberG/src/jabber_disco.h2
-rw-r--r--protocols/JabberG/src/jabber_events.cpp2
-rw-r--r--protocols/JabberG/src/jabber_file.cpp2
-rw-r--r--protocols/JabberG/src/jabber_form.cpp2
-rw-r--r--protocols/JabberG/src/jabber_ft.cpp2
-rw-r--r--protocols/JabberG/src/jabber_groupchat.cpp2
-rw-r--r--protocols/JabberG/src/jabber_ibb.cpp2
-rw-r--r--protocols/JabberG/src/jabber_ibb.h2
-rw-r--r--protocols/JabberG/src/jabber_icolib.cpp2
-rw-r--r--protocols/JabberG/src/jabber_icolib.h2
-rw-r--r--protocols/JabberG/src/jabber_iq.cpp2
-rw-r--r--protocols/JabberG/src/jabber_iq.h2
-rw-r--r--protocols/JabberG/src/jabber_iq_handlers.cpp2
-rw-r--r--protocols/JabberG/src/jabber_iqid.cpp2
-rw-r--r--protocols/JabberG/src/jabber_iqid_muc.cpp2
-rw-r--r--protocols/JabberG/src/jabber_libstr.cpp2
-rw-r--r--protocols/JabberG/src/jabber_list.cpp2
-rw-r--r--protocols/JabberG/src/jabber_list.h2
-rw-r--r--protocols/JabberG/src/jabber_mam.cpp320
-rw-r--r--protocols/JabberG/src/jabber_menu.cpp2
-rw-r--r--protocols/JabberG/src/jabber_message_handlers.cpp2
-rw-r--r--protocols/JabberG/src/jabber_message_manager.cpp2
-rw-r--r--protocols/JabberG/src/jabber_message_manager.h2
-rw-r--r--protocols/JabberG/src/jabber_misc.cpp2
-rw-r--r--protocols/JabberG/src/jabber_notes.cpp2
-rw-r--r--protocols/JabberG/src/jabber_notes.h2
-rw-r--r--protocols/JabberG/src/jabber_omemo.cpp2
-rw-r--r--protocols/JabberG/src/jabber_omemo.h2
-rw-r--r--protocols/JabberG/src/jabber_opt.cpp2
-rw-r--r--protocols/JabberG/src/jabber_password.cpp2
-rw-r--r--protocols/JabberG/src/jabber_presence_manager.cpp2
-rw-r--r--protocols/JabberG/src/jabber_presence_manager.h2
-rw-r--r--protocols/JabberG/src/jabber_privacy.cpp2
-rw-r--r--protocols/JabberG/src/jabber_privacy.h2
-rw-r--r--protocols/JabberG/src/jabber_proto.cpp2
-rw-r--r--protocols/JabberG/src/jabber_proto.h2
-rw-r--r--protocols/JabberG/src/jabber_rc.cpp2
-rw-r--r--protocols/JabberG/src/jabber_rc.h2
-rw-r--r--protocols/JabberG/src/jabber_roster.cpp1102
-rw-r--r--protocols/JabberG/src/jabber_search.cpp1530
-rw-r--r--protocols/JabberG/src/jabber_search.h2
-rw-r--r--protocols/JabberG/src/jabber_secur.cpp2
-rw-r--r--protocols/JabberG/src/jabber_secur.h2
-rw-r--r--protocols/JabberG/src/jabber_send_manager.cpp2
-rw-r--r--protocols/JabberG/src/jabber_send_manager.h2
-rw-r--r--protocols/JabberG/src/jabber_strm_mgmt.cpp2
-rw-r--r--protocols/JabberG/src/jabber_strm_mgmt.h2
-rw-r--r--protocols/JabberG/src/jabber_svc.cpp2
-rw-r--r--protocols/JabberG/src/jabber_thread.cpp2
-rw-r--r--protocols/JabberG/src/jabber_treelist.cpp2
-rw-r--r--protocols/JabberG/src/jabber_userinfo.cpp1824
-rw-r--r--protocols/JabberG/src/jabber_util.cpp2
-rw-r--r--protocols/JabberG/src/jabber_vcard.cpp2
-rw-r--r--protocols/JabberG/src/jabber_xml.cpp2
-rw-r--r--protocols/JabberG/src/jabber_xml.h2
-rw-r--r--protocols/JabberG/src/jabber_xstatus.cpp2
-rw-r--r--protocols/JabberG/src/jabber_xstatus.h2
-rw-r--r--protocols/JabberG/src/jabber_zstream.cpp2
-rw-r--r--protocols/JabberG/src/stdafx.cxx2
-rw-r--r--protocols/JabberG/src/stdafx.h2
-rw-r--r--protocols/JabberG/src/version.h2
-rw-r--r--protocols/LotusNotify/src/stdafx.cxx34
-rw-r--r--protocols/MinecraftDynmap/src/chat.cpp368
-rw-r--r--protocols/MinecraftDynmap/src/communication.cpp884
-rw-r--r--protocols/MinecraftDynmap/src/constants.h92
-rw-r--r--protocols/MinecraftDynmap/src/dialogs.cpp196
-rw-r--r--protocols/MinecraftDynmap/src/dialogs.h54
-rw-r--r--protocols/MinecraftDynmap/src/main.cpp180
-rw-r--r--protocols/MinecraftDynmap/src/proto.cpp360
-rw-r--r--protocols/MinecraftDynmap/src/proto.h272
-rw-r--r--protocols/MinecraftDynmap/src/stdafx.cxx34
-rw-r--r--protocols/MinecraftDynmap/src/stdafx.h140
-rw-r--r--protocols/MinecraftDynmap/src/utils.h126
-rw-r--r--protocols/MinecraftDynmap/src/version.h2
-rw-r--r--protocols/NewsAggregator/Src/stdafx.cxx34
-rw-r--r--protocols/NewsAggregator/Src/version.h2
-rw-r--r--protocols/Non-IM Contact/src/stdafx.cxx34
-rw-r--r--protocols/Omegle/src/chat.cpp2
-rw-r--r--protocols/Omegle/src/client.h2
-rw-r--r--protocols/Omegle/src/communication.cpp1524
-rw-r--r--protocols/Omegle/src/connection.cpp2
-rw-r--r--protocols/Omegle/src/constants.h2
-rw-r--r--protocols/Omegle/src/db.h2
-rw-r--r--protocols/Omegle/src/dialogs.cpp2
-rw-r--r--protocols/Omegle/src/dialogs.h2
-rw-r--r--protocols/Omegle/src/http.cpp2
-rw-r--r--protocols/Omegle/src/http.h2
-rw-r--r--protocols/Omegle/src/main.cpp2
-rw-r--r--protocols/Omegle/src/messages.cpp148
-rw-r--r--protocols/Omegle/src/proto.cpp2
-rw-r--r--protocols/Omegle/src/proto.h2
-rw-r--r--protocols/Omegle/src/stdafx.cxx2
-rw-r--r--protocols/Omegle/src/stdafx.h2
-rw-r--r--protocols/Omegle/src/theme.cpp2
-rw-r--r--protocols/Omegle/src/theme.h2
-rw-r--r--protocols/Omegle/src/version.h2
-rw-r--r--protocols/Sametime/src/version.h2
-rw-r--r--protocols/SkypeWeb/src/main.cpp2
-rw-r--r--protocols/SkypeWeb/src/request_queue.cpp2
-rw-r--r--protocols/SkypeWeb/src/requests/avatars.h2
-rw-r--r--protocols/SkypeWeb/src/requests/capabilities.h2
-rw-r--r--protocols/SkypeWeb/src/requests/chatrooms.h2
-rw-r--r--protocols/SkypeWeb/src/requests/contacts.h2
-rw-r--r--protocols/SkypeWeb/src/requests/endpoint.h2
-rw-r--r--protocols/SkypeWeb/src/requests/history.h2
-rw-r--r--protocols/SkypeWeb/src/requests/login.h2
-rw-r--r--protocols/SkypeWeb/src/requests/messages.h2
-rw-r--r--protocols/SkypeWeb/src/requests/oauth.h148
-rw-r--r--protocols/SkypeWeb/src/requests/poll.h2
-rw-r--r--protocols/SkypeWeb/src/requests/profile.h2
-rw-r--r--protocols/SkypeWeb/src/requests/search.h2
-rw-r--r--protocols/SkypeWeb/src/requests/status.h2
-rw-r--r--protocols/SkypeWeb/src/requests/subscriptions.h2
-rw-r--r--protocols/SkypeWeb/src/skype_avatars.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_chatrooms.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_contacts.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_db.cpp238
-rw-r--r--protocols/SkypeWeb/src/skype_db.h2
-rw-r--r--protocols/SkypeWeb/src/skype_events.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_history_sync.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_icons.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_login.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_menus.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_menus.h2
-rw-r--r--protocols/SkypeWeb/src/skype_messages.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_oauth.cpp330
-rw-r--r--protocols/SkypeWeb/src/skype_options.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_polling.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_profile.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_proto.cpp686
-rw-r--r--protocols/SkypeWeb/src/skype_proto.h2
-rw-r--r--protocols/SkypeWeb/src/skype_search.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_trouter.cpp2
-rw-r--r--protocols/SkypeWeb/src/skype_utils.cpp1358
-rw-r--r--protocols/SkypeWeb/src/skype_utils.h2
-rw-r--r--protocols/SkypeWeb/src/stdafx.cxx2
-rw-r--r--protocols/SkypeWeb/src/stdafx.h2
-rw-r--r--protocols/SkypeWeb/src/version.h2
-rw-r--r--protocols/Steam/src/stdafx.cxx2
-rw-r--r--protocols/Steam/src/version.h2
-rw-r--r--protocols/Telegram/src/auth.cpp2
-rw-r--r--protocols/Telegram/src/options.cpp2
-rw-r--r--protocols/Telegram/src/server.cpp2
-rw-r--r--protocols/Telegram/src/stdafx.cxx2
-rw-r--r--protocols/Telegram/src/utils.cpp2
-rw-r--r--protocols/Telegram/src/version.h2
-rw-r--r--protocols/Tox/src/stdafx.cxx2
-rw-r--r--protocols/Tox/src/version.h2
-rw-r--r--protocols/Twitter/src/StringUtil.cpp2
-rw-r--r--protocols/Twitter/src/chat.cpp2
-rw-r--r--protocols/Twitter/src/connection.cpp2
-rw-r--r--protocols/Twitter/src/contacts.cpp2
-rw-r--r--protocols/Twitter/src/http.h2
-rw-r--r--protocols/Twitter/src/main.cpp2
-rw-r--r--protocols/Twitter/src/oauth.cpp2
-rw-r--r--protocols/Twitter/src/proto.cpp2
-rw-r--r--protocols/Twitter/src/proto.h2
-rw-r--r--protocols/Twitter/src/stdafx.cxx2
-rw-r--r--protocols/Twitter/src/theme.cpp2
-rw-r--r--protocols/Twitter/src/theme.h2
-rw-r--r--protocols/Twitter/src/twitter.cpp2
-rw-r--r--protocols/Twitter/src/ui.cpp2
-rw-r--r--protocols/Twitter/src/ui.h2
-rw-r--r--protocols/Twitter/src/utility.cpp2
-rw-r--r--protocols/Twitter/src/utility.h2
-rw-r--r--protocols/Twitter/src/version.h2
-rw-r--r--protocols/VKontakte/src/main.cpp2
-rw-r--r--protocols/VKontakte/src/misc.cpp2
-rw-r--r--protocols/VKontakte/src/stdafx.cxx2
-rw-r--r--protocols/VKontakte/src/stdafx.h2
-rw-r--r--protocols/VKontakte/src/version.h2
-rw-r--r--protocols/VKontakte/src/vk.h2
-rw-r--r--protocols/VKontakte/src/vk_avatars.cpp2
-rw-r--r--protocols/VKontakte/src/vk_captcha.cpp2
-rw-r--r--protocols/VKontakte/src/vk_chats.cpp2
-rw-r--r--protocols/VKontakte/src/vk_dialogs.cpp2
-rw-r--r--protocols/VKontakte/src/vk_dialogs.h2
-rw-r--r--protocols/VKontakte/src/vk_feed.cpp2
-rw-r--r--protocols/VKontakte/src/vk_files.cpp2
-rw-r--r--protocols/VKontakte/src/vk_history.cpp2
-rw-r--r--protocols/VKontakte/src/vk_messages.cpp2
-rw-r--r--protocols/VKontakte/src/vk_options.cpp2
-rw-r--r--protocols/VKontakte/src/vk_options.h2
-rw-r--r--protocols/VKontakte/src/vk_pollserver.cpp2
-rw-r--r--protocols/VKontakte/src/vk_proto.cpp2
-rw-r--r--protocols/VKontakte/src/vk_proto.h2
-rw-r--r--protocols/VKontakte/src/vk_queue.cpp2
-rw-r--r--protocols/VKontakte/src/vk_search.cpp2
-rw-r--r--protocols/VKontakte/src/vk_status.cpp2
-rw-r--r--protocols/VKontakte/src/vk_struct.cpp2
-rw-r--r--protocols/VKontakte/src/vk_struct.h2
-rw-r--r--protocols/VKontakte/src/vk_thread.cpp2
-rw-r--r--protocols/VKontakte/src/vk_wallpost.cpp2
-rw-r--r--protocols/VKontakte/src/vkjs.js2
-rw-r--r--protocols/Weather/src/stdafx.cxx34
-rw-r--r--protocols/Weather/src/stdafx.h1094
-rw-r--r--protocols/Weather/src/version.h2
-rw-r--r--protocols/WebView/src/main.cpp548
-rw-r--r--protocols/WebView/src/stdafx.cxx34
-rw-r--r--protocols/WhatsApp/src/appsync.cpp2
-rw-r--r--protocols/WhatsApp/src/avatars.cpp338
-rw-r--r--protocols/WhatsApp/src/chats.cpp2
-rw-r--r--protocols/WhatsApp/src/crypt.cpp354
-rw-r--r--protocols/WhatsApp/src/db.h86
-rw-r--r--protocols/WhatsApp/src/dicts.h388
-rw-r--r--protocols/WhatsApp/src/iq.cpp1140
-rw-r--r--protocols/WhatsApp/src/main.cpp142
-rw-r--r--protocols/WhatsApp/src/message.cpp1004
-rw-r--r--protocols/WhatsApp/src/noise.cpp402
-rw-r--r--protocols/WhatsApp/src/options.cpp190
-rw-r--r--protocols/WhatsApp/src/proto.cpp2
-rw-r--r--protocols/WhatsApp/src/proto.h2
-rw-r--r--protocols/WhatsApp/src/qrcode.cpp274
-rw-r--r--protocols/WhatsApp/src/server.cpp2
-rw-r--r--protocols/WhatsApp/src/signal.cpp1576
-rw-r--r--protocols/WhatsApp/src/stdafx.cxx16
-rw-r--r--protocols/WhatsApp/src/stdafx.h144
-rw-r--r--protocols/WhatsApp/src/utils.cpp1178
-rw-r--r--protocols/WhatsApp/src/utils.h534
-rw-r--r--protocols/WhatsApp/src/version.h2
-rw-r--r--protocols/WhatsApp/src/wanode.cpp1216
-rw-r--r--protocols/YAMN/src/stdafx.cxx34
287 files changed, 20728 insertions, 20728 deletions
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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-*/
-
-#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<CDummyProto>(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 <http://www.gnu.org/licenses/>.
+*/
+
+#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<CDummyProto>(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 <http://www.gnu.org/licenses/>.
-*/
-
-#pragma once
-
-struct CDummyProto;
-
-struct CDummyProto : public PROTO<CDummyProto>
-{
- 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 <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+struct CDummyProto;
+
+struct CDummyProto : public PROTO<CDummyProto>
+{
+ 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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-*/
-
-#pragma once
-
-#include <Windows.h>
-#include <Shlwapi.h>
-#include <Wincrypt.h>
-
-#include <stdio.h>
-#include <malloc.h>
-#include <time.h>
-
-#include <newpluginapi.h>
-#include <m_system.h>
-
-#include <m_avatars.h>
-#include <m_clistint.h>
-#include <m_contacts.h>
-#include <m_database.h>
-#include <m_extraicons.h>
-#include <m_file.h>
-#include <m_fontservice.h>
-#include <m_genmenu.h>
-#include <m_hotkeys.h>
-#include <m_icolib.h>
-#include <m_idle.h>
-#include <m_imgsrvc.h>
-#include <m_json.h>
-#include <m_langpack.h>
-#include <m_message.h>
-#include <m_netlib.h>
-#include <m_options.h>
-#include <m_protosvc.h>
-#include <m_protoint.h>
-#include <m_skin.h>
-#include <m_timezones.h>
-#include <m_toptoolbar.h>
-#include <m_userinfo.h>
-#include <m_utils.h>
-#include <m_proto_listeningto.h>
-#include <m_folders.h>
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+#include <Windows.h>
+#include <Shlwapi.h>
+#include <Wincrypt.h>
+
+#include <stdio.h>
+#include <malloc.h>
+#include <time.h>
+
+#include <newpluginapi.h>
+#include <m_system.h>
+
+#include <m_avatars.h>
+#include <m_clistint.h>
+#include <m_contacts.h>
+#include <m_database.h>
+#include <m_extraicons.h>
+#include <m_file.h>
+#include <m_fontservice.h>
+#include <m_genmenu.h>
+#include <m_hotkeys.h>
+#include <m_icolib.h>
+#include <m_idle.h>
+#include <m_imgsrvc.h>
+#include <m_json.h>
+#include <m_langpack.h>
+#include <m_message.h>
+#include <m_netlib.h>
+#include <m_options.h>
+#include <m_protosvc.h>
+#include <m_protoint.h>
+#include <m_skin.h>
+#include <m_timezones.h>
+#include <m_toptoolbar.h>
+#include <m_userinfo.h>
+#include <m_utils.h>
+#include <m_proto_listeningto.h>
+#include <m_folders.h>
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#include "stdafx.h"
-
-INT_PTR CALLBACK FBAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
-{
- FacebookProto *proto = reinterpret_cast<FacebookProto*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
-
- switch (message) {
-
- case WM_INITDIALOG:
- {
- TranslateDialogDefault(hwnd);
-
- proto = reinterpret_cast<FacebookProto*>(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<HWND>(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<NMHDR*>(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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#include "stdafx.h"
+
+INT_PTR CALLBACK FBAccountProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
+{
+ FacebookProto *proto = reinterpret_cast<FacebookProto*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
+
+ switch (message) {
+
+ case WM_INITDIALOG:
+ {
+ TranslateDialogDefault(hwnd);
+
+ proto = reinterpret_cast<FacebookProto*>(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<HWND>(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<NMHDR*>(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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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<SESSION_INFO>(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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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<SESSION_INFO>(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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 &param)
-{
- pReq->params.insert(new AsyncHttpRequest::Param(param.szName, param.szValue));
- return pReq;
-}
-
-AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const INT_PARAM &param)
-{
- 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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 &param)
+{
+ pReq->params.insert(new AsyncHttpRequest::Param(param.szName, param.szValue));
+ return pReq;
+}
+
+AsyncHttpRequest* operator<<(AsyncHttpRequest *pReq, const INT_PARAM &param)
+{
+ 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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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<FacebookProto>(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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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<FacebookProto>(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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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<uint8_t> 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<uint8_t> 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", "<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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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<uint8_t> 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<uint8_t> 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", "<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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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<FacebookProto>(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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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<FacebookProto>(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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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<FacebookProto>
-{
- struct Param
- {
- Param(const char *p1, const char *p2) :
- key(p1), val(p2)
- {}
-
- CMStringA key, val;
- };
- OBJLIST<Param> 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<FacebookProto>
-{
- 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<COwnMessage> arOwnMessages;
- bool ExtractOwnMessage(__int64 msgId, COwnMessage &res);
-
- mir_cs m_csUsers;
- OBJLIST<FacebookUser> 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<wchar_t *> m_wszDefaultGroup; // clist group to store contacts
- CMOption<bool> m_bUseBigAvatars; // use big or small avatars by default
- CMOption<bool> m_bUseGroupchats; // do we need group chats at all?
- CMOption<bool> m_bHideGroupchats; // do not open chat windows on creation
- CMOption<bool> m_bLoginInvisible; // login in the invisible mode
- CMOption<bool> m_bKeepUnread; // do not mark incoming messages as read
- CMOption<bool> 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<FacebookProto> CFBDlgBase;
-
-struct CMPlugin : public ACCPROTOPLUGIN<FacebookProto>
-{
- 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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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<FacebookProto>
+{
+ struct Param
+ {
+ Param(const char *p1, const char *p2) :
+ key(p1), val(p2)
+ {}
+
+ CMStringA key, val;
+ };
+ OBJLIST<Param> 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<FacebookProto>
+{
+ 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<COwnMessage> arOwnMessages;
+ bool ExtractOwnMessage(__int64 msgId, COwnMessage &res);
+
+ mir_cs m_csUsers;
+ OBJLIST<FacebookUser> 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<wchar_t *> m_wszDefaultGroup; // clist group to store contacts
+ CMOption<bool> m_bUseBigAvatars; // use big or small avatars by default
+ CMOption<bool> m_bUseGroupchats; // do we need group chats at all?
+ CMOption<bool> m_bHideGroupchats; // do not open chat windows on creation
+ CMOption<bool> m_bLoginInvisible; // login in the invisible mode
+ CMOption<bool> m_bKeepUnread; // do not mark incoming messages as read
+ CMOption<bool> 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<FacebookProto> CFBDlgBase;
+
+struct CMPlugin : public ACCPROTOPLUGIN<FacebookProto>
+{
+ 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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#pragma once
-
-#include <windows.h>
-#include <malloc.h>
-#include <time.h>
-#include <stdio.h>
-#include <stdarg.h>
-#include <string.h>
-#include <assert.h>
-
-#include <newpluginapi.h>
-#include <m_avatars.h>
-#include <m_chat_int.h>
-#include <m_clistint.h>
-#include <m_contacts.h>
-#include <m_database.h>
-#include <m_idle.h>
-#include <m_ignore.h>
-#include <m_langpack.h>
-#include <m_message.h>
-#include <m_netlib.h>
-#include <m_options.h>
-#include <m_popup.h>
-#include <m_protosvc.h>
-#include <m_protoint.h>
-#include <m_skin.h>
-#include <m_icolib.h>
-#include <m_hotkeys.h>
-#include <m_folders.h>
-#include <m_smileyadd.h>
-#include <m_toptoolbar.h>
-#include <m_json.h>
-#include <m_imgsrvc.h>
-#include <m_http.h>
-#include <m_messagestate.h>
-#include <m_gui.h>
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#pragma once
+
+#include <windows.h>
+#include <malloc.h>
+#include <time.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <assert.h>
+
+#include <newpluginapi.h>
+#include <m_avatars.h>
+#include <m_chat_int.h>
+#include <m_clistint.h>
+#include <m_contacts.h>
+#include <m_database.h>
+#include <m_idle.h>
+#include <m_ignore.h>
+#include <m_langpack.h>
+#include <m_message.h>
+#include <m_netlib.h>
+#include <m_options.h>
+#include <m_popup.h>
+#include <m_protosvc.h>
+#include <m_protoint.h>
+#include <m_skin.h>
+#include <m_icolib.h>
+#include <m_hotkeys.h>
+#include <m_folders.h>
+#include <m_smileyadd.h>
+#include <m_toptoolbar.h>
+#include <m_json.h>
+#include <m_imgsrvc.h>
+#include <m_http.h>
+#include <m_messagestate.h>
+#include <m_gui.h>
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-/////////////////////////////////////////////////////////////////////////////////////////
-// 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 <http://www.gnu.org/licenses/>.
+
+*/
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// 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 <http://www.gnu.org/licenses/>.
-*/
-
-#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("<not specified>"));
- 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 <http://www.gnu.org/licenses/>.
+*/
+
+#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("<not specified>"));
+ 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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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 &param)
-{
- 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 &param)
-{
- 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 &param)
+{
+ 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 &param)
+{
+ 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<CIcqProto>
-{
- CMPluginMra() : ACCPROTOPLUGIN<CIcqProto>("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<CIcqProto>(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<CIcqProto>
+{
+ CMPluginMra() : ACCPROTOPLUGIN<CIcqProto>("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<CIcqProto>(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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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<IcqGroup> 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<IcqGroup> 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<CIcqProto>(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<CIcqProto>(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<CIcqProto> 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<CIcqProto>
-{
- 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<IcqCacheItem> 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<IcqOwnMessage> m_arOwnIds;
-
- OBJLIST<IcqGroup> 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<AsyncHttpRequest> 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<IcqCacheItem> 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<wchar_t*> m_szOwnId; // our own aim id
- CMOption<uint8_t> m_bHideGroupchats; // don't pop up group chat windows on startup
- CMOption<uint8_t> m_bUseTrayIcon; // use tray icon notifications
- CMOption<uint8_t> m_bErrorPopups; // display popups with errors
- CMOption<uint8_t> m_bLaunchMailbox; // launch browser to view email
- CMOption<uint32_t> m_iTimeDiff1; // set this status to m_iStatus1 after this interval of secs
- CMOption<uint32_t> m_iStatus1;
- CMOption<uint32_t> m_iTimeDiff2; // set this status to m_iStatus2 after this interval of secs
- CMOption<uint32_t> 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<CIcqProto>
-{
- 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<CIcqProto> 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<CIcqProto>
+{
+ 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<IcqCacheItem> 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<IcqOwnMessage> m_arOwnIds;
+
+ OBJLIST<IcqGroup> 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<AsyncHttpRequest> 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<IcqCacheItem> 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<wchar_t*> m_szOwnId; // our own aim id
+ CMOption<uint8_t> m_bHideGroupchats; // don't pop up group chat windows on startup
+ CMOption<uint8_t> m_bUseTrayIcon; // use tray icon notifications
+ CMOption<uint8_t> m_bErrorPopups; // display popups with errors
+ CMOption<uint8_t> m_bLaunchMailbox; // launch browser to view email
+ CMOption<uint32_t> m_iTimeDiff1; // set this status to m_iStatus1 after this interval of secs
+ CMOption<uint32_t> m_iStatus1;
+ CMOption<uint32_t> m_iTimeDiff2; // set this status to m_iStatus2 after this interval of secs
+ CMOption<uint32_t> 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<CIcqProto>
+{
+ 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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <windows.h>
-
-// Standard includes
-#include <stdio.h>
-#include <time.h>
-#include <io.h>
-#include <malloc.h>
-#include <direct.h>
-#include <fcntl.h>
-#include <process.h>
-
-// Miranda IM SDK includes
-#include <newpluginapi.h> // This must be included first
-#include <m_avatars.h>
-#include <m_chat_int.h>
-#include <m_clistint.h>
-#include <m_contacts.h>
-#include <m_database.h>
-#include <m_gui.h>
-#include <m_idle.h>
-#include <m_icolib.h>
-#include <m_ignore.h>
-#include <m_json.h>
-#include <m_langpack.h>
-#include <m_message.h>
-#include <m_messagestate.h>
-#include <m_netlib.h>
-#include <m_protocols.h>
-#include <m_protosvc.h>
-#include <m_options.h>
-#include <m_popup.h>
-#include <m_skin.h>
-#include <m_smileyadd.h>
-#include <m_system.h>
-#include <m_timezones.h>
-#include <m_userinfo.h>
-#include <m_utils.h>
-
-#include <openssl/evp.h>
-#include <openssl/hmac.h>
-#include <openssl/rand.h>
-#include <openssl/sha.h>
-
-// 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 <windows.h>
+
+// Standard includes
+#include <stdio.h>
+#include <time.h>
+#include <io.h>
+#include <malloc.h>
+#include <direct.h>
+#include <fcntl.h>
+#include <process.h>
+
+// Miranda IM SDK includes
+#include <newpluginapi.h> // This must be included first
+#include <m_avatars.h>
+#include <m_chat_int.h>
+#include <m_clistint.h>
+#include <m_contacts.h>
+#include <m_database.h>
+#include <m_gui.h>
+#include <m_idle.h>
+#include <m_icolib.h>
+#include <m_ignore.h>
+#include <m_json.h>
+#include <m_langpack.h>
+#include <m_message.h>
+#include <m_messagestate.h>
+#include <m_netlib.h>
+#include <m_protocols.h>
+#include <m_protosvc.h>
+#include <m_options.h>
+#include <m_popup.h>
+#include <m_skin.h>
+#include <m_smileyadd.h>
+#include <m_system.h>
+#include <m_timezones.h>
+#include <m_userinfo.h>
+#include <m_utils.h>
+
+#include <openssl/evp.h>
+#include <openssl/hmac.h>
+#include <openssl/rand.h>
+#include <openssl/sha.h>
+
+// 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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <io.h>
-
-//////////////////////////////////////////////////////////////////////////
-// 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 <io.h>
+
+//////////////////////////////////////////////////////////////////////////
+// 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 <CommCtrl.h>
-#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 <field Name, field Label> 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<UNIQUE_MAP> &plUsersInfo, UNIQUE_MAP &pmAllFields)
-{
- LIST<char> ListOfNonEmptyFields(20, TCharKeyCmp);
- LIST<char> 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<UNIQUE_MAP> 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 <Go>"));
-}
-
-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 <CommCtrl.h>
+#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 <field Name, field Label> 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<UNIQUE_MAP> &plUsersInfo, UNIQUE_MAP &pmAllFields)
+{
+ LIST<char> ListOfNonEmptyFields(20, TCharKeyCmp);
+ LIST<char> 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<UNIQUE_MAP> 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 <Go>"));
+}
+
+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 <fcntl.h>
-#include <io.h>
-#include <sys/stat.h>
-
-#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("<not specified>") : 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("<currently online>"), _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("<currently online>"), _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("<no information available>"), _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("<Photo not available while offline>"));
- else if (!hBitmap)
- SetDlgItemText(m_hwnd, IDC_CANVAS, TranslateT("<No photo>"));
- 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<char>*)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<char>*)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 <fcntl.h>
+#include <io.h>
+#include <sys/stat.h>
+
+#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("<not specified>") : 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("<currently online>"), _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("<currently online>"), _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("<no information available>"), _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("<Photo not available while offline>"));
+ else if (!hBitmap)
+ SetDlgItemText(m_hwnd, IDC_CANVAS, TranslateT("<No photo>"));
+ 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<char>*)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<char>*)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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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 <http://www.gnu.org/licenses/>.
-*/
-
-#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<GCHOOK*>(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 <http://www.gnu.org/licenses/>.
+*/
+
+#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<GCHOOK*>(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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 &timestamp_ = 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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 &timestamp_ = 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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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<MinecraftDynmapProto*>(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<NMHDR*>(lparam)->code == PSN_APPLY) {
- proto = reinterpret_cast<MinecraftDynmapProto*>(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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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<MinecraftDynmapProto*>(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<NMHDR*>(lparam)->code == PSN_APPLY) {
+ proto = reinterpret_cast<MinecraftDynmapProto*>(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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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<MinecraftDynmapProto>("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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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<MinecraftDynmapProto>("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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#include "stdafx.h"
-
-MinecraftDynmapProto::MinecraftDynmapProto(const char* proto_name, const wchar_t* username) :
- PROTO<MinecraftDynmapProto>(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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#include "stdafx.h"
+
+MinecraftDynmapProto::MinecraftDynmapProto(const char* proto_name, const wchar_t* username) :
+ PROTO<MinecraftDynmapProto>(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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#pragma once
-
-class MinecraftDynmapProto : public PROTO<MinecraftDynmapProto>
-{
- 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<MinecraftDynmapProto>
-{
- 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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#pragma once
+
+class MinecraftDynmapProto : public PROTO<MinecraftDynmapProto>
+{
+ 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<MinecraftDynmapProto>
+{
+ 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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#pragma once
-
-#pragma warning(disable:4996)
-
-#include <string>
-#include <cstring>
-#include <sstream>
-#include <fstream>
-#include <map>
-#include <stdarg.h>
-#include <time.h>
-#include <assert.h>
-#include <io.h>
-
-#include <windows.h>
-#include <commctrl.h>
-
-#include <newpluginapi.h>
-#include <m_system.h>
-#include <m_chat_int.h>
-#include <m_clistint.h>
-#include <m_langpack.h>
-#include <m_netlib.h>
-#include <m_options.h>
-#include <m_popup.h>
-#include <m_protocols.h>
-#include <m_protosvc.h>
-#include <m_protoint.h>
-#include <m_skin.h>
-#include <statusmodes.h>
-#include <m_icolib.h>
-#include <m_utils.h>
-#include <m_hotkeys.h>
-#include <m_message.h>
-#include <m_json.h>
-#include <m_http.h>
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#pragma once
+
+#pragma warning(disable:4996)
+
+#include <string>
+#include <cstring>
+#include <sstream>
+#include <fstream>
+#include <map>
+#include <stdarg.h>
+#include <time.h>
+#include <assert.h>
+#include <io.h>
+
+#include <windows.h>
+#include <commctrl.h>
+
+#include <newpluginapi.h>
+#include <m_system.h>
+#include <m_chat_int.h>
+#include <m_clistint.h>
+#include <m_langpack.h>
+#include <m_netlib.h>
+#include <m_options.h>
+#include <m_popup.h>
+#include <m_protocols.h>
+#include <m_protosvc.h>
+#include <m_protoint.h>
+#include <m_skin.h>
+#include <statusmodes.h>
+#include <m_icolib.h>
+#include <m_utils.h>
+#include <m_hotkeys.h>
+#include <m_message.h>
+#include <m_json.h>
+#include <m_http.h>
+
+#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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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 <http://www.gnu.org/licenses/>.
-*/
-
-#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<std::string> 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 <http://www.gnu.org/licenses/>.
+*/
+
+#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<std::string> 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 <http://www.gnu.org/licenses/>.
-
-*/
-
-#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<int*>(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 <http://www.gnu.org/licenses/>.
+
+*/
+
+#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<int*>(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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 <http://www.gnu.org/licenses/>.
-*/
-
-#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 = "<input.+?type=\"hidden\".+?name=\"PPFT\".+?id=\"i0327\".+?value=\"(.+?)\".*?/>";
-
- 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<std::string, std::string> 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("<input.+?type=\"hidden\".+?name=\"t\".+?id=\"t\".+?value=\"(.+?)\".*?>")))
- if (!std::regex_search(content, match, std::regex("<input.+?type=\"hidden\".+?name=\"ipt\".+?id=\"ipt\".+?value=\"(.+?)\".*?>")))
- 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 = "<input.+?type=\"hidden\".+?name=\"skypetoken\".+?value=\"(.+?)\".*?/>";
- 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 = "<input.+?type=\"hidden\".+?name=\"expires_in\".+?value=\"(.+?)\".*?/>";
-
- 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 <http://www.gnu.org/licenses/>.
+*/
+
+#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 = "<input.+?type=\"hidden\".+?name=\"PPFT\".+?id=\"i0327\".+?value=\"(.+?)\".*?/>";
+
+ 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<std::string, std::string> 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("<input.+?type=\"hidden\".+?name=\"t\".+?id=\"t\".+?value=\"(.+?)\".*?>")))
+ if (!std::regex_search(content, match, std::regex("<input.+?type=\"hidden\".+?name=\"ipt\".+?id=\"ipt\".+?value=\"(.+?)\".*?>")))
+ 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 = "<input.+?type=\"hidden\".+?name=\"skypetoken\".+?value=\"(.+?)\".*?/>";
+ 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 = "<input.+?type=\"hidden\".+?name=\"expires_in\".+?value=\"(.+?)\".*?/>";
+
+ 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 <http://www.gnu.org/licenses/>.
-*/
-
-#include "stdafx.h"
-
-CSkypeProto::CSkypeProto(const char* protoName, const wchar_t* userName) :
- PROTO<CSkypeProto>(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 <http://www.gnu.org/licenses/>.
+*/
+
+#include "stdafx.h"
+
+CSkypeProto::CSkypeProto(const char* protoName, const wchar_t* userName) :
+ PROTO<CSkypeProto>(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 <http://www.gnu.org/licenses/>.
-*/
-
-#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", &timestamp.tm_hour, &timestamp.tm_min, &timestamp.tm_sec) != 3)
- return (time_t)0;
-
- timestamp.tm_isdst = 0; // DST is already present in _timezone below
- time_t t = mktime(&timestamp);
-
- _tzset();
- t -= _timezone;
- return (t >= 0) ? t : 0;
-}
-
-struct HtmlEntity
-{
- const char *entity;
- const char *symbol;
-};
-
-const HtmlEntity htmlEntities[] =
-{
- { "AElig", "\u00C6" },
- { "Aacute", "\u00C1" },
- { "Acirc", "\u00C2" },
- { "Agrave", "\u00C0" },
- { "Alpha", "\u0391" },
- { "Aring", "\u00C5" },
- { "Atilde", "\u00C3" },
- { "Auml", "\u00C4" },
- { "Beta", "\u0392" },
- { "Ccedil", "\u00C7" },
- { "Chi", "\u03A7" },
- { "Dagger", "‡" },
- { "Delta", "\u0394" },
- { "ETH", "\u00D0" },
- { "Eacute", "\u00C9" },
- { "Ecirc", "\u00CA" },
- { "Egrave", "\u00C8" },
- { "Epsilon", "\u0395" },
- { "Eta", "\u0397" },
- { "Euml", "\u00CB" },
- { "Gamma", "\u0393" },
- { "Iacute", "\u00CD" },
- { "Icirc", "\u00CE" },
- { "Igrave", "\u00CC" },
- { "Iota", "\u0399" },
- { "Iuml", "\u00CF" },
- { "Kappa", "\u039A" },
- { "Lambda", "\u039B" },
- { "Mu", "\u039C" },
- { "Ntilde", "\u00D1" },
- { "Nu", "\u039D" },
- { "OElig", "\u0152" },
- { "Oacute", "\u00D3" },
- { "Ocirc", "\u00D4" },
- { "Ograve", "\u00D2" },
- { "Omega", "\u03A9" },
- { "Omicron", "\u039F" },
- { "Oslash", "\u00D8" },
- { "Otilde", "\u00D5" },
- { "Ouml", "\u00D6" },
- { "Phi", "\u03A6" },
- { "Pi", "\u03A0" },
- { "Prime", "\u2033" },
- { "Psi", "\u03A8" },
- { "Rho", "\u03A1" },
- { "Scaron", "Š" },
- { "Sigma", "Σ" },
- { "THORN", "Þ" },
- { "Tau", "Τ" },
- { "Theta", "Θ" },
- { "Uacute", "Ú" },
- { "Ucirc", "Û" },
- { "Ugrave", "Ù" },
- { "Upsilon", "Υ" },
- { "Uuml", "Ü" },
- { "Xi", "Ξ" },
- { "Yacute", "Ý" },
- { "Yuml", "Ÿ" },
- { "Zeta", "Ζ" },
- { "aacute", "á" },
- { "acirc", "â" },
- { "acute", "´" },
- { "aelig", "æ" },
- { "agrave", "à" },
- { "alefsym", "ℵ" },
- { "alpha", "α" },
- { "amp", "&" },
- { "and", "∧" },
- { "ang", "∠" },
- { "apos", "'" },
- { "aring", "å" },
- { "asymp", "≈" },
- { "atilde", "ã" },
- { "auml", "ä" },
- { "bdquo", "„" },
- { "beta", "β" },
- { "brvbar", "¦" },
- { "bull", "•" },
- { "cap", "∩" },
- { "ccedil", "ç" },
- { "cedil", "¸" },
- { "cent", "¢" },
- { "chi", "χ" },
- { "circ", "ˆ" },
- { "clubs", "♣" },
- { "cong", "≅" },
- { "copy", "©" },
- { "crarr", "↵" },
- { "cup", "∪" },
- { "curren", "¤" },
- { "dArr", "⇓" },
- { "dagger", "†" },
- { "darr", "↓" },
- { "deg", "°" },
- { "delta", "δ" },
- { "diams", "♦" },
- { "divide", "÷" },
- { "eacute", "é" },
- { "ecirc", "ê" },
- { "egrave", "è" },
- { "empty", "∅" },
- { "emsp", " " },
- { "ensp", " " },
- { "epsilon", "ε" },
- { "equiv", "≡" },
- { "eta", "η" },
- { "eth", "ð" },
- { "euml", "ë" },
- { "euro", "€" },
- { "exist", "∃" },
- { "fnof", "ƒ" },
- { "forall", "∀" },
- { "frac12", "½" },
- { "frac14", "¼" },
- { "frac34", "¾" },
- { "frasl", "⁄" },
- { "gamma", "γ" },
- { "ge", "≥" },
- { "gt", ">" },
- { "hArr", "⇔" },
- { "harr", "↔" },
- { "hearts", "♥" },
- { "hellip", "…" },
- { "iacute", "í" },
- { "icirc", "î" },
- { "iexcl", "¡" },
- { "igrave", "ì" },
- { "image", "ℑ" },
- { "infin", "∞" },
- { "int", "∫" },
- { "iota", "ι" },
- { "iquest", "¿" },
- { "isin", "∈" },
- { "iuml", "ï" },
- { "kappa", "κ" },
- { "lArr", "⇐" },
- { "lambda", "λ" },
- { "lang", "〈" },
- { "laquo", "«" },
- { "larr", "←" },
- { "lceil", "⌈" },
- { "ldquo", "“" },
- { "le", "≤" },
- { "lfloor", "⌊" },
- { "lowast", "∗" },
- { "loz", "◊" },
- { "lrm", "\xE2\x80\x8E" },
- { "lsaquo", "‹" },
- { "lsquo", "‘" },
- { "lt", "<" },
- { "macr", "¯" },
- { "mdash", "—" },
- { "micro", "µ" },
- { "middot", "·" },
- { "minus", "−" },
- { "mu", "μ" },
- { "nabla", "∇" },
- { "nbsp", " " },
- { "ndash", "–" },
- { "ne", "≠" },
- { "ni", "∋" },
- { "not", "¬" },
- { "notin", "∉" },
- { "nsub", "⊄" },
- { "ntilde", "ñ" },
- { "nu", "ν" },
- { "oacute", "ó" },
- { "ocirc", "ô" },
- { "oelig", "œ" },
- { "ograve", "ò" },
- { "oline", "‾" },
- { "omega", "ω" },
- { "omicron", "ο" },
- { "oplus", "⊕" },
- { "or", "∨" },
- { "ordf", "ª" },
- { "ordm", "º" },
- { "oslash", "ø" },
- { "otilde", "õ" },
- { "otimes", "⊗" },
- { "ouml", "ö" },
- { "para", "¶" },
- { "part", "∂" },
- { "permil", "‰" },
- { "perp", "⊥" },
- { "phi", "φ" },
- { "pi", "π" },
- { "piv", "ϖ" },
- { "plusmn", "±" },
- { "pound", "£" },
- { "prime", "′" },
- { "prod", "∏" },
- { "prop", "∝" },
- { "psi", "ψ" },
- { "quot", "\"" },
- { "rArr", "⇒" },
- { "radic", "√" },
- { "rang", "〉" },
- { "raquo", "»" },
- { "rarr", "→" },
- { "rceil", "⌉" },
- { "rdquo", "”" },
- { "real", "ℜ" },
- { "reg", "®" },
- { "rfloor", "⌋" },
- { "rho", "ρ" },
- { "rlm", "\xE2\x80\x8F" },
- { "rsaquo", "›" },
- { "rsquo", "’" },
- { "sbquo", "‚" },
- { "scaron", "š" },
- { "sdot", "⋅" },
- { "sect", "§" },
- { "shy", "\xC2\xAD" },
- { "sigma", "σ" },
- { "sigmaf", "ς" },
- { "sim", "∼" },
- { "spades", "♠" },
- { "sub", "⊂" },
- { "sube", "⊆" },
- { "sum", "∑" },
- { "sup", "⊃" },
- { "sup1", "¹" },
- { "sup2", "²" },
- { "sup3", "³" },
- { "supe", "⊇" },
- { "szlig", "ß" },
- { "tau", "τ" },
- { "there4", "∴" },
- { "theta", "θ" },
- { "thetasym", "ϑ" },
- { "thinsp", " " },
- { "thorn", "þ" },
- { "tilde", "˜" },
- { "times", "×" },
- { "trade", "™" },
- { "uArr", "⇑" },
- { "uacute", "ú" },
- { "uarr", "↑" },
- { "ucirc", "û" },
- { "ugrave", "ù" },
- { "uml", "¨" },
- { "upsih", "ϒ" },
- { "upsilon", "υ" },
- { "uuml", "ü" },
- { "weierp", "℘" },
- { "xi", "ξ" },
- { "yacute", "ý" },
- { "yen", "¥" },
- { "yuml", "ÿ" },
- { "zeta", "ζ" },
- { "zwj", "\xE2\x80\x8D" },
- { "zwnj", "\xE2\x80\x8C" }
-};
-
-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 <http://www.gnu.org/licenses/>.
+*/
+
+#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", &timestamp.tm_hour, &timestamp.tm_min, &timestamp.tm_sec) != 3)
+ return (time_t)0;
+
+ timestamp.tm_isdst = 0; // DST is already present in _timezone below
+ time_t t = mktime(&timestamp);
+
+ _tzset();
+ t -= _timezone;
+ return (t >= 0) ? t : 0;
+}
+
+struct HtmlEntity
+{
+ const char *entity;
+ const char *symbol;
+};
+
+const HtmlEntity htmlEntities[] =
+{
+ { "AElig", "\u00C6" },
+ { "Aacute", "\u00C1" },
+ { "Acirc", "\u00C2" },
+ { "Agrave", "\u00C0" },
+ { "Alpha", "\u0391" },
+ { "Aring", "\u00C5" },
+ { "Atilde", "\u00C3" },
+ { "Auml", "\u00C4" },
+ { "Beta", "\u0392" },
+ { "Ccedil", "\u00C7" },
+ { "Chi", "\u03A7" },
+ { "Dagger", "‡" },
+ { "Delta", "\u0394" },
+ { "ETH", "\u00D0" },
+ { "Eacute", "\u00C9" },
+ { "Ecirc", "\u00CA" },
+ { "Egrave", "\u00C8" },
+ { "Epsilon", "\u0395" },
+ { "Eta", "\u0397" },
+ { "Euml", "\u00CB" },
+ { "Gamma", "\u0393" },
+ { "Iacute", "\u00CD" },
+ { "Icirc", "\u00CE" },
+ { "Igrave", "\u00CC" },
+ { "Iota", "\u0399" },
+ { "Iuml", "\u00CF" },
+ { "Kappa", "\u039A" },
+ { "Lambda", "\u039B" },
+ { "Mu", "\u039C" },
+ { "Ntilde", "\u00D1" },
+ { "Nu", "\u039D" },
+ { "OElig", "\u0152" },
+ { "Oacute", "\u00D3" },
+ { "Ocirc", "\u00D4" },
+ { "Ograve", "\u00D2" },
+ { "Omega", "\u03A9" },
+ { "Omicron", "\u039F" },
+ { "Oslash", "\u00D8" },
+ { "Otilde", "\u00D5" },
+ { "Ouml", "\u00D6" },
+ { "Phi", "\u03A6" },
+ { "Pi", "\u03A0" },
+ { "Prime", "\u2033" },
+ { "Psi", "\u03A8" },
+ { "Rho", "\u03A1" },
+ { "Scaron", "Š" },
+ { "Sigma", "Σ" },
+ { "THORN", "Þ" },
+ { "Tau", "Τ" },
+ { "Theta", "Θ" },
+ { "Uacute", "Ú" },
+ { "Ucirc", "Û" },
+ { "Ugrave", "Ù" },
+ { "Upsilon", "Υ" },
+ { "Uuml", "Ü" },
+ { "Xi", "Ξ" },
+ { "Yacute", "Ý" },
+ { "Yuml", "Ÿ" },
+ { "Zeta", "Ζ" },
+ { "aacute", "á" },
+ { "acirc", "â" },
+ { "acute", "´" },
+ { "aelig", "æ" },
+ { "agrave", "à" },
+ { "alefsym", "ℵ" },
+ { "alpha", "α" },
+ { "amp", "&" },
+ { "and", "∧" },
+ { "ang", "∠" },
+ { "apos", "'" },
+ { "aring", "å" },
+ { "asymp", "≈" },
+ { "atilde", "ã" },
+ { "auml", "ä" },
+ { "bdquo", "„" },
+ { "beta", "β" },
+ { "brvbar", "¦" },
+ { "bull", "•" },
+ { "cap", "∩" },
+ { "ccedil", "ç" },
+ { "cedil", "¸" },
+ { "cent", "¢" },
+ { "chi", "χ" },
+ { "circ", "ˆ" },
+ { "clubs", "♣" },
+ { "cong", "≅" },
+ { "copy", "©" },
+ { "crarr", "↵" },
+ { "cup", "∪" },
+ { "curren", "¤" },
+ { "dArr", "⇓" },
+ { "dagger", "†" },
+ { "darr", "↓" },
+ { "deg", "°" },
+ { "delta", "δ" },
+ { "diams", "♦" },
+ { "divide", "÷" },
+ { "eacute", "é" },
+ { "ecirc", "ê" },
+ { "egrave", "è" },
+ { "empty", "∅" },
+ { "emsp", " " },
+ { "ensp", " " },
+ { "epsilon", "ε" },
+ { "equiv", "≡" },
+ { "eta", "η" },
+ { "eth", "ð" },
+ { "euml", "ë" },
+ { "euro", "€" },
+ { "exist", "∃" },
+ { "fnof", "ƒ" },
+ { "forall", "∀" },
+ { "frac12", "½" },
+ { "frac14", "¼" },
+ { "frac34", "¾" },
+ { "frasl", "⁄" },
+ { "gamma", "γ" },
+ { "ge", "≥" },
+ { "gt", ">" },
+ { "hArr", "⇔" },
+ { "harr", "↔" },
+ { "hearts", "♥" },
+ { "hellip", "…" },
+ { "iacute", "í" },
+ { "icirc", "î" },
+ { "iexcl", "¡" },
+ { "igrave", "ì" },
+ { "image", "ℑ" },
+ { "infin", "∞" },
+ { "int", "∫" },
+ { "iota", "ι" },
+ { "iquest", "¿" },
+ { "isin", "∈" },
+ { "iuml", "ï" },
+ { "kappa", "κ" },
+ { "lArr", "⇐" },
+ { "lambda", "λ" },
+ { "lang", "〈" },
+ { "laquo", "«" },
+ { "larr", "←" },
+ { "lceil", "⌈" },
+ { "ldquo", "“" },
+ { "le", "≤" },
+ { "lfloor", "⌊" },
+ { "lowast", "∗" },
+ { "loz", "◊" },
+ { "lrm", "\xE2\x80\x8E" },
+ { "lsaquo", "‹" },
+ { "lsquo", "‘" },
+ { "lt", "<" },
+ { "macr", "¯" },
+ { "mdash", "—" },
+ { "micro", "µ" },
+ { "middot", "·" },
+ { "minus", "−" },
+ { "mu", "μ" },
+ { "nabla", "∇" },
+ { "nbsp", " " },
+ { "ndash", "–" },
+ { "ne", "≠" },
+ { "ni", "∋" },
+ { "not", "¬" },
+ { "notin", "∉" },
+ { "nsub", "⊄" },
+ { "ntilde", "ñ" },
+ { "nu", "ν" },
+ { "oacute", "ó" },
+ { "ocirc", "ô" },
+ { "oelig", "œ" },
+ { "ograve", "ò" },
+ { "oline", "‾" },
+ { "omega", "ω" },
+ { "omicron", "ο" },
+ { "oplus", "⊕" },
+ { "or", "∨" },
+ { "ordf", "ª" },
+ { "ordm", "º" },
+ { "oslash", "ø" },
+ { "otilde", "õ" },
+ { "otimes", "⊗" },
+ { "ouml", "ö" },
+ { "para", "¶" },
+ { "part", "∂" },
+ { "permil", "‰" },
+ { "perp", "⊥" },
+ { "phi", "φ" },
+ { "pi", "π" },
+ { "piv", "ϖ" },
+ { "plusmn", "±" },
+ { "pound", "£" },
+ { "prime", "′" },
+ { "prod", "∏" },
+ { "prop", "∝" },
+ { "psi", "ψ" },
+ { "quot", "\"" },
+ { "rArr", "⇒" },
+ { "radic", "√" },
+ { "rang", "〉" },
+ { "raquo", "»" },
+ { "rarr", "→" },
+ { "rceil", "⌉" },
+ { "rdquo", "”" },
+ { "real", "ℜ" },
+ { "reg", "®" },
+ { "rfloor", "⌋" },
+ { "rho", "ρ" },
+ { "rlm", "\xE2\x80\x8F" },
+ { "rsaquo", "›" },
+ { "rsquo", "’" },
+ { "sbquo", "‚" },
+ { "scaron", "š" },
+ { "sdot", "⋅" },
+ { "sect", "§" },
+ { "shy", "\xC2\xAD" },
+ { "sigma", "σ" },
+ { "sigmaf", "ς" },
+ { "sim", "∼" },
+ { "spades", "♠" },
+ { "sub", "⊂" },
+ { "sube", "⊆" },
+ { "sum", "∑" },
+ { "sup", "⊃" },
+ { "sup1", "¹" },
+ { "sup2", "²" },
+ { "sup3", "³" },
+ { "supe", "⊇" },
+ { "szlig", "ß" },
+ { "tau", "τ" },
+ { "there4", "∴" },
+ { "theta", "θ" },
+ { "thetasym", "ϑ" },
+ { "thinsp", " " },
+ { "thorn", "þ" },
+ { "tilde", "˜" },
+ { "times", "×" },
+ { "trade", "™" },
+ { "uArr", "⇑" },
+ { "uacute", "ú" },
+ { "uarr", "↑" },
+ { "ucirc", "û" },
+ { "ugrave", "ù" },
+ { "uml", "¨" },
+ { "upsih", "ϒ" },
+ { "upsilon", "υ" },
+ { "uuml", "ü" },
+ { "weierp", "℘" },
+ { "xi", "ξ" },
+ { "yacute", "ý" },
+ { "yen", "¥" },
+ { "yuml", "ÿ" },
+ { "zeta", "ζ" },
+ { "zwj", "\xE2\x80\x8D" },
+ { "zwnj", "\xE2\x80\x8C" }
+};
+
+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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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 <http://www.gnu.org/licenses/>.
-*/
-
-/* This file contains the includes, weather constants/declarations,
- the structs, and the primitives for some of the functions.
-*/
-
-#pragma once
-
-//============ THE INCLUDES ===========
-
-#include <share.h>
-#include <time.h>
-#include <windows.h>
-#include <commctrl.h>
-#include <richedit.h>
-#include <malloc.h>
-
-#include <newpluginapi.h>
-#include <m_acc.h>
-#include <m_avatars.h>
-#include <m_button.h>
-#include <m_clc.h>
-#include <m_cluiframes.h>
-#include <m_contacts.h>
-#include <m_database.h>
-#include <m_findadd.h>
-#include <m_fontservice.h>
-#include <m_history.h>
-#include <m_icolib.h>
-#include <m_ignore.h>
-#include <m_langpack.h>
-#include <m_netlib.h>
-#include <m_options.h>
-#include <m_popup.h>
-#include <m_protosvc.h>
-#include <m_skin.h>
-#include <m_skin_eng.h>
-#include <m_userinfo.h>
-#include <m_xstatus.h>
-
-#include <m_tipper.h>
-#include <m_weather.h>
-#include <m_toptoolbar.h>
-
-#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>
-{
- CMPlugin();
-
- HINSTANCE hIconsDll = nullptr;
- CMOption<bool> 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 <http://www.gnu.org/licenses/>.
+*/
+
+/* This file contains the includes, weather constants/declarations,
+ the structs, and the primitives for some of the functions.
+*/
+
+#pragma once
+
+//============ THE INCLUDES ===========
+
+#include <share.h>
+#include <time.h>
+#include <windows.h>
+#include <commctrl.h>
+#include <richedit.h>
+#include <malloc.h>
+
+#include <newpluginapi.h>
+#include <m_acc.h>
+#include <m_avatars.h>
+#include <m_button.h>
+#include <m_clc.h>
+#include <m_cluiframes.h>
+#include <m_contacts.h>
+#include <m_database.h>
+#include <m_findadd.h>
+#include <m_fontservice.h>
+#include <m_history.h>
+#include <m_icolib.h>
+#include <m_ignore.h>
+#include <m_langpack.h>
+#include <m_netlib.h>
+#include <m_options.h>
+#include <m_popup.h>
+#include <m_protosvc.h>
+#include <m_skin.h>
+#include <m_skin_eng.h>
+#include <m_userinfo.h>
+#include <m_xstatus.h>
+
+#include <m_tipper.h>
+#include <m_weather.h>
+#include <m_toptoolbar.h>
+
+#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>
+{
+ CMPlugin();
+
+ HINSTANCE hIconsDll = nullptr;
+ CMOption<bool> 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<CMPlugin>(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<CMPlugin>(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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#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<char> 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<char> 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<WhatsAppProto>(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<WhatsAppProto>(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<char> 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<char> 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(&regId, 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(&regId, 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<WhatsAppProto>
-{
- CCtrlCheck chkHideChats, chkBbcodes;
- CCtrlEdit edtGroup, edtNick, edtDevName;
- CCtrlButton btnUnregister;
- ptrW m_wszOldGroup;
-
-public:
- COptionsDlg(WhatsAppProto *ppro, int iDlgID, bool bFullDlg) :
- CProtoDlgBase<WhatsAppProto>(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<WhatsAppProto>
+{
+ CCtrlCheck chkHideChats, chkBbcodes;
+ CCtrlEdit edtGroup, edtNick, edtDevName;
+ CCtrlButton btnUnregister;
+ ptrW m_wszOldGroup;
+
+public:
+ COptionsDlg(WhatsAppProto *ppro, int iDlgID, bool bFullDlg) :
+ CProtoDlgBase<WhatsAppProto>(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<WhatsAppProto>
-{
-public:
- CWhatsAppQRDlg(WhatsAppProto *ppro) :
- CProtoDlgBase<WhatsAppProto>(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<BYTE> 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<WhatsAppProto>
+{
+public:
+ CWhatsAppQRDlg(WhatsAppProto *ppro) :
+ CProtoDlgBase<WhatsAppProto>(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<BYTE> 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 <fcntl.h>
-#include <malloc.h>
-#include <io.h>
-#include <time.h>
-#include <windows.h>
-
-#include <list>
-#include <map>
-#include <memory>
-#include <string>
-
-#include <newpluginapi.h>
-#include <m_avatars.h>
-#include <m_chat_int.h>
-#include <m_clist.h>
-#include <m_contacts.h>
-#include <m_database.h>
-#include <m_history.h>
-#include <m_imgsrvc.h>
-#include <m_ignore.h>
-#include <m_json.h>
-#include <m_langpack.h>
-#include <m_message.h>
-#include <m_netlib.h>
-#include <m_options.h>
-#include <m_popup.h>
-#include <m_protocols.h>
-#include <m_protosvc.h>
-#include <m_protoint.h>
-#include <m_skin.h>
-#include <m_string.h>
-#include <statusmodes.h>
-#include <m_userinfo.h>
-#include <m_icolib.h>
-#include <m_utils.h>
-#include <m_xml.h>
-#include <m_hotkeys.h>
-#include <m_folders.h>
-#include <m_json.h>
-#include <m_gui.h>
-#include <m_messagestate.h>
-
-#include <openssl/evp.h>
-#include <openssl/hmac.h>
-#include <openssl/sha.h>
-#include <openssl/kdf.h>
-
-#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 <fcntl.h>
+#include <malloc.h>
+#include <io.h>
+#include <time.h>
+#include <windows.h>
+
+#include <list>
+#include <map>
+#include <memory>
+#include <string>
+
+#include <newpluginapi.h>
+#include <m_avatars.h>
+#include <m_chat_int.h>
+#include <m_clist.h>
+#include <m_contacts.h>
+#include <m_database.h>
+#include <m_history.h>
+#include <m_imgsrvc.h>
+#include <m_ignore.h>
+#include <m_json.h>
+#include <m_langpack.h>
+#include <m_message.h>
+#include <m_netlib.h>
+#include <m_options.h>
+#include <m_popup.h>
+#include <m_protocols.h>
+#include <m_protosvc.h>
+#include <m_protoint.h>
+#include <m_skin.h>
+#include <m_string.h>
+#include <statusmodes.h>
+#include <m_userinfo.h>
+#include <m_icolib.h>
+#include <m_utils.h>
+#include <m_xml.h>
+#include <m_hotkeys.h>
+#include <m_folders.h>
+#include <m_json.h>
+#include <m_gui.h>
+#include <m_messagestate.h>
+
+#include <openssl/evp.h>
+#include <openssl/hmac.h>
+#include <openssl/sha.h>
+#include <openssl/kdf.h>
+
+#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<struct Attr> attrs;
- OBJLIST<WANode> 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<WANode> &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 &param)
-{
- node.addAttr(param.szName, param.szValue);
- return node;
-}
-
-__forceinline WANode &operator<<(WANode &node, const INT_PARAM &param)
-{
- 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<WAJid> 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<struct Attr> attrs;
+ OBJLIST<WANode> 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<WANode> &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 &param)
+{
+ node.addAttr(param.szName, param.szValue);
+ return node;
+}
+
+__forceinline WANode &operator<<(WANode &node, const INT_PARAM &param)
+{
+ 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<WAJid> 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("</%s>\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<WANode> 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("</%s>\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<WANode> 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 <http://www.gnu.org/licenses/>.
-*/
-
+/*
+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 <http://www.gnu.org/licenses/>.
+*/
+
#include "stdafx.h" \ No newline at end of file