255 lines
14 KiB
C++
255 lines
14 KiB
C++
#include <QCoreApplication>
|
|
#include "devgroupdialog.h"
|
|
#include "../transport/gui_bridge.h"
|
|
#include "debug_ui.h"
|
|
#include <QDialog>
|
|
#include <QVBoxLayout>
|
|
#include <QLabel>
|
|
#include <QPushButton>
|
|
#include <QLineEdit>
|
|
#include <QInputDialog>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QTimer>
|
|
#include <QPointer>
|
|
#include <QHash>
|
|
#include <QMessageBox>
|
|
#include <QSet>
|
|
#include <QDialogButtonBox>
|
|
#include <QRegularExpressionValidator>
|
|
extern "C" {
|
|
#include "../../../src/chat/chat_devgroup.h"
|
|
#include "../../../lib/mem.h"
|
|
}
|
|
class DevgroupDialog;
|
|
static QPointer<DevgroupDialog> active;
|
|
static QHash<quint64, QPointer<QPushButton>> leaveRequests;
|
|
static QHash<quint64, QPointer<QWidget>> renameRequests;
|
|
static quint64 nextRequest = 0;
|
|
static QPointer<QWidget> invitationWindow;
|
|
static quint64 statusRequest = 0, lastAutomaticSnapshot = 0;
|
|
static QJsonObject invitationSnapshot;
|
|
static void refreshInvitations();
|
|
static void devgroupResult(const uint8_t* data, int len);
|
|
static bool pendingState(int state) { return state == 1 || state == 2 || state == 3 || state == 8 || state == 9 || state == 10; }
|
|
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, const QString& id = {})
|
|
: QDialog(parent), channel(channel), outgoingId(id), peer(peer) {
|
|
setAttribute(Qt::WA_DeleteOnClose); setObjectName("devgroupPairingDialog"); setModal(true);
|
|
setWindowTitle(tr("Linking devices")); resize(480, 280);
|
|
auto* layout = new QVBoxLayout(this);
|
|
status = new QLabel(tr("Waiting for PIN"), this); status->setWordWrap(true);
|
|
status->setTextFormat(Qt::PlainText);
|
|
status->setTextInteractionFlags(Qt::TextSelectableByMouse); layout->addWidget(status);
|
|
pin = new QLineEdit(this); pin->setObjectName("devgroupPinInput"); pin->setMaxLength(4);
|
|
pin->setValidator(new QRegularExpressionValidator(QRegularExpression("[0-9]{0,4}"), pin)); layout->addWidget(pin);
|
|
submit = new QPushButton(tr("Check PIN"), this); submit->setObjectName("devgroupCheckPin"); layout->addWidget(submit);
|
|
connect(pin, &QLineEdit::textChanged, this, [this] { submit->setEnabled(input && !accepting && pin->text().size() == 4); });
|
|
connect(submit, &QPushButton::clicked, this, [this] {
|
|
if (!input || accepting || pin->text().size() != 4) return;
|
|
send(CHAT_DEVGROUP_ACCEPT); pin->clear();
|
|
});
|
|
auto* close = new QDialogButtonBox(QDialogButtonBox::Close, this); layout->addWidget(close);
|
|
connect(close, &QDialogButtonBox::rejected, this, &QDialog::close);
|
|
auto* timer = new QTimer(this); timer->setInterval(1000);
|
|
connect(timer, &QTimer::timeout, this, [this] {
|
|
if (!gui_bridge_is_uasync_ready()) { pending.clear(); status->setText(tr("Core unavailable")); }
|
|
else if (!pending.values().contains(CHAT_DEVGROUP_STATUS)) refresh();
|
|
});
|
|
timer->start();
|
|
if (id.isEmpty()) { pin->hide(); submit->hide(); send(CHAT_DEVGROUP_INVITE); }
|
|
else update(invitationSnapshot);
|
|
}
|
|
void update(const QJsonObject& snapshot) {
|
|
for (const auto value : snapshot["invitations"].toArray()) {
|
|
const auto invitation = value.toObject(); if (invitation["id"].toString() != outgoingId) continue;
|
|
const int state = invitation["state"].toInt(), nextInput = invitation["input"].toInt();
|
|
const bool outgoing = invitation["outgoing"].toInt() != 0;
|
|
const QString shownPin = invitation["pin"].toString();
|
|
const QStringList states = {{}, tr("Waiting for PIN"), tr("Checking PIN"), tr("Transferring keys"), tr("Joined"),
|
|
tr("Rejected"), tr("Expired"), tr("Result unknown"), tr("Waiting for return PIN"), tr("Checking PIN"), tr("Transferring keys")};
|
|
QString text = invitation["peer"].toString() + "\n\n";
|
|
if (!outgoing) text += tr("Linking joins this device to your personal device subgroup and gives it the shared subgroup keys.") +
|
|
"\n\n" + tr("If you are not linking your own personal devices, simply close this window.") + "\n\n";
|
|
if (!shownPin.isEmpty()) text += (outgoing ? tr("PIN: %1 — enter it on the other device within 2 minutes.") :
|
|
tr("Return PIN: %1 — enter it on the device that started linking." )).arg(shownPin) + "\n\n";
|
|
if (nextInput) text += tr(nextInput == 1 ? "PIN shown on the inviting device" : "Enter the return PIN shown on the other device") + "\n";
|
|
if (invitation["pin_error"].toInt()) text += tr("PIN was not accepted. Try again.") + "\n";
|
|
status->setText(text + states.value(state));
|
|
if (input != nextInput) pin->clear();
|
|
input = nextInput; pin->setVisible(input); submit->setVisible(input);
|
|
pin->setEnabled(!accepting); submit->setEnabled(input && !accepting && pin->text().size() == 4);
|
|
return;
|
|
}
|
|
input = 0; pin->hide(); submit->hide(); status->setText(tr("Expired"));
|
|
}
|
|
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 (action == CHAT_DEVGROUP_ACCEPT) accepting = false;
|
|
if (!data[8]) { status->setText(tr("Operation failed. Check connection and invitation status.")); pin->setEnabled(true); return; }
|
|
const auto json = QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data+9), len-9)).object();
|
|
if (action == CHAT_DEVGROUP_INVITE) {
|
|
outgoingId = json["id"].toString();
|
|
status->setText(tr("PIN: %1 — enter it on the other device within 2 minutes.").arg(json["pin"].toString())); refresh(); return;
|
|
}
|
|
update(json);
|
|
}
|
|
private:
|
|
void send(chat_devgroup_action action) {
|
|
auto* req = static_cast<chat_devgroup_request*>(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; const quint64 id = req->request_id;
|
|
if (action == CHAT_DEVGROUP_ACCEPT) {
|
|
const auto invitation = outgoingId.toLatin1(), entered = pin->text().toLatin1();
|
|
memcpy(req->text, invitation.constData(), qMin(invitation.size(), qsizetype(32))); memcpy(req->pin, entered.constData(), 4);
|
|
}
|
|
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;
|
|
}
|
|
if (action == CHAT_DEVGROUP_ACCEPT) {
|
|
accepting = true; pin->setEnabled(false); submit->setEnabled(false);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: PIN submitted invite=%s step=%d", qPrintable(outgoingId), input);
|
|
}
|
|
pending.insert(id, action);
|
|
}
|
|
QString channel, outgoingId; quint64 peer;
|
|
QLabel* status; QLineEdit* pin; QPushButton* submit;
|
|
int input = 0; bool accepting = false;
|
|
QHash<quint64, int> pending;
|
|
};
|
|
static void devgroupResult(const uint8_t* data, int len) {
|
|
if (!len) {
|
|
refreshInvitations();
|
|
if (active) active->refresh();
|
|
} else {
|
|
if (len >= 9) {
|
|
quint64 id = 0; for (int i = 0; i < 8; ++i) id |= quint64(data[i]) << (8*i);
|
|
if (renameRequests.contains(id)) {
|
|
const auto parent = renameRequests.take(id);
|
|
if (!data[8]) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: rename refused");
|
|
if (parent) QMessageBox::warning(parent, QCoreApplication::translate("ChatGui", "Rename"),
|
|
QCoreApplication::translate("ChatGui", "Could not rename device group"));
|
|
} else DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: group renamed");
|
|
return;
|
|
}
|
|
if (leaveRequests.contains(id)) {
|
|
const auto button = leaveRequests.take(id);
|
|
if (!data[8]) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: disconnect refused");
|
|
if (button) { button->setEnabled(true); QMessageBox::warning(button->window(),
|
|
QCoreApplication::translate("ChatGui", "Disconnect from my devices"),
|
|
DevgroupDialog::tr("Operation failed. Check connection and invitation status.")); }
|
|
} else DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: own device disconnected");
|
|
return;
|
|
}
|
|
const bool automatic = id == statusRequest;
|
|
if (id == statusRequest) statusRequest = 0;
|
|
if (automatic) {
|
|
if (data[8] && id >= lastAutomaticSnapshot) {
|
|
lastAutomaticSnapshot = id;
|
|
invitationSnapshot = QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data+9), len-9)).object();
|
|
if (active) active->update(invitationSnapshot);
|
|
}
|
|
else if (!data[8]) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: automatic PIN/status request refused");
|
|
}
|
|
}
|
|
if (active) active->result(data, len);
|
|
}
|
|
}
|
|
static void refreshInvitations() {
|
|
if (!invitationWindow || statusRequest || !gui_bridge_is_uasync_ready()) return;
|
|
auto* req = static_cast<chat_devgroup_request*>(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;
|
|
}
|
|
bool hasPendingDevicePairing() {
|
|
if (!gui_bridge_is_uasync_ready()) return false;
|
|
for (const auto value : invitationSnapshot["invitations"].toArray()) if (pendingState(value.toObject()["state"].toInt())) return true;
|
|
return false;
|
|
}
|
|
void finishDevicePairing(QWidget* parent) {
|
|
QStringList labels, ids;
|
|
for (const auto value : invitationSnapshot["invitations"].toArray()) {
|
|
const auto entry = value.toObject(); if (!pendingState(entry["state"].toInt())) continue;
|
|
ids << entry["id"].toString(); labels << entry["peer"].toString() + " / " + entry["id"].toString().left(8);
|
|
}
|
|
if (ids.isEmpty() || !gui_bridge_is_uasync_ready()) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: no pending pairing"); return; }
|
|
QString id = ids.first();
|
|
if (ids.size() > 1) {
|
|
bool ok = false;
|
|
const QString choice = QInputDialog::getItem(parent, DevgroupDialog::tr("Linking devices"),
|
|
DevgroupDialog::tr("Select device"), labels, 0, false, &ok);
|
|
if (!ok || !labels.contains(choice)) return;
|
|
id = ids.at(labels.indexOf(choice));
|
|
}
|
|
if (active) active->close();
|
|
active = new DevgroupDialog(parent, {}, 0, id); active->show(); active->refresh();
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: pairing opened from self menu invite=%s", qPrintable(id));
|
|
}
|
|
void initDevgroupUi(QWidget* window) {
|
|
if (invitationWindow) return;
|
|
statusRequest = 0; lastAutomaticSnapshot = 0; invitationSnapshot = {};
|
|
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; invitationSnapshot = {};
|
|
if (active) active->update(invitationSnapshot);
|
|
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); active->show();
|
|
}
|
|
|
|
void disconnectMyDevice(QPushButton* button) {
|
|
auto* req = static_cast<chat_devgroup_request*>(u_calloc(1, sizeof(chat_devgroup_request)));
|
|
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: disconnect allocation failed"); return; }
|
|
req->inst = gui_bridge_get_inst(); req->action = CHAT_DEVGROUP_LEAVE; req->request_id = ++nextRequest;
|
|
const quint64 id = req->request_id; gui_bridge_set_devgroup_result_cb(devgroupResult);
|
|
if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) {
|
|
u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: disconnect enqueue failed");
|
|
QMessageBox::warning(button->window(), QCoreApplication::translate("ChatGui", "Disconnect from my devices"),
|
|
DevgroupDialog::tr("Core unavailable")); return;
|
|
}
|
|
leaveRequests.insert(id, button); button->setEnabled(false);
|
|
}
|
|
|
|
void renameMyDeviceGroup(QWidget* parent, const QString& name) {
|
|
const auto bytes = name.toUtf8();
|
|
if (bytes.isEmpty() || bytes.size() > 63 || bytes.contains('\0')) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: rename invalid name bytes=%lld", (long long)bytes.size()); return;
|
|
}
|
|
auto* req = static_cast<chat_devgroup_request*>(u_calloc(1, sizeof(chat_devgroup_request)));
|
|
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup UI: rename allocation failed"); return; }
|
|
req->inst = gui_bridge_get_inst(); req->action = CHAT_DEVGROUP_RENAME; req->request_id = ++nextRequest;
|
|
memcpy(req->text, bytes.constData(), bytes.size());
|
|
const quint64 id = req->request_id; gui_bridge_set_devgroup_result_cb(devgroupResult);
|
|
if (gui_bridge_post_uasync_owned_fn(chat_devgroup_trampoline, req) < 0) {
|
|
u_free(req); DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup UI: rename enqueue failed");
|
|
QMessageBox::warning(parent, QCoreApplication::translate("ChatGui", "Rename"), DevgroupDialog::tr("Core unavailable")); return;
|
|
}
|
|
renameRequests.insert(id, parent);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup UI: rename submitted name_bytes=%lld", (long long)bytes.size());
|
|
}
|