aboutsummaryrefslogtreecommitdiffstats
path: root/psi-plus
diff options
context:
space:
mode:
authornathan <nathansmith@disroot.org>2025-11-06 10:05:54 +0000
committernathan <nathansmith@disroot.org>2025-11-06 10:05:54 +0000
commit16bbfb5a5181d769704b7315cd44621a1c8044dc (patch)
tree8f1235b94c7bf7c3ccc1007303f4b978adc1c41e /psi-plus
parent42ee7b46e5bc86685e176f6626c6336e77289f8d (diff)
downloadpsi-plus-void-main.tar.gz
psi-plus-void-main.tar.bz2
psi-plus-void-main.zip
Patched clientswitcherHEADmain
Diffstat (limited to 'psi-plus')
-rw-r--r--psi-plus/patches/clientswitcher-fix.patch2459
-rw-r--r--psi-plus/template3
2 files changed, 2461 insertions, 1 deletions
diff --git a/psi-plus/patches/clientswitcher-fix.patch b/psi-plus/patches/clientswitcher-fix.patch
new file mode 100644
index 0000000..952a940
--- /dev/null
+++ b/psi-plus/patches/clientswitcher-fix.patch
@@ -0,0 +1,2459 @@
+diff -Nru a/plugins/generic/clientswitcherplugin/accountsettings.cpp b/plugins/generic/clientswitcherplugin/accountsettings.cpp
+--- a/plugins/generic/clientswitcherplugin/accountsettings.cpp 2025-08-26 14:50:07.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/accountsettings.cpp 2020-07-10 11:50:10.000000000 +0000
+@@ -38,8 +38,9 @@
+
+ bool AccountSettings::isEmpty()
+ {
+- if (response_mode == RespAllow && !lock_time_requ) {
+- if (os_name.isNull() && client_name.isEmpty() && caps_node.isEmpty()) {
++ if (!enable_contacts && !enable_conferences && response_mode == RespAllow && !lock_time_requ
++ && show_requ_mode == LogNever && log_mode == LogNever) {
++ if (os_name.isEmpty() && client_name.isEmpty() && caps_node.isEmpty() && caps_version.isEmpty()) {
+ return true;
+ }
+ }
+@@ -77,6 +78,10 @@
+ QString param_value = param_list.join("=");
+ if (param_name == "acc_id") {
+ account_id = stripSlashes(param_value);
++ } else if (param_name == "en_cnt") {
++ enable_contacts = (param_value == "true") ? true : false;
++ } else if (param_name == "en_cnf") {
++ enable_conferences = (param_value == "true") ? true : false;
+ } else if (param_name == "l_req") {
+ if (param_value == "true")
+ response_mode = RespNotImpl;
+@@ -86,16 +91,30 @@
+ response_mode = RespAllow;
+ } else if (param_name == "l_treq") {
+ lock_time_requ = (param_value == "true") ? true : false;
++ } else if (param_name == "s_req") {
++ show_requ_mode = LogNever;
++ if (param_value == "true" || param_value == "if-repl") {
++ show_requ_mode = LogIfReplace;
++ } else if (param_value == "always") {
++ show_requ_mode = LogAlways;
++ }
+ } else if (param_name == "os_nm") {
+ os_name = stripSlashes(param_value);
+- } else if (param_name == "os_ver") {
+- os_version = stripSlashes(param_value);
+ } else if (param_name == "cl_nm") {
+ client_name = stripSlashes(param_value);
+ } else if (param_name == "cl_ver") {
+ client_version = stripSlashes(param_value);
+ } else if (param_name == "cp_nd") {
+ caps_node = stripSlashes(param_value);
++ } else if (param_name == "cp_ver") {
++ caps_version = stripSlashes(param_value);
++ } else if (param_name == "log") {
++ log_mode = LogNever;
++ if (param_value == "true" || param_value == "if-repl") {
++ log_mode = LogIfReplace;
++ } else if (param_value == "always") {
++ log_mode = LogAlways;
++ }
+ }
+ }
+ }
+@@ -104,6 +123,8 @@
+ QString AccountSettings::toString()
+ {
+ QString s_res = "acc_id=" + addSlashes(account_id);
++ s_res.append(";en_cnt=").append((enable_contacts) ? "true" : "false");
++ s_res.append(";en_cnf=").append((enable_conferences) ? "true" : "false");
+ QString str1;
+ if (response_mode == RespNotImpl)
+ str1 = "true";
+@@ -113,29 +134,44 @@
+ str1 = "false";
+ s_res.append(";l_req=").append(str1);
+ s_res.append(";l_treq=").append((lock_time_requ) ? "true" : "false");
+- if (!os_name.isNull())
+- s_res.append(";os_nm=").append(addSlashes(os_name));
+- if (!os_version.isNull())
+- s_res.append(";os_ver=").append(addSlashes(os_version));
+- if (!client_name.isNull())
+- s_res.append(";cl_nm=").append(addSlashes(client_name));
+- if (!client_version.isNull())
+- s_res.append(";cl_ver=").append(addSlashes(client_version));
+- if (!caps_node.isNull())
+- s_res.append(";cp_nd=").append(addSlashes(caps_node));
++ if (show_requ_mode == LogIfReplace) {
++ str1 = "if-repl";
++ } else if (show_requ_mode == LogAlways) {
++ str1 = "always";
++ } else {
++ str1 = "never";
++ }
++ s_res.append(";s_req=").append(str1);
++ s_res.append(";os_nm=").append(addSlashes(os_name));
++ s_res.append(";cl_nm=").append(addSlashes(client_name));
++ s_res.append(";cl_ver=").append(addSlashes(client_version));
++ s_res.append(";cp_nd=").append(addSlashes(caps_node));
++ s_res.append(";cp_ver=").append(addSlashes(caps_version));
++ if (log_mode == LogIfReplace) {
++ str1 = "if-repl";
++ } else if (log_mode == LogAlways) {
++ str1 = "always";
++ } else {
++ str1 = "never";
++ }
++ s_res.append(";log=").append(str1);
+ return s_res;
+ }
+
+ void AccountSettings::init()
+ {
+- account_id = "";
+- response_mode = RespAllow;
+- lock_time_requ = false;
+- os_name = "";
+- os_version = "";
+- client_name = "";
+- client_version = "";
+- caps_node = "";
++ account_id = "";
++ enable_contacts = false;
++ enable_conferences = false;
++ response_mode = RespAllow;
++ lock_time_requ = false;
++ show_requ_mode = LogNever;
++ os_name = "";
++ client_name = "";
++ client_version = "";
++ caps_node = "";
++ caps_version = "";
++ log_mode = LogNever;
+ }
+
+ QString AccountSettings::addSlashes(QString &str)
+diff -Nru a/plugins/generic/clientswitcherplugin/accountsettings.h b/plugins/generic/clientswitcherplugin/accountsettings.h
+--- a/plugins/generic/clientswitcherplugin/accountsettings.h 2025-08-26 14:50:07.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/accountsettings.h 2020-07-10 11:50:10.000000000 +0000
+@@ -31,6 +31,7 @@
+
+ public:
+ enum { RespAllow = 0, RespNotImpl = 1, RespIgnore = 2 }; // как номер индекса в combobox
++ enum { LogNever = 0, LogIfReplace = 1, LogAlways = 2 };
+
+ AccountSettings();
+ AccountSettings(const QString &);
+@@ -41,13 +42,17 @@
+ QString toString();
+ //--
+ QString account_id;
++ bool enable_contacts;
++ bool enable_conferences;
+ int response_mode;
+ bool lock_time_requ;
++ int show_requ_mode;
+ QString os_name;
+- QString os_version;
+ QString client_name;
+ QString client_version;
+ QString caps_node;
++ QString caps_version;
++ int log_mode;
+
+ private:
+ //--
+diff -Nru a/plugins/generic/clientswitcherplugin/changelog.txt b/plugins/generic/clientswitcherplugin/changelog.txt
+--- a/plugins/generic/clientswitcherplugin/changelog.txt 2025-08-26 14:50:07.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/changelog.txt 2020-07-10 11:50:10.000000000 +0000
+@@ -1,11 +1,3 @@
+-2024-03-19
+-v0.2
+- + added initial Qt6 support
+-
+-2020-07-22
+-v0.1
+- * The plugin was rewritten to reuse caps generation of iris
+-
+ 2013-09-02
+ v0.0.18 - taurus
+ * Использовать слово Groupchat вместо Conference
+diff -Nru a/plugins/generic/clientswitcherplugin/clientswitcherplugin.cpp b/plugins/generic/clientswitcherplugin/clientswitcherplugin.cpp
+--- a/plugins/generic/clientswitcherplugin/clientswitcherplugin.cpp 2025-08-26 14:50:07.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/clientswitcherplugin.cpp 2020-07-10 11:50:10.000000000 +0000
+@@ -22,21 +22,27 @@
+ *
+ */
+
+-// #include <QtCore>
+-// #include <QDomDocument>
++//#include <QtCore>
++//#include <QDomDocument>
+ #include <QMetaObject>
+
+ #include "clientswitcherplugin.h"
++#include "viewer.h"
+
+ #define constPluginShortName "clientswitcher"
+ #define constPluginName "Client Switcher Plugin"
+ #define constForAllAcc "for_all_acc"
+ #define constAccSettingList "accsettlist"
++#define constShowLogHeight "showlogheight"
++#define constShowLogWidth "showlogwidth"
++#define constLastLogItem "lastlogview"
++#define constPopupDuration "popupduration"
+
+ ClientSwitcherPlugin::ClientSwitcherPlugin() :
+- psiOptions(nullptr), psiInfo(nullptr), psiAccount(nullptr), psiAccountCtl(nullptr), enabled(false),
+- for_all_acc(false), def_os_name(""), def_client_name(""), def_client_version(""), def_caps_node(""),
+- def_caps_version("")
++ sender_(nullptr), psiOptions(nullptr), psiPopup(nullptr), psiInfo(nullptr), psiAccount(nullptr),
++ psiAccountCtl(nullptr), psiContactInfo(nullptr), psiIcon(nullptr), enabled(false), for_all_acc(false),
++ def_os_name(""), def_client_name(""), def_client_version(""), def_caps_node(""), def_caps_version(""),
++ heightLogsView(500), widthLogsView(600), lastLogItem(""), popupId(0)
+
+ {
+ settingsList.clear();
+@@ -56,86 +62,73 @@
+ if (!psiOptions)
+ return false;
+ enabled = true;
+-
+- os_presets = { { "Windows", "95" },
+- { "Windows", "98" },
+- { "Windows", "ME" },
+- { "Windows", "2000" },
+- { "Windows", "XP" },
+- { "Windows", "2003" },
+- { "Windows", "2008" },
+- { "Windows", "Vista" },
+- { "Windows", "7" },
+- { "Windows", "10" },
+- { "Arch" },
+- { "Debian GNU/Linux", "6.0.1 (squeeze)" },
+- { "Ubuntu", "10.04.2 LTS" },
+- { "RFRemix", "14.1 (Laughlin)" },
+- { "openSUSE", "11.4" },
+- { "Gentoo Base System", "2.0.3" },
+- { "Mac OS X" },
+- { "Mac OS X", "10.6" },
+- { "Android OS", "2.3.6 (build XXLB1)" },
+- { "Plan9" },
+- { "Solaris" },
+- { "FreeBSD" },
+- { "NetBSD" },
+- { "OpenBSD" },
+- { "Nokia5130c-2", "07.91" },
+- { "SonyEricssonW580i", "R8BE001" } };
+-
++ os_presets.clear();
++ os_presets << OsStruct("Windows 98") << OsStruct("Windows ME") << OsStruct("Windows 2000");
++ os_presets << OsStruct("Windows XP") << OsStruct("Windows Server 2003") << OsStruct("Windows Server 2008");
++ os_presets << OsStruct("Windows Vista") << OsStruct("Windows 7, 32-bit") << OsStruct("Windows 7, 64-bit");
++ os_presets << OsStruct("Arch Linux") << OsStruct("Debian GNU/Linux 6.0.1 (squeeze)");
++ os_presets << OsStruct("Ubuntu 10.04.2 LTS") << OsStruct("RFRemix release 14.1 (Laughlin)");
++ os_presets << OsStruct("openSUSE 11.4") << OsStruct("Gentoo Base System release 2.0.3");
++ os_presets << OsStruct("Mac OS X") << OsStruct("Mac OS X 10.6");
++ os_presets << OsStruct("Android OS 2.3.6 (build XXLB1)");
++ os_presets << OsStruct("Plan9") << OsStruct("Solaris");
++ os_presets << OsStruct("FreeBSD") << OsStruct("NetBSD") << OsStruct("OpenBSD");
++ os_presets << OsStruct("Nokia5130c-2/07.91") << OsStruct("SonyEricssonW580i/R8BE001");
+ client_presets.clear();
+- client_presets << ClientStruct("Bombus", "0.7.1429M-Zlib", "http://bombus-im.org/java");
+- client_presets << ClientStruct("Gajim", "0.12.5", "https://gajim.org");
+- client_presets << ClientStruct("Mcabber", "0.9.10", "http://mcabber.com/caps");
+- client_presets << ClientStruct("Miranda", "0.9.16.0", "http://miranda-im.org/caps");
+- client_presets << ClientStruct("Pidgin", "2.8.0", "http://pidgin.im");
+- client_presets << ClientStruct("Psi", "0.14", "https://psi-im.org/caps");
+- client_presets << ClientStruct("QIP Infium", "9034", "http://qip.ru/caps");
+- client_presets << ClientStruct("qutIM", "0.2", "http://qutim.org");
+- client_presets << ClientStruct("Swift", "1.0", "http://swift.im");
+- client_presets << ClientStruct("Talisman", "0.1.1.15", "http://jabrvista.net.ru");
+- client_presets << ClientStruct("Tkabber", "0.11.1", "http://tkabber.jabber.ru");
+- client_presets << ClientStruct("Talkonaut", "1.0.0.84", "http://www.google.com/xmpp/client/caps");
+- client_presets << ClientStruct(QString::fromUtf8("Я.Онлайн"), "3.2.0.8873", "yandex-webchat");
++ client_presets << ClientStruct("Bombus", "0.7.1429M-Zlib", "http://bombus-im.org/java", "0.7.1429M-Zlib");
++ client_presets << ClientStruct("Gajim", "0.12.5", "https://gajim.org", "0.12.5");
++ client_presets << ClientStruct("Mcabber", "0.9.10", "http://mcabber.com/caps", "0.9.10");
++ client_presets << ClientStruct("Miranda", "0.9.16.0", "http://miranda-im.org/caps", "0.9.16.0");
++ client_presets << ClientStruct("Pidgin", "2.8.0", "http://pidgin.im", "2.8.0");
++ client_presets << ClientStruct("Psi", "0.14", "https://psi-im.org/caps", "0.14");
++ client_presets << ClientStruct("QIP Infium", "9034", "http://qip.ru/caps", "9034");
++ client_presets << ClientStruct("qutIM", "0.2", "http://qutim.org", "0.2");
++ client_presets << ClientStruct("Swift", "1.0", "http://swift.im", "1.0");
++ client_presets << ClientStruct("Talisman", "0.1.1.15", "http://jabrvista.net.ru", "0.1.1.15");
++ client_presets << ClientStruct("Tkabber", "0.11.1", "http://tkabber.jabber.ru", "0.11.1");
++ client_presets << ClientStruct("Talkonaut", "1.0.0.84", "http://www.google.com/xmpp/client/caps", "1.0.0.84");
++ client_presets << ClientStruct(QString::fromUtf8("Я.Онлайн"), "3.2.0.8873", "yandex-webchat", "3.2.0.8873");
+ // Флаг "для всех аккаунтов"
+ for_all_acc = psiOptions->getPluginOption(constForAllAcc, QVariant(false)).toBool();
+ // Получаем настройки аккаунтов
+ QStringList sett_list = psiOptions->getPluginOption(constAccSettingList, QVariant()).toStringList();
++ // Get and register popup option
++ int popup_duration = psiOptions->getPluginOption(constPopupDuration, QVariant(5000)).toInt() / 1000;
++ popupId = psiPopup->registerOption(constPluginName, popup_duration,
++ QLatin1String("plugins.options.clientswitcher.") + constPopupDuration);
+ // Формируем структуры
+ int cnt = sett_list.size();
+ for (int i = 0; i < cnt; i++) {
+- AccountSettings *as = new AccountSettings(sett_list.at(i));
+- if (as) {
+- if (as->isValid())
+- settingsList.push_back(as);
++ AccountSettings *ac = new AccountSettings(sett_list.at(i));
++ if (ac) {
++ if (ac->isValid())
++ settingsList.push_back(ac);
+ else
+- delete as;
++ delete ac;
+ }
+ }
++ // Настройки для просмотра логов
++ logsDir = psiInfo->appCurrentProfileDir(ApplicationInfoAccessingHost::DataLocation) + "/logs/clientswitcher";
++ QDir dir(logsDir);
++ if (!dir.exists(logsDir))
++ dir.mkpath(logsDir);
++ logsDir.append("/");
++ heightLogsView = psiOptions->getPluginOption(constShowLogHeight, QVariant(heightLogsView)).toInt();
++ widthLogsView = psiOptions->getPluginOption(constShowLogWidth, QVariant(widthLogsView)).toInt();
++ lastLogItem = psiOptions->getPluginOption(constLastLogItem, QVariant(widthLogsView)).toString();
+
+ return true;
+ }
+
+ bool ClientSwitcherPlugin::disable()
+ {
+- if (!enabled)
+- return true;
+-
+ while (settingsList.size() != 0) {
+ AccountSettings *as = settingsList.takeLast();
+ if (as)
+ delete as;
+ }
+-
+- for (int i = 0;; i++) {
+- QString id = psiAccount->getId(i);
+- if (id == "-1")
+- break;
+- psiAccountCtl->setClientVersionInfo(i, {});
+- }
+-
+ enabled = false;
++ psiPopup->unregisterOption(constPluginName);
+ return true;
+ }
+
+@@ -151,7 +144,7 @@
+ ui_options.cb_ospreset->addItem("user defined", "user");
+ int cnt = os_presets.size();
+ for (int i = 0; i < cnt; i++) {
+- ui_options.cb_ospreset->addItem(QString("%1 %2").arg(os_presets.at(i).name, os_presets.at(i).version));
++ ui_options.cb_ospreset->addItem(os_presets.at(i).name);
+ }
+ // Заполняем виджет пресетов клиента
+ ui_options.cb_clientpreset->addItem("default", "default");
+@@ -160,13 +153,20 @@
+ for (int i = 0; i < cnt; i++) {
+ ui_options.cb_clientpreset->addItem(client_presets.at(i).name);
+ }
+-
++ // Элементы для просмотра логов
++ QDir dir(logsDir);
++ int pos = -1;
++ for (const QString &file : dir.entryList(QDir::Files)) {
++ ui_options.cb_logslist->addItem(file);
++ ++pos;
++ if (file == lastLogItem)
++ ui_options.cb_logslist->setCurrentIndex(pos);
++ }
++ if (pos < 0)
++ ui_options.bt_viewlog->setEnabled(false);
+ //--
+-#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0)
+ connect(ui_options.cb_allaccounts, &QCheckBox::stateChanged, this, &ClientSwitcherPlugin::enableAccountsList);
+-#else
+- connect(ui_options.cb_allaccounts, &QCheckBox::checkStateChanged, this, &ClientSwitcherPlugin::enableAccountsList);
+-#endif
++ connect(ui_options.bt_viewlog, &QPushButton::released, this, &ClientSwitcherPlugin::viewFromOpt);
+ // TODO: update after stopping support of Ubuntu Xenial:
+ connect(ui_options.cb_accounts, SIGNAL(currentIndexChanged(int)), this, SLOT(restoreOptionsAcc(int)));
+ connect(ui_options.cmb_lockrequ, SIGNAL(currentIndexChanged(int)), this, SLOT(enableMainParams(int)));
+@@ -179,11 +179,13 @@
+
+ void ClientSwitcherPlugin::applyOptions()
+ {
++ bool caps_updated = false;
+ // Аккаунт
+ bool for_all_acc_old = for_all_acc;
+ for_all_acc = ui_options.cb_allaccounts->isChecked();
+- bool caps_updated = (for_all_acc != for_all_acc_old);
+-
++ if (for_all_acc != for_all_acc_old) {
++ caps_updated = true;
++ }
+ int acc_index = ui_options.cb_accounts->currentIndex();
+ if (acc_index == -1 && !for_all_acc)
+ return;
+@@ -196,39 +198,53 @@
+ as->account_id = acc_id;
+ settingsList.push_back(as);
+ }
+-
++ // Подмена/блокировка для контактов
++ bool tmp_flag = ui_options.cb_contactsenable->isChecked();
++ if (as->enable_contacts != tmp_flag) {
++ as->enable_contacts = tmp_flag;
++ caps_updated = true;
++ }
++ // Подмена/блокировка для конференций
++ tmp_flag = ui_options.cb_conferencesenable->isChecked();
++ if (as->enable_conferences != tmp_flag) {
++ as->enable_conferences = tmp_flag;
++ caps_updated = true;
++ }
+ // Блокировка запроса версии
+ int respMode = ui_options.cmb_lockrequ->currentIndex();
+ if (as->response_mode != respMode) {
+- caps_updated = (as->response_mode == AccountSettings::RespAllow || respMode == AccountSettings::RespAllow);
++ if (as->response_mode == AccountSettings::RespAllow || respMode == AccountSettings::RespAllow)
++ caps_updated = true;
+ as->response_mode = respMode;
+ }
+ // Блокировка запроса времени
+- bool tmp_flag = ui_options.cb_locktimerequ->isChecked();
++ tmp_flag = ui_options.cb_locktimerequ->isChecked();
+ if (as->lock_time_requ != tmp_flag) {
+ as->lock_time_requ = tmp_flag;
+ caps_updated = true;
+ }
++ // Уведомления при запросах версии
++ as->show_requ_mode = ui_options.cmb_showrequ->currentIndex();
++ // Ведение лога
++ as->log_mode = ui_options.cmb_savetolog->currentIndex();
+ // Наименование ОС
+ if (ui_options.cb_ospreset->currentIndex() == 0) {
+- caps_updated = !(as->os_name.isEmpty() && as->os_version.isEmpty());
+- as->os_name = QString();
+- as->os_version = QString();
++ as->os_name = "";
+ } else {
+- auto name = ui_options.le_osname->text().trimmed();
+- auto version = ui_options.le_osversion->text().trimmed();
+- caps_updated = !(name == as->os_name && version == as->os_version);
+- as->os_name = name;
+- as->os_version = version;
++ as->os_name = ui_options.le_osname->text().trimmed();
+ }
+ // Клиент
+ if (ui_options.cb_clientpreset->currentIndex() == 0) {
+- as->client_name = QString();
+- as->client_version = QString();
++ as->client_name = "";
++ as->client_version = "";
+ if (!as->caps_node.isEmpty()) {
+- as->caps_node = QString();
++ as->caps_node = "";
+ caps_updated = true;
+ }
++ if (!as->caps_version.isEmpty()) {
++ as->caps_version = "";
++ caps_updated = true;
++ }
+ } else {
+ // Название клиента
+ as->client_name = ui_options.le_clientname->text().trimmed();
+@@ -240,6 +256,12 @@
+ as->caps_node = str1;
+ caps_updated = true;
+ }
++ // Caps version клиента
++ str1 = ui_options.le_capsversion->text().trimmed();
++ if (as->caps_version != str1) {
++ as->caps_version = str1;
++ caps_updated = true;
++ }
+ }
+ // Сохраняем опции
+ psiOptions->setPluginOption(constForAllAcc, QVariant(for_all_acc));
+@@ -284,7 +306,7 @@
+ QString name = psiAccount->getName(i);
+ if (name.isEmpty())
+ name = "?";
+- ui_options.cb_accounts->addItem(QString("%1 (%2)").arg(name, psiAccount->getJid(i)), QVariant(id));
++ ui_options.cb_accounts->addItem(QString("%1 (%2)").arg(name).arg(psiAccount->getJid(i)), QVariant(id));
+ cnt++;
+ }
+ }
+@@ -304,8 +326,66 @@
+
+ void ClientSwitcherPlugin::optionChanged(const QString & /*option*/) { }
+
++//-- StanzaSender ---------------------------------------------------
++
++void ClientSwitcherPlugin::setStanzaSendingHost(StanzaSendingHost *host) { sender_ = host; }
++
+ // ----------------------- StanzaFilter ------------------------------
+-bool ClientSwitcherPlugin::incomingStanza(int, const QDomElement &) { return false; }
++bool ClientSwitcherPlugin::incomingStanza(int account, const QDomElement &stanza)
++{
++ if (!enabled)
++ return false;
++ QString acc_id = (for_all_acc) ? "all" : psiAccount->getId(account);
++ AccountSettings *as = getAccountSetting(acc_id);
++ if (!as)
++ return false;
++ if (!as->enable_contacts && !as->enable_conferences)
++ return false;
++ int respMode = as->response_mode;
++ if (respMode != AccountSettings::RespAllow || as->lock_time_requ || !as->caps_node.isEmpty()
++ || !as->caps_version.isEmpty()) {
++ if (stanza.tagName() == "iq" && stanza.attribute("type") == "get") {
++ const QString s_to = stanza.attribute("from");
++ if (isSkipStanza(as, account, s_to))
++ return false;
++ QDomNode s_child = stanza.firstChild();
++ while (!s_child.isNull()) {
++ const QString xmlns = s_child.toElement().namespaceURI();
++ if (s_child.toElement().tagName() == "query") {
++ if (xmlns == "http://jabber.org/protocol/disco#info") {
++ QString node = s_child.toElement().attribute("node");
++ if (!node.isEmpty()) {
++ QString new_node = def_caps_node;
++ QStringList split_node = node.split("#");
++ if (split_node.size() > 1) {
++ split_node.removeFirst();
++ QString node_ver = split_node.join("#");
++ if (node_ver == ((respMode == AccountSettings::RespAllow) ? as->caps_version : "n/a")) {
++ node_ver = def_caps_version;
++ }
++ new_node.append("#" + node_ver);
++ }
++ s_child.toElement().setAttribute("node", new_node);
++ }
++ } else if (xmlns == "jabber:iq:version") {
++ if (respMode == AccountSettings::RespIgnore) {
++ // Showing popup if it is necessary
++ if (as->show_requ_mode == AccountSettings::LogAlways)
++ showPopup(jidToNick(account, s_to));
++ // Write log if it is necessary
++ if (as->log_mode == AccountSettings::LogAlways)
++ saveToLog(account, s_to, "ignored");
++ return true;
++ }
++ }
++ }
++ s_child = s_child.nextSibling();
++ }
++ }
++ }
++
++ return false;
++}
+
+ bool ClientSwitcherPlugin::outgoingStanza(int account, QDomElement &stanza)
+ {
+@@ -315,15 +395,216 @@
+ AccountSettings *as = getAccountSetting(acc_id);
+ if (!as)
+ return false;
+-
++ if (as->response_mode != AccountSettings::RespAllow || !as->caps_node.isEmpty() || !as->caps_version.isEmpty()) {
++ // --------------- presence ---------------
++ if (stanza.tagName() == "presence") {
++ if (!as->enable_contacts && !as->enable_conferences)
++ return false;
++ // Анализ сообщения о присутствии
++ bool skipFlag = isSkipStanza(as, account, stanza.attribute("to"));
++ if (skipFlag && !as->enable_conferences) // Конференция не определяется при входе в нее, проверим позже
++ return false;
++ short unsigned int found_flag = 0; // Чтобы исключить перебор лишних элементов
++ QDomNode s_child = stanza.firstChild();
++ QDomNode caps_node;
++ while (!s_child.isNull()) {
++ if (((found_flag & 1) == 0) && s_child.toElement().tagName() == "c") {
++ // Подмена будет отложенной
++ caps_node = s_child;
++ found_flag |= 1;
++ if (found_flag == 3)
++ break;
++ s_child = s_child.nextSibling();
++ continue;
++ }
++ if (((found_flag & 2) == 0) && s_child.toElement().tagName() == "x") {
++ if (s_child.namespaceURI() == "http://jabber.org/protocol/muc") {
++ found_flag |= 2;
++ if (found_flag == 3)
++ break;
++ }
++ }
++ s_child = s_child.nextSibling();
++ }
++ if ((found_flag == 1 && as->enable_contacts) || (found_flag == 3 && as->enable_conferences)) {
++ // Подменяем капс
++ if (as->response_mode != AccountSettings::RespNotImpl) {
++ caps_node.toElement().setAttribute("node", as->caps_node);
++ caps_node.toElement().setAttribute("ver", as->caps_version);
++ } else {
++ caps_node.toElement().setAttribute("node", "unknown");
++ caps_node.toElement().setAttribute("ver", "n/a");
++ }
++ }
++ return false;
++ }
++ }
+ if (stanza.tagName() == "iq" && stanza.attribute("type") == "result") {
+- // QString s_to = stanza.attribute("to");
++ bool is_version_query = false;
++ bool is_version_replaced = false;
++ QString s_to = stanza.attribute("to");
+ QStringList send_ver_list;
+- auto s_child = stanza.firstChildElement();
++ QDomNode s_child = stanza.firstChild();
++ int respMode = as->response_mode;
+ while (!s_child.isNull()) {
+- if (s_child.tagName() == "time") {
++ if (s_child.toElement().tagName() == "query") {
++ QString xmlns_str = s_child.namespaceURI();
++ if (xmlns_str == "http://jabber.org/protocol/disco#info") {
++ // --- Ответ disco
++ if (isSkipStanza(as, account, s_to))
++ return false;
++ if (respMode == AccountSettings::RespAllow && !as->lock_time_requ && as->caps_node.isEmpty()
++ && as->caps_version.isEmpty())
++ return false;
++ // Подменяем ноду, если она есть
++ QString node = s_child.toElement().attribute("node");
++ if (!node.isEmpty()) {
++ QString new_node = (respMode == AccountSettings::RespAllow) ? as->caps_node : "unknown";
++ QStringList split_node = node.split("#");
++ if (split_node.size() > 1) {
++ split_node.removeFirst();
++ QString new_ver = split_node.join("#");
++ if (new_ver == def_caps_version)
++ new_ver = (respMode == AccountSettings::RespAllow) ? as->caps_version : "n/a";
++ new_node.append("#" + new_ver);
++ }
++ s_child.toElement().setAttribute("node", new_node);
++ }
++ // Подменяем identity и удаляем feature для версии, если есть блокировка
++ int update = 0;
++ if (respMode == AccountSettings::RespAllow)
++ ++update;
++ if (!as->lock_time_requ)
++ ++update;
++ QDomNode ver_domnode;
++ QDomNode time_domnode;
++ QDomNode q_child = s_child.firstChild();
++ while (!q_child.isNull()) {
++ QString tag_name = q_child.toElement().tagName();
++ if (tag_name == "feature") {
++ if (respMode != AccountSettings::RespAllow
++ && q_child.toElement().attribute("var") == "jabber:iq:version") {
++ ver_domnode = q_child;
++ if (++update >= 3)
++ break;
++ } else if (as->lock_time_requ && q_child.toElement().attribute("var") == "urn:xmpp:time") {
++ time_domnode = q_child;
++ if (++update >= 3)
++ break;
++ }
++ } else if (tag_name == "identity") {
++ if (!q_child.toElement().attribute("name").isEmpty())
++ q_child.toElement().setAttribute(
++ "name", (respMode == AccountSettings::RespAllow) ? as->client_name : "unknown");
++ if (++update >= 3)
++ break;
++ }
++ q_child = q_child.nextSibling();
++ }
++ if (!s_child.isNull()) {
++ if (!ver_domnode.isNull())
++ s_child.removeChild(ver_domnode);
++ if (!time_domnode.isNull())
++ s_child.removeChild(time_domnode);
++ }
++ } else if (xmlns_str == "jabber:iq:version") {
++ // Ответ version
++ is_version_query = true;
++ bool skip_stanza = isSkipStanza(as, account, s_to);
++ if (skip_stanza && as->log_mode != AccountSettings::LogAlways)
++ break;
++ QDomDocument xmldoc = stanza.ownerDocument();
++ if (respMode == AccountSettings::RespAllow) {
++ // Подменяем ответ
++ bool f_os_name = false;
++ bool f_client_name = false;
++ bool f_client_ver = false;
++ QDomNode q_child = s_child.firstChild();
++ while (!q_child.isNull()) {
++ QString tag_name = q_child.toElement().tagName().toLower();
++ if (tag_name == "os") {
++ QString str1;
++ if (!skip_stanza && !as->os_name.isEmpty()) {
++ str1 = as->os_name;
++ q_child.toElement().replaceChild(xmldoc.createTextNode(str1), q_child.firstChild());
++ is_version_replaced = true;
++ } else {
++ str1 = q_child.toElement().text();
++ }
++ send_ver_list.push_back("OS: " + str1);
++ f_os_name = true;
++ } else if (tag_name == "name") {
++ QString str1;
++ if (!skip_stanza && !as->client_name.isEmpty()) {
++ str1 = as->client_name;
++ q_child.toElement().replaceChild(xmldoc.createTextNode(str1), q_child.firstChild());
++ is_version_replaced = true;
++ } else {
++ str1 = q_child.toElement().text();
++ }
++ send_ver_list.push_back("Client name: " + str1);
++ f_client_name = true;
++ } else if (tag_name == "version") {
++ QString str1;
++ if (!skip_stanza && !as->caps_version.isEmpty()) {
++ str1 = as->client_version;
++ q_child.toElement().replaceChild(xmldoc.createTextNode(str1), q_child.firstChild());
++ is_version_replaced = true;
++ } else {
++ str1 = q_child.toElement().text();
++ }
++ send_ver_list.push_back("Client version: " + str1);
++ f_client_ver = true;
++ }
++ q_child = q_child.nextSibling();
++ }
++ // Создаем теги, если их нет в ответе
++ QDomDocument doc;
++ if (!f_client_name && !as->client_name.isEmpty()) {
++ doc = s_child.ownerDocument();
++ QDomElement cl_name = doc.createElement("name");
++ cl_name.appendChild(doc.createTextNode(as->client_name));
++ s_child.appendChild(cl_name);
++ }
++ if (!f_client_ver && !as->client_version.isEmpty()) {
++ if (doc.isNull())
++ doc = s_child.ownerDocument();
++ QDomElement cl_ver = doc.createElement("version");
++ cl_ver.appendChild(doc.createTextNode(as->client_version));
++ s_child.appendChild(cl_ver);
++ }
++ if (!f_os_name && !as->os_name.isEmpty()) {
++ if (doc.isNull())
++ doc = s_child.ownerDocument();
++ QDomElement os_name = doc.createElement("os");
++ os_name.appendChild(doc.createTextNode(as->os_name));
++ s_child.appendChild(os_name);
++ }
++ } else if (respMode == AccountSettings::RespNotImpl) {
++ // Отклонение запроса, как будто не реализовано
++ stanza.setAttribute("type", "error");
++ QDomNode q_child = s_child.firstChild();
++ while (!q_child.isNull()) {
++ s_child.removeChild(q_child);
++ q_child = s_child.firstChild();
++ }
++ QDomElement err = xmldoc.createElement("error");
++ err.setAttribute("type", "cancel");
++ err.setAttribute("code", "501");
++ stanza.appendChild(err);
++ QDomElement not_imp
++ = xmldoc.createElementNS("urn:ietf:params:xml:ns:xmpp-stanzas", "feature-not-implemented");
++ err.appendChild(not_imp);
++ send_ver_list.push_back("feature-not-implemented");
++ }
++ }
++ break;
++ } else if (s_child.toElement().tagName() == "time") {
+ QString xmlns_str = s_child.namespaceURI();
+ if (xmlns_str == "urn:xmpp:time") {
++ // Ответ time
++ if (isSkipStanza(as, account, s_to))
++ break;
+ if (as->lock_time_requ) {
+ QDomDocument xmldoc = stanza.ownerDocument();
+ // Отклонение запроса, как будто не реализовано
+@@ -343,8 +624,18 @@
+ }
+ }
+ }
+- s_child = s_child.nextSiblingElement();
++ s_child = s_child.nextSibling();
+ }
++
++ // Showing popup if it is necessary
++ if (is_version_query && as->show_requ_mode != AccountSettings::LogNever
++ && (as->show_requ_mode == AccountSettings::LogAlways || is_version_replaced))
++ showPopup(jidToNick(account, s_to));
++
++ // Write log if it is necessary
++ if (is_version_query && as->log_mode != AccountSettings::LogNever
++ && (as->log_mode == AccountSettings::LogAlways || is_version_replaced))
++ saveToLog(account, s_to, send_ver_list.join(", "));
+ }
+ return false;
+ }
+@@ -355,6 +646,8 @@
+ "You can specify the version of the client and OS or to select them from the preset list.\n");
+ }
+
++void ClientSwitcherPlugin::setPopupAccessingHost(PopupAccessingHost *host) { psiPopup = host; }
++
+ // ----------------------- ApplicationInfoAccessor ------------------------------
+ void ClientSwitcherPlugin::setApplicationInfoAccessingHost(ApplicationInfoAccessingHost *host)
+ {
+@@ -366,7 +659,6 @@
+ def_caps_node = psiInfo->appCapsNode();
+ def_caps_version = psiInfo->appCapsVersion();
+ def_os_name = psiInfo->appOsName();
+- def_os_version = psiInfo->appOsVersion();
+ }
+ }
+
+@@ -374,22 +666,23 @@
+ void ClientSwitcherPlugin::setAccountInfoAccessingHost(AccountInfoAccessingHost *host) { psiAccount = host; }
+
+ // ----------------------- PsiAccountController ----------------------------------
+-void ClientSwitcherPlugin::setPsiAccountControllingHost(PsiAccountControllingHost *host)
+-{
+- psiAccountCtl = host;
++void ClientSwitcherPlugin::setPsiAccountControllingHost(PsiAccountControllingHost *host) { psiAccountCtl = host; }
+
+- psiAccountCtl->subscribeBeforeLogin(this, [this](int accountId) { updateInfo(accountId); });
+-}
++// ----------------------- ContactInfoAccessor -----------------------
++void ClientSwitcherPlugin::setContactInfoAccessingHost(ContactInfoAccessingHost *host) { psiContactInfo = host; }
++
++// ----------------------- IconFactoryAccessor -----------------------
++void ClientSwitcherPlugin::setIconFactoryAccessingHost(IconFactoryAccessingHost *host) { psiIcon = host; }
+
+ // ----------------------- Private ------------------------------
+
+-int ClientSwitcherPlugin::getOsTemplateIndex(const QString &os_name, const QString &os_version)
++int ClientSwitcherPlugin::getOsTemplateIndex(const QString &os_name)
+ {
+ if (os_name.isEmpty())
+ return 0; // default
+ int cnt = os_presets.size();
+ for (int i = 0; i < cnt; i++) {
+- if (os_name == os_presets.at(i).name && os_version == os_presets.at(i).version) {
++ if (os_name == os_presets.at(i).name) {
+ i += 2; // Т.к. впереди default и user defined
+ return i;
+ }
+@@ -397,14 +690,15 @@
+ return 1; // user defined
+ }
+
+-int ClientSwitcherPlugin::getClientTemplateIndex(const QString &cl_name, const QString &cl_ver, const QString &cp_node)
++int ClientSwitcherPlugin::getClientTemplateIndex(const QString &cl_name, const QString &cl_ver, const QString &cp_node,
++ const QString &cp_ver)
+ {
+- if (cl_name.isEmpty() && cl_ver.isEmpty() && cp_node.isEmpty())
++ if (cl_name.isEmpty() && cl_ver.isEmpty() && cp_node.isEmpty() && cp_ver.isEmpty())
+ return 0; // default
+ int cnt = client_presets.size();
+ for (int i = 0; i < cnt; i++) {
+ if (cl_name == client_presets.at(i).name && cl_ver == client_presets.at(i).version) {
+- if (cp_node == client_presets.at(i).caps_node) {
++ if (cp_node == client_presets.at(i).caps_node && cp_ver == client_presets.at(i).caps_version) {
+ i += 2; // есть еще default и user defined
+ return i;
+ }
+@@ -438,28 +732,6 @@
+ return nullptr;
+ }
+
+-ClientSwitcherPlugin::UpdateStatus ClientSwitcherPlugin::updateInfo(int account)
+-{
+- if (!enabled || !psiAccount || !psiAccountCtl)
+- return UpdateStatus::Disabled;
+-
+- QString acc_id = psiAccount->getId(account);
+- if (acc_id == "-1" || acc_id.isEmpty())
+- return UpdateStatus::NoAccount;
+-
+- AccountSettings *as = getAccountSetting(acc_id);
+- if (!as || !as->isValid())
+- return UpdateStatus::NoSettings;
+-
+- QVariantMap info = { { "os-name", as->os_name },
+- { "os-version", as->os_version },
+- { "client-name", as->client_name },
+- { "client-version", as->client_version },
+- { "caps-node", as->caps_node } };
+- psiAccountCtl->setClientVersionInfo(account, info);
+- return UpdateStatus::Ok;
+-}
+-
+ /**
+ * Отсылка пресентса с новыми капсами от имени указанного аккаунта
+ * Если account == -1, то капсы посылаются всем активным аккаунтам
+@@ -472,10 +744,10 @@
+ if (acc == -1)
+ acc = 0;
+ for (;; acc++) {
+- auto ret = updateInfo(acc);
+- if (ret == UpdateStatus::NoAccount)
++ QString acc_id = psiAccount->getId(acc);
++ if (acc_id == "-1")
+ break;
+- if (ret == UpdateStatus::Ok) {
++ if (!acc_id.isEmpty()) {
+ QString acc_status = psiAccount->getStatus(acc);
+ if (!acc_status.isEmpty() && acc_status != "offline" && acc_status != "invisible") {
+ // Отсылаем тот же статус, а капсы заменим в outgoingStanza()
+@@ -487,6 +759,32 @@
+ }
+ }
+
++bool ClientSwitcherPlugin::isSkipStanza(AccountSettings *as, const int account, const QString &to)
++{
++ if (to.isEmpty()) { // Широковещательный
++ if (!as->enable_contacts)
++ return true;
++ } else { // Адресный
++ QString to_jid = to.split("/").takeFirst();
++ if (!to_jid.contains("@")) {
++ if (as->enable_contacts) {
++ if (!to.contains("/"))
++ return false; // Предполагаем что это сервер
++ return true; // Ошибочный запрос
++ }
++ }
++ if (psiContactInfo->isConference(account, to_jid) || psiContactInfo->isPrivate(account, to)) {
++ // if (to.contains("conference.")) {
++ if (!as->enable_conferences)
++ return true;
++ } else {
++ if (!as->enable_contacts)
++ return true;
++ }
++ }
++ return false;
++}
++
+ // ----------------------- Slots ------------------------------
+ void ClientSwitcherPlugin::enableAccountsList(int all_acc_mode)
+ {
+@@ -516,23 +814,30 @@
+ as->account_id = acc_id;
+ settingsList.push_back(as);
+ }
++ // Подмена/блокировка для контактов
++ ui_options.cb_contactsenable->setChecked(as->enable_contacts);
++ // Подмена/блокировка для конференций
++ ui_options.cb_conferencesenable->setChecked(as->enable_conferences);
+ // Блокировка запроса версии
+ ui_options.cmb_lockrequ->setCurrentIndex(as->response_mode);
+ // Блокировка запроса времени
+ ui_options.cb_locktimerequ->setChecked(as->lock_time_requ);
++ // Уведомления при запросах версии
++ ui_options.cmb_showrequ->setCurrentIndex(as->show_requ_mode);
++ // Ведение лога
++ ui_options.cmb_savetolog->setCurrentIndex(as->log_mode);
+ // Виджет шаблона ОС
+- QString os_name = as->os_name;
+- QString os_version = as->os_version;
+- int os_templ = getOsTemplateIndex(os_name, os_version);
++ QString os_name = as->os_name;
++ int os_templ = getOsTemplateIndex(os_name);
+ ui_options.cb_ospreset->setCurrentIndex(os_templ);
+ // Название ОС
+ ui_options.le_osname->setText(os_name);
+- ui_options.le_osversion->setText(os_version);
+ // Виджет шаблона клиента
+ QString cl_name = as->client_name;
+ QString cl_ver = as->client_version;
+ QString cp_node = as->caps_node;
+- int cl_templ = getClientTemplateIndex(cl_name, cl_ver, cp_node);
++ QString cp_ver = as->caps_version;
++ int cl_templ = getClientTemplateIndex(cl_name, cl_ver, cp_node, cp_ver);
+ ui_options.cb_clientpreset->setCurrentIndex(cl_templ);
+ // Название клиента
+ ui_options.le_clientname->setText(cl_name);
+@@ -540,7 +845,10 @@
+ ui_options.le_clientversion->setText(cl_ver);
+ // Caps node клиента
+ ui_options.le_capsnode->setText(cp_node);
++ // Caps version клиента
++ ui_options.le_capsversion->setText(cp_ver);
+ // Блокировка/снятие блокировки виджетов
++ ui_options.gb_enablefor->setEnabled(true);
+ ui_options.cmb_lockrequ->setEnabled(true);
+ enableMainParams(as->response_mode);
+ enableOsParams(os_templ);
+@@ -549,6 +857,9 @@
+ }
+ }
+ // Блокировка виджетов при отсутствии валидного акка
++ ui_options.cb_contactsenable->setChecked(false);
++ ui_options.cb_conferencesenable->setChecked(false);
++ ui_options.gb_enablefor->setEnabled(false);
+ ui_options.cmb_lockrequ->setCurrentIndex(AccountSettings::RespAllow);
+ ui_options.cmb_lockrequ->setEnabled(false);
+ ui_options.cb_ospreset->setCurrentIndex(0);
+@@ -570,20 +881,16 @@
+ {
+ if (mode == 1) { // user defined
+ ui_options.le_osname->setEnabled(true);
+- ui_options.le_osversion->setEnabled(true);
+ } else {
+ if (mode == 0) { // default os
+ ui_options.le_osname->setText(def_os_name);
+- ui_options.le_osversion->setText(def_os_version);
+ } else {
+ int pres_index = mode - 2;
+ if (pres_index >= 0 && pres_index < os_presets.size()) {
+ ui_options.le_osname->setText(os_presets.at(pres_index).name);
+- ui_options.le_osversion->setText(os_presets.at(pres_index).version);
+ }
+ }
+ ui_options.le_osname->setEnabled(false);
+- ui_options.le_osversion->setEnabled(false);
+ }
+ }
+
+@@ -606,6 +913,7 @@
+ ui_options.le_clientname->setText(client_presets.at(pres_index).name);
+ ui_options.le_clientversion->setText(client_presets.at(pres_index).version);
+ ui_options.le_capsnode->setText(client_presets.at(pres_index).caps_node);
++ ui_options.le_capsversion->setText(client_presets.at(pres_index).caps_version);
+ }
+ }
+ ui_options.le_clientname->setEnabled(false);
+@@ -614,3 +922,74 @@
+ ui_options.le_capsversion->setEnabled(false);
+ }
+ }
++
++void ClientSwitcherPlugin::viewFromOpt()
++{
++ lastLogItem = ui_options.cb_logslist->currentText();
++ if (lastLogItem.isEmpty())
++ return;
++ psiOptions->setPluginOption(constLastLogItem, QVariant(lastLogItem));
++ showLog(lastLogItem);
++}
++
++QString ClientSwitcherPlugin::jidToNick(int account, const QString &jid)
++{
++ QString nick;
++ if (psiContactInfo)
++ nick = psiContactInfo->name(account, jid);
++ if (nick.isEmpty())
++ nick = jid;
++ return nick;
++}
++
++void ClientSwitcherPlugin::showPopup(const QString &nick)
++{
++ int msecs = psiPopup->popupDuration(constPluginName);
++ if (msecs > 0)
++ psiPopup->initPopup(tr("%1 has requested your version").arg(sender_->escape(nick)), constPluginName,
++ "psi/headline", popupId);
++}
++
++void ClientSwitcherPlugin::showLog(const QString &filename)
++{
++ QString fullname = logsDir + filename;
++ Viewer *v = new Viewer(fullname, psiIcon);
++ v->resize(widthLogsView, heightLogsView);
++ if (!v->init()) {
++ delete (v);
++ return;
++ }
++ connect(v, &Viewer::onClose, this, &ClientSwitcherPlugin::onCloseView);
++ v->show();
++}
++
++void ClientSwitcherPlugin::saveToLog(const int account, const QString &to_jid, const QString &ver_str)
++{
++ QString acc_jid = psiAccount->getJid(account);
++ if (acc_jid.isEmpty() || acc_jid == "-1")
++ return;
++ QFile file(logsDir + acc_jid.replace("@", "_at_") + ".log");
++ if (file.open(QIODevice::WriteOnly | QIODevice::Append)) {
++ QString time_str = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
++ QTextStream out(&file);
++ out.setCodec("UTF-8");
++ out.setGenerateByteOrderMark(false);
++#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
++ out << time_str << " " << to_jid << " <-- " << ver_str << Qt::endl;
++#else
++ out << time_str << " " << to_jid << " <-- " << ver_str << endl;
++#endif
++ }
++}
++
++void ClientSwitcherPlugin::onCloseView(int w, int h)
++{
++ if (widthLogsView != w) {
++ widthLogsView = w;
++ psiOptions->setPluginOption(constShowLogWidth, QVariant(w));
++ }
++ if (heightLogsView != h) {
++ heightLogsView = h;
++ psiOptions->setPluginOption(constShowLogHeight, QVariant(h));
++ }
++}
+diff -Nru a/plugins/generic/clientswitcherplugin/clientswitcherplugin.h b/plugins/generic/clientswitcherplugin/clientswitcherplugin.h
+--- a/plugins/generic/clientswitcherplugin/clientswitcherplugin.h 2025-08-26 14:50:07.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/clientswitcherplugin.h 2020-07-10 11:50:10.000000000 +0000
+@@ -42,6 +42,8 @@
+ #include "optionaccessinghost.h"
+ #include "optionaccessor.h"
+ #include "plugininfoprovider.h"
++#include "popupaccessinghost.h"
++#include "popupaccessor.h"
+ #include "psiaccountcontroller.h"
+ #include "psiaccountcontrollinghost.h"
+ #include "psiplugin.h"
+@@ -54,17 +56,22 @@
+ class ClientSwitcherPlugin : public QObject,
+ public PsiPlugin,
+ public OptionAccessor,
++ public StanzaSender,
+ public StanzaFilter,
+ public PluginInfoProvider,
++ public PopupAccessor,
+ public ApplicationInfoAccessor,
+ public AccountInfoAccessor,
+- public PsiAccountController
++ public PsiAccountController,
++ public ContactInfoAccessor,
++ public IconFactoryAccessor
+
+ {
+ Q_OBJECT
+ Q_PLUGIN_METADATA(IID "com.psi-plus.ClientSwitcherPlugin" FILE "psiplugin.json")
+- Q_INTERFACES(PsiPlugin OptionAccessor StanzaFilter PluginInfoProvider ApplicationInfoAccessor AccountInfoAccessor
+- PsiAccountController)
++ Q_INTERFACES(
++ PsiPlugin OptionAccessor StanzaSender StanzaFilter PluginInfoProvider PopupAccessor ApplicationInfoAccessor
++ AccountInfoAccessor PsiAccountController ContactInfoAccessor IconFactoryAccessor)
+
+ public:
+ ClientSwitcherPlugin();
+@@ -78,59 +85,79 @@
+ // OptionAccessor
+ virtual void setOptionAccessingHost(OptionAccessingHost *);
+ virtual void optionChanged(const QString &);
++ // StanzaSender
++ virtual void setStanzaSendingHost(StanzaSendingHost *);
+ // StanzaFilter
+ virtual bool incomingStanza(int, const QDomElement &);
+ virtual bool outgoingStanza(int, QDomElement &);
+ // EventFilter
+ virtual QString pluginInfo();
++ // PopupAccessor
++ virtual void setPopupAccessingHost(PopupAccessingHost *);
+ // ApplicationInfoAccessor
+ virtual void setApplicationInfoAccessingHost(ApplicationInfoAccessingHost *);
+ // AccountInfoAccessing
+ virtual void setAccountInfoAccessingHost(AccountInfoAccessingHost *);
+ // PsiAccountController
+ virtual void setPsiAccountControllingHost(PsiAccountControllingHost *);
++ // ContactInfoAccessor
++ virtual void setContactInfoAccessingHost(ContactInfoAccessingHost *);
++ // IconFactoryAccessor
++ virtual void setIconFactoryAccessingHost(IconFactoryAccessingHost *host);
+
+ private:
+ struct OsStruct {
+ QString name;
+- QString version;
+- OsStruct(const QString &n, const QString &v = QString()) : name(n), version(v) { }
++ OsStruct(const QString &n) : name(n) { }
+ };
+
+ struct ClientStruct {
+ QString name;
+ QString version;
+ QString caps_node;
+- ClientStruct(const QString &n, const QString &v, const QString &cn) : name(n), version(v), caps_node(cn) { }
++ QString caps_version;
++ ClientStruct(const QString &n, const QString &v, const QString &cn, const QString &cv) :
++ name(n), version(v), caps_node(cn), caps_version(cv)
++ {
++ }
+ };
+-
+ Ui::OptionsWidget ui_options;
+- OptionAccessingHost *psiOptions;
++ StanzaSendingHost * sender_;
++ OptionAccessingHost * psiOptions;
++ PopupAccessingHost * psiPopup;
+ ApplicationInfoAccessingHost *psiInfo;
+- AccountInfoAccessingHost *psiAccount;
+- PsiAccountControllingHost *psiAccountCtl;
++ AccountInfoAccessingHost * psiAccount;
++ PsiAccountControllingHost * psiAccountCtl;
++ ContactInfoAccessingHost * psiContactInfo;
++ IconFactoryAccessingHost * psiIcon;
+ //--
+ bool enabled;
+ bool for_all_acc;
+ QList<AccountSettings *> settingsList;
+ //--
+ QString def_os_name;
+- QString def_os_version;
+ QString def_client_name;
+ QString def_client_version;
+ QString def_caps_node;
+ QString def_caps_version;
+ QList<struct OsStruct> os_presets;
+ QList<struct ClientStruct> client_presets;
++ QString logsDir;
++ int heightLogsView;
++ int widthLogsView;
++ QString lastLogItem;
++ int popupId;
++ //--
+
+- int getOsTemplateIndex(const QString &, const QString &os_version);
+- int getClientTemplateIndex(const QString &, const QString &, const QString &);
++ int getOsTemplateIndex(const QString &);
++ int getClientTemplateIndex(const QString &, const QString &, const QString &, const QString &);
+ int getAccountById(const QString &);
+ AccountSettings *getAccountSetting(const QString &);
+-
+-private:
+- enum class UpdateStatus { Ok, Disabled, NoAccount, NoSettings };
+- UpdateStatus updateInfo(int account);
++ bool isSkipStanza(AccountSettings *, const int, const QString &);
++ QString jidToNick(int account, const QString &jid);
++ void showPopup(const QString &nick);
++ void showLog(const QString &filename);
++ void saveToLog(const int, const QString &, const QString &);
+
+ private slots:
+ void enableAccountsList(int);
+@@ -139,6 +166,8 @@
+ void enableOsParams(int);
+ void enableClientParams(int);
+ void setNewCaps(int);
++ void viewFromOpt();
++ void onCloseView(int, int);
+ };
+
+ #endif
+diff -Nru a/plugins/generic/clientswitcherplugin/CMakeLists.txt b/plugins/generic/clientswitcherplugin/CMakeLists.txt
+--- a/plugins/generic/clientswitcherplugin/CMakeLists.txt 2025-08-26 14:50:07.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/CMakeLists.txt 2020-07-10 11:50:10.000000000 +0000
+@@ -6,12 +6,9 @@
+
+ set( PLUGIN clientswitcherplugin )
+ project(${PLUGIN} LANGUAGES CXX)
+-cmake_minimum_required(VERSION 3.10.0)
++cmake_minimum_required(VERSION 3.1.0)
+ if(POLICY CMP0071)
+- cmake_policy(SET CMP0071 NEW)
+-endif()
+-if(POLICY CMP0074)
+- cmake_policy(SET CMP0074 NEW)
++ cmake_policy(SET CMP0071 OLD)
+ endif()
+ set( CMAKE_AUTOMOC TRUE )
+
+@@ -24,6 +21,7 @@
+ find_package(PsiPluginsApi REQUIRED)
+ include(${PsiPluginsApi_DIR}/variables.cmake)
+ include_directories(
++ ${CMAKE_SOURCE_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
+ ${CMAKE_CURRENT_LIST_DIR}
+ ${PsiPluginsApi_INCLUDE_DIR}
+@@ -35,20 +33,24 @@
+ set( _HDRS
+ ${PLUGIN}.h
+ accountsettings.h
++ viewer.h
++ typeaheadfind.h
+ )
+ set( _SRCS
+ ${PLUGIN}.cpp
+ accountsettings.cpp
++ viewer.cpp
++ typeaheadfind.cpp
+ )
+ set( _UIS
+ options.ui
+ )
+-find_package( Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Widgets Xml REQUIRED )
++find_package( Qt5 COMPONENTS Widgets Xml REQUIRED )
+ set(QT_DEPLIBS
+- Qt${QT_DEFAULT_MAJOR_VERSION}::Widgets
+- Qt${QT_DEFAULT_MAJOR_VERSION}::Xml
++ Qt5::Widgets
++ Qt5::Xml
+ )
+-qt_wrap_ui(UIS ${_UIS})
++qt5_wrap_ui(UIS ${_UIS})
+
+ if(WIN32)
+ set(LIB_TYPE "MODULE")
+diff -Nru a/plugins/generic/clientswitcherplugin/options.ui b/plugins/generic/clientswitcherplugin/options.ui
+--- a/plugins/generic/clientswitcherplugin/options.ui 2025-08-26 14:50:07.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/options.ui 2020-07-10 11:50:10.000000000 +0000
+@@ -13,109 +13,57 @@
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+- <layout class="QVBoxLayout" name="verticalLayout">
++ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+- <layout class="QHBoxLayout" name="horizontalLayout_3">
+- <item>
+- <widget class="QLabel" name="label">
+- <property name="text">
+- <string>Account:</string>
+- </property>
+- </widget>
+- </item>
+- <item>
+- <widget class="QComboBox" name="cb_accounts">
+- <property name="minimumSize">
+- <size>
+- <width>141</width>
+- <height>0</height>
+- </size>
+- </property>
+- </widget>
+- </item>
+- <item>
+- <widget class="QCheckBox" name="cb_allaccounts">
+- <property name="text">
+- <string>For all accounts</string>
+- </property>
+- </widget>
+- </item>
+- <item>
+- <spacer name="horizontalSpacer">
+- <property name="orientation">
+- <enum>Qt::Horizontal</enum>
+- </property>
+- <property name="sizeHint" stdset="0">
+- <size>
+- <width>40</width>
+- <height>20</height>
+- </size>
+- </property>
+- </spacer>
+- </item>
+- </layout>
+- </item>
+- <item>
+- <widget class="QScrollArea" name="scrollArea">
+- <property name="widgetResizable">
+- <bool>true</bool>
++ <widget class="QTabWidget" name="tabWidget">
++ <property name="sizePolicy">
++ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
++ <horstretch>0</horstretch>
++ <verstretch>1</verstretch>
++ </sizepolicy>
++ </property>
++ <property name="currentIndex">
++ <number>0</number>
+ </property>
+- <widget class="QWidget" name="scrollAreaWidgetContents">
+- <property name="geometry">
+- <rect>
+- <x>0</x>
+- <y>0</y>
+- <width>942</width>
+- <height>664</height>
+- </rect>
+- </property>
+- <layout class="QVBoxLayout" name="verticalLayout_2">
++ <widget class="QWidget" name="tab1">
++ <attribute name="title">
++ <string>General</string>
++ </attribute>
++ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+- <layout class="QHBoxLayout" name="horizontalLayout_4">
++ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+- <widget class="QLabel" name="label_4">
++ <widget class="QLabel" name="label">
+ <property name="text">
+- <string>Response mode</string>
++ <string>Account:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+- <widget class="QComboBox" name="cmb_lockrequ">
+- <item>
+- <property name="text">
+- <string>allow</string>
+- </property>
+- </item>
+- <item>
+- <property name="text">
+- <string>not implemented</string>
+- </property>
+- </item>
+- <item>
+- <property name="text">
+- <string>ignore</string>
+- </property>
+- </item>
++ <widget class="QComboBox" name="cb_accounts">
++ <property name="minimumSize">
++ <size>
++ <width>141</width>
++ <height>0</height>
++ </size>
++ </property>
+ </widget>
+ </item>
+ <item>
+- <widget class="QCheckBox" name="cb_locktimerequ">
++ <widget class="QCheckBox" name="cb_allaccounts">
+ <property name="text">
+- <string>Deny iq time request</string>
++ <string>For all accounts</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+- <spacer name="spacer_2">
++ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+- <property name="sizeType">
+- <enum>QSizePolicy::Expanding</enum>
+- </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+- <width>18</width>
++ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+@@ -124,133 +72,347 @@
+ </layout>
+ </item>
+ <item>
+- <widget class="QGroupBox" name="gb_os">
+- <property name="enabled">
++ <widget class="QScrollArea" name="scrollArea">
++ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+- <property name="title">
+- <string>OS</string>
+- </property>
+- <layout class="QHBoxLayout" name="horizontalLayout_2">
+- <item>
+- <layout class="QGridLayout" name="gridLayout">
+- <item row="1" column="0">
+- <widget class="QLabel" name="lb_osname">
+- <property name="text">
+- <string>OS name</string>
+- </property>
+- </widget>
+- </item>
+- <item row="0" column="0">
+- <widget class="QLabel" name="lb_ospreset">
+- <property name="text">
+- <string>Template</string>
+- </property>
+- </widget>
+- </item>
+- <item row="0" column="1">
+- <widget class="QComboBox" name="cb_ospreset"/>
+- </item>
+- <item row="1" column="1">
+- <widget class="QLineEdit" name="le_osname"/>
+- </item>
+- <item row="2" column="0">
+- <widget class="QLabel" name="lb_osversion">
+- <property name="text">
+- <string>OS version</string>
+- </property>
+- </widget>
+- </item>
+- <item row="2" column="1">
+- <widget class="QLineEdit" name="le_osversion"/>
+- </item>
+- </layout>
+- </item>
+- </layout>
+- </widget>
+- </item>
+- <item>
+- <widget class="QGroupBox" name="gb_client">
+- <property name="title">
+- <string>Client</string>
+- </property>
+- <layout class="QHBoxLayout" name="horizontalLayout">
+- <item>
+- <layout class="QGridLayout" name="gridLayout_2">
+- <item row="0" column="0">
+- <widget class="QLabel" name="lb_clientpreset">
+- <property name="text">
+- <string>Template</string>
+- </property>
+- </widget>
+- </item>
+- <item row="0" column="1">
+- <widget class="QComboBox" name="cb_clientpreset"/>
+- </item>
+- <item row="1" column="0">
+- <widget class="QLabel" name="lb_clientname">
+- <property name="text">
+- <string>Client name</string>
+- </property>
+- </widget>
+- </item>
+- <item row="1" column="1">
+- <widget class="QLineEdit" name="le_clientname"/>
+- </item>
+- <item row="2" column="0">
+- <widget class="QLabel" name="lb_clientversion">
+- <property name="text">
+- <string>Client version</string>
+- </property>
+- </widget>
+- </item>
+- <item row="2" column="1">
+- <widget class="QLineEdit" name="le_clientversion"/>
+- </item>
+- <item row="3" column="0">
+- <widget class="QLabel" name="lb_capsnode">
+- <property name="text">
+- <string>Caps node</string>
+- </property>
+- </widget>
+- </item>
+- <item row="3" column="1">
+- <widget class="QLineEdit" name="le_capsnode"/>
+- </item>
+- <item row="4" column="0">
+- <widget class="QLabel" name="lb_capsversion">
+- <property name="text">
+- <string>Caps version</string>
+- </property>
+- </widget>
+- </item>
+- <item row="4" column="1">
+- <widget class="QLineEdit" name="le_capsversion"/>
+- </item>
+- </layout>
+- </item>
+- </layout>
++ <widget class="QWidget" name="scrollAreaWidgetContents">
++ <property name="geometry">
++ <rect>
++ <x>0</x>
++ <y>0</y>
++ <width>930</width>
++ <height>573</height>
++ </rect>
++ </property>
++ <layout class="QVBoxLayout" name="verticalLayout_2">
++ <item>
++ <layout class="QHBoxLayout" name="horizontalLayout_4">
++ <item>
++ <widget class="QLabel" name="label_4">
++ <property name="text">
++ <string>Response mode</string>
++ </property>
++ </widget>
++ </item>
++ <item>
++ <widget class="QComboBox" name="cmb_lockrequ">
++ <item>
++ <property name="text">
++ <string>allow</string>
++ </property>
++ </item>
++ <item>
++ <property name="text">
++ <string>not implemented</string>
++ </property>
++ </item>
++ <item>
++ <property name="text">
++ <string>ignore</string>
++ </property>
++ </item>
++ </widget>
++ </item>
++ <item>
++ <widget class="QCheckBox" name="cb_locktimerequ">
++ <property name="text">
++ <string>Deny iq time request</string>
++ </property>
++ </widget>
++ </item>
++ <item>
++ <spacer name="spacer_2">
++ <property name="orientation">
++ <enum>Qt::Horizontal</enum>
++ </property>
++ <property name="sizeType">
++ <enum>QSizePolicy::Expanding</enum>
++ </property>
++ <property name="sizeHint" stdset="0">
++ <size>
++ <width>18</width>
++ <height>20</height>
++ </size>
++ </property>
++ </spacer>
++ </item>
++ </layout>
++ </item>
++ <item>
++ <widget class="QGroupBox" name="gb_os">
++ <property name="enabled">
++ <bool>true</bool>
++ </property>
++ <property name="title">
++ <string>OS</string>
++ </property>
++ <layout class="QHBoxLayout" name="horizontalLayout_2">
++ <item>
++ <layout class="QGridLayout" name="gridLayout">
++ <item row="0" column="0">
++ <widget class="QLabel" name="lb_ospreset">
++ <property name="text">
++ <string>Template</string>
++ </property>
++ </widget>
++ </item>
++ <item row="0" column="1">
++ <widget class="QComboBox" name="cb_ospreset"/>
++ </item>
++ <item row="1" column="0">
++ <widget class="QLabel" name="lb_osname">
++ <property name="text">
++ <string>OS name</string>
++ </property>
++ </widget>
++ </item>
++ <item row="1" column="1">
++ <widget class="QLineEdit" name="le_osname"/>
++ </item>
++ </layout>
++ </item>
++ </layout>
++ </widget>
++ </item>
++ <item>
++ <widget class="QGroupBox" name="gb_client">
++ <property name="title">
++ <string>Client</string>
++ </property>
++ <layout class="QHBoxLayout" name="horizontalLayout">
++ <item>
++ <layout class="QGridLayout" name="gridLayout_2">
++ <item row="0" column="0">
++ <widget class="QLabel" name="lb_clientpreset">
++ <property name="text">
++ <string>Template</string>
++ </property>
++ </widget>
++ </item>
++ <item row="0" column="1">
++ <widget class="QComboBox" name="cb_clientpreset"/>
++ </item>
++ <item row="1" column="0">
++ <widget class="QLabel" name="lb_clientname">
++ <property name="text">
++ <string>Client name</string>
++ </property>
++ </widget>
++ </item>
++ <item row="1" column="1">
++ <widget class="QLineEdit" name="le_clientname"/>
++ </item>
++ <item row="2" column="0">
++ <widget class="QLabel" name="lb_clientversion">
++ <property name="text">
++ <string>Client version</string>
++ </property>
++ </widget>
++ </item>
++ <item row="2" column="1">
++ <widget class="QLineEdit" name="le_clientversion"/>
++ </item>
++ <item row="3" column="0">
++ <widget class="QLabel" name="lb_capsnode">
++ <property name="text">
++ <string>Caps node</string>
++ </property>
++ </widget>
++ </item>
++ <item row="3" column="1">
++ <widget class="QLineEdit" name="le_capsnode"/>
++ </item>
++ <item row="4" column="0">
++ <widget class="QLabel" name="lb_capsversion">
++ <property name="text">
++ <string>Caps version</string>
++ </property>
++ </widget>
++ </item>
++ <item row="4" column="1">
++ <widget class="QLineEdit" name="le_capsversion"/>
++ </item>
++ </layout>
++ </item>
++ </layout>
++ </widget>
++ </item>
++ <item>
++ <widget class="QGroupBox" name="gb_enablefor">
++ <property name="sizePolicy">
++ <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
++ <horstretch>0</horstretch>
++ <verstretch>0</verstretch>
++ </sizepolicy>
++ </property>
++ <property name="title">
++ <string>Enable for:</string>
++ </property>
++ <layout class="QVBoxLayout" name="verticalLayout">
++ <item>
++ <layout class="QHBoxLayout" name="horizontalLayout_5">
++ <item>
++ <widget class="QCheckBox" name="cb_contactsenable">
++ <property name="text">
++ <string>Contacts</string>
++ </property>
++ </widget>
++ </item>
++ <item>
++ <spacer name="spacer_3">
++ <property name="orientation">
++ <enum>Qt::Horizontal</enum>
++ </property>
++ <property name="sizeType">
++ <enum>QSizePolicy::Maximum</enum>
++ </property>
++ <property name="sizeHint" stdset="0">
++ <size>
++ <width>30</width>
++ <height>20</height>
++ </size>
++ </property>
++ </spacer>
++ </item>
++ <item>
++ <widget class="QCheckBox" name="cb_conferencesenable">
++ <property name="text">
++ <string>Groupchats</string>
++ </property>
++ </widget>
++ </item>
++ <item>
++ <spacer name="spacer_4">
++ <property name="orientation">
++ <enum>Qt::Horizontal</enum>
++ </property>
++ <property name="sizeType">
++ <enum>QSizePolicy::Expanding</enum>
++ </property>
++ <property name="sizeHint" stdset="0">
++ <size>
++ <width>18</width>
++ <height>20</height>
++ </size>
++ </property>
++ </spacer>
++ </item>
++ </layout>
++ </item>
++ </layout>
++ </widget>
++ </item>
++ <item>
++ <layout class="QGridLayout" name="gridLayout_3">
++ <item row="0" column="1">
++ <widget class="QLabel" name="label_2">
++ <property name="text">
++ <string>Show popup at version iq</string>
++ </property>
++ </widget>
++ </item>
++ <item row="0" column="2">
++ <widget class="QComboBox" name="cmb_showrequ">
++ <item>
++ <property name="text">
++ <string>never</string>
++ </property>
++ </item>
++ <item>
++ <property name="text">
++ <string>if iq replaced</string>
++ </property>
++ </item>
++ <item>
++ <property name="text">
++ <string>always</string>
++ </property>
++ </item>
++ </widget>
++ </item>
++ <item row="1" column="1">
++ <widget class="QLabel" name="label_3">
++ <property name="text">
++ <string>Save queries to log</string>
++ </property>
++ </widget>
++ </item>
++ <item row="1" column="2">
++ <widget class="QComboBox" name="cmb_savetolog">
++ <item>
++ <property name="text">
++ <string>never</string>
++ </property>
++ </item>
++ <item>
++ <property name="text">
++ <string>if iq replaced</string>
++ </property>
++ </item>
++ <item>
++ <property name="text">
++ <string>always</string>
++ </property>
++ </item>
++ </widget>
++ </item>
++ </layout>
++ </item>
++ <item>
++ <spacer name="verticalSpacer">
++ <property name="orientation">
++ <enum>Qt::Vertical</enum>
++ </property>
++ <property name="sizeHint" stdset="0">
++ <size>
++ <width>20</width>
++ <height>40</height>
++ </size>
++ </property>
++ </spacer>
++ </item>
++ </layout>
++ </widget>
+ </widget>
+ </item>
++ </layout>
++ </widget>
++ <widget class="QWidget" name="tab2">
++ <attribute name="title">
++ <string>Logs</string>
++ </attribute>
++ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+- <widget class="QLabel" name="warningLabel">
+- <property name="text">
+- <string>&lt;b&gt;Attention!&lt;/b&gt; Thoughtless usage of Client Switcher Plugin may cause to inability of using OMEMO and OpenPGP encryption. Use functions of this plugin very carefully...</string>
+- </property>
+- <property name="wordWrap">
+- <bool>true</bool>
+- </property>
+- </widget>
++ <layout class="QHBoxLayout" name="horizontalLayout_7">
++ <item>
++ <widget class="QComboBox" name="cb_logslist">
++ <property name="sizePolicy">
++ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
++ <horstretch>1</horstretch>
++ <verstretch>0</verstretch>
++ </sizepolicy>
++ </property>
++ </widget>
++ </item>
++ <item>
++ <widget class="QPushButton" name="bt_viewlog">
++ <property name="text">
++ <string>View log</string>
++ </property>
++ </widget>
++ </item>
++ </layout>
+ </item>
+ <item>
+- <spacer name="verticalSpacer">
++ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+- <height>40</height>
++ <height>471</height>
+ </size>
+ </property>
+ </spacer>
+@@ -260,6 +422,16 @@
+ </widget>
+ </item>
+ <item>
++ <widget class="QLabel" name="warningLabel">
++ <property name="text">
++ <string>&lt;b&gt;Attention!&lt;/b&gt; Thoughtless usage of Client Switcher Plugin may cause to inability of using OMEMO and OpenPGP encryption. Use functions of this plugin very carefully...</string>
++ </property>
++ <property name="wordWrap">
++ <bool>true</bool>
++ </property>
++ </widget>
++ </item>
++ <item>
+ <widget class="QLabel" name="lb_link">
+ <property name="text">
+ <string>&lt;a href=&quot;https://psi-plus.com/wiki/en:plugins#client_switcher_plugin&quot;&gt;Wiki (Online)&lt;/a&gt;</string>
+@@ -271,6 +443,27 @@
+ </item>
+ </layout>
+ </widget>
++ <tabstops>
++ <tabstop>tabWidget</tabstop>
++ <tabstop>cb_accounts</tabstop>
++ <tabstop>cb_allaccounts</tabstop>
++ <tabstop>scrollArea</tabstop>
++ <tabstop>cmb_lockrequ</tabstop>
++ <tabstop>cb_locktimerequ</tabstop>
++ <tabstop>cb_ospreset</tabstop>
++ <tabstop>le_osname</tabstop>
++ <tabstop>cb_clientpreset</tabstop>
++ <tabstop>le_clientname</tabstop>
++ <tabstop>le_clientversion</tabstop>
++ <tabstop>le_capsnode</tabstop>
++ <tabstop>le_capsversion</tabstop>
++ <tabstop>cb_contactsenable</tabstop>
++ <tabstop>cb_conferencesenable</tabstop>
++ <tabstop>cmb_showrequ</tabstop>
++ <tabstop>cmb_savetolog</tabstop>
++ <tabstop>cb_logslist</tabstop>
++ <tabstop>bt_viewlog</tabstop>
++ </tabstops>
+ <resources/>
+ <connections/>
+ </ui>
+diff -Nru a/plugins/generic/clientswitcherplugin/psiplugin.json b/plugins/generic/clientswitcherplugin/psiplugin.json
+--- a/plugins/generic/clientswitcherplugin/psiplugin.json 2025-08-26 14:50:07.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/psiplugin.json 2020-07-10 11:50:10.000000000 +0000
+@@ -2,7 +2,7 @@
+ "name": "Client Switcher",
+ "name:ru": "Подмена клиента",
+ "shortname": "clientswitcher",
+- "version": "0.2",
++ "version": "0.0.18",
+ "priority": 2,
+ "icon": "base64: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",
+ "description": "Hide or replace information about your XMPP client and/or operating system.",
+diff -Nru a/plugins/generic/clientswitcherplugin/typeaheadfind.cpp b/plugins/generic/clientswitcherplugin/typeaheadfind.cpp
+--- a/plugins/generic/clientswitcherplugin/typeaheadfind.cpp 1970-01-01 00:00:00.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/typeaheadfind.cpp 2020-07-10 11:50:10.000000000 +0000
+@@ -0,0 +1,178 @@
++/*
++ * typeaheadfind.cpp - plugin
++ * Copyright (C) 2009-2010 Evgeny Khryukin
++ *
++ * This program is free software; 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 <https://www.gnu.org/licenses/>.
++ *
++ */
++
++#include "typeaheadfind.h"
++
++#include <QCheckBox>
++#include <QLabel>
++#include <QLineEdit>
++#include <QPushButton>
++
++using namespace ClientSwitcher;
++
++class TypeAheadFindBar::Private {
++public:
++ void doFind(bool backward = false)
++ {
++ QTextDocument::FindFlags options;
++ if (caseSensitive)
++ options |= QTextDocument::FindCaseSensitively;
++ if (backward) {
++ options |= QTextDocument::FindBackward;
++ QTextCursor cursor = te->textCursor();
++ cursor.setPosition(cursor.selectionStart());
++ cursor.movePosition(QTextCursor::Left);
++ te->setTextCursor(cursor);
++ }
++ if (find(text, options)) {
++ le_find->setStyleSheet("");
++ } else {
++ le_find->setStyleSheet("QLineEdit { background: #ff6666; color: #ffffff }");
++ }
++ }
++
++ bool find(const QString &str, QTextDocument::FindFlags options,
++ QTextCursor::MoveOperation start = QTextCursor::NoMove)
++ {
++ Q_UNUSED(str);
++ if (start != QTextCursor::NoMove) {
++ QTextCursor cursor = te->textCursor();
++ cursor.movePosition(start);
++ te->setTextCursor(cursor);
++ }
++ bool found = te->find(text, options);
++ if (!found) {
++ if (start == QTextCursor::NoMove)
++ return find(text, options,
++ options & QTextDocument::FindBackward ? QTextCursor::End : QTextCursor::Start);
++ return false;
++ }
++ return true;
++ }
++
++ QString text;
++ bool caseSensitive;
++ QTextEdit * te;
++ QLineEdit * le_find;
++ QPushButton *but_next;
++ QPushButton *but_prev;
++ QPushButton *first_page, *next_page, *last_page, *prev_page;
++ QCheckBox * cb_case;
++};
++
++TypeAheadFindBar::TypeAheadFindBar(IconFactoryAccessingHost *IcoHost, QTextEdit *textedit, const QString &title,
++ QWidget *parent) :
++ QToolBar(title, parent),
++ icoHost_(IcoHost)
++{
++ d = new Private();
++ d->te = textedit;
++ init();
++}
++
++void TypeAheadFindBar::init()
++{
++ d->caseSensitive = false;
++ d->text = "";
++ addWidget(new QLabel(tr("Search: "), this));
++
++ d->le_find = new QLineEdit(this);
++ d->le_find->setMaximumWidth(128);
++ connect(d->le_find, &QLineEdit::textEdited, this, &TypeAheadFindBar::textChanged);
++ addWidget(d->le_find);
++
++ d->but_prev = new QPushButton(this);
++ d->but_prev->setFixedSize(25, 25);
++ d->but_prev->setIcon(icoHost_->getIcon("psi/arrowUp"));
++ d->but_prev->setEnabled(false);
++ connect(d->but_prev, &QPushButton::released, this, &TypeAheadFindBar::findPrevious);
++ addWidget(d->but_prev);
++
++ d->but_next = new QPushButton(this);
++ d->but_next->setFixedSize(25, 25);
++ d->but_next->setIcon(icoHost_->getIcon("psi/arrowDown"));
++ d->but_next->setEnabled(false);
++ connect(d->but_next, &QPushButton::released, this, &TypeAheadFindBar::findNext);
++ addWidget(d->but_next);
++
++ d->cb_case = new QCheckBox(tr("&Case sensitive"), this);
++ connect(d->cb_case, &QCheckBox::clicked, this, &TypeAheadFindBar::caseToggled);
++ addWidget(d->cb_case);
++
++ addSeparator();
++
++ d->first_page = new QPushButton(this);
++ d->first_page->setToolTip(tr("First page"));
++ connect(d->first_page, &QPushButton::released, this, &TypeAheadFindBar::firstPage);
++ d->first_page->setFixedSize(25, 25);
++ d->first_page->setIcon(icoHost_->getIcon("psi/doubleBackArrow"));
++ addWidget(d->first_page);
++
++ d->prev_page = new QPushButton(this);
++ d->prev_page->setToolTip(tr("Previous page"));
++ connect(d->prev_page, &QPushButton::released, this, &TypeAheadFindBar::prevPage);
++ d->prev_page->setFixedSize(25, 25);
++ d->prev_page->setIcon(icoHost_->getIcon("psi/arrowLeft"));
++ addWidget(d->prev_page);
++
++ d->next_page = new QPushButton(this);
++ d->next_page->setToolTip(tr("Next page"));
++ connect(d->next_page, &QPushButton::released, this, &TypeAheadFindBar::nextPage);
++ d->next_page->setFixedSize(25, 25);
++ d->next_page->setIcon(icoHost_->getIcon("psi/arrowRight"));
++ addWidget(d->next_page);
++
++ d->last_page = new QPushButton(this);
++ d->last_page->setToolTip(tr("Last page"));
++ connect(d->last_page, &QPushButton::released, this, &TypeAheadFindBar::lastPage);
++ d->last_page->setFixedSize(25, 25);
++ d->last_page->setIcon(icoHost_->getIcon("psi/doubleNextArrow"));
++ addWidget(d->last_page);
++}
++
++TypeAheadFindBar::~TypeAheadFindBar()
++{
++ delete d;
++ d = nullptr;
++}
++
++void TypeAheadFindBar::textChanged(const QString &str)
++{
++ QTextCursor cursor = d->te->textCursor();
++ if (str.isEmpty()) {
++ d->but_next->setEnabled(false);
++ d->but_prev->setEnabled(false);
++ d->le_find->setStyleSheet("");
++ cursor.clearSelection();
++ d->te->setTextCursor(cursor);
++ } else {
++ d->but_next->setEnabled(true);
++ d->but_prev->setEnabled(true);
++ cursor.setPosition(cursor.selectionStart());
++ d->te->setTextCursor(cursor);
++ d->text = str;
++ d->doFind();
++ }
++}
++
++void TypeAheadFindBar::findNext() { d->doFind(); }
++
++void TypeAheadFindBar::findPrevious() { d->doFind(true); }
++
++void TypeAheadFindBar::caseToggled() { d->caseSensitive = d->cb_case->checkState(); }
+diff -Nru a/plugins/generic/clientswitcherplugin/typeaheadfind.h b/plugins/generic/clientswitcherplugin/typeaheadfind.h
+--- a/plugins/generic/clientswitcherplugin/typeaheadfind.h 1970-01-01 00:00:00.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/typeaheadfind.h 2020-07-10 11:50:10.000000000 +0000
+@@ -0,0 +1,58 @@
++/*
++ * typeaheadfind.h - plugin
++ * Copyright (C) 2009-2010 Evgeny Khryukin
++ *
++ * This program is free software; 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 <https://www.gnu.org/licenses/>.
++ *
++ */
++
++#ifndef TYPEAHEADFIND_H
++#define TYPEAHEADFIND_H
++
++#include <QTextEdit>
++#include <QToolBar>
++
++#include "iconfactoryaccessinghost.h"
++
++namespace ClientSwitcher {
++
++class TypeAheadFindBar : public QToolBar {
++ Q_OBJECT
++public:
++ TypeAheadFindBar(IconFactoryAccessingHost *IcoHost, QTextEdit *textedit, const QString &title,
++ QWidget *parent = nullptr);
++
++ ~TypeAheadFindBar();
++ void init();
++
++signals:
++ void firstPage();
++ void lastPage();
++ void nextPage();
++ void prevPage();
++
++private slots:
++ void textChanged(const QString &);
++ void findNext();
++ void findPrevious();
++ void caseToggled();
++
++private:
++ class Private;
++ Private * d;
++ IconFactoryAccessingHost *icoHost_;
++};
++
++}
++#endif
+diff -Nru a/plugins/generic/clientswitcherplugin/viewer.cpp b/plugins/generic/clientswitcherplugin/viewer.cpp
+--- a/plugins/generic/clientswitcherplugin/viewer.cpp 1970-01-01 00:00:00.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/viewer.cpp 2020-07-10 11:50:10.000000000 +0000
+@@ -0,0 +1,190 @@
++/*
++ * viewer.cpp - plugin
++ * Copyright (C) 2009-2010 Evgeny Khryukin
++ *
++ * This program is free software; 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 <https://www.gnu.org/licenses/>.
++ *
++ */
++
++#include "viewer.h"
++
++#include <QFile>
++#include <QFileInfo>
++#include <QHBoxLayout>
++#include <QMessageBox>
++#include <QPushButton>
++#include <QTextStream>
++
++Viewer::Viewer(QString filename, IconFactoryAccessingHost *IcoHost, QWidget *parent) :
++ QDialog(parent), icoHost_(IcoHost), fileName_(filename)
++{
++ setAttribute(Qt::WA_DeleteOnClose);
++ setWindowTitle(filename);
++ setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint
++ | Qt::CustomizeWindowHint);
++ QVBoxLayout *layout = new QVBoxLayout(this);
++ textWid = new QTextEdit();
++ QPalette pal = textWid->palette();
++ pal.setColor(QPalette::Inactive, QPalette::Highlight, pal.color(QPalette::Active, QPalette::Highlight));
++ pal.setColor(QPalette::Inactive, QPalette::HighlightedText, pal.color(QPalette::Active, QPalette::HighlightedText));
++ textWid->setPalette(pal);
++ layout->addWidget(textWid);
++ findBar = new ClientSwitcher::TypeAheadFindBar(icoHost_, textWid, tr("Find"), this);
++ QPushButton *Close = new QPushButton(icoHost_->getIcon("psi/quit"), tr("Close"));
++ QPushButton *Save = new QPushButton(icoHost_->getIcon("psi/save"), tr("Save Changes"));
++ QPushButton *Delete = new QPushButton(icoHost_->getIcon("psi/remove"), tr("Delete Log"));
++ QPushButton *Update = new QPushButton(icoHost_->getIcon("psi/reload"), tr("Update Log"));
++ QHBoxLayout *butLayout = new QHBoxLayout();
++ butLayout->addWidget(Delete);
++ butLayout->addStretch();
++ butLayout->addWidget(Update);
++ butLayout->addWidget(Save);
++ butLayout->addWidget(Close);
++ layout->addWidget(findBar);
++ layout->addLayout(butLayout);
++ connect(Close, &QPushButton::released, this, &Viewer::close);
++ connect(Delete, &QPushButton::released, this, &Viewer::deleteLog);
++ connect(Save, &QPushButton::released, this, &Viewer::saveLog);
++ connect(Update, &QPushButton::released, this, &Viewer::updateLog);
++
++ connect(findBar, &ClientSwitcher::TypeAheadFindBar::firstPage, this, &Viewer::firstPage);
++ connect(findBar, &ClientSwitcher::TypeAheadFindBar::lastPage, this, &Viewer::lastPage);
++ connect(findBar, &ClientSwitcher::TypeAheadFindBar::prevPage, this, &Viewer::prevPage);
++ connect(findBar, &ClientSwitcher::TypeAheadFindBar::nextPage, this, &Viewer::nextPage);
++}
++
++void Viewer::closeEvent(QCloseEvent *e)
++{
++ emit onClose(width(), height());
++ QDialog::closeEvent(e);
++ e->accept();
++}
++
++void Viewer::deleteLog()
++{
++ int ret = QMessageBox::question(this, tr("Delete log file"), tr("Are you sure?"), QMessageBox::Yes,
++ QMessageBox::Cancel);
++ if (ret == QMessageBox::Cancel) {
++ return;
++ }
++ close();
++ QFile file(fileName_);
++ if (file.open(QIODevice::ReadWrite)) {
++ file.remove();
++ }
++}
++
++void Viewer::saveLog()
++{
++ QDateTime Modified = QFileInfo(fileName_).lastModified();
++ if (lastModified_ < Modified) {
++ QMessageBox msgBox;
++ msgBox.setWindowTitle(tr("Save log"));
++ msgBox.setText(tr("New messages has been added to log. If you save your changes, you will lose them"));
++ msgBox.setInformativeText(tr("Do you want to save your changes?"));
++ msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel);
++ msgBox.setDefaultButton(QMessageBox::Cancel);
++ int ret = msgBox.exec();
++ if (ret == QMessageBox::Cancel) {
++ return;
++ }
++ } else {
++ int ret
++ = QMessageBox::question(this, tr("Save log"), tr("Are you sure?"), QMessageBox::Yes, QMessageBox::Cancel);
++ if (ret == QMessageBox::Cancel) {
++ return;
++ }
++ }
++ QFile file(fileName_);
++ if (file.open(QIODevice::ReadWrite)) {
++ file.remove();
++ }
++ if (file.open(QIODevice::ReadWrite)) {
++ QTextStream out(&file);
++ out.setCodec("UTF-8");
++ QString Text = textWid->toPlainText();
++ pages_.insert(currentPage_, Text);
++ for (int i = 0; i < pages_.size(); i++) {
++ out.setGenerateByteOrderMark(false);
++ out << pages_.value(i);
++ }
++ }
++}
++
++void Viewer::updateLog()
++{
++ pages_.clear();
++ init();
++}
++
++bool Viewer::init()
++{
++ bool b = false;
++ QFile file(fileName_);
++ if (file.open(QIODevice::ReadOnly)) {
++ QString page;
++ int numPage = 0;
++ QTextStream in(&file);
++ in.setCodec("UTF-8");
++ while (!in.atEnd()) {
++ page = "";
++ for (int i = 0; i < 500; i++) {
++ if (in.atEnd())
++ break;
++ page += in.readLine() + "\n";
++ }
++ pages_.insert(numPage++, page);
++ }
++ currentPage_ = pages_.size() - 1;
++ lastModified_ = QDateTime::currentDateTime();
++ setPage();
++ b = true;
++ }
++ return b;
++}
++
++void Viewer::setPage()
++{
++ QString text = pages_.value(currentPage_);
++ textWid->setText(text);
++ QTextCursor cur = textWid->textCursor();
++ cur.setPosition(text.length());
++ textWid->setTextCursor(cur);
++}
++
++void Viewer::nextPage()
++{
++ if (currentPage_ < pages_.size() - 1)
++ currentPage_++;
++ setPage();
++}
++
++void Viewer::prevPage()
++{
++ if (currentPage_ > 0)
++ currentPage_--;
++ setPage();
++}
++
++void Viewer::lastPage()
++{
++ currentPage_ = pages_.size() - 1;
++ setPage();
++}
++
++void Viewer::firstPage()
++{
++ currentPage_ = 0;
++ setPage();
++}
+diff -Nru a/plugins/generic/clientswitcherplugin/viewer.h b/plugins/generic/clientswitcherplugin/viewer.h
+--- a/plugins/generic/clientswitcherplugin/viewer.h 1970-01-01 00:00:00.000000000 +0000
++++ b/plugins/generic/clientswitcherplugin/viewer.h 2020-07-10 11:50:10.000000000 +0000
+@@ -0,0 +1,63 @@
++/*
++ * viewer.h - plugin
++ * Copyright (C) 2009-2010 Evgeny Khryukin
++ *
++ * This program is free software; 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 <https://www.gnu.org/licenses/>.
++ *
++ */
++
++#ifndef VIEWER_H
++#define VIEWER_H
++
++#include <QCloseEvent>
++#include <QDateTime>
++#include <QDialog>
++#include <QTextEdit>
++
++#include "iconfactoryaccessinghost.h"
++#include "typeaheadfind.h"
++
++class Viewer : public QDialog {
++ Q_OBJECT
++public:
++ Viewer(QString filename, IconFactoryAccessingHost *IcoHost, QWidget *parent = nullptr);
++ bool init();
++
++private:
++ IconFactoryAccessingHost * icoHost_;
++ QString fileName_;
++ QDateTime lastModified_;
++ QTextEdit * textWid;
++ ClientSwitcher::TypeAheadFindBar *findBar;
++ QMap<int, QString> pages_;
++ int currentPage_;
++ void setPage();
++
++private slots:
++ void saveLog();
++ void updateLog();
++ void deleteLog();
++ void nextPage();
++ void prevPage();
++ void firstPage();
++ void lastPage();
++
++protected:
++ void closeEvent(QCloseEvent *e);
++
++signals:
++ void onClose(int, int);
++};
++
++#endif // VIEWER_H
diff --git a/psi-plus/template b/psi-plus/template
index 14990b3..d29ecbf 100644
--- a/psi-plus/template
+++ b/psi-plus/template
@@ -19,4 +19,5 @@ maintainer="Nathan Smith <nathansmith@disroot.org>"
license="GPL-2.0-only"
homepage="https://psi-plus.com"
distfiles="https://github.com/psi-plus/psi-plus-snapshots/archive/refs/tags/${version}.tar.gz"
-checksum="add907b7e81f6b9ef0f49d31dee2b9093dd0cf4fb92eaaea231d6656aea83b99" \ No newline at end of file
+checksum="add907b7e81f6b9ef0f49d31dee2b9093dd0cf4fb92eaaea231d6656aea83b99"
+patch_args="-Np1" \ No newline at end of file