#include #include #include "devgroupdialog.h" #include "../transport/gui_bridge.h" #include "debug_ui.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern "C" { #include "../../../src/chat/chat_devgroup.h" #include "../../../lib/mem.h" } class DevgroupDialog; static QPointer active; static QList> buttons; static quint64 nextRequest = 0; static QPointer invitationWindow; static quint64 statusRequest = 0, lastAutomaticSnapshot = 0; static QHash acceptRequests; static QHash> pinDialogs; static QSet dismissedInvites, awaitingPins, attemptedPins; static void refreshInvitations(); static void automaticInvitations(const QJsonObject& snapshot); static void devgroupResult(const uint8_t* data, int len); class DevgroupDialog : public QDialog { public: static QString tr(const char* text) { return QCoreApplication::translate("DevgroupDialog", text); } void refresh() { send(CHAT_DEVGROUP_STATUS); } DevgroupDialog(QWidget* parent, const QString& channel, quint64 peer, bool inviting = false) : QDialog(parent), channel(channel), peer(peer), inviting(inviting) { setAttribute(Qt::WA_DeleteOnClose); setModal(inviting); setWindowTitle(inviting ? tr("Add my device") : tr("My devices")); resize(480, 360); auto* layout = new QVBoxLayout(this); title = new QLabel(this); layout->addWidget(title); name = new QLineEdit(this); name->setPlaceholderText(tr("Device group name")); layout->addWidget(name); auto* rename = new QPushButton(tr("Create / rename"), this); layout->addWidget(rename); connect(rename, &QPushButton::clicked, this, [this] { send(CHAT_DEVGROUP_RENAME, name->text()); }); if (peer && !inviting) { auto* invite = new QPushButton(tr("Add device %1").arg(QString::number(peer, 16)), this); layout->addWidget(invite); connect(invite, &QPushButton::clicked, this, [this] { send(CHAT_DEVGROUP_INVITE); }); } status = new QLabel(this); status->setWordWrap(true); status->setTextInteractionFlags(Qt::TextSelectableByMouse); layout->addWidget(status); list = new QListWidget(this); layout->addWidget(list); auto* accept = new QPushButton(tr("Enter PIN for selected invitation"), this); layout->addWidget(accept); connect(accept, &QPushButton::clicked, this, [this] { const auto* item = list->currentItem(); if (!item || item->data(Qt::UserRole).toString().isEmpty()) return; const QString id = item->data(Qt::UserRole).toString(); bool ok = false; const QString pin = QInputDialog::getText(this, tr("Add device"), tr("PIN shown on the inviting device"), QLineEdit::Normal, {}, &ok); if (ok) send(CHAT_DEVGROUP_ACCEPT, id, pin); }); auto* leave = new QPushButton(tr("Leave device group"), this); layout->addWidget(leave); connect(leave, &QPushButton::clicked, this, [this] { send(CHAT_DEVGROUP_LEAVE); }); auto* timer = new QTimer(this); timer->setInterval(1000); connect(timer, &QTimer::timeout, this, [this] { if (!gui_bridge_is_uasync_ready()) pending.clear(); else if (!pending.values().contains(CHAT_DEVGROUP_STATUS)) send(CHAT_DEVGROUP_STATUS); }); if (inviting) { title->hide(); name->hide(); rename->hide(); list->hide(); accept->hide(); leave->hide(); } timer->start(); send(inviting ? CHAT_DEVGROUP_INVITE : CHAT_DEVGROUP_STATUS); } void result(const uint8_t* data, int len) { if (len < 9) return; quint64 id = 0; for (int i = 0; i < 8; ++i) id |= quint64(data[i]) << (8*i); if (!pending.contains(id)) return; const int action = pending.take(id); if (!data[8]) { status->setText(tr("Operation failed. Check connection and invitation status.")); return; } const auto json = QJsonDocument::fromJson(QByteArray(reinterpret_cast(data+9), len-9)).object(); if (action == CHAT_DEVGROUP_INVITE) { outgoingId = json["id"].toString(); pinText = tr("PIN: %1 — enter it on the other device within 2 minutes.").arg(json["pin"].toString()); status->setText(pinText); return; } title->setText(json["group"].toString().isEmpty() ? tr("No device group") : json["name"].toString()); const QString selected = list->currentItem() ? list->currentItem()->data(Qt::UserRole).toString() : QString(); list->clear(); const QStringList states = {{}, tr("Waiting for PIN"), tr("Checking PIN"), tr("Transferring keys"), tr("Joined"), tr("Rejected"), tr("Expired"), tr("Result unknown")}; for (const auto value : json["invitations"].toArray()) { const auto invitation = value.toObject(); const int state = invitation["state"].toInt(); if (inviting && invitation["id"].toString() == outgoingId) { status->setText((state < 4 ? pinText + "\n" : QString()) + states.value(state)); } auto* item = new QListWidgetItem(invitation["peer"].toString() + " — " + states.value(state), list); if (!invitation["outgoing"].toInt() && state == 1) { item->setData(Qt::UserRole, invitation["id"].toString()); if (item->data(Qt::UserRole).toString() == selected) list->setCurrentItem(item); } } } private: void send(chat_devgroup_action action, const QString& text = {}, const QString& pin = {}) { const auto bytes = text.toUtf8(), code = pin.toUtf8(); if (bytes.size() >= 128 || bytes.contains('\0') || code.size() >= 5 || code.contains('\0')) { status->setText(tr("Invalid name or PIN")); return; } auto* req = static_cast(u_calloc(1, sizeof(chat_devgroup_request))); if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: allocation failed"); return; } req->inst = gui_bridge_get_inst(); req->request_id = ++nextRequest; req->group = channel.toULongLong(); req->peer = peer; req->action = action; memcpy(req->text, bytes.constData(), bytes.size()); memcpy(req->pin, code.constData(), code.size()); const quint64 id = req->request_id; if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) { memset(req, 0, sizeof(*req)); u_free(req); status->setText(tr("Core unavailable")); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: core unavailable"); return; } pending.insert(id, action); } QString channel, outgoingId, pinText; quint64 peer; bool inviting; QLabel *title, *status; QLineEdit* name; QListWidget* list; QHash pending; }; static void devgroupResult(const uint8_t* data, int len) { if (!len) { refreshInvitations(); for (const auto& button : buttons) if (button) button->setText(QCoreApplication::translate("AccountList", "My devices") + " •"); if (active) active->refresh(); } else { if (len >= 9) { quint64 id = 0; for (int i = 0; i < 8; ++i) id |= quint64(data[i]) << (8*i); const bool automatic = id == statusRequest || acceptRequests.contains(id); if (id == statusRequest) statusRequest = 0; if (acceptRequests.contains(id)) awaitingPins.remove(acceptRequests.take(id)); if (automatic) { if (data[8] && id >= lastAutomaticSnapshot) { lastAutomaticSnapshot = id; automaticInvitations(QJsonDocument::fromJson(QByteArray(reinterpret_cast(data+9), len-9)).object()); } else if (!data[8]) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: automatic PIN/status request refused"); } } if (active) active->result(data, len); } } void watchDevgroupButton(QPushButton* button) { buttons.erase(std::remove_if(buttons.begin(), buttons.end(), [](const QPointer& b) { return b.isNull(); }), buttons.end()); buttons.append(button); gui_bridge_set_devgroup_result_cb(devgroupResult); } void showDevgroupDialog(QWidget* parent, const QString& channel, quint64 peer) { if (active) { active->close(); active = nullptr; } gui_bridge_set_devgroup_result_cb(devgroupResult); active = new DevgroupDialog(parent, channel, peer); active->show(); } static void refreshInvitations() { if (!invitationWindow || statusRequest || !gui_bridge_is_uasync_ready()) return; auto* req = static_cast(u_calloc(1, sizeof(chat_devgroup_request))); if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: status allocation failed"); return; } req->inst = gui_bridge_get_inst(); req->action = CHAT_DEVGROUP_STATUS; req->request_id = ++nextRequest; const quint64 id = req->request_id; if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) { u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: status enqueue failed"); return; } statusRequest = id; } static void automaticInvitations(const QJsonObject& snapshot) { if (!invitationWindow) return; QSet waiting; for (const auto value : snapshot["invitations"].toArray()) { const auto entry = value.toObject(); const QString id = entry["id"].toString(); if (entry["outgoing"].toInt() || entry["state"].toInt() != 1) continue; waiting.insert(id); if (pinDialogs.value(id) || dismissedInvites.contains(id) || awaitingPins.contains(id)) continue; auto* dialog = new QInputDialog(invitationWindow); pinDialogs.insert(id, dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setObjectName("devgroupIncomingPin"); dialog->setWindowTitle(DevgroupDialog::tr("Add my device")); dialog->setLabelText(entry["peer"].toString() + "\n" + DevgroupDialog::tr(attemptedPins.contains(id) ? "PIN was not accepted. Try again." : "PIN shown on the inviting device")); dialog->setInputMode(QInputDialog::TextInput); dialog->setWindowModality(Qt::ApplicationModal); if (auto* edit = dialog->findChild()) { edit->setMaxLength(4); edit->setValidator(new QRegularExpressionValidator(QRegularExpression("[0-9]{0,4}"), edit)); } QObject::connect(dialog, &QInputDialog::textValueChanged, dialog, [dialog](const QString& text) { if (auto* box = dialog->findChild()) box->button(QDialogButtonBox::Ok)->setEnabled(text.size() == 4); }); QObject::connect(dialog, &QDialog::finished, dialog, [dialog, id](int result) { pinDialogs.remove(id); if (result != QDialog::Accepted) { dismissedInvites.insert(id); return; } auto* req = static_cast(u_calloc(1, sizeof(chat_devgroup_request))); if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: PIN request allocation failed"); return; } const auto pin = dialog->textValue().toLatin1(), invite = id.toLatin1(); if (pin.size() != 4 || invite.size() != 32) { u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: invalid PIN request"); return; } req->inst = gui_bridge_get_inst(); req->action = CHAT_DEVGROUP_ACCEPT; req->request_id = ++nextRequest; memcpy(req->text, invite.constData(), 32); memcpy(req->pin, pin.constData(), 4); const quint64 request = req->request_id; if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) { memset(req, 0, sizeof(*req)); u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: PIN enqueue failed"); return; } awaitingPins.insert(id); attemptedPins.insert(id); acceptRequests.insert(request, id); DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: PIN submitted invite=%s", qPrintable(id)); }); dialog->open(); if (auto* box = dialog->findChild()) box->button(QDialogButtonBox::Ok)->setEnabled(false); DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: incoming PIN dialog opened invite=%s", qPrintable(id)); } for (const auto& id : pinDialogs.keys()) if (!waiting.contains(id)) { auto dialog = pinDialogs.take(id); if (dialog) { dialog->disconnect(); dialog->close(); } } } void initDevgroupUi(QWidget* window) { if (invitationWindow) return; statusRequest = 0; lastAutomaticSnapshot = 0; acceptRequests.clear(); dismissedInvites.clear(); awaitingPins.clear(); attemptedPins.clear(); invitationWindow = window; gui_bridge_set_devgroup_result_cb(devgroupResult); auto* timer = new QTimer(window); timer->setInterval(1000); QObject::connect(timer, &QTimer::timeout, window, [] { if (!gui_bridge_is_uasync_ready()) { statusRequest = 0; lastAutomaticSnapshot = 0; acceptRequests.clear(); awaitingPins.clear(); attemptedPins.clear(); dismissedInvites.clear(); automaticInvitations(QJsonObject{{"invitations", QJsonArray{}}}); return; } refreshInvitations(); }); timer->start(); refreshInvitations(); } void inviteMyDevice(QWidget* parent, const QString& channel, quint64 peer) { if (active) { active->close(); active = nullptr; } gui_bridge_set_devgroup_result_cb(devgroupResult); active = new DevgroupDialog(parent, channel, peer, true); active->show(); }