diff --git a/qtox.pro b/qtox.pro index 5a84dc9ef..aad755409 100644 --- a/qtox.pro +++ b/qtox.pro @@ -149,7 +149,10 @@ HEADERS += src/widget/form/addfriendform.h \ src/widget/form/setpassworddialog.h \ src/widget/form/tabcompleter.h \ src/video/videoframe.h \ - src/misc/flowlayout.h + src/misc/flowlayout.h \ + src/ipc.h \ + src/widget/toxuri.h \ + src/toxdns.h SOURCES += \ src/widget/form/addfriendform.cpp \ @@ -208,4 +211,7 @@ SOURCES += \ src/video/netvideosource.cpp \ src/widget/form/tabcompleter.cpp \ src/video/videoframe.cpp \ - src/misc/flowlayout.cpp + src/misc/flowlayout.cpp \ + src/widget/toxuri.cpp \ + src/toxdns.cpp \ + src/ipc.cpp diff --git a/src/core.cpp b/src/core.cpp index b77d33d0a..d0b735441 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -47,7 +47,7 @@ QList Core::fileSendQueue; QList Core::fileRecvQueue; Core::Core(Camera* cam, QThread *coreThread, QString loadPath) : - tox(nullptr), camera(cam), loadPath(loadPath) + tox(nullptr), camera(cam), loadPath(loadPath), ready{false} { qDebug() << "Core: loading Tox from" << loadPath; @@ -297,6 +297,8 @@ void Core::start() else qDebug() << "Core: Error loading self avatar"; + ready = true; + process(); // starts its own timer } @@ -1353,8 +1355,9 @@ void Core::switchConfiguration(const QString& profile) saveConfiguration(); clearPassword(ptMain); clearPassword(ptHistory); + + ready = false; toxTimer->stop(); - Widget::getInstance()->setEnabledThreadsafe(false); if (tox) { toxav_kill(toxav); @@ -1747,3 +1750,8 @@ QString Core::getPeerName(const ToxID& id) const delete[] cname; return name; } + +bool Core::isReady() +{ + return ready; +} diff --git a/src/core.h b/src/core.h index e664426ec..2ad0f7ad7 100644 --- a/src/core.h +++ b/src/core.h @@ -69,6 +69,7 @@ public: bool anyActiveCalls(); ///< true is any calls are currently active (note: a call about to start is not yet active) bool isPasswordSet(PasswordType passtype); + bool isReady(); ///< Most of the API shouldn't be used until Core is ready, call start() first public slots: void start(); ///< Initializes the core, must be called before anything else @@ -260,6 +261,7 @@ private: static QList fileSendQueue, fileRecvQueue; static ToxCall calls[]; QMutex fileSendMutex; + bool ready; uint8_t* pwsaltedkeys[PasswordType::ptCounter]; // use the pw's hash as the "pw" diff --git a/src/corestructs.cpp b/src/corestructs.cpp index 97030e2d1..9a6c17416 100644 --- a/src/corestructs.cpp +++ b/src/corestructs.cpp @@ -1,6 +1,10 @@ #include "src/corestructs.h" #include "src/core.h" +#include #include +#include + +#define TOX_ID_LENGTH 2*TOX_FRIEND_ADDRESS_SIZE ToxFile::ToxFile(int FileNum, int FriendId, QByteArray FileName, QString FilePath, FileDirection Direction) : fileNum(FileNum), friendId(FriendId), fileName{FileName}, filePath{FilePath}, file{new QFile(filePath)}, @@ -48,3 +52,9 @@ bool ToxID::isMine() const { return *this == Core::getInstance()->getSelfId(); } + +bool ToxID::isToxId(const QString& value) +{ + const QRegularExpression hexRegExp("^[A-Fa-f0-9]+$"); + return value.length() == TOX_ID_LENGTH && value.contains(hexRegExp); +} diff --git a/src/corestructs.h b/src/corestructs.h index 05a2ab3e2..82848695e 100644 --- a/src/corestructs.h +++ b/src/corestructs.h @@ -21,7 +21,8 @@ struct ToxID QString checkSum; QString toString() const; - ToxID static fromString(QString id); + static ToxID fromString(QString id); + static bool isToxId(const QString& id); bool operator==(const ToxID& other) const; bool operator!=(const ToxID& other) const; diff --git a/src/ipc.cpp b/src/ipc.cpp new file mode 100644 index 000000000..9da3f5d91 --- /dev/null +++ b/src/ipc.cpp @@ -0,0 +1,254 @@ +#include "src/ipc.h" +#include +#include + +IPC::IPC() : + globalMemory{"qtox"} +{ + qRegisterMetaType("IPCEventHandler"); + + ownerTimer.setInterval(EVENT_TIMER_MS); + ownerTimer.setSingleShot(true); + connect(&ownerTimer, &QTimer::timeout, this, &IPC::processEvents); + + // The first started instance gets to manage the shared memory by taking ownership + // Every time it processes events it updates the global shared timestamp + // If the timestamp isn't updated, that's a timeout and someone else can take ownership + // This is a safety measure, in case one of the clients crashes + // If the owner exits normally, it can set the timestamp to 0 first to immediately give ownership + // We keep one shared page, starting with the 64bit ID of the current owner and last timestamp + // then various events to be processed by the owner with a 16bit size then data each + // Each event is in its own chunk of data, the last chunk is followed by a chunk of size 0 + + qsrand(time(0)); + globalId = ((uint64_t)qrand()) * ((uint64_t)qrand()) * ((uint64_t)qrand()); + qDebug() << "IPC: Our global ID is "<= OWNERSHIP_TIMEOUT_S) + { + qDebug() << "IPC: Previous owner timed out, taking ownership"; + *(uint64_t*)globalMemory.data() = globalId; + } + else + { + goto unlockAndRestartTimer; + } + } + + // We're the owner, let's process those events + forever { + QByteArray eventData = fetchEvent(); + if (eventData.isEmpty()) + break; + + qDebug() << "IPC: Processing event: "<= 65535) + { + qWarning() << "IPC: sendEvent: Too much data for a single chunk, giving up"; + return 0; + } + + if (globalMemory.lock()) + { + // Check that we have enough room for that new chunk + char* nextChunk = getFirstFreeChunk(); + if (nextChunk == nullptr + || nextChunk + 2 + dataSize + 2 - (char*)globalMemory.data() >= MEMORY_SIZE) + { + qWarning() << "IPC: sendEvent: Not enough memory left, giving up"; + return 0; + } + + // Commit the new chunk to shared memory + *(uint16_t*)nextChunk = dataSize; + memcpy(nextChunk+2, data.data(), dataSize); + *(uint16_t*)(nextChunk+2+dataSize) = 0; + + globalMemory.unlock(); + qDebug() << "IPC: Posted event: "<= MEMORY_SIZE) + return nullptr; + + ptr += chunkSize; + } + return nullptr; // Never reached +} + +char* IPC::getLastUsedChunk() +{ + char* ptr = (char*)globalMemory.data() + MEMORY_HEADER_SIZE; + char* lastPtr = nullptr; + + forever + { + uint16_t chunkSize = *(uint16_t*)ptr; + + if (!chunkSize) + return lastPtr; + + if ((ptr + chunkSize + 2) - (char*)globalMemory.data() > MEMORY_SIZE) + return lastPtr; + + lastPtr = ptr; + ptr += chunkSize; + } + return nullptr; // Never reached +} + +QByteArray IPC::fetchEvent() +{ + QByteArray eventData; + + // Get a pointer to the last chunk + char* nextChunk = getLastUsedChunk(); + if (nextChunk == nullptr) + return eventData; + + // Read that chunk and remove it from memory + uint16_t dataSize = *(uint16_t*)nextChunk; + *(uint16_t*)nextChunk = 0; + eventData.resize(dataSize); + memcpy(eventData.data(), nextChunk+2, dataSize); + + return eventData; +} + +void IPC::updateGlobalTimestamp() +{ + *(time_t*)((char*)globalMemory.data()+sizeof(globalId)) = time(0); +} + +time_t IPC::getGlobalTimestamp() +{ + return *(time_t*)((char*)globalMemory.data()+sizeof(globalId)); +} + +bool IPC::isEventProcessed(time_t postTime) +{ + return (difftime(lastSeenTimestamp, postTime) > 0); +} + +void IPC::waitUntilProcessed(time_t postTime) +{ + while (difftime(lastSeenTimestamp, postTime) <= 0) + qApp->processEvents(); +} + +void IPC::runEventHandler(IPCEventHandler handler, const QByteArray& arg) +{ + if (QThread::currentThread() != qApp->thread()) + { + QMetaObject::invokeMethod(this, "runEventHandler", Qt::BlockingQueuedConnection, + Q_ARG(IPCEventHandler, handler), Q_ARG(const QByteArray&, arg)); + return; + } + else + { + handler(arg); + return; + } +} diff --git a/src/ipc.h b/src/ipc.h new file mode 100644 index 000000000..30b33207b --- /dev/null +++ b/src/ipc.h @@ -0,0 +1,67 @@ +#ifndef IPC_H +#define IPC_H + +#include +#include +#include +#include +#include +#include +#include + +/// Handles an IPC event, must filter out and ignore events it doesn't recognize +using IPCEventHandler = std::function; + +/// Class used for inter-process communication with other qTox instances +/// IPC event handlers will be called from the GUI thread after its event loop starts +class IPC : public QThread +{ + Q_OBJECT + +public: + IPC(); + ~IPC(); + /// Posts an event to the global shared memory, returns the time at wich it was posted + time_t postEvent(const QByteArray& data); + bool isCurrentOwner(); ///< Returns whether we're responsible for event processing of the global shared memory + void registerEventHandler(IPCEventHandler handler); ///< Registers a function to be called whenever an event is received + bool isEventProcessed(time_t postTime); ///< Returns wether a previously posted event was already processed + void waitUntilProcessed(time_t postTime); ///< Blocks until a previously posted event is processed + +protected slots: + void processEvents(); + +private: + /// Runs an IPC event handler from the main (GUI) thread, will block until the handler returns + Q_INVOKABLE void runEventHandler(IPCEventHandler handler, const QByteArray& arg); + /// Assumes that the memory IS LOCKED + /// Returns a pointer to the first free chunk of shared memory or a nullptr on error + char* getFirstFreeChunk(); + /// Assumes that the memory IS LOCKED + /// Returns a pointer to the last used chunk of shared memory or a nullptr on error + char* getLastUsedChunk(); + /// Assumes that the memory IS LOCKED + /// Removes the last event from the shared memory and returns its data, or an empty object on error + QByteArray fetchEvent(); + /// Assumes that the memory IS LOCKED + /// Updates the global shared timestamp + void updateGlobalTimestamp(); + /// Assumes that the memory IS LOCKED + /// Returns the global shared timestamp + time_t getGlobalTimestamp(); + +private: + QSharedMemory globalMemory; + uint64_t globalId; + QTimer ownerTimer; + QVector eventHandlers; + time_t lastSeenTimestamp; + + // Constants + static const int MEMORY_SIZE = 4096; + static const int MEMORY_HEADER_SIZE = sizeof(globalId) + sizeof(time_t); + static const int EVENT_TIMER_MS = 1000; + static const int OWNERSHIP_TIMEOUT_S = 5; +}; + +#endif // IPC_H diff --git a/src/main.cpp b/src/main.cpp index 31f65c26f..96e689408 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,8 @@ #include "widget/widget.h" #include "misc/settings.h" +#include "src/ipc.h" +#include "src/widget/toxuri.h" #include #include #include @@ -77,8 +79,35 @@ int main(int argc, char *argv[]) // Install Unicode 6.1 supporting font QFontDatabase::addApplicationFont("://DejaVuSans.ttf"); - Widget* w = Widget::getInstance(); + // Inter-process communication + IPC ipc; + ipc.registerEventHandler(&toxURIEventHandler); + // Process arguments + if (argc >= 2) + { + QString firstParam(argv[1]); + // Tox URIs. If there's already another qTox instance running, we ask it to handle the URI and we exit + // Otherwise we start a new qTox instance and process it ourselves + if (firstParam.startsWith("tox:")) + { + if (ipc.isCurrentOwner()) // Don't bother sending an event if we're going to process it ourselves + { + handleToxURI(firstParam.toUtf8()); + } + else + { + time_t event = ipc.postEvent(firstParam.toUtf8()); + ipc.waitUntilProcessed(event); + // If someone else processed it, we're done here, no need to actually start qTox + if (!ipc.isCurrentOwner()) + return EXIT_SUCCESS; + } + } + } + + // Run + Widget* w = Widget::getInstance(); int errorcode = a.exec(); delete w; diff --git a/src/toxdns.cpp b/src/toxdns.cpp new file mode 100644 index 000000000..385b74bc8 --- /dev/null +++ b/src/toxdns.cpp @@ -0,0 +1,244 @@ +#include "src/toxdns.h" +#include "src/misc/cdata.h" +#include +#include +#include +#include +#include +#include + +#define TOX_ID_LENGTH 2*TOX_FRIEND_ADDRESS_SIZE + +const ToxDNS::tox3_server ToxDNS::pinnedServers[] +{ + {"toxme.se", (uint8_t[32]){0x5D, 0x72, 0xC5, 0x17, 0xDF, 0x6A, 0xEC, 0x54, 0xF1, 0xE9, 0x77, 0xA6, 0xB6, 0xF2, 0x59, 0x14, + 0xEA, 0x4C, 0xF7, 0x27, 0x7A, 0x85, 0x02, 0x7C, 0xD9, 0xF5, 0x19, 0x6D, 0xF1, 0x7E, 0x0B, 0x13}}, + {"utox.org", (uint8_t[32]){0xD3, 0x15, 0x4F, 0x65, 0xD2, 0x8A, 0x5B, 0x41, 0xA0, 0x5D, 0x4A, 0xC7, 0xE4, 0xB3, 0x9C, 0x6B, + 0x1C, 0x23, 0x3C, 0xC8, 0x57, 0xFB, 0x36, 0x5C, 0x56, 0xE8, 0x39, 0x27, 0x37, 0x46, 0x2A, 0x12}} +}; + +void ToxDNS::showWarning(const QString &message) +{ + QMessageBox warning; + warning.setWindowTitle("Tox"); + warning.setText(message); + warning.setIcon(QMessageBox::Warning); + warning.exec(); +} + +QByteArray ToxDNS::fetchLastTextRecord(const QString& record, bool silent) +{ + QByteArray result; + + QDnsLookup dns; + dns.setType(QDnsLookup::TXT); + dns.setName(record); + dns.lookup(); + + int timeout; + for (timeout = 0; timeout<30 && !dns.isFinished(); ++timeout) + { + qApp->processEvents(); + QThread::msleep(100); + } + if (timeout >= 30) { + dns.abort(); + if (!silent) + showWarning(tr("The connection timed out","The DNS gives the Tox ID associated to toxme.se addresses")); + return result; + } + + if (dns.error() == QDnsLookup::NotFoundError) { + if (!silent) + showWarning(tr("This address does not exist","The DNS gives the Tox ID associated to toxme.se addresses")); + return result; + } + else if (dns.error() != QDnsLookup::NoError) { + if (!silent) + showWarning(tr("Error while looking up DNS","The DNS gives the Tox ID associated to toxme.se addresses")); + return result; + } + + const QList textRecords = dns.textRecords(); + if (textRecords.isEmpty()) { + if (!silent) + showWarning(tr("No text record found", "Error with the DNS")); + return result; + } + + const QList textRecordValues = textRecords.last().values(); + if (textRecordValues.length() != 1) { + if (!silent) + showWarning(tr("Unexpected number of values in text record", "Error with the DNS")); + return result; + } + + result = textRecordValues.first(); + return result; +} + +QString ToxDNS::queryTox1(const QString& record, bool silent) +{ + QString realRecord = record, toxId; + realRecord.replace("@", "._tox."); + const QString entry = fetchLastTextRecord(realRecord, silent); + if (entry.isEmpty()) + return toxId; + + // Check toxdns protocol version + int verx = entry.indexOf("v="); + if (verx) { + verx += 2; + int verend = entry.indexOf(';', verx); + if (verend) + { + QString ver = entry.mid(verx, verend-verx); + if (ver != "tox1") + { + if (!silent) + showWarning(tr("The version of Tox DNS used by this server is not supported", "Error with the DNS")); + return toxId; + } + } + } + + // Get the tox id + int idx = entry.indexOf("id="); + if (idx < 0) { + if (!silent) + showWarning(tr("The DNS lookup does not contain any Tox ID", "Error with the DNS")); + return toxId; + } + + idx += 3; + if (entry.length() < idx + static_cast(TOX_ID_LENGTH)) { + if (!silent) + showWarning(tr("The DNS lookup does not contain a valid Tox ID", "Error with the DNS")); + return toxId; + } + + toxId = entry.mid(idx, TOX_ID_LENGTH); + if (!ToxID::isToxId(toxId)) { + if (!silent) + showWarning(tr("The DNS lookup does not contain a valid Tox ID", "Error with the DNS")); + return toxId; + } + + return toxId; +} + +QString ToxDNS::queryTox3(const tox3_server& server, const QString &record, bool silent) +{ + QByteArray nameData = record.left(record.indexOf('@')).toUtf8(), id, realRecord; + QString entry, toxIdStr; + int toxIdSize, idx, verx, dns_string_len; + const int dns_string_maxlen = 128; + + void* tox_dns3 = tox_dns3_new(server.pubkey); + if (!tox_dns3) + { + qWarning() << "queryTox3: failed to create a tox_dns3 object for "< +#include + +/// Handles tox1 and tox3 DNS queries +class ToxDNS : public QObject +{ +public: + struct tox3_server ///< Represents a tox3 server + { + tox3_server()=default; + tox3_server(const char* _name, uint8_t _pk[32]):name{_name},pubkey{_pk}{} + + const char* name; ///< Hostname of the server, e.g. toxme.se + uint8_t* pubkey; ///< Public key of the tox3 server, usually 256bit long + }; + +public: + /// Tries to map a text string to a ToxID struct, will query Tox DNS records if necessary + static ToxID resolveToxAddress(const QString& address, bool silent=true); + + static QString queryTox1(const QString& record, bool silent=true); ///< Record should look like user@domain.tld + static QString queryTox3(const tox3_server& server, const QString& record, bool silent=true); ///< Record should look like user@domain.tld, may fallback on queryTox1 + +protected: + static void showWarning(const QString& message); + ToxDNS()=default; + +private: + /// Try to fetch the first entry of the given TXT record + /// Returns an empty object on failure. May block for up to ~3s + /// May display message boxes on error if silent if false + static QByteArray fetchLastTextRecord(const QString& record, bool silent=true); + +public: + static const tox3_server pinnedServers[2]; +}; + +#endif // QTOXDNS_H diff --git a/src/widget/form/addfriendform.cpp b/src/widget/form/addfriendform.cpp index a6d244820..cae06b785 100644 --- a/src/widget/form/addfriendform.cpp +++ b/src/widget/form/addfriendform.cpp @@ -18,29 +18,14 @@ #include #include -#include #include #include "ui_mainwindow.h" #include "src/core.h" #include "src/misc/cdata.h" -#include "tox/toxdns.h" +#include "src/toxdns.h" -#include - -#define TOX_ID_LENGTH 2*TOX_FRIEND_ADDRESS_SIZE - -const AddFriendForm::tox3_server AddFriendForm::pinnedServers[] +AddFriendForm::AddFriendForm() { - {"toxme.se", (uint8_t[32]){0x5D, 0x72, 0xC5, 0x17, 0xDF, 0x6A, 0xEC, 0x54, 0xF1, 0xE9, 0x77, 0xA6, 0xB6, 0xF2, 0x59, 0x14, - 0xEA, 0x4C, 0xF7, 0x27, 0x7A, 0x85, 0x02, 0x7C, 0xD9, 0xF5, 0x19, 0x6D, 0xF1, 0x7E, 0x0B, 0x13}}, - {"utox.org", (uint8_t[32]){0xD3, 0x15, 0x4F, 0x65, 0xD2, 0x8A, 0x5B, 0x41, 0xA0, 0x5D, 0x4A, 0xC7, 0xE4, 0xB3, 0x9C, 0x6B, - 0x1C, 0x23, 0x3C, 0xC8, 0x57, 0xFB, 0x36, 0x5C, 0x56, 0xE8, 0x39, 0x27, 0x37, 0x46, 0x2A, 0x12}} -}; - -AddFriendForm::AddFriendForm() : dns(this) -{ - dns.setType(QDnsLookup::TXT); - main = new QWidget(), head = new QWidget(); QFont bold; bold.setBold(true); @@ -79,10 +64,10 @@ void AddFriendForm::show(Ui::MainWindow &ui) head->show(); } -bool AddFriendForm::isToxId(const QString &value) const +QString AddFriendForm::getMessage() const { - const QRegularExpression hexRegExp("^[A-Fa-f0-9]+$"); - return value.length() == TOX_ID_LENGTH && value.contains(hexRegExp); + const QString msg = message.toPlainText(); + return !msg.isEmpty() ? msg : message.placeholderText(); } void AddFriendForm::showWarning(const QString &message) const @@ -94,19 +79,13 @@ void AddFriendForm::showWarning(const QString &message) const warning.exec(); } -QString AddFriendForm::getMessage() const -{ - const QString msg = message.toPlainText(); - return !msg.isEmpty() ? msg : message.placeholderText(); -} - void AddFriendForm::onSendTriggered() { QString id = toxId.text().trimmed(); if (id.isEmpty()) { showWarning(tr("Please fill in a valid Tox ID","Tox ID of the friend you're sending a friend request to")); - } else if (isToxId(id)) { + } else if (ToxID::isToxId(id)) { if (id.toUpper() == Core::getInstance()->getSelfId().toString().toUpper()) showWarning(tr("You can't add yourself as a friend!","When trying to add your own Tox ID as friend")); else @@ -114,208 +93,16 @@ void AddFriendForm::onSendTriggered() this->toxId.clear(); this->message.clear(); } else { - // If we're querying one of our pinned server, do a tox3 request directly - QString servname = id.mid(id.indexOf('@')+1); - for (const AddFriendForm::tox3_server& pin : pinnedServers) + ToxID toxId = ToxDNS::resolveToxAddress(id, true); + + if (toxId.toString().isEmpty()) { - if (servname == pin.name) - { - queryTox3(pin, id); - return; - } + showWarning(tr("This Tox ID does not exist","DNS error")); + return; } - // Otherwise try tox3 if we can get a pubkey or fallback to tox1 - QByteArray pubkey = fetchLastTextRecord("_tox."+servname); - if (!pubkey.isEmpty()) - { - QByteArray servnameData = servname.toUtf8(); - AddFriendForm::tox3_server server; - server.name = servnameData.data(); - server.pubkey = (uint8_t*)pubkey.data(); - queryTox3(server, id); - } - else - { - queryTox1(id); - } + emit friendRequested(toxId.toString(), getMessage()); + this->toxId.clear(); + this->message.clear(); } } - -QByteArray AddFriendForm::fetchLastTextRecord(const QString& record, bool silent) -{ - QByteArray result; - - dns.setName(record); - dns.lookup(); - - int timeout; - for (timeout = 0; timeout<30 && !dns.isFinished(); ++timeout) - { - qApp->processEvents(); - QThread::msleep(100); - } - if (timeout >= 30) { - dns.abort(); - if (!silent) - showWarning(tr("The connection timed out","The DNS gives the Tox ID associated to toxme.se addresses")); - return result; - } - - if (dns.error() == QDnsLookup::NotFoundError) { - if (!silent) - showWarning(tr("This address does not exist","The DNS gives the Tox ID associated to toxme.se addresses")); - return result; - } - else if (dns.error() != QDnsLookup::NoError) { - if (!silent) - showWarning(tr("Error while looking up DNS","The DNS gives the Tox ID associated to toxme.se addresses")); - return result; - } - - const QList textRecords = dns.textRecords(); - if (textRecords.isEmpty()) { - if (!silent) - showWarning(tr("No text record found", "Error with the DNS")); - return result; - } - - const QList textRecordValues = textRecords.last().values(); - if (textRecordValues.length() != 1) { - if (!silent) - showWarning(tr("Unexpected number of values in text record", "Error with the DNS")); - return result; - } - - result = textRecordValues.first(); - return result; -} - -void AddFriendForm::queryTox1(const QString& record) -{ - QString realRecord = record; - realRecord.replace("@", "._tox."); - const QString entry = fetchLastTextRecord(realRecord, false); - if (entry.isEmpty()) - return; - - // Check toxdns protocol version - int verx = entry.indexOf("v="); - if (verx) { - verx += 2; - int verend = entry.indexOf(';', verx); - if (verend) - { - QString ver = entry.mid(verx, verend-verx); - if (ver != "tox1") - { - showWarning(tr("The version of Tox DNS used by this server is not supported", "Error with the DNS")); - return; - } - } - } - - // Get the tox id - int idx = entry.indexOf("id="); - if (idx < 0) { - showWarning(tr("The DNS lookup does not contain any Tox ID", "Error with the DNS")); - return; - } - - idx += 3; - if (entry.length() < idx + static_cast(TOX_ID_LENGTH)) { - showWarning(tr("The DNS lookup does not contain a valid Tox ID", "Error with the DNS")); - return; - } - - const QString friendAdress = entry.mid(idx, TOX_ID_LENGTH); - if (!isToxId(friendAdress)) { - showWarning(tr("The DNS lookup does not contain a valid Tox ID", "Error with the DNS")); - return; - } - - // finally we got it - emit friendRequested(friendAdress, getMessage()); - this->toxId.clear(); - this->message.clear(); -} - -void AddFriendForm::queryTox3(const tox3_server& server, QString &record) -{ - QByteArray nameData = record.left(record.indexOf('@')).toUtf8(), id, realRecord; - QString entry, toxIdStr; - int toxIdSize, idx, verx, dns_string_len; - const int dns_string_maxlen = 128; - - void* tox_dns3 = tox_dns3_new(server.pubkey); - if (!tox_dns3) - { - qWarning() << "queryTox3: failed to create a tox_dns3 object for "<toxId.clear(); - this->message.clear(); - return; - - // Centralized error handling, fallback on tox1 queries -fallbackOnTox1: - if (tox_dns3) - tox_dns3_kill(tox_dns3); - queryTox1(record); - return; -} diff --git a/src/widget/form/addfriendform.h b/src/widget/form/addfriendform.h index 896407c73..08ba98ac5 100644 --- a/src/widget/form/addfriendform.h +++ b/src/widget/form/addfriendform.h @@ -22,7 +22,6 @@ #include #include #include -#include namespace Ui {class MainWindow;} @@ -34,34 +33,17 @@ public: ~AddFriendForm(); void show(Ui::MainWindow &ui); - bool isToxId(const QString& value) const; - void showWarning(const QString& message) const; QString getMessage() const; signals: void friendRequested(const QString& friendAddress, const QString& message); +protected: + void showWarning(const QString& message) const; + private slots: void onSendTriggered(); -private: - struct tox3_server - { - tox3_server()=default; - tox3_server(const char* _name, uint8_t _pk[32]):name{_name},pubkey{_pk}{} - - const char* name; ///< Hostname of the server, e.g. toxme.se - uint8_t* pubkey; ///< Public key of the tox3 server, usually 256bit long - }; - -private: - void queryTox1(const QString& record); ///< Record should look like user@domain.tld - void queryTox3(const tox3_server& server, QString& record); ///< Record should look like user@domain.tld, may fallback on queryTox1 - /// Try to fetch the first entry of the given TXT record - /// Returns an empty object on failure. May block for up to ~3s - /// May display message boxes on error if silent if false - QByteArray fetchLastTextRecord(const QString& record, bool silent=true); - private: QLabel headLabel, toxIdLabel, messageLabel; QPushButton sendButton; @@ -69,10 +51,6 @@ private: QTextEdit message; QVBoxLayout layout, headLayout; QWidget *head, *main; - - /** will be used for dns discovery if necessary */ - QDnsLookup dns; - static const tox3_server pinnedServers[]; }; #endif // ADDFRIENDFORM_H diff --git a/src/widget/toxuri.cpp b/src/widget/toxuri.cpp new file mode 100644 index 000000000..d628bdc6b --- /dev/null +++ b/src/widget/toxuri.cpp @@ -0,0 +1,92 @@ +#include "src/widget/toxuri.h" +#include "src/toxdns.h" +#include "src/widget/tool/friendrequestdialog.h" +#include "src/core.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +void toxURIEventHandler(const QByteArray& eventData) +{ + if (!eventData.startsWith("tox:")) + return; + + handleToxURI(eventData); +} + +void handleToxURI(const QString &toxURI) +{ + Core* core = Core::getInstance(); + + while (!core) + { + core = Core::getInstance(); + qApp->processEvents(); + } + + while (!core->isReady()) + { + qApp->processEvents(); + } + + QString toxaddr = toxURI.mid(4); + QString toxid = ToxDNS::resolveToxAddress(toxaddr, true).toString(); + + if (toxid.isEmpty()) + { + QMessageBox::warning(0, "qTox", toxaddr+" is not a valid Tox address."); + } + else + { + ToxURIDialog dialog(0, toxaddr, QObject::tr("Tox me maybe?","Default message in Tox URI friend requests. Write something appropriate!")); + if (dialog.exec() == QDialog::Accepted) + Core::getInstance()->requestFriendship(toxid, dialog.getRequestMessage()); + } +} + +ToxURIDialog::ToxURIDialog(QWidget *parent, const QString &userId, const QString &message) : + QDialog(parent) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setWindowTitle(tr("Add a friend","Title of the window to add a friend through Tox URI")); + + QLabel *friendsLabel = new QLabel(tr("Do you want to add %1 as a friend ?").arg(userId), this); + QLabel *userIdLabel = new QLabel(tr("User ID:"), this); + QLineEdit *userIdEdit = new QLineEdit(userId, this); + userIdEdit->setCursorPosition(0); + userIdEdit->setReadOnly(true); + QLabel *messageLabel = new QLabel(tr("Friend request message:"), this); + messageEdit = new QPlainTextEdit(message, this); + + QDialogButtonBox *buttonBox = new QDialogButtonBox(Qt::Horizontal, this); + + buttonBox->addButton(tr("Send","Send a friend request"), QDialogButtonBox::AcceptRole); + buttonBox->addButton(tr("Cancel","Don't send a friend request"), QDialogButtonBox::RejectRole); + + connect(buttonBox, &QDialogButtonBox::accepted, this, &FriendRequestDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &FriendRequestDialog::reject); + + QVBoxLayout *layout = new QVBoxLayout(this); + + layout->addWidget(friendsLabel); + layout->addSpacing(12); + layout->addWidget(userIdLabel); + layout->addWidget(userIdEdit); + layout->addWidget(messageLabel); + layout->addWidget(messageEdit); + layout->addWidget(buttonBox); + + resize(300, 200); +} + +QString ToxURIDialog::getRequestMessage() +{ + return messageEdit->toPlainText(); +} diff --git a/src/widget/toxuri.h b/src/widget/toxuri.h new file mode 100644 index 000000000..d545b6bdc --- /dev/null +++ b/src/widget/toxuri.h @@ -0,0 +1,25 @@ +#ifndef TOXURI_H +#define TOXURI_H + +#include + +/// Shows a dialog asking whether or not to add this tox address as a friend +/// Will wait until the core is ready first +void handleToxURI(const QString& toxURI); + +// Internals +class QByteArray; +class QPlainTextEdit; +void toxURIEventHandler(const QByteArray& eventData); +class ToxURIDialog : public QDialog +{ + Q_OBJECT +public: + explicit ToxURIDialog(QWidget *parent, const QString &userId, const QString &message); + QString getRequestMessage(); + +private: + QPlainTextEdit *messageEdit; +}; + +#endif // TOXURI_H diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index 5dbf7d076..89feba47e 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -62,7 +62,6 @@ Widget::Widget(QWidget *parent) void Widget::init() { - ui->setupUi(this); if (QSystemTrayIcon::isSystemTrayAvailable() == true)