]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/App.java
995da8d423dd5449293a305da1ec7d944f30eeff
[signal-cli] / src / main / java / org / asamk / signal / App.java
1 package org.asamk.signal;
2
3 import net.sourceforge.argparse4j.ArgumentParsers;
4 import net.sourceforge.argparse4j.impl.Arguments;
5 import net.sourceforge.argparse4j.inf.ArgumentParser;
6 import net.sourceforge.argparse4j.inf.Namespace;
7
8 import org.asamk.Signal;
9 import org.asamk.SignalControl;
10 import org.asamk.signal.commands.Command;
11 import org.asamk.signal.commands.Commands;
12 import org.asamk.signal.commands.LocalCommand;
13 import org.asamk.signal.commands.MultiLocalCommand;
14 import org.asamk.signal.commands.ProvisioningCommand;
15 import org.asamk.signal.commands.RegistrationCommand;
16 import org.asamk.signal.commands.exceptions.CommandException;
17 import org.asamk.signal.commands.exceptions.IOErrorException;
18 import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
19 import org.asamk.signal.commands.exceptions.UserErrorException;
20 import org.asamk.signal.dbus.DbusManagerImpl;
21 import org.asamk.signal.dbus.DbusMultiAccountManagerImpl;
22 import org.asamk.signal.dbus.DbusProvisioningManagerImpl;
23 import org.asamk.signal.dbus.DbusRegistrationManagerImpl;
24 import org.asamk.signal.manager.Manager;
25 import org.asamk.signal.manager.MultiAccountManagerImpl;
26 import org.asamk.signal.manager.NotRegisteredException;
27 import org.asamk.signal.manager.ProvisioningManager;
28 import org.asamk.signal.manager.RegistrationManager;
29 import org.asamk.signal.manager.config.ServiceConfig;
30 import org.asamk.signal.manager.config.ServiceEnvironment;
31 import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
32 import org.asamk.signal.output.JsonWriterImpl;
33 import org.asamk.signal.output.OutputWriter;
34 import org.asamk.signal.output.PlainTextWriterImpl;
35 import org.asamk.signal.util.IOUtils;
36 import org.freedesktop.dbus.connections.impl.DBusConnection;
37 import org.freedesktop.dbus.errors.ServiceUnknown;
38 import org.freedesktop.dbus.errors.UnknownMethod;
39 import org.freedesktop.dbus.exceptions.DBusException;
40 import org.freedesktop.dbus.exceptions.DBusExecutionException;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 import java.io.BufferedWriter;
45 import java.io.File;
46 import java.io.IOException;
47 import java.io.OutputStreamWriter;
48 import java.nio.charset.Charset;
49 import java.util.ArrayList;
50 import java.util.List;
51
52 import static net.sourceforge.argparse4j.DefaultSettings.VERSION_0_9_0_DEFAULT_SETTINGS;
53
54 public class App {
55
56 private final static Logger logger = LoggerFactory.getLogger(App.class);
57
58 private final Namespace ns;
59
60 static ArgumentParser buildArgumentParser() {
61 var parser = ArgumentParsers.newFor("signal-cli", VERSION_0_9_0_DEFAULT_SETTINGS)
62 .includeArgumentNamesAsKeysInResult(true)
63 .build()
64 .defaultHelp(true)
65 .description("Commandline interface for Signal.")
66 .version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION);
67
68 parser.addArgument("-v", "--version").help("Show package version.").action(Arguments.version());
69 parser.addArgument("--verbose")
70 .help("Raise log level and include lib signal logs. Specify multiple times for even more logs.")
71 .action(Arguments.count());
72 parser.addArgument("-c", "--config")
73 .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli).");
74
75 parser.addArgument("-a", "--account", "-u", "--username")
76 .help("Specify your phone number, that will be your identifier.");
77
78 var mut = parser.addMutuallyExclusiveGroup();
79 mut.addArgument("--dbus").dest("global-dbus").help("Make request via user dbus.").action(Arguments.storeTrue());
80 mut.addArgument("--dbus-system")
81 .dest("global-dbus-system")
82 .help("Make request via system dbus.")
83 .action(Arguments.storeTrue());
84
85 parser.addArgument("-o", "--output")
86 .help("Choose to output in plain text or JSON")
87 .type(Arguments.enumStringType(OutputType.class));
88
89 parser.addArgument("--service-environment")
90 .help("Choose the server environment to use.")
91 .type(Arguments.enumStringType(ServiceEnvironmentCli.class))
92 .setDefault(ServiceEnvironmentCli.LIVE);
93
94 parser.addArgument("--trust-new-identities")
95 .help("Choose when to trust new identities.")
96 .type(Arguments.enumStringType(TrustNewIdentityCli.class))
97 .setDefault(TrustNewIdentityCli.ON_FIRST_USE);
98
99 var subparsers = parser.addSubparsers().title("subcommands").dest("command");
100
101 Commands.getCommandSubparserAttachers().forEach((key, value) -> {
102 var subparser = subparsers.addParser(key);
103 value.attachToSubparser(subparser);
104 });
105
106 return parser;
107 }
108
109 public App(final Namespace ns) {
110 this.ns = ns;
111 }
112
113 public void init() throws CommandException {
114 var commandKey = ns.getString("command");
115 var command = Commands.getCommand(commandKey);
116 if (command == null) {
117 throw new UserErrorException("Command not implemented!");
118 }
119
120 var outputTypeInput = ns.<OutputType>get("output");
121 var outputType = outputTypeInput == null
122 ? command.getSupportedOutputTypes().stream().findFirst().orElse(null)
123 : outputTypeInput;
124 var writer = new BufferedWriter(new OutputStreamWriter(System.out, Charset.defaultCharset()));
125 var outputWriter = outputType == null
126 ? null
127 : outputType == OutputType.JSON ? new JsonWriterImpl(writer) : new PlainTextWriterImpl(writer);
128
129 if (outputWriter != null && !command.getSupportedOutputTypes().contains(outputType)) {
130 throw new UserErrorException("Command doesn't support output type " + outputType);
131 }
132
133 var account = ns.getString("account");
134
135 final var useDbus = Boolean.TRUE.equals(ns.getBoolean("global-dbus"));
136 final var useDbusSystem = Boolean.TRUE.equals(ns.getBoolean("global-dbus-system"));
137 if (useDbus || useDbusSystem) {
138 // If account is null, it will connect to the default object path
139 initDbusClient(command, account, useDbusSystem, outputWriter);
140 return;
141 }
142
143 final File dataPath;
144 var config = ns.getString("config");
145 if (config != null) {
146 dataPath = new File(config);
147 } else {
148 dataPath = getDefaultDataPath();
149 }
150
151 if (!ServiceConfig.isSignalClientAvailable()) {
152 throw new UserErrorException("Missing required native library dependency: libsignal-client");
153 }
154
155 final var serviceEnvironmentCli = ns.<ServiceEnvironmentCli>get("service-environment");
156 final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE
157 ? ServiceEnvironment.LIVE
158 : ServiceEnvironment.SANDBOX;
159
160 final var trustNewIdentityCli = ns.<TrustNewIdentityCli>get("trust-new-identities");
161 final var trustNewIdentity = trustNewIdentityCli == TrustNewIdentityCli.ON_FIRST_USE
162 ? TrustNewIdentity.ON_FIRST_USE
163 : trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
164
165 if (command instanceof ProvisioningCommand provisioningCommand) {
166 if (account != null) {
167 throw new UserErrorException("You cannot specify a account (phone number) when linking");
168 }
169
170 handleProvisioningCommand(provisioningCommand, dataPath, serviceEnvironment, outputWriter);
171 return;
172 }
173
174 if (account == null) {
175 var accounts = Manager.getAllLocalAccountNumbers(dataPath);
176
177 if (command instanceof MultiLocalCommand multiLocalCommand) {
178 handleMultiLocalCommand(multiLocalCommand,
179 dataPath,
180 serviceEnvironment,
181 accounts,
182 outputWriter,
183 trustNewIdentity);
184 return;
185 }
186
187 if (accounts.size() == 0) {
188 throw new UserErrorException("No local users found, you first need to register or link an account");
189 } else if (accounts.size() > 1) {
190 throw new UserErrorException(
191 "Multiple users found, you need to specify an account (phone number) with -a");
192 }
193
194 account = accounts.get(0);
195 } else if (!Manager.isValidNumber(account, null)) {
196 throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
197 }
198
199 if (command instanceof RegistrationCommand registrationCommand) {
200 handleRegistrationCommand(registrationCommand, account, dataPath, serviceEnvironment);
201 return;
202 }
203
204 if (!(command instanceof LocalCommand)) {
205 throw new UserErrorException("Command only works in multi-account mode");
206 }
207
208 handleLocalCommand((LocalCommand) command,
209 account,
210 dataPath,
211 serviceEnvironment,
212 outputWriter,
213 trustNewIdentity);
214 }
215
216 private void handleProvisioningCommand(
217 final ProvisioningCommand command,
218 final File dataPath,
219 final ServiceEnvironment serviceEnvironment,
220 final OutputWriter outputWriter
221 ) throws CommandException {
222 var pm = ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
223 command.handleCommand(ns, pm, outputWriter);
224 }
225
226 private void handleProvisioningCommand(
227 final ProvisioningCommand c, final DBusConnection dBusConn, final OutputWriter outputWriter
228 ) throws CommandException, DBusException {
229 final var signalControl = dBusConn.getRemoteObject(DbusConfig.getBusname(),
230 DbusConfig.getObjectPath(),
231 SignalControl.class);
232 final var provisioningManager = new DbusProvisioningManagerImpl(signalControl, dBusConn);
233 try {
234 c.handleCommand(ns, provisioningManager, outputWriter);
235 } catch (UnsupportedOperationException e) {
236 throw new UserErrorException("Command is not yet implemented via dbus", e);
237 } catch (DBusExecutionException e) {
238 throw new UnexpectedErrorException(e.getMessage(), e);
239 }
240 }
241
242 private void handleRegistrationCommand(
243 final RegistrationCommand command,
244 final String account,
245 final File dataPath,
246 final ServiceEnvironment serviceEnvironment
247 ) throws CommandException {
248 final RegistrationManager manager;
249 try {
250 manager = RegistrationManager.init(account, dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
251 } catch (Throwable e) {
252 throw new UnexpectedErrorException("Error loading or creating state file: "
253 + e.getMessage()
254 + " ("
255 + e.getClass().getSimpleName()
256 + ")", e);
257 }
258 try (manager) {
259 command.handleCommand(ns, manager);
260 } catch (IOException e) {
261 logger.warn("Cleanup failed", e);
262 }
263 }
264
265 private void handleRegistrationCommand(
266 final RegistrationCommand c, String account, final DBusConnection dBusConn, final OutputWriter outputWriter
267 ) throws CommandException, DBusException {
268 final var signalControl = dBusConn.getRemoteObject(DbusConfig.getBusname(),
269 DbusConfig.getObjectPath(),
270 SignalControl.class);
271 try (final var registrationManager = new DbusRegistrationManagerImpl(account, signalControl, dBusConn)) {
272 c.handleCommand(ns, registrationManager);
273 } catch (UnsupportedOperationException e) {
274 throw new UserErrorException("Command is not yet implemented via dbus", e);
275 } catch (DBusExecutionException e) {
276 throw new UnexpectedErrorException(e.getMessage(), e);
277 }
278 }
279
280 private void handleLocalCommand(
281 final LocalCommand command,
282 final String account,
283 final File dataPath,
284 final ServiceEnvironment serviceEnvironment,
285 final OutputWriter outputWriter,
286 final TrustNewIdentity trustNewIdentity
287 ) throws CommandException {
288 try (var m = loadManager(account, dataPath, serviceEnvironment, trustNewIdentity)) {
289 command.handleCommand(ns, m, outputWriter);
290 } catch (IOException e) {
291 logger.warn("Cleanup failed", e);
292 }
293 }
294
295 private void handleLocalCommand(
296 final LocalCommand c,
297 String accountObjectPath,
298 final DBusConnection dBusConn,
299 final OutputWriter outputWriter
300 ) throws CommandException, DBusException {
301 var signal = dBusConn.getRemoteObject(DbusConfig.getBusname(), accountObjectPath, Signal.class);
302 try (final var m = new DbusManagerImpl(signal, dBusConn)) {
303 c.handleCommand(ns, m, outputWriter);
304 } catch (UnsupportedOperationException e) {
305 throw new UserErrorException("Command is not yet implemented via dbus", e);
306 } catch (DBusExecutionException e) {
307 throw new UnexpectedErrorException(e.getMessage(), e);
308 }
309 }
310
311 private void handleMultiLocalCommand(
312 final MultiLocalCommand command,
313 final File dataPath,
314 final ServiceEnvironment serviceEnvironment,
315 final List<String> accounts,
316 final OutputWriter outputWriter,
317 final TrustNewIdentity trustNewIdentity
318 ) throws CommandException {
319 final var managers = new ArrayList<Manager>();
320 for (String a : accounts) {
321 try {
322 managers.add(loadManager(a, dataPath, serviceEnvironment, trustNewIdentity));
323 } catch (CommandException e) {
324 logger.warn("Ignoring {}: {}", a, e.getMessage());
325 }
326 }
327
328 try (var multiAccountManager = new MultiAccountManagerImpl(managers,
329 dataPath,
330 serviceEnvironment,
331 BaseConfig.USER_AGENT)) {
332 command.handleCommand(ns, multiAccountManager, outputWriter);
333 }
334 }
335
336 private void handleMultiLocalCommand(
337 final MultiLocalCommand c, final DBusConnection dBusConn, final OutputWriter outputWriter
338 ) throws CommandException, DBusException {
339 final var signalControl = dBusConn.getRemoteObject(DbusConfig.getBusname(),
340 DbusConfig.getObjectPath(),
341 SignalControl.class);
342 try (final var multiAccountManager = new DbusMultiAccountManagerImpl(signalControl, dBusConn)) {
343 c.handleCommand(ns, multiAccountManager, outputWriter);
344 } catch (UnsupportedOperationException e) {
345 throw new UserErrorException("Command is not yet implemented via dbus", e);
346 }
347 }
348
349 private Manager loadManager(
350 final String account,
351 final File dataPath,
352 final ServiceEnvironment serviceEnvironment,
353 final TrustNewIdentity trustNewIdentity
354 ) throws CommandException {
355 Manager manager;
356 logger.trace("Loading account file for {}", account);
357 try {
358 manager = Manager.init(account, dataPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity);
359 } catch (NotRegisteredException e) {
360 throw new UserErrorException("User " + account + " is not registered.");
361 } catch (Throwable e) {
362 throw new UnexpectedErrorException("Error loading state file for user "
363 + account
364 + ": "
365 + e.getMessage()
366 + " ("
367 + e.getClass().getSimpleName()
368 + ")", e);
369 }
370
371 logger.trace("Checking account state");
372 try {
373 manager.checkAccountState();
374 } catch (IOException e) {
375 try {
376 manager.close();
377 } catch (IOException ie) {
378 logger.warn("Failed to close broken account", ie);
379 }
380 throw new IOErrorException("Error while checking account " + account + ": " + e.getMessage(), e);
381 }
382
383 return manager;
384 }
385
386 private void initDbusClient(
387 final Command command, final String account, final boolean systemBus, final OutputWriter outputWriter
388 ) throws CommandException {
389 try {
390 DBusConnection.DBusBusType busType;
391 if (systemBus) {
392 busType = DBusConnection.DBusBusType.SYSTEM;
393 } else {
394 busType = DBusConnection.DBusBusType.SESSION;
395 }
396 try (var dBusConn = DBusConnection.getConnection(busType)) {
397 if (command instanceof ProvisioningCommand c) {
398 if (account != null) {
399 throw new UserErrorException("You cannot specify a account (phone number) when linking");
400 }
401
402 handleProvisioningCommand(c, dBusConn, outputWriter);
403 return;
404 }
405
406 if (account == null && command instanceof MultiLocalCommand c) {
407 handleMultiLocalCommand(c, dBusConn, outputWriter);
408 return;
409 }
410 if (account != null && command instanceof RegistrationCommand c) {
411 handleRegistrationCommand(c, account, dBusConn, outputWriter);
412 return;
413 }
414 if (!(command instanceof LocalCommand localCommand)) {
415 throw new UserErrorException("Command only works in multi-account mode");
416 }
417
418 var accountObjectPath = account == null ? tryGetSingleAccountObjectPath(dBusConn) : null;
419 if (accountObjectPath == null) {
420 accountObjectPath = DbusConfig.getObjectPath(account);
421 }
422 handleLocalCommand(localCommand, accountObjectPath, dBusConn, outputWriter);
423 }
424 } catch (ServiceUnknown e) {
425 throw new UserErrorException("signal-cli DBus daemon not running on "
426 + (systemBus ? "system" : "session")
427 + " bus: "
428 + e.getMessage(), e);
429 } catch (DBusExecutionException | DBusException | IOException e) {
430 throw new UnexpectedErrorException("Dbus client failed: " + e.getMessage(), e);
431 }
432 }
433
434 private String tryGetSingleAccountObjectPath(final DBusConnection dBusConn) throws DBusException, CommandException {
435 var control = dBusConn.getRemoteObject(DbusConfig.getBusname(),
436 DbusConfig.getObjectPath(),
437 SignalControl.class);
438 try {
439 final var accounts = control.listAccounts();
440 if (accounts.size() == 0) {
441 throw new UserErrorException("No local users found, you first need to register or link an account");
442 } else if (accounts.size() > 1) {
443 throw new UserErrorException(
444 "Multiple users found, you need to specify an account (phone number) with -a");
445 }
446
447 return accounts.get(0).getPath();
448 } catch (UnknownMethod e) {
449 // dbus daemon not running in multi-account mode
450 return null;
451 }
452 }
453
454 /**
455 * @return the default data directory to be used by signal-cli.
456 */
457 private static File getDefaultDataPath() {
458 return new File(IOUtils.getDataHomeDir(), "signal-cli");
459 }
460 }