File indexing completed on 2024-04-14 04:51:37

0001 /*
0002  * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez <aleixpol@kde.org>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include <QCoreApplication>
0008 #include <QCryptographicHash>
0009 #include <QDBusMessage>
0010 #include <QFile>
0011 #include <QIODevice>
0012 #include <QTextStream>
0013 
0014 #include <KAboutData>
0015 
0016 #include "interfaces/conversationmessage.h"
0017 #include "interfaces/dbushelpers.h"
0018 #include "interfaces/dbusinterfaces.h"
0019 #include "interfaces/devicesmodel.h"
0020 #include "interfaces/notificationsmodel.h"
0021 #include "kdeconnect-version.h"
0022 
0023 #include <dbushelper.h>
0024 
0025 int main(int argc, char **argv)
0026 {
0027     QCoreApplication app(argc, argv);
0028     KAboutData about(QStringLiteral("kdeconnect-cli"),
0029                      QStringLiteral("kdeconnect-cli"),
0030                      QStringLiteral(KDECONNECT_VERSION_STRING),
0031                      i18n("KDE Connect CLI tool"),
0032                      KAboutLicense::GPL,
0033                      i18n("(C) 2015 Aleix Pol Gonzalez"));
0034     KAboutData::setApplicationData(about);
0035 
0036     about.addAuthor(i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@kde.org"));
0037     about.addAuthor(i18n("Albert Vaca Cintora"), QString(), QStringLiteral("albertvaka@gmail.com"));
0038     QCommandLineParser parser;
0039     parser.addOption(QCommandLineOption(QStringList{QStringLiteral("l"), QStringLiteral("list-devices")}, i18n("List all devices")));
0040     parser.addOption(
0041         QCommandLineOption(QStringList{QStringLiteral("a"), QStringLiteral("list-available")}, i18n("List available (paired and reachable) devices")));
0042     parser.addOption(
0043         QCommandLineOption(QStringLiteral("id-only"), i18n("Make --list-devices or --list-available print only the devices id, to ease scripting")));
0044     parser.addOption(
0045         QCommandLineOption(QStringLiteral("name-only"), i18n("Make --list-devices or --list-available print only the devices name, to ease scripting")));
0046     parser.addOption(QCommandLineOption(QStringLiteral("id-name-only"),
0047                                         i18n("Make --list-devices or --list-available print only the devices id and name, to ease scripting")));
0048     parser.addOption(QCommandLineOption(QStringLiteral("refresh"), i18n("Search for devices in the network and re-establish connections")));
0049     parser.addOption(QCommandLineOption(QStringLiteral("pair"), i18n("Request pairing to a said device")));
0050     parser.addOption(QCommandLineOption(QStringLiteral("ring"), i18n("Find the said device by ringing it.")));
0051     parser.addOption(QCommandLineOption(QStringLiteral("unpair"), i18n("Stop pairing to a said device")));
0052     parser.addOption(QCommandLineOption(QStringLiteral("ping"), i18n("Sends a ping to said device")));
0053     parser.addOption(QCommandLineOption(QStringLiteral("ping-msg"), i18n("Same as ping but you can set the message to display"), i18n("message")));
0054     parser.addOption(QCommandLineOption(QStringLiteral("send-clipboard"), i18n("Sends the current clipboard to said device")));
0055     parser.addOption(QCommandLineOption(QStringLiteral("share"), i18n("Share a file/URL to a said device"), QStringLiteral("path or URL")));
0056     parser.addOption(QCommandLineOption(QStringLiteral("share-text"), i18n("Share text to a said device"), QStringLiteral("text")));
0057     parser.addOption(QCommandLineOption(QStringLiteral("list-notifications"), i18n("Display the notifications on a said device")));
0058     parser.addOption(QCommandLineOption(QStringLiteral("lock"), i18n("Lock the specified device")));
0059     parser.addOption(QCommandLineOption(QStringLiteral("unlock"), i18n("Unlock the specified device")));
0060     parser.addOption(QCommandLineOption(QStringLiteral("send-sms"), i18n("Sends an SMS. Requires destination"), i18n("message")));
0061     parser.addOption(QCommandLineOption(QStringLiteral("destination"), i18n("Phone number to send the message"), i18n("phone number")));
0062     parser.addOption(QCommandLineOption(QStringLiteral("attachment"),
0063                                         i18n("File urls to send attachments with the message (can be passed multiple times)"),
0064                                         i18n("file urls")));
0065     parser.addOption(QCommandLineOption(QStringList{QStringLiteral("device"), QStringLiteral("d")}, i18n("Device ID"), QStringLiteral("dev")));
0066     parser.addOption(QCommandLineOption(QStringList{QStringLiteral("name"), QStringLiteral("n")}, i18n("Device Name"), QStringLiteral("name")));
0067     parser.addOption(QCommandLineOption(QStringLiteral("encryption-info"), i18n("Get encryption info about said device")));
0068     parser.addOption(QCommandLineOption(QStringLiteral("list-commands"), i18n("Lists remote commands and their ids")));
0069     parser.addOption(QCommandLineOption(QStringLiteral("execute-command"), i18n("Executes a remote command by id"), QStringLiteral("id")));
0070     parser.addOption(
0071         QCommandLineOption(QStringList{QStringLiteral("k"), QStringLiteral("send-keys")}, i18n("Sends keys to a said device"), QStringLiteral("key")));
0072     parser.addOption(QCommandLineOption(QStringLiteral("my-id"), i18n("Display this device's id and exit")));
0073 
0074     // Hidden because it's an implementation detail
0075     QCommandLineOption deviceAutocomplete(QStringLiteral("shell-device-autocompletion"));
0076     deviceAutocomplete.setFlags(QCommandLineOption::HiddenFromHelp);
0077     deviceAutocomplete.setDescription(
0078         QStringLiteral("Outputs all available devices id's with their name and paired status")); // Not visible, so no translation needed
0079     deviceAutocomplete.setValueName(QStringLiteral("shell"));
0080     parser.addOption(deviceAutocomplete);
0081     about.setupCommandLine(&parser);
0082 
0083     parser.process(app);
0084     about.processCommandLine(&parser);
0085 
0086     DaemonDbusInterface iface;
0087 
0088     if (parser.isSet(QStringLiteral("my-id"))) {
0089         QTextStream(stdout) << iface.selfId() << Qt::endl;
0090     } else if (parser.isSet(QStringLiteral("l")) || parser.isSet(QStringLiteral("a"))) {
0091         bool available = false;
0092         if (parser.isSet(QStringLiteral("a"))) {
0093             available = true;
0094         } else {
0095             QThread::sleep(2);
0096         }
0097         const QStringList devices = blockOnReply<QStringList>(iface.devices(available, available));
0098 
0099         bool displayCount = true;
0100         for (const QString &id : devices) {
0101             if (parser.isSet(QStringLiteral("id-only"))) {
0102                 QTextStream(stdout) << id << Qt::endl;
0103                 displayCount = false;
0104             } else if (parser.isSet(QStringLiteral("name-only"))) {
0105                 DeviceDbusInterface deviceIface(id);
0106                 QTextStream(stdout) << deviceIface.name() << Qt::endl;
0107                 displayCount = false;
0108             } else if (parser.isSet(QStringLiteral("id-name-only"))) {
0109                 DeviceDbusInterface deviceIface(id);
0110                 QTextStream(stdout) << id << ' ' << deviceIface.name() << Qt::endl;
0111                 displayCount = false;
0112             } else {
0113                 DeviceDbusInterface deviceIface(id);
0114                 QString statusInfo;
0115                 const bool isReachable = deviceIface.isReachable();
0116                 const bool isPaired = deviceIface.isPaired();
0117                 if (isReachable && isPaired) {
0118                     statusInfo = i18n("(paired and reachable)");
0119                 } else if (isReachable) {
0120                     statusInfo = i18n("(reachable)");
0121                 } else if (isPaired) {
0122                     statusInfo = i18n("(paired)");
0123                 }
0124                 QTextStream(stdout) << "- " << deviceIface.name() << ": " << deviceIface.id() << ' ' << statusInfo << Qt::endl;
0125             }
0126         }
0127         if (displayCount) {
0128             QTextStream(stderr) << i18np("1 device found", "%1 devices found", devices.size()) << Qt::endl;
0129         } else if (devices.isEmpty()) {
0130             QTextStream(stderr) << i18n("No devices found") << Qt::endl;
0131         }
0132 
0133     } else if (parser.isSet(QStringLiteral("shell-device-autocompletion"))) {
0134         // Outputs a list of reachable devices in zsh autocomplete format, with the name as description
0135         const QStringList devices = blockOnReply<QStringList>(iface.devices(true, false));
0136         for (const QString &id : devices) {
0137             DeviceDbusInterface deviceIface(id);
0138             QString statusInfo;
0139             const bool isPaired = deviceIface.isPaired();
0140             if (isPaired) {
0141                 statusInfo = i18n("(paired)");
0142             } else {
0143                 statusInfo = i18n("(unpaired)");
0144             }
0145 
0146             // Description: "device name (paired/unpaired)"
0147             QString description = deviceIface.name() + QLatin1Char(' ') + statusInfo;
0148             // Replace characters
0149             description.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
0150             description.replace(QLatin1Char('['), QStringLiteral("\\["));
0151             description.replace(QLatin1Char(']'), QStringLiteral("\\]"));
0152             description.replace(QLatin1Char('\''), QStringLiteral("\\'"));
0153             description.replace(QLatin1Char('\"'), QStringLiteral("\\\""));
0154             description.replace(QLatin1Char('\n'), QLatin1Char(' '));
0155             description.remove(QLatin1Char('\0'));
0156 
0157             // Output id and description
0158             QTextStream(stdout) << id << '[' << description << ']' << Qt::endl;
0159         }
0160 
0161         // Exit with 1 if we didn't find a device
0162         return int(devices.isEmpty());
0163     } else if (parser.isSet(QStringLiteral("refresh"))) {
0164         QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
0165                                                           QStringLiteral("/modules/kdeconnect"),
0166                                                           QStringLiteral("org.kde.kdeconnect.daemon"),
0167                                                           QStringLiteral("forceOnNetworkChange"));
0168         blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
0169     } else {
0170         QString device = parser.value(QStringLiteral("device"));
0171         if (device.isEmpty() && parser.isSet(QStringLiteral("name"))) {
0172             device = blockOnReply(iface.deviceIdByName(parser.value(QStringLiteral("name"))));
0173             if (device.isEmpty()) {
0174                 QTextStream(stderr) << "Couldn't find device: " << parser.value(QStringLiteral("name")) << Qt::endl;
0175                 return 1;
0176             }
0177         }
0178         if (device.isEmpty()) {
0179             QTextStream(stderr) << i18n(
0180                 "No device specified: Use -d <Device ID> or -n <Device Name> to specify a device. \nDevice ID's and names may be found using \"kdeconnect-cli "
0181                 "-l\" \nView complete help with --help option")
0182                                 << Qt::endl;
0183             return 1;
0184         }
0185 
0186         if (!blockOnReply<QStringList>(iface.devices(false, false)).contains(device)) {
0187             QTextStream(stderr) << "Couldn't find device with id \"" << device << "\". To specify a device by name use -n <devicename>" << Qt::endl;
0188             return 1;
0189         }
0190 
0191         if (parser.isSet(QStringLiteral("share"))) {
0192             QStringList urls;
0193 
0194             QString firstArg = parser.value(QStringLiteral("share"));
0195             const auto args = QStringList(firstArg) + parser.positionalArguments();
0196 
0197             for (const QString &input : args) {
0198                 QUrl url = QUrl::fromUserInput(input, QDir::currentPath());
0199                 if (url.isEmpty()) {
0200                     qWarning() << "URL not valid:" << input;
0201                     continue;
0202                 }
0203                 urls.append(url.toString());
0204             }
0205 
0206             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
0207                                                               QLatin1String("/modules/kdeconnect/devices/%1/share").arg(device),
0208                                                               QStringLiteral("org.kde.kdeconnect.device.share"),
0209                                                               QStringLiteral("shareUrls"));
0210 
0211             msg.setArguments(QVariantList{QVariant(urls)});
0212             blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
0213 
0214             for (const QString &url : qAsConst(urls)) {
0215                 QTextStream(stdout) << i18n("Shared %1", url) << Qt::endl;
0216             }
0217         } else if (parser.isSet(QStringLiteral("share-text"))) {
0218             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
0219                                                               QLatin1String("/modules/kdeconnect/devices/%1/share").arg(device),
0220                                                               QStringLiteral("org.kde.kdeconnect.device.share"),
0221                                                               QStringLiteral("shareText"));
0222             msg.setArguments(QVariantList{parser.value(QStringLiteral("share-text"))});
0223             blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
0224             QTextStream(stdout) << i18n("Shared text: %1", parser.value(QStringLiteral("share-text"))) << Qt::endl;
0225         } else if (parser.isSet(QStringLiteral("lock")) || parser.isSet(QStringLiteral("unlock"))) {
0226             LockDeviceDbusInterface iface(device);
0227             iface.setLocked(parser.isSet(QStringLiteral("lock")));
0228 
0229             DeviceDbusInterface deviceIface(device);
0230             if (parser.isSet(QStringLiteral("lock"))) {
0231                 QTextStream(stdout) << i18nc("device has requested to lock peer device", "Requested to lock %1.", deviceIface.name()) << Qt::endl;
0232             } else {
0233                 QTextStream(stdout) << i18nc("device has requested to unlock peer device", "Requested to unlock %1.", deviceIface.name()) << Qt::endl;
0234             }
0235         } else if (parser.isSet(QStringLiteral("pair"))) {
0236             DeviceDbusInterface dev(device);
0237             if (!dev.isReachable()) {
0238                 // Device doesn't exist, go into discovery mode and wait up to 30 seconds for the device to appear
0239                 QEventLoop wait;
0240                 QTextStream(stderr) << i18n("waiting for device...") << Qt::endl;
0241 
0242                 QObject::connect(&iface, &DaemonDbusInterface::deviceAdded, &iface, [&](const QString &deviceAddedId) {
0243                     if (device == deviceAddedId) {
0244                         wait.quit();
0245                     }
0246                 });
0247                 QTimer::singleShot(30 * 1000, &wait, &QEventLoop::quit);
0248 
0249                 wait.exec();
0250             }
0251 
0252             if (!dev.isReachable()) {
0253                 QTextStream(stderr) << i18n("Device not found") << Qt::endl;
0254             } else if (blockOnReply<bool>(dev.isPaired())) {
0255                 QTextStream(stderr) << i18n("Already paired") << Qt::endl;
0256             } else {
0257                 QTextStream(stderr) << i18n("Pair requested") << Qt::endl;
0258                 blockOnReply(dev.requestPairing());
0259             }
0260         } else if (parser.isSet(QStringLiteral("unpair"))) {
0261             DeviceDbusInterface dev(device);
0262             if (!dev.isPaired()) {
0263                 QTextStream(stderr) << i18n("Already not paired") << Qt::endl;
0264             } else {
0265                 QTextStream(stderr) << i18n("Unpaired") << Qt::endl;
0266                 blockOnReply(dev.unpair());
0267             }
0268         } else if (parser.isSet(QStringLiteral("send-clipboard"))) {
0269             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
0270                                                               QLatin1String("/modules/kdeconnect/devices/%1/clipboard").arg(device),
0271                                                               QStringLiteral("org.kde.kdeconnect.device.clipboard"),
0272                                                               QStringLiteral("sendClipboard"));
0273             blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
0274         } else if (parser.isSet(QStringLiteral("ping")) || parser.isSet(QStringLiteral("ping-msg"))) {
0275             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
0276                                                               QLatin1String("/modules/kdeconnect/devices/%1/ping").arg(device),
0277                                                               QStringLiteral("org.kde.kdeconnect.device.ping"),
0278                                                               QStringLiteral("sendPing"));
0279             if (parser.isSet(QStringLiteral("ping-msg"))) {
0280                 QString message = parser.value(QStringLiteral("ping-msg"));
0281                 msg.setArguments(QVariantList{message});
0282             }
0283             blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
0284         } else if (parser.isSet(QStringLiteral("send-sms"))) {
0285             if (parser.isSet(QStringLiteral("destination"))) {
0286                 qDBusRegisterMetaType<ConversationAddress>();
0287                 QVariantList addresses;
0288 
0289                 const QStringList addressList = parser.value(QStringLiteral("destination")).split(QRegularExpression(QStringLiteral("\\s+")));
0290 
0291                 for (const QString &input : addressList) {
0292                     ConversationAddress address(input);
0293                     addresses << QVariant::fromValue(address);
0294                 }
0295 
0296                 const QString message = parser.value(QStringLiteral("send-sms"));
0297 
0298                 const QStringList rawAttachmentUrlsList = parser.values(QStringLiteral("attachment"));
0299 
0300                 QVariantList attachments;
0301                 for (const QString &attachmentUrl : rawAttachmentUrlsList) {
0302                     // TODO: Construct attachment objects from the list of Urls
0303                     Q_UNUSED(attachmentUrl);
0304                 }
0305 
0306                 DeviceConversationsDbusInterface conversationDbusInterface(device);
0307                 auto reply = conversationDbusInterface.sendWithoutConversation(addresses, message, attachments);
0308 
0309                 reply.waitForFinished();
0310             } else {
0311                 QTextStream(stderr) << i18n("error: should specify the SMS's recipient by passing --destination <phone number>") << Qt::endl;
0312                 return 1;
0313             }
0314         } else if (parser.isSet(QStringLiteral("ring"))) {
0315             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
0316                                                               QLatin1String("/modules/kdeconnect/devices/%1/findmyphone").arg(device),
0317                                                               QStringLiteral("org.kde.kdeconnect.device.findmyphone"),
0318                                                               QStringLiteral("ring"));
0319             blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
0320         } else if (parser.isSet(QStringLiteral("send-keys"))) {
0321             QString seq = parser.value(QStringLiteral("send-keys"));
0322             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
0323                                                               QLatin1String("/modules/kdeconnect/devices/%1/remotekeyboard").arg(device),
0324                                                               QStringLiteral("org.kde.kdeconnect.device.remotekeyboard"),
0325                                                               QStringLiteral("sendKeyPress"));
0326             if (seq.trimmed() == QLatin1String("-")) {
0327                 // from stdin
0328                 QFile in;
0329                 if (in.open(stdin, QIODevice::ReadOnly | QIODevice::Unbuffered)) {
0330                     while (!in.atEnd()) {
0331                         QByteArray line = in.readLine(); // sanitize to ASCII-codes > 31?
0332                         msg.setArguments({QString::fromLatin1(line), -1, false, false, false});
0333                         blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
0334                     }
0335                     in.close();
0336                 }
0337             } else {
0338                 msg.setArguments({seq, -1, false, false, false});
0339                 blockOnReply(QDBusConnection::sessionBus().asyncCall(msg));
0340             }
0341         } else if (parser.isSet(QStringLiteral("list-notifications"))) {
0342             NotificationsModel notifications;
0343             notifications.setDeviceId(device);
0344             for (int i = 0, rows = notifications.rowCount(); i < rows; ++i) {
0345                 QModelIndex idx = notifications.index(i);
0346                 QTextStream(stdout) << "- " << idx.data(NotificationsModel::AppNameModelRole).toString() << ": "
0347                                     << idx.data(NotificationsModel::NameModelRole).toString() << Qt::endl;
0348             }
0349         } else if (parser.isSet(QStringLiteral("list-commands"))) {
0350             RemoteCommandsDbusInterface iface(device);
0351             const auto cmds = QJsonDocument::fromJson(iface.commands()).object();
0352             for (auto it = cmds.constBegin(), itEnd = cmds.constEnd(); it != itEnd; ++it) {
0353                 const QJsonObject cont = it->toObject();
0354                 QTextStream(stdout) << it.key() << ": " << cont.value(QStringLiteral("name")).toString() << ": "
0355                                     << cont.value(QStringLiteral("command")).toString() << Qt::endl;
0356             }
0357         } else if (parser.isSet(QStringLiteral("execute-command"))) {
0358             RemoteCommandsDbusInterface iface(device);
0359             blockOnReply(iface.triggerCommand(parser.value(QStringLiteral("execute-command"))));
0360         } else if (parser.isSet(QStringLiteral("encryption-info"))) {
0361             DeviceDbusInterface dev(device);
0362             QString info = blockOnReply<QString>(dev.encryptionInfo()); // QSsl::Der = 1
0363             QTextStream(stdout) << info << Qt::endl;
0364         } else {
0365             QTextStream(stderr) << i18n("Nothing to be done") << Qt::endl;
0366         }
0367     }
0368     QMetaObject::invokeMethod(&app, "quit", Qt::QueuedConnection);
0369 
0370     return app.exec();
0371 }