/* * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include #include #include #include #include #include #include #include "interfaces/conversationmessage.h" #include "interfaces/dbushelpers.h" #include "interfaces/dbusinterfaces.h" #include "interfaces/devicesmodel.h" #include "interfaces/notificationsmodel.h" #include "kdeconnect-version.h" #include int main(int argc, char **argv) { QCoreApplication app(argc, argv); KAboutData about(QStringLiteral("kdeconnect-cli"), QStringLiteral("kdeconnect-cli"), QStringLiteral(KDECONNECT_VERSION_STRING), i18n("KDE Connect CLI tool"), KAboutLicense::GPL, i18n("(C) 2015 Aleix Pol Gonzalez")); KAboutData::setApplicationData(about); about.addAuthor(i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@kde.org")); about.addAuthor(i18n("Albert Vaca Cintora"), QString(), QStringLiteral("albertvaka@gmail.com")); QCommandLineParser parser; parser.addOption(QCommandLineOption(QStringList(QStringLiteral("l")) << QStringLiteral("list-devices"), i18n("List all devices"))); parser.addOption( QCommandLineOption(QStringList(QStringLiteral("a")) << QStringLiteral("list-available"), i18n("List available (paired and reachable) devices"))); parser.addOption( QCommandLineOption(QStringLiteral("id-only"), i18n("Make --list-devices or --list-available print only the devices id, to ease scripting"))); parser.addOption( QCommandLineOption(QStringLiteral("name-only"), i18n("Make --list-devices or --list-available print only the devices name, to ease scripting"))); parser.addOption(QCommandLineOption(QStringLiteral("id-name-only"), i18n("Make --list-devices or --list-available print only the devices id and name, to ease scripting"))); parser.addOption(QCommandLineOption(QStringLiteral("refresh"), i18n("Search for devices in the network and re-establish connections"))); parser.addOption(QCommandLineOption(QStringLiteral("pair"), i18n("Request pairing to a said device"))); parser.addOption(QCommandLineOption(QStringLiteral("ring"), i18n("Find the said device by ringing it."))); parser.addOption(QCommandLineOption(QStringLiteral("unpair"), i18n("Stop pairing to a said device"))); parser.addOption(QCommandLineOption(QStringLiteral("ping"), i18n("Sends a ping to said device"))); parser.addOption(QCommandLineOption(QStringLiteral("ping-msg"), i18n("Same as ping but you can set the message to display"), i18n("message"))); parser.addOption(QCommandLineOption(QStringLiteral("share"), i18n("Share a file/URL to a said device"), QStringLiteral("path or URL"))); parser.addOption(QCommandLineOption(QStringLiteral("share-text"), i18n("Share text to a said device"), QStringLiteral("text"))); parser.addOption(QCommandLineOption(QStringLiteral("list-notifications"), i18n("Display the notifications on a said device"))); parser.addOption(QCommandLineOption(QStringLiteral("lock"), i18n("Lock the specified device"))); parser.addOption(QCommandLineOption(QStringLiteral("unlock"), i18n("Unlock the specified device"))); parser.addOption(QCommandLineOption(QStringLiteral("send-sms"), i18n("Sends an SMS. Requires destination"), i18n("message"))); parser.addOption(QCommandLineOption(QStringLiteral("destination"), i18n("Phone number to send the message"), i18n("phone number"))); parser.addOption(QCommandLineOption(QStringLiteral("attachment"), i18n("File urls to send attachments with the message (can be passed multiple times)"), i18n("file urls"))); parser.addOption(QCommandLineOption(QStringList(QStringLiteral("device")) << QStringLiteral("d"), i18n("Device ID"), QStringLiteral("dev"))); parser.addOption(QCommandLineOption(QStringList(QStringLiteral("name")) << QStringLiteral("n"), i18n("Device Name"), QStringLiteral("name"))); parser.addOption(QCommandLineOption(QStringLiteral("encryption-info"), i18n("Get encryption info about said device"))); parser.addOption(QCommandLineOption(QStringLiteral("list-commands"), i18n("Lists remote commands and their ids"))); parser.addOption(QCommandLineOption(QStringLiteral("execute-command"), i18n("Executes a remote command by id"), QStringLiteral("id"))); parser.addOption( QCommandLineOption(QStringList{QStringLiteral("k"), QStringLiteral("send-keys")}, i18n("Sends keys to a said device"), QStringLiteral("key"))); parser.addOption(QCommandLineOption(QStringLiteral("my-id"), i18n("Display this device's id and exit"))); parser.addOption(QCommandLineOption(QStringLiteral("photo"), i18n("Open the connected device's camera and transfer the photo"), QStringLiteral("path"))); // Hidden because it's an implementation detail QCommandLineOption deviceAutocomplete(QStringLiteral("shell-device-autocompletion")); deviceAutocomplete.setFlags(QCommandLineOption::HiddenFromHelp); deviceAutocomplete.setDescription( QStringLiteral("Outputs all available devices id's with their name and paired status")); // Not visible, so no translation needed deviceAutocomplete.setValueName(QStringLiteral("shell")); parser.addOption(deviceAutocomplete); about.setupCommandLine(&parser); parser.process(app); about.processCommandLine(&parser); const QString id = QStringLiteral("kdeconnect-cli-") + QString::number(QCoreApplication::applicationPid()); DaemonDbusInterface iface; if (parser.isSet(QStringLiteral("my-id"))) { QTextStream(stdout) << iface.selfId() << endl; } else if (parser.isSet(QStringLiteral("l")) || parser.isSet(QStringLiteral("a"))) { bool available = false; if (parser.isSet(QStringLiteral("a"))) { available = true; } else { blockOnReply(iface.acquireDiscoveryMode(id)); QThread::sleep(2); } const QStringList devices = blockOnReply(iface.devices(available, available)); bool displayCount = true; for (const QString &id : devices) { if (parser.isSet(QStringLiteral("id-only"))) { QTextStream(stdout) << id << endl; displayCount = false; } else if (parser.isSet(QStringLiteral("name-only"))) { DeviceDbusInterface deviceIface(id); QTextStream(stdout) << deviceIface.name() << endl; displayCount = false; } else if (parser.isSet(QStringLiteral("id-name-only"))) { DeviceDbusInterface deviceIface(id); QTextStream(stdout) << id << ' ' << deviceIface.name() << endl; displayCount = false; } else { DeviceDbusInterface deviceIface(id); QString statusInfo; const bool isReachable = deviceIface.isReachable(); const bool isTrusted = deviceIface.isTrusted(); if (isReachable && isTrusted) { statusInfo = i18n("(paired and reachable)"); } else if (isReachable) { statusInfo = i18n("(reachable)"); } else if (isTrusted) { statusInfo = i18n("(paired)"); } QTextStream(stdout) << "- " << deviceIface.name() << ": " << deviceIface.id() << ' ' << statusInfo << endl; } } if (displayCount) { QTextStream(stderr) << i18np("1 device found", "%1 devices found", devices.size()) << endl; } else if (devices.isEmpty()) { QTextStream(stderr) << i18n("No devices found") << endl; } blockOnReply(iface.releaseDiscoveryMode(id)); } else if (parser.isSet(QStringLiteral("shell-device-autocompletion"))) { // Outputs a list of reachable devices in zsh autocomplete format, with the name as description const QStringList devices = blockOnReply(iface.devices(true, false)); for (const QString &id : devices) { DeviceDbusInterface deviceIface(id); QString statusInfo; const bool isTrusted = deviceIface.isTrusted(); if (isTrusted) { statusInfo = i18n("(paired)"); } else { statusInfo = i18n("(unpaired)"); } // Description: "device name (paired/unpaired)" QString description = deviceIface.name() + QLatin1Char(' ') + statusInfo; // Replace characters description.replace(QLatin1Char('\\'), QStringLiteral("\\\\")); description.replace(QLatin1Char('['), QStringLiteral("\\[")); description.replace(QLatin1Char(']'), QStringLiteral("\\]")); description.replace(QLatin1Char('\''), QStringLiteral("\\'")); description.replace(QLatin1Char('\"'), QStringLiteral("\\\"")); description.replace(QLatin1Char('\n'), QLatin1Char(' ')); description.remove(QLatin1Char('\0')); // Output id and description QTextStream(stdout) << id << '[' << description << ']' << endl; } // Exit with 1 if we didn't find a device return int(devices.isEmpty()); } else if (parser.isSet(QStringLiteral("refresh"))) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect"), QStringLiteral("org.kde.kdeconnect.daemon"), QStringLiteral("forceOnNetworkChange")); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } else { QString device = parser.value(QStringLiteral("device")); if (device.isEmpty() && parser.isSet(QStringLiteral("name"))) { device = blockOnReply(iface.deviceIdByName(parser.value(QStringLiteral("name")))); if (device.isEmpty()) { QTextStream(stderr) << "Couldn't find device: " << parser.value(QStringLiteral("name")) << endl; return 1; } } if (device.isEmpty()) { QTextStream(stderr) << i18n( "No device specified: Use -d or -n to specify a device. \nDevice ID's and names may be found using \"kdeconnect-cli " "-l\" \nView complete help with --help option") << endl; return 1; } if (!blockOnReply(iface.devices(false, false)).contains(device)) { QTextStream(stderr) << "Couldn't find device with id \"" << device << "\". To specify a device by name use -n " << endl; return 1; } if (parser.isSet(QStringLiteral("share"))) { QStringList urls; QUrl url = QUrl::fromUserInput(parser.value(QStringLiteral("share")), QDir::currentPath()); urls.append(url.toString()); // Check for more arguments const auto args = parser.positionalArguments(); for (const QString &input : args) { QUrl url = QUrl::fromUserInput(input, QDir::currentPath()); urls.append(url.toString()); } QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/share"), QStringLiteral("org.kde.kdeconnect.device.share"), QStringLiteral("shareUrls")); msg.setArguments(QVariantList() << QVariant(urls)); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); for (const QString &url : qAsConst(urls)) { QTextStream(stdout) << i18n("Shared %1", url) << endl; } } else if (parser.isSet(QStringLiteral("share-text"))) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/share"), QStringLiteral("org.kde.kdeconnect.device.share"), QStringLiteral("shareText")); msg.setArguments(QVariantList() << parser.value(QStringLiteral("share-text"))); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); QTextStream(stdout) << i18n("Shared text: %1", parser.value(QStringLiteral("share-text"))) << endl; } else if (parser.isSet(QStringLiteral("lock")) || parser.isSet(QStringLiteral("unlock"))) { LockDeviceDbusInterface iface(device); iface.setLocked(parser.isSet(QStringLiteral("lock"))); DeviceDbusInterface deviceIface(device); if (parser.isSet(QStringLiteral("lock"))) { QTextStream(stdout) << i18nc("device has requested to lock peer device", "Requested to lock %1.", deviceIface.name()) << endl; } else { QTextStream(stdout) << i18nc("device has requested to unlock peer device", "Requested to unlock %1.", deviceIface.name()) << endl; } } else if (parser.isSet(QStringLiteral("pair"))) { DeviceDbusInterface dev(device); if (!dev.isReachable()) { // Device doesn't exist, go into discovery mode and wait up to 30 seconds for the device to appear QEventLoop wait; QTextStream(stderr) << i18n("waiting for device...") << endl; blockOnReply(iface.acquireDiscoveryMode(id)); QObject::connect(&iface, &DaemonDbusInterface::deviceAdded, &iface, [&](const QString &deviceAddedId) { if (device == deviceAddedId) { wait.quit(); } }); QTimer::singleShot(30 * 1000, &wait, &QEventLoop::quit); wait.exec(); } if (!dev.isReachable()) { QTextStream(stderr) << i18n("Device not found") << endl; } else if (blockOnReply(dev.isTrusted())) { QTextStream(stderr) << i18n("Already paired") << endl; } else { QTextStream(stderr) << i18n("Pair requested") << endl; blockOnReply(dev.requestPair()); } blockOnReply(iface.releaseDiscoveryMode(id)); } else if (parser.isSet(QStringLiteral("unpair"))) { DeviceDbusInterface dev(device); if (!dev.isTrusted()) { QTextStream(stderr) << i18n("Already not paired") << endl; } else { QTextStream(stderr) << i18n("Unpaired") << endl; blockOnReply(dev.unpair()); } } else if (parser.isSet(QStringLiteral("ping")) || parser.isSet(QStringLiteral("ping-msg"))) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/ping"), QStringLiteral("org.kde.kdeconnect.device.ping"), QStringLiteral("sendPing")); if (parser.isSet(QStringLiteral("ping-msg"))) { QString message = parser.value(QStringLiteral("ping-msg")); msg.setArguments(QVariantList() << message); } blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } else if (parser.isSet(QStringLiteral("send-sms"))) { if (parser.isSet(QStringLiteral("destination"))) { qDBusRegisterMetaType(); QVariantList addresses; const QStringList addressList = parser.value(QStringLiteral("destination")).split(QRegularExpression(QStringLiteral("\\s+"))); for (const QString &input : addressList) { ConversationAddress address(input); addresses << QVariant::fromValue(address); } const QString message = parser.value(QStringLiteral("send-sms")); const QStringList rawAttachmentUrlsList = parser.values(QStringLiteral("attachment")); QVariantList attachments; for (const QString &attachmentUrl : rawAttachmentUrlsList) { // TODO: Construct attachment objects from the list of Urls Q_UNUSED(attachmentUrl); } DeviceConversationsDbusInterface conversationDbusInterface(device); auto reply = conversationDbusInterface.sendWithoutConversation(addresses, message, attachments); reply.waitForFinished(); } else { QTextStream(stderr) << i18n("error: should specify the SMS's recipient by passing --destination ") << endl; return 1; } } else if (parser.isSet(QStringLiteral("ring"))) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/findmyphone"), QStringLiteral("org.kde.kdeconnect.device.findmyphone"), QStringLiteral("ring")); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } else if (parser.isSet(QStringLiteral("photo"))) { const QString fileName = parser.value(QStringLiteral("photo")); if (!fileName.isEmpty()) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/photo"), QStringLiteral("org.kde.kdeconnect.device.photo"), QStringLiteral("requestPhoto")); msg.setArguments({QUrl::fromLocalFile(fileName).toString()}); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } else { QTextStream(stderr) << i18n("Please specify a filename for the photo") << endl; } } else if (parser.isSet(QStringLiteral("send-keys"))) { QString seq = parser.value(QStringLiteral("send-keys")); QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/remotekeyboard"), QStringLiteral("org.kde.kdeconnect.device.remotekeyboard"), QStringLiteral("sendKeyPress")); if (seq.trimmed() == QLatin1String("-")) { // from stdin QFile in; if (in.open(stdin, QIODevice::ReadOnly | QIODevice::Unbuffered)) { while (!in.atEnd()) { QByteArray line = in.readLine(); // sanitize to ASCII-codes > 31? msg.setArguments({QString::fromLatin1(line), -1, false, false, false}); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } in.close(); } } else { msg.setArguments({seq, -1, false, false, false}); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } } else if (parser.isSet(QStringLiteral("list-notifications"))) { NotificationsModel notifications; notifications.setDeviceId(device); for (int i = 0, rows = notifications.rowCount(); i < rows; ++i) { QModelIndex idx = notifications.index(i); QTextStream(stdout) << "- " << idx.data(NotificationsModel::AppNameModelRole).toString() << ": " << idx.data(NotificationsModel::NameModelRole).toString() << endl; } } else if (parser.isSet(QStringLiteral("list-commands"))) { RemoteCommandsDbusInterface iface(device); const auto cmds = QJsonDocument::fromJson(iface.commands()).object(); for (auto it = cmds.constBegin(), itEnd = cmds.constEnd(); it != itEnd; ++it) { const QJsonObject cont = it->toObject(); QTextStream(stdout) << it.key() << ": " << cont.value(QStringLiteral("name")).toString() << ": " << cont.value(QStringLiteral("command")).toString() << endl; } } else if (parser.isSet(QStringLiteral("execute-command"))) { RemoteCommandsDbusInterface iface(device); blockOnReply(iface.triggerCommand(parser.value(QStringLiteral("execute-command")))); } else if (parser.isSet(QStringLiteral("encryption-info"))) { DeviceDbusInterface dev(device); QString info = blockOnReply(dev.encryptionInfo()); // QSsl::Der = 1 QTextStream(stdout) << info << endl; } else { QTextStream(stderr) << i18n("Nothing to be done") << endl; } } QMetaObject::invokeMethod(&app, "quit", Qt::QueuedConnection); return app.exec(); }