Verified Commit a76a20d3 authored by Linus Jahn's avatar Linus Jahn

Open 'xmpp:'-URIs, Allow only one app instance

Closes #219.
parent b4c071bd
......@@ -24,7 +24,9 @@ SOURCES += \
src/ServiceDiscoveryManager.cpp \
src/VCardManager.cpp \
src/XmlLogHandler.cpp \
src/StatusBar.cpp
src/StatusBar.cpp \
src/singleapp/singleapplication.cpp \
src/singleapp/singleapplication_p.cpp
HEADERS += \
src/Database.h \
......@@ -46,7 +48,9 @@ HEADERS += \
src/VCardManager.h \
src/Globals.h \
src/Enums.h \
src/StatusBar.h
src/StatusBar.h \
src/singleapp/singleapplication.h \
src/singleapp/singleapplication_p.h
android: INCLUDEPATH += $$PWD/3rdparty/gloox/include
android: LIBS += -L$$PWD/3rdparty/gloox/lib/
......
......@@ -19,6 +19,7 @@ License: GPL-3+ with OpenSSL exception
Files: i18n/*
Copyright: 2017-2018, LNJ <git@lnj.li>
2017-2018, JBB <jbb.prv@gmx.de>
2017-2018, Muhammad Nur Hidayat Yasuyoshi <mnh48mail@gmail.com>
2018, Allan Nordhøy <epost@anotheragency.no>
2018, advocatux <advocatux@airpost.net>
2017-2018, Joeke de Graaf <mappack@null.net>
......@@ -26,16 +27,17 @@ Copyright: 2017-2018, LNJ <git@lnj.li>
2017, ZatroxDE <zatroxde@outlook.com>
2018, Andreas Kleinert <Andy.Kleinert@gmail.com>
2017, Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
2017, Muhammad Nur Hidayat Yasuyoshi (MNH48.com) <muhdnurhidayat96@yahoo.com>
License: GPL-3+ with OpenSSL exception
Files: src/StatusBar.cpp
src/StatusBar.h
src/singleapp/*
data/images/xmpp.svg
utils/generate-license.py
Copyright: 2016, J-P Nurmi <jpnurmi@gmail.com>
2007, Raja Sandhu, XMPP Standards Foundation
2018, LNJ <git@lnj.li>
2015-2018, Itay Grudev <itay+github.com@grudev.com>
License: MIT
Files: data/images/message_checkmark.svg
......
......@@ -80,6 +80,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -146,6 +146,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation>Konnte den Kontakt nicht entfernen, da Sie nicht verbunden sind.</translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -115,6 +115,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation>No se pudo eliminar el contacto debido a que no está conectado.</translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -126,6 +126,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -103,6 +103,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -122,6 +122,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation>Tidak dapat membuang kenalan, kerana anda tiada sambungan internet.</translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -103,6 +103,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation>Kunne ikke fjerne kontakt, som følge av manglende tilkobling.</translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -103,6 +103,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -103,6 +103,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -130,6 +130,11 @@
<source>Could not remove contact, as a result of not being connected.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The link will be opened after you have connected.</source>
<extracomment>The link is an XMPP-URI (i.e. &apos;xmpp:kaidan@muc.kaidan.im?join&apos; for joining a chat)</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginPage</name>
......
......@@ -50,10 +50,10 @@ Comment[de]=Ein Jabber-Client in QtQuick
Comment[el]=Ένα πρόγραμμα για το Jabber γραμμένο σε QtQuick
Comment[en_GB]=A QtQuick Jabber client
Comment[eo]=QtQuickbazita jabber-kliento
Comment[es]=Un cliente de Jabber en GTK
Comment[es]=Un cliente de Jabber en QtQuick
Comment[eu]=QtQuick Jabber bezeroa
Comment[fr]=Un client Jabber en QtQuick
Comment[gl]=Un cliente de Jabber de GTK
Comment[gl]=Un cliente de Jabber de QtQuick
Comment[he]=לקוח ג׳אבּר QtQuick‎
Comment[hr]=QtQuick Jabber klijent
Comment[hu]=Egy QtQuick Jabber kliens
......@@ -76,9 +76,10 @@ Comment[uk]=Jabber клієнт, що базується на QtQuick
Comment[zh_CN]=一个 QtQuick 的 Jabber 客户端
Comment[zh_TW]=一個 QtQuick 的 Jabber 客戶端
Keywords=chat;messaging;im;jabber;xmpp;qt;network;
Exec=kaidan
Exec=kaidan %u
Icon=kaidan
StartupNotify=true
StartupWMClass=Kaidan
Terminal=false
Categories=Network;
Categories=Network;InstantMessaging;Qt;
MimeType=x-scheme-handler/xmpp;
......@@ -22,6 +22,10 @@ set(KAIDAN_SOURCES
${CURDIR}/XmlLogHandler.cpp
${CURDIR}/StatusBar.cpp
# SingleApplication
${CURDIR}/singleapp/singleapplication.cpp
${CURDIR}/singleapp/singleapplication_p.cpp
# needed to trigger moc generation
${CURDIR}/Enums.h
)
......@@ -96,6 +96,18 @@ Kaidan::Kaidan(QGuiApplication *app, bool enableLogging, QObject *parent) : QObj
connect(client, &ClientThread::newCredentialsNeeded, this, &Kaidan::newCredentialsNeeded);
connect(client, &ClientThread::logInWorked, this, &Kaidan::logInWorked);
connect(this, &Kaidan::chatPartnerChanged, client, &ClientThread::chatPartnerChanged);
connect(client, &ClientThread::connectionStateChanged, [=] (ConnectionState state) {
// Open (possible) cached URI when connected.
// This is needed because the XMPP URIs can't be opened when Kaidan is not connected.
if (state == ConnectionState::StateConnected && !openUriCache.isEmpty()) {
// delay is needed because sometimes the RosterPage needs to be loaded first
QTimer::singleShot(300, [=] () {
emit xmppUriReceived(openUriCache);
openUriCache = "";
});
}
});
}
Kaidan::~Kaidan()
......@@ -126,7 +138,6 @@ bool Kaidan::mainConnect()
return false;
}
// TODO: check if jid is valid first
client->setCredentials(creds);
emit client->connectRequested();
return true;
......@@ -255,3 +266,19 @@ QString Kaidan::getResourcePath(QString name) const
qWarning() << "[main] Could NOT find media file:" << name;
return QString("");
}
void Kaidan::addOpenUri(QByteArray uri)
{
qDebug() << "[main]" << uri;
if (!uri.startsWith("xmpp:") || !uri.contains("@"))
return;
if (client->isConnected()) {
emit xmppUriReceived(QString::fromUtf8(uri));
} else {
//: The link is an XMPP-URI (i.e. 'xmpp:kaidan@muc.kaidan.im?join' for joining a chat)
emit passiveNotificationRequested(tr("The link will be opened after you have connected."));
openUriCache = QString::fromUtf8(uri);
}
}
......@@ -262,6 +262,11 @@ public:
return presenceCache;
}
/**
* Adds XMPP URI to open as soon as possible
*/
void addOpenUri(QByteArray uri);
signals:
void rosterModelChanged();
void messageModelChanged();
......@@ -337,6 +342,23 @@ signals:
*/
void vCardRequested(QString jid);
/**
* XMPP URI received
*
* Is called when Kaidan was used to open an XMPP URI (i.e. 'xmpp:kaidan@muc.kaidan.im?join')
*/
void xmppUriReceived(QString uri);
public slots:
/**
* Receives messages from another instance of the application
*/
void receiveMessage(quint32 instanceId, QByteArray msg)
{
// currently we only send XMPP URIs
addOpenUri(msg);
}
private:
void connectDatabases();
......@@ -351,6 +373,8 @@ private:
ClientThread::Credentials creds;
QString chatPartner;
QString openUriCache;
};
#endif
......@@ -54,6 +54,8 @@
#include "Globals.h"
#include "Enums.h"
#include "StatusBar.h"
// SingleApplication (Qt5 replacement for QtSingleApplication)
#include "singleapp/singleapplication.h"
#ifdef QMAKE_BUILD
#include "./3rdparty/kirigami/src/kirigamiplugin.h"
......@@ -72,10 +74,17 @@ enum CommandLineParseResult {
CommandLineParseResult parseCommandLine(QCommandLineParser &parser, QString *errorMessage)
{
// application description
parser.setApplicationDescription(QString(APPLICATION_DISPLAY_NAME) +
" - " + QString(APPLICATION_DESCRIPTION));
// add all possible arguments
QCommandLineOption helpOption = parser.addHelpOption();
QCommandLineOption versionOption = parser.addVersionOption();
parser.addOption({"disable-xml-log", "Disable output of full XMPP XML stream."});
parser.addOption({{"m", "multiple"}, "Allow multiple instances to be started."});
parser.addPositionalArgument("xmpp-uri", "An XMPP-URI to open (i.e. join a chat).",
"[xmpp-uri]");
// parse arguments
if (!parser.parse(QGuiApplication::arguments())) {
......@@ -103,17 +112,12 @@ int main(int argc, char *argv[])
//
// create a qt app
#if HAVE_QWIDGETS
QApplication app(argc, argv);
#else
QGuiApplication app(argc, argv);
#endif
SingleApplication app(argc, argv, true);
// name, display name, description
QGuiApplication::setApplicationName(APPLICATION_NAME);
QGuiApplication::setApplicationDisplayName(APPLICATION_DISPLAY_NAME);
QGuiApplication::setApplicationVersion(VERSION_STRING);
// attributes
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
......@@ -148,9 +152,6 @@ int main(int argc, char *argv[])
// create parser and add a description
QCommandLineParser parser;
parser.setApplicationDescription(QString(APPLICATION_DISPLAY_NAME) +
" - " + QString(APPLICATION_DESCRIPTION));
// parse the arguments
QString commandLineErrorMessage;
switch (parseCommandLine(parser, &commandLineErrorMessage)) {
......@@ -167,12 +168,34 @@ int main(int argc, char *argv[])
break;
}
// check if another instance already runs
if (app.isSecondary() && !parser.isSet("multiple")) {
qDebug().noquote() << QString("Another instance of %1 is already running.")
.arg(APPLICATION_DISPLAY_NAME)
<< "You can enable multiple instances by specifying '--multiple'.";
// send a possible link to the primary instance
if (!parser.positionalArguments().isEmpty())
app.sendMessage(parser.positionalArguments()[0].toUtf8());
return 0;
}
//
// Kaidan back-end
//
Kaidan kaidan(&app, !parser.isSet("disable-xml-log"));
// receive messages from other instances of Kaidan
kaidan.connect(&app, &SingleApplication::receivedMessage,
&kaidan, &Kaidan::receiveMessage);
// open the XMPP-URI/link (if given)
if (!parser.positionalArguments().isEmpty())
kaidan.addOpenUri(parser.positionalArguments()[0].toUtf8());
//
// QML-GUI
//
......
......@@ -44,6 +44,7 @@ Kirigami.ScrollablePage {
RosterAddContactSheet {
id: addContactSheet
jid: ""
}
RosterRemoveContactSheet {
id: removeContactSheet
......@@ -105,11 +106,19 @@ Kirigami.ScrollablePage {
}
}
function xmppUriReceived(uri) {
// 'xmpp:' has length of 5
addContactSheet.jid = uri.substr(5)
addContactSheet.open()
}
Component.onCompleted: {
kaidan.presenceCache.presenceChanged.connect(newPresenceArrived)
kaidan.xmppUriReceived.connect(xmppUriReceived)
}
Component.onDestruction: {
kaidan.presenceCache.presenceChanged.disconnect(newPresenceArrived)
kaidan.xmppUriReceived.disconnect(xmppUriReceived)
}
}
}
......
......@@ -34,6 +34,8 @@ import QtQuick.Layouts 1.3
import org.kde.kirigami 2.0 as Kirigami
Kirigami.OverlaySheet {
property string jid: ""
ColumnLayout {
Layout.fillWidth: true
......@@ -56,6 +58,7 @@ Kirigami.OverlaySheet {
}
Controls.TextField {
id: jidField
text: jid
placeholderText: qsTr("user@example.org")
inputMethodHints: Qt.ImhEmailCharactersOnly | Qt.ImhPreferLowercase
selectByMouse: true
......@@ -117,6 +120,7 @@ Kirigami.OverlaySheet {
}
function clearInput() {
jid = "";
jidField.text = "";
nickField.text = "";
msgField.text = "";
......
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#include <QtCore/QTime>
#include <QtCore/QThread>
#include <QtCore/QDateTime>
#include <QtCore/QByteArray>
#include <QtCore/QSharedMemory>
#include "singleapplication.h"
#include "singleapplication_p.h"
/**
* @brief Constructor. Checks and fires up LocalServer or closes the program
* if another instance already exists
* @param argc
* @param argv
* @param {bool} allowSecondaryInstances
*/
SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout )
: app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) )
{
Q_D(SingleApplication);
// Store the current mode of the program
d->options = options;
// Generating an application ID used for identifying the shared memory
// block and QLocalServer
d->genBlockServerName();
#ifdef Q_OS_UNIX
// By explicitly attaching it and then deleting it we make sure that the
// memory is deleted even after the process has crashed on Unix.
d->memory = new QSharedMemory( d->blockServerName );
d->memory->attach();
delete d->memory;
#endif
// Guarantee thread safe behaviour with a shared memory block.
d->memory = new QSharedMemory( d->blockServerName );
// Create a shared memory block
if( d->memory->create( sizeof( InstancesInfo ) ) ) {
// Initialize the shared memory block
d->memory->lock();
d->initializeMemoryBlock();
d->memory->unlock();
} else {
// Attempt to attach to the memory segment
if( ! d->memory->attach() ) {
qCritical() << "SingleApplication: Unable to attach to shared memory block.";
qCritical() << d->memory->errorString();
delete d;
::exit( EXIT_FAILURE );
}
}
InstancesInfo* inst = static_cast<InstancesInfo*>( d->memory->data() );
QTime time;
time.start();
// Make sure the shared memory block is initialised and in consistent state
while( true ) {
d->memory->lock();
if( d->blockChecksum() == inst->checksum ) break;
if( time.elapsed() > 5000 ) {
qWarning() << "SingleApplication: Shared memory block has been in an inconsistent state from more than 5s. Assuming primary instance failure.";
d->initializeMemoryBlock();
}
d->memory->unlock();
// Random sleep here limits the probability of a colision between two racing apps
qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max() );
QThread::sleep( 8 + static_cast <unsigned long>( static_cast <float>( qrand() ) / RAND_MAX * 10 ) );
}
if( inst->primary == false) {
d->startPrimary();
d->memory->unlock();
return;
}
// Check if another instance can be started
if( allowSecondary ) {
inst->secondary += 1;
inst->checksum = d->blockChecksum();
d->instanceNumber = inst->secondary;
d->startSecondary();
if( d->options & Mode::SecondaryNotification ) {
d->connectToPrimary( timeout, SingleApplicationPrivate::SecondaryInstance );
}
d->memory->unlock();
return;
}
d->memory->unlock();
d->connectToPrimary( timeout, SingleApplicationPrivate::NewInstance );
delete d;
::exit( EXIT_SUCCESS );
}
/**
* @brief Destructor
*/
SingleApplication::~SingleApplication()
{
Q_D(SingleApplication);
delete d;
}
bool SingleApplication::isPrimary()
{
Q_D(SingleApplication);
return d->server != nullptr;
}
bool SingleApplication::isSecondary()
{
Q_D(SingleApplication);
return d->server == nullptr;
}
quint32 SingleApplication::instanceId()
{
Q_D(SingleApplication);
return d->instanceNumber;
}
qint64 SingleApplication::primaryPid()
{
Q_D(SingleApplication);
return d->primaryPid();
}
bool SingleApplication::sendMessage( QByteArray message, int timeout )
{
Q_D(SingleApplication);
// Nobody to connect to
if( isPrimary() ) return false;
// Make sure the socket is connected
d->connectToPrimary( timeout, SingleApplicationPrivate::Reconnect );
d->socket->write( message );
bool dataWritten = d->socket->flush();
d->socket->waitForBytesWritten( timeout );
return dataWritten;
}
// The MIT License (MIT)
//
// Copyright (c) Itay Grudev 2015 - 2018
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE