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