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