1 /*
2  * SPDX-FileCopyrightText: 2015 Aleix Pol Gonzalez <aleixpol@kde.org>
3  *
4  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5  */
6 
7 #include <QCryptographicHash>
8 #include <QIODevice>
9 #include <QDBusMessage>
10 #include <QCoreApplication>
11 #include <QTextStream>
12 #include <QFile>
13 
14 #include <KAboutData>
15 
16 #include "interfaces/devicesmodel.h"
17 #include "interfaces/notificationsmodel.h"
18 #include "interfaces/dbusinterfaces.h"
19 #include "interfaces/dbushelpers.h"
20 #include "interfaces/conversationmessage.h"
21 #include "kdeconnect-version.h"
22 
23 #include <dbushelper.h>
24 
main(int argc,char ** argv)25 int main(int argc, char** argv)
26 {
27     QCoreApplication app(argc, argv);
28     KAboutData about(QStringLiteral("kdeconnect-cli"),
29                      QStringLiteral("kdeconnect-cli"),
30                      QStringLiteral(KDECONNECT_VERSION_STRING),
31                      i18n("KDE Connect CLI tool"),
32                      KAboutLicense::GPL,
33                      i18n("(C) 2015 Aleix Pol Gonzalez"));
34     KAboutData::setApplicationData(about);
35 
36     about.addAuthor( i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@kde.org") );
37     about.addAuthor( i18n("Albert Vaca Cintora"), QString(), QStringLiteral("albertvaka@gmail.com") );
38     QCommandLineParser parser;
39     parser.addOption(QCommandLineOption(QStringList(QStringLiteral("l")) << QStringLiteral("list-devices"), i18n("List all devices")));
40     parser.addOption(QCommandLineOption(QStringList(QStringLiteral("a")) << QStringLiteral("list-available"), i18n("List available (paired and reachable) devices")));
41     parser.addOption(QCommandLineOption(QStringLiteral("id-only"), i18n("Make --list-devices or --list-available print only the devices id, to ease scripting")));
42     parser.addOption(QCommandLineOption(QStringLiteral("name-only"), i18n("Make --list-devices or --list-available print only the devices name, to ease scripting")));
43     parser.addOption(QCommandLineOption(QStringLiteral("id-name-only"), i18n("Make --list-devices or --list-available print only the devices id and name, to ease scripting")));
44     parser.addOption(QCommandLineOption(QStringLiteral("refresh"), i18n("Search for devices in the network and re-establish connections")));
45     parser.addOption(QCommandLineOption(QStringLiteral("pair"), i18n("Request pairing to a said device")));
46     parser.addOption(QCommandLineOption(QStringLiteral("ring"), i18n("Find the said device by ringing it.")));
47     parser.addOption(QCommandLineOption(QStringLiteral("unpair"), i18n("Stop pairing to a said device")));
48     parser.addOption(QCommandLineOption(QStringLiteral("ping"), i18n("Sends a ping to said device")));
49     parser.addOption(QCommandLineOption(QStringLiteral("ping-msg"), i18n("Same as ping but you can set the message to display"), i18n("message")));
50     parser.addOption(QCommandLineOption(QStringLiteral("share"), i18n("Share a file/URL to a said device"), QStringLiteral("path or URL")));
51     parser.addOption(QCommandLineOption(QStringLiteral("share-text"), i18n("Share text to a said device"), QStringLiteral("text")));
52     parser.addOption(QCommandLineOption(QStringLiteral("list-notifications"), i18n("Display the notifications on a said device")));
53     parser.addOption(QCommandLineOption(QStringLiteral("lock"), i18n("Lock the specified device")));
54     parser.addOption(QCommandLineOption(QStringLiteral("unlock"), i18n("Unlock the specified device")));
55     parser.addOption(QCommandLineOption(QStringLiteral("send-sms"), i18n("Sends an SMS. Requires destination"), i18n("message")));
56     parser.addOption(QCommandLineOption(QStringLiteral("destination"), i18n("Phone number to send the message"), i18n("phone number")));
57     parser.addOption(QCommandLineOption(QStringLiteral("attachment"), i18n("File urls to send attachments with the message (can be passed multiple times)"), i18n("file urls")));
58     parser.addOption(QCommandLineOption(QStringList(QStringLiteral("device")) << QStringLiteral("d"), i18n("Device ID"), QStringLiteral("dev")));
59     parser.addOption(QCommandLineOption(QStringList(QStringLiteral("name")) << QStringLiteral("n"), i18n("Device Name"), QStringLiteral("name")));
60     parser.addOption(QCommandLineOption(QStringLiteral("encryption-info"), i18n("Get encryption info about said device")));
61     parser.addOption(QCommandLineOption(QStringLiteral("list-commands"), i18n("Lists remote commands and their ids")));
62     parser.addOption(QCommandLineOption(QStringLiteral("execute-command"), i18n("Executes a remote command by id"), QStringLiteral("id")));
63     parser.addOption(QCommandLineOption(QStringList{QStringLiteral("k"), QStringLiteral("send-keys")}, i18n("Sends keys to a said device"), QStringLiteral("key")));
64     parser.addOption(QCommandLineOption(QStringLiteral("my-id"), i18n("Display this device's id and exit")));
65     parser.addOption(QCommandLineOption(QStringLiteral("photo"), i18n("Open the connected device's camera and transfer the photo"), QStringLiteral("path")));
66 
67     //Hidden because it's an implementation detail
68     QCommandLineOption deviceAutocomplete(QStringLiteral("shell-device-autocompletion"));
69     deviceAutocomplete.setFlags(QCommandLineOption::HiddenFromHelp);
70     deviceAutocomplete.setDescription(QStringLiteral("Outputs all available devices id's with their name and paired status")); //Not visible, so no translation needed
71     deviceAutocomplete.setValueName(QStringLiteral("shell"));
72     parser.addOption(deviceAutocomplete);
73     about.setupCommandLine(&parser);
74 
75     parser.process(app);
76     about.processCommandLine(&parser);
77 
78     const QString id = QStringLiteral("kdeconnect-cli-") + QString::number(QCoreApplication::applicationPid());
79     DaemonDbusInterface iface;
80 
81     if (parser.isSet(QStringLiteral("my-id"))) {
82         QTextStream(stdout) << iface.selfId() << endl;
83     } else if (parser.isSet(QStringLiteral("l")) || parser.isSet(QStringLiteral("a"))) {
84         bool reachable = false;
85         if (parser.isSet(QStringLiteral("a"))) {
86             reachable = true;
87         } else {
88             blockOnReply(iface.acquireDiscoveryMode(id));
89             QThread::sleep(2);
90         }
91         const QStringList devices = blockOnReply<QStringList>(iface.devices(reachable, false));
92 
93         bool displayCount = true;
94         for (const QString& id : devices) {
95             if (parser.isSet(QStringLiteral("id-only"))) {
96                 QTextStream(stdout) << id << endl;
97                 displayCount = false;
98             } else if (parser.isSet(QStringLiteral("name-only"))) {
99                 DeviceDbusInterface deviceIface(id);
100                 QTextStream(stdout) << deviceIface.name() << endl;
101                 displayCount = false;
102             } else if (parser.isSet(QStringLiteral("id-name-only"))) {
103                 DeviceDbusInterface deviceIface(id);
104                 QTextStream(stdout) << id << ' ' << deviceIface.name() << endl;
105                 displayCount = false;
106             } else {
107                 DeviceDbusInterface deviceIface(id);
108                 QString statusInfo;
109                 const bool isReachable = deviceIface.isReachable();
110                 const bool isTrusted = deviceIface.isTrusted();
111                 if (isReachable && isTrusted) {
112                     statusInfo = i18n("(paired and reachable)");
113                 } else if (isReachable) {
114                     statusInfo = i18n("(reachable)");
115                 } else if (isTrusted) {
116                     statusInfo = i18n("(paired)");
117                 }
118                 QTextStream(stdout) << "- " << deviceIface.name()
119                         << ": " << deviceIface.id() << ' ' << statusInfo << endl;
120             }
121         }
122         if (displayCount) {
123             QTextStream(stderr) << i18np("1 device found", "%1 devices found", devices.size()) << endl;
124         } else if (devices.isEmpty()) {
125             QTextStream(stderr) << i18n("No devices found") << endl;
126         }
127 
128         blockOnReply(iface.releaseDiscoveryMode(id));
129     } else if (parser.isSet(QStringLiteral("shell-device-autocompletion"))) {
130         //Outputs a list of reachable devices in zsh autocomplete format, with the name as description
131         const QStringList devices = blockOnReply<QStringList>(iface.devices(true, false));
132         for (const QString &id : devices) {
133             DeviceDbusInterface deviceIface(id);
134             QString statusInfo;
135             const bool isTrusted = deviceIface.isTrusted();
136             if (isTrusted) {
137                 statusInfo = i18n("(paired)");
138             } else {
139                 statusInfo = i18n("(unpaired)");
140             }
141 
142             //Description: "device name (paired/unpaired)"
143             QString description = deviceIface.name() + QLatin1Char(' ') + statusInfo;
144             //Replace characters
145             description.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
146             description.replace(QLatin1Char('['),  QStringLiteral("\\["));
147             description.replace(QLatin1Char(']'),  QStringLiteral("\\]"));
148             description.replace(QLatin1Char('\''), QStringLiteral("\\'"));
149             description.replace(QLatin1Char('\"'), QStringLiteral("\\\""));
150             description.replace(QLatin1Char('\n'), QLatin1Char(' '));
151             description.remove(QLatin1Char('\0'));
152 
153             //Output id and description
154             QTextStream(stdout) << id << '[' << description << ']' << endl;
155         }
156 
157         //Exit with 1 if we didn't find a device
158         return int(devices.isEmpty());
159     } else if(parser.isSet(QStringLiteral("refresh"))) {
160         QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect"), QStringLiteral("org.kde.kdeconnect.daemon"), QStringLiteral("forceOnNetworkChange"));
161         blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
162     } else {
163 
164         QString device = parser.value(QStringLiteral("device"));
165         if (device.isEmpty() && parser.isSet(QStringLiteral("name"))) {
166             device = blockOnReply(iface.deviceIdByName(parser.value(QStringLiteral("name"))));
167             if (device.isEmpty()) {
168                 QTextStream(stderr) << "Couldn't find device: " << parser.value(QStringLiteral("name")) << endl;
169                 return 1;
170             }
171         }
172         if(device.isEmpty()) {
173             QTextStream(stderr) << i18n("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 -l\" \nView complete help with --help option") << endl;
174             return 1;
175         }
176 
177         if (!blockOnReply<QStringList>(iface.devices(false, false)).contains(device)) {
178             QTextStream(stderr) << "Couldn't find device with id \"" << device << "\". To specify a device by name use -n <devicename>" << endl;
179             return 1;
180         }
181 
182         if (parser.isSet(QStringLiteral("share"))) {
183             QStringList urls;
184 
185             QUrl url = QUrl::fromUserInput(parser.value(QStringLiteral("share")), QDir::currentPath());
186             urls.append(url.toString());
187 
188             // Check for more arguments
189             const auto args = parser.positionalArguments();
190             for (const QString& input : args) {
191                 QUrl url = QUrl::fromUserInput(input, QDir::currentPath());
192                 urls.append(url.toString());
193             }
194 
195             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/share"),
196                                                               QStringLiteral("org.kde.kdeconnect.device.share"), QStringLiteral("shareUrls"));
197 
198             msg.setArguments(QVariantList() << QVariant(urls));
199             blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
200 
201             for (const QString& url : qAsConst(urls)) {
202                 QTextStream(stdout) << i18n("Shared %1", url) << endl;
203             }
204         } else if (parser.isSet(QStringLiteral("share-text"))) {
205             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/share"), QStringLiteral("org.kde.kdeconnect.device.share"), QStringLiteral("shareText"));
206             msg.setArguments(QVariantList() << parser.value(QStringLiteral("share-text")));
207             blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
208             QTextStream(stdout) << i18n("Shared text: %1", parser.value(QStringLiteral("share-text"))) << endl;
209         } else if (parser.isSet(QStringLiteral("lock")) || parser.isSet(QStringLiteral("unlock"))) {
210             LockDeviceDbusInterface iface(device);
211             iface.setLocked(parser.isSet(QStringLiteral("lock")));
212 
213             DeviceDbusInterface deviceIface(device);
214             if (parser.isSet(QStringLiteral("lock"))) {
215                 QTextStream(stdout) << i18nc("device has requested to lock peer device", "Requested to lock %1.", deviceIface.name()) << endl;
216             } else {
217                 QTextStream(stdout) << i18nc("device has requested to unlock peer device", "Requested to unlock %1.", deviceIface.name()) << endl;
218             }
219         } else if(parser.isSet(QStringLiteral("pair"))) {
220             DeviceDbusInterface dev(device);
221             if (!dev.isReachable()) {
222                 //Device doesn't exist, go into discovery mode and wait up to 30 seconds for the device to appear
223                 QEventLoop wait;
224                 QTextStream(stderr) << i18n("waiting for device...") << endl;
225                 blockOnReply(iface.acquireDiscoveryMode(id));
226 
227                 QObject::connect(&iface, &DaemonDbusInterface::deviceAdded, &iface, [&](const QString& deviceAddedId) {
228                     if (device == deviceAddedId) {
229                         wait.quit();
230                     }
231                 });
232                 QTimer::singleShot(30 * 1000, &wait, &QEventLoop::quit);
233 
234                 wait.exec();
235             }
236 
237             if (!dev.isReachable()) {
238                 QTextStream(stderr) << i18n("Device not found") << endl;
239             } else if(blockOnReply<bool>(dev.isTrusted())) {
240                 QTextStream(stderr) << i18n("Already paired") << endl;
241             } else {
242                 QTextStream(stderr) << i18n("Pair requested") << endl;
243                 blockOnReply(dev.requestPair());
244             }
245             blockOnReply(iface.releaseDiscoveryMode(id));
246         } else if(parser.isSet(QStringLiteral("unpair"))) {
247             DeviceDbusInterface dev(device);
248             if (!dev.isTrusted()) {
249                 QTextStream(stderr) << i18n("Already not paired") << endl;
250             } else {
251                 QTextStream(stderr) << i18n("Unpaired") << endl;
252                 blockOnReply(dev.unpair());
253             }
254         } else if(parser.isSet(QStringLiteral("ping")) || parser.isSet(QStringLiteral("ping-msg"))) {
255             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/ping"), QStringLiteral("org.kde.kdeconnect.device.ping"), QStringLiteral("sendPing"));
256             if (parser.isSet(QStringLiteral("ping-msg"))) {
257                 QString message = parser.value(QStringLiteral("ping-msg"));
258                 msg.setArguments(QVariantList() << message);
259             }
260             blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
261         } else if(parser.isSet(QStringLiteral("send-sms"))) {
262             if (parser.isSet(QStringLiteral("destination"))) {
263                 qDBusRegisterMetaType<ConversationAddress>();
264                 QVariantList addresses;
265 
266                 const QStringList addressList = parser.value(QStringLiteral("destination")).split(QRegularExpression(QStringLiteral("\\s+")));
267 
268                 for (const QString& input : addressList) {
269                     ConversationAddress address(input);
270                     addresses << QVariant::fromValue(address);
271                 }
272 
273                 const QString message = parser.value(QStringLiteral("send-sms"));
274 
275                 const QStringList rawAttachmentUrlsList = parser.values(QStringLiteral("attachment"));
276 
277                 QVariantList attachments;
278                 for (const QString& attachmentUrl : rawAttachmentUrlsList) {
279                     // TODO: Construct attachment objects from the list of Urls
280                     Q_UNUSED(attachmentUrl);
281                 }
282 
283                 DeviceConversationsDbusInterface conversationDbusInterface(device);
284                 auto reply = conversationDbusInterface.sendWithoutConversation(addresses, message, attachments);
285 
286                 reply.waitForFinished();
287             } else {
288                 QTextStream(stderr) << i18n("error: should specify the SMS's recipient by passing --destination <phone number>") << endl;
289                 return 1;
290             }
291         } else if(parser.isSet(QStringLiteral("ring"))) {
292             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/findmyphone"), QStringLiteral("org.kde.kdeconnect.device.findmyphone"), QStringLiteral("ring"));
293             blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
294         } else if(parser.isSet(QStringLiteral("photo"))) {
295             const QString fileName = parser.value(QStringLiteral("photo"));
296             if (!fileName.isEmpty()) {
297                 QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/photo"), QStringLiteral("org.kde.kdeconnect.device.photo"), QStringLiteral("requestPhoto"));
298                 msg.setArguments({fileName});
299                 blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
300             } else {
301                 QTextStream(stderr) << i18n("Please specify a filename for the photo") << endl;
302             }
303         } else if(parser.isSet(QStringLiteral("send-keys"))) {
304             QString seq = parser.value(QStringLiteral("send-keys"));
305             QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + device + QStringLiteral("/remotekeyboard"), QStringLiteral("org.kde.kdeconnect.device.remotekeyboard"), QStringLiteral("sendKeyPress"));
306             if (seq.trimmed() == QLatin1String("-")) {
307                 // from stdin
308                 QFile in;
309                 if(in.open(stdin,QIODevice::ReadOnly | QIODevice::Unbuffered)) {
310                     while (!in.atEnd()) {
311                         QByteArray line = in.readLine();  // sanitize to ASCII-codes > 31?
312                         msg.setArguments({QString::fromLatin1(line), -1, false, false, false});
313                         blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
314                     }
315                     in.close();
316                 }
317             } else {
318                 msg.setArguments({seq, -1, false, false, false});
319                 blockOnReply(DBusHelper::sessionBus().asyncCall(msg));
320             }
321         } else if(parser.isSet(QStringLiteral("list-notifications"))) {
322             NotificationsModel notifications;
323             notifications.setDeviceId(device);
324             for(int i=0, rows=notifications.rowCount(); i<rows; ++i) {
325                 QModelIndex idx = notifications.index(i);
326                 QTextStream(stdout) << "- " << idx.data(NotificationsModel::AppNameModelRole).toString()
327                     << ": " << idx.data(NotificationsModel::NameModelRole).toString() << endl;
328             }
329         } else if(parser.isSet(QStringLiteral("list-commands"))) {
330             RemoteCommandsDbusInterface iface(device);
331             const auto cmds = QJsonDocument::fromJson(iface.commands()).object();
332             for (auto it = cmds.constBegin(), itEnd = cmds.constEnd(); it!=itEnd; ++it) {
333                 const QJsonObject cont = it->toObject();
334                 QTextStream(stdout) << it.key() << ": " << cont.value(QStringLiteral("name")).toString() << ": " << cont.value(QStringLiteral("command")).toString() << endl;
335             }
336         } else if(parser.isSet(QStringLiteral("execute-command"))) {
337             RemoteCommandsDbusInterface iface(device);
338             blockOnReply(iface.triggerCommand(parser.value(QStringLiteral("execute-command"))));
339         } else if(parser.isSet(QStringLiteral("encryption-info"))) {
340             DeviceDbusInterface dev(device);
341             QString info = blockOnReply<QString>(dev.encryptionInfo()); // QSsl::Der = 1
342             QTextStream(stdout) << info << endl;
343         } else {
344             QTextStream(stderr) << i18n("Nothing to be done") << endl;
345         }
346     }
347     QMetaObject::invokeMethod(&app, "quit", Qt::QueuedConnection);
348 
349     return app.exec();
350 }
351